从零开始搭建前后端分离的NetCore2.2(EF Core CodeFirst+Autofac)+Vue的项目框架之八MemoryCache与redis缓存的使用
1.缓存概念
1.什么是缓存
这里要讲到的缓存是服务端缓存,简单的说,缓存就是将一些实时性不高,但访问又十分频繁,或者说要很长时间才能取到的数据给存在内存当中,当有请求时直接返回,不用经过数据库或接口获取。这样就可以减轻数据库的负担。
2.为什么要用缓存
总的来说就是为了提高响应速度(用户体验度),减少数据库访问频率。
在一个用户看来,软件使用的体验度才是关键,在对实时性要求不高的情况下,用户肯定会觉得打开界面的响应速度快,能保证平常工作的应用才是好的。因此为了满足这个需求,通过使用缓存,就可以保证满足在正常工作的前提下响应时间尽可能短。
例如:当客户端向服务器请求某个数据时,服务器先在缓存中找,如果在缓存中,就直接返回,无需查询数据库;如果请求的数据不在缓存中,这时再去数据库中找,找到后返回给客户端,并将这个资源加入缓存中。这样下次请求相同资源时,就不需
要连接数据库了。而且如果把缓存放在内存中,因为对内存的操作要比对数据库操作快得多,这样请求时间也会缩短。每当数据发生变化的时候(比如,数据有被修改,或被删除的情况下),要同步的更新缓存信息,确保用户不会在缓存取到旧的数据。
如果没有使用缓存,用户去请求某个数据,当用户量和数据逐渐增加的时候,就会发现每次用户请求的时间越来越长,且数据库无时不刻都在工作。这样用户和数据库都很痛苦,时间一长,就有可能发生下以下事情:
1.用户常抱怨应用打开速度太慢,页面经常无响应,偶尔还会出现崩溃的情况。
2.数据库连接数满或者说数据库响应慢(处理不过来)。
3.当并发量上来的时候,可能会导致数据库崩溃,使得应用无法正常使用。
2.选用redis还是memcached
简单说下这两者的区别,两者都是通过key-value的方式进行存储的,memcached只有简单的字符串格式,而redis还支持更多的格式(list、 set、sorted set、hash table ),缓存时使用到的数据都是在内存当中,
不同的在于redis支持持久化,集群、简单事务、发布/订阅、主从同步等功能,当断电或软件重启时,memcached中的数据就已经不存在了,而redis可以通过读取磁盘中的数据再次使用。
这里提高windows版的安装包:传送门 可视化工具:因文件太大无法上传到博客园。代码仓库中有,需要的私信哦~
3.两者在netcore 中的使用
memcached的使用还是相当简单的,首先在 startup 类中做以下更改,添加缓存参数 赋值给外部类来方便使用
public void configure(iapplicationbuilder app, ihostingenvironment env, imemorycache memorycache) { //.... 省略部分代码 demoweb.memorycache = memorycache; //.... 省略部分代码 }
demoweb中的代码:
public class demoweb { //....省略部分代码 /// <summary> /// memorycache /// </summary> public static imemorycache memorycache { get; set; } /// <summary> /// 获取当前请求客户端ip /// </summary> /// <returns></returns> public static string getclientip() { var ip = httpcontext.request.headers["x-forwarded-for"].firstordefault()?.split(',')[0].trim(); if (string.isnullorempty(ip)) { ip = httpcontext.connection.remoteipaddress.tostring(); } return ip; } }
然后创建 memorycache 来封装些缓存的简单方法
/// <summary> /// memorycache缓存 /// </summary> public class memorycache { private static readonly hashset<string> keys = new hashset<string>(); /// <summary> /// 缓存前缀 /// </summary> public string prefix { get; } /// <summary> /// 构造函数 /// </summary> /// <param name="prefix"></param> public memorycache(string prefix) { prefix = prefix + "_"; } /// <summary> /// 获取 /// </summary> /// <typeparam name="t"></typeparam> /// <param name="key"></param> /// <returns></returns> public t get<t>(string key) { return demoweb.memorycache.get<t>(prefix + key); } /// <summary> /// 设置 无过期时间 /// </summary> /// <param name="key"></param> /// <param name="data"></param> public void set(string key, object data) { key = prefix + key; demoweb.memorycache.set(key, data); if (!keys.contains(key)) { keys.add(key); } } /// <summary> /// 设置 /// </summary> /// <param name="key"></param> /// <param name="data"></param> /// <param name="absoluteexpiration"></param> public void set(string key, object data, datetimeoffset absoluteexpiration) { key = prefix + key; demoweb.memorycache.set(key, data, absoluteexpiration); if (!keys.contains(key)) { keys.add(key); } } /// <summary> /// 设置 /// </summary> /// <param name="key"></param> /// <param name="data"></param> /// <param name="absoluteexpirationrelativetonow"></param> public void set(string key, object data, timespan absoluteexpirationrelativetonow) { key = prefix + key; demoweb.memorycache.set(key, data, absoluteexpirationrelativetonow); if (!keys.contains(key)) { keys.add(key); } } /// <summary> /// 设置 /// </summary> /// <param name="key"></param> /// <param name="data"></param> /// <param name="expirationtoken"></param> public void set(string key, object data, ichangetoken expirationtoken) { key = prefix + key; demoweb.memorycache.set(key, data, expirationtoken); if (!keys.contains(key)) { keys.add(key); } } /// <summary> /// 设置 /// </summary> /// <param name="key"></param> /// <param name="data"></param> /// <param name="options"></param> public void set(string key, object data, memorycacheentryoptions options) { key = prefix + key; demoweb.memorycache.set(key, data, options); if (!keys.contains(key)) { keys.add(key); } } /// <summary> /// 移除某个 /// </summary> /// <param name="key"></param> public void remove(string key) { key = prefix + key; demoweb.memorycache.remove(key); if (keys.contains(key)) { keys.remove(key); } } /// <summary> /// 清空所有 /// </summary> public void clearall() { foreach (var key in keys) { demoweb.memorycache.remove(key); } keys.clear(); } }
其实接下来就可以直接使用缓存了,但为了方便使用,再建一个缓存类别的中间类来管理。
public class usercache { private static readonly memorycache cache = new memorycache("user"); private static timespan _timeout = timespan.zero; private static timespan timeout { get { if (_timeout != timespan.zero) return _timeout; try { _timeout = timespan.fromminutes(20); return _timeout; } catch (exception) { return timespan.fromminutes(10); } } } public static void set(string key,string cache) { if (string.isnullorempty(cache)) return; cache.set(key, cache, timeout); } public static string get(string key) { if (string.isnullorempty(key)) return default(string); return cache.get<string>(key); } }
测试是否可以正常使用:代码与截图
[httpget] [route("mecache")] public actionresult validtoken() { var key = "tkey"; usercache.set(key, "测试数据"); return succeed(usercache.get(key)); }
可以清楚的看到 memorycache 可以正常使用。
那么接下来将讲到如何使用 redis 缓存。先在需要封装基础类的项目 nuget 包中添加 stackexchange.redis 依赖。然后添加redis 连接类
internal class redisconnectionfactory { public string connectionstring { get; set; } public string password { get; set; } public connectionmultiplexer currentconnectionmultiplexer { get; set; } /// <summary> /// 设置连接字符串 /// </summary> /// <returns></returns> public void setconnectionstring(string connectionstring) { connectionstring = connectionstring; } /// <summary> /// 设置连接字符串 /// </summary> /// <returns></returns> public void setpassword(string password) { password = password; } public connectionmultiplexer getconnectionmultiplexer() { if (currentconnectionmultiplexer == null || !currentconnectionmultiplexer.isconnected) { if (currentconnectionmultiplexer != null) { currentconnectionmultiplexer.dispose(); } currentconnectionmultiplexer = getconnectionmultiplexer(connectionstring); } return currentconnectionmultiplexer; } private connectionmultiplexer getconnectionmultiplexer(string connectionstring) { connectionmultiplexer connectionmultiplexer; if (!string.isnullorwhitespace(password) && !connectionstring.tolower().contains("password")) { connectionstring += $",password={password}"; } var redisconfiguration = configurationoptions.parse(connectionstring); redisconfiguration.abortonconnectfail = true; redisconfiguration.allowadmin = false; redisconfiguration.connectretry = 5; redisconfiguration.connecttimeout = 3000; redisconfiguration.defaultdatabase = 0; redisconfiguration.keepalive = 20; redisconfiguration.synctimeout = 30 * 1000; redisconfiguration.ssl = false; connectionmultiplexer = connectionmultiplexer.connect(redisconfiguration); return connectionmultiplexer; } }
再添加redis客户端类
/// <summary> /// redis client /// </summary> public class redisclient : idisposable { public int defaultdatabase { get; set; } = 0; private readonly connectionmultiplexer _client; private idatabase _db; public redisclient(connectionmultiplexer client) { _client = client; usedatabase(); } public void usedatabase(int db = -1) { if (db == -1) db = defaultdatabase; _db = _client.getdatabase(db); } public string stringget(string key) { return _db.stringget(key).tostring(); } public void stringset(string key, string data) { _db.stringset(key, data); } public void stringset(string key, string data, timespan timeout) { _db.stringset(key, data, timeout); } public t get<t>(string key) { var json = stringget(key); if (string.isnullorempty(json)) { return default(t); } return json.tonettype<t>(); } public void set(string key, object data) { var json = data.tojson(); _db.stringset(key, json); } public void set(string key, object data, timespan timeout) { var json = data.tojson(); _db.stringset(key, json, timeout); } /// <summary> /// exist /// </summary> /// <param name="key"></param> /// <returns></returns> public bool exist(string key) { return _db.keyexists(key); } /// <summary> /// delete /// </summary> /// <param name="key"></param> /// <returns></returns> public bool delete(string key) { return _db.keydelete(key); } /// <summary> /// set expire to key /// </summary> /// <param name="key"></param> /// <param name="expiry"></param> /// <returns></returns> public bool expire(string key, timespan? expiry) { return _db.keyexpire(key, expiry); } /// <summary> /// 计数器 如果不存在则设置值,如果存在则添加值 如果key存在且类型不为long 则会异常 /// </summary> /// <param name="key"></param> /// <param name="value"></param> /// <param name="expiry">只有第一次设置有效期生效</param> /// <returns></returns> public long setstringincr(string key, long value = 1, timespan? expiry = null) { var nubmer = _db.stringincrement(key, value); if (nubmer == 1 && expiry != null)//只有第一次设置有效期(防止覆盖) _db.keyexpireasync(key, expiry);//设置有效期 return nubmer; } /// <summary> /// 读取计数器 /// </summary> /// <param name="key"></param> /// <returns></returns> public long getstringincr(string key) { var value = stringget(key); return string.isnullorwhitespace(value) ? 0 : long.parse(value); } /// <summary> /// 计数器-减少 如果不存在则设置值,如果存在则减少值 如果key存在且类型不为long 则会异常 /// </summary> /// <param name="key"></param> /// <returns></returns> public long stringdecrement(string key, long value = 1) { var nubmer = _db.stringdecrement(key, value); return nubmer; } public void dispose() { _client?.dispose(); } }
然后再添加redis连接生成工具类
public static class redisfactory { private static readonly object locker = new object(); private static redisconnectionfactory factory; private static void initredisconnection() { try { factory = new redisconnectionfactory(); var connectionstring = demoweb.configuration["redis:connectionstring"]; #if debug connectionstring = "127.0.0.1:6379"; #endif factory.connectionstring = connectionstring; factory.password = demoweb.configuration["redis:pwd"]; } catch (exception e) { loghelper.logger.fatal(e, "redis连接创建失败。"); } } public static redisclient getclient() { //先判断一轮,减少锁,提高效率 if (factory == null || string.isnullorempty(factory.connectionstring)) { //防止并发创建 lock (locker) { initredisconnection(); } } return new redisclient(factory.getconnectionmultiplexer()) { defaultdatabase = demoweb.configuration["redis:defaultdatabase"].toint() }; } }
这里要使用到前面的静态扩展方法。请自行添加 传送门 ,还需要将 startup 类中的 configuration 给赋值到 demoweb中的 configuration 字段值来使用。
在配置文件 appsettings.json 中添加
"redis": { "connectionstring": "127.0.0.1:6379", "pwd": "", "defaultdatabase": 0 }
再添加redis缓存使用类
/// <summary> /// redis缓存 /// </summary> public class rediscache { private static redisclient _client; private static redisclient client => _client ?? (_client = redisfactory.getclient()); private static string tokey(string key) { return $"cache_redis_{key}"; } /// <summary> /// 获取 /// </summary> /// <typeparam name="t"></typeparam> /// <param name="key"></param> /// <returns></returns> public static t get<t>(string key) { try { var rediskey = tokey(key); return client.get<t>(rediskey); } catch (exception e) { loghelper.logger.fatal(e, "rediscache.get \n key:{0}", key); return default(t); } } /// <summary> /// 尝试获取 /// </summary> /// <typeparam name="t"></typeparam> /// <param name="key"></param> /// <param name="result"></param> /// <returns></returns> private static t tryget<t>(string key, out bool result) { result = true; try { var rediskey = tokey(key); return client.get<t>(rediskey); } catch (exception e) { loghelper.logger.fatal(e, "rediscache.tryget \n key:{0}", key); result = false; return default(t); } } /// <summary> /// 获取 /// </summary> /// <typeparam name="t"></typeparam> /// <param name="key"></param> /// <param name="setfunc"></param> /// <param name="expiry"></param> /// <param name="resolver"></param> /// <returns></returns> public static t get<t>(string key, func<t> setfunc, timespan? expiry = null) { var rediskey = tokey(key); var result = tryget<t>(rediskey, out var success); if (success && result == null) { result = setfunc(); try { set(rediskey, result, expiry); } catch (exception e) { loghelper.logger.fatal(e, "rediscache.get<t> \n key:{0}", key); } } return result; } /// <summary> /// 设置 /// </summary> /// <typeparam name="t"></typeparam> /// <param name="key"></param> /// <param name="value"></param> /// <param name="expiry"></param> /// <returns></returns> public static bool set<t>(string key, t value, timespan? expiry = null) { var allrediskey = tokey("||keys||"); var rediskey = tokey(key); var allkeyredisvalue = client.stringget(allrediskey); var keys = allkeyredisvalue.tonettype<list<string>>() ?? new list<string>(); if (!keys.contains(rediskey)) { keys.add(rediskey); client.set(allrediskey, keys); } if (expiry.hasvalue) { client.stringset(rediskey, value.tojson(), expiry.value); } else { client.stringset(rediskey, value.tojson()); } return true; } /// <summary> /// 重新设置过期时间 /// </summary> /// <param name="key"></param> /// <param name="expiry"></param> public static void resetitemtimeout(string key, timespan expiry) { var rediskey = tokey(key); client.expire(rediskey, expiry); } /// <summary> /// exist /// </summary> /// <param name="key">原始key</param> /// <returns></returns> public static bool exist(string key) { var rediskey = tokey(key); return client.exist(rediskey); } /// <summary> /// 计数器 增加 能设置过期时间的都设置过期时间 /// </summary> /// <param name="key"></param> /// <param name="value"></param> /// <param name="expiry"></param> /// <returns></returns> public static bool setstringincr(string key, long value = 1, timespan? expiry = null, bool needrest0 = false) { var rediskey = tokey(key); try { if (expiry.hasvalue) { if (exist(key) && needrest0) { var exitvalue = getstringincr(key); client.setstringincr(rediskey, value - exitvalue, expiry.value); } else { client.setstringincr(rediskey, value, expiry.value); } } else { if (exist(key) && needrest0) { var exitvalue = getstringincr(key); client.setstringincr(rediskey, value - exitvalue); } else { client.setstringincr(rediskey, value); } } } catch (exception e) { loghelper.logger.fatal($"计数器-增加错误,原因:{e.message}"); return false; } return true; } /// <summary> /// 读取计数器 /// </summary> /// <param name="key"></param> /// <returns></returns> public static long getstringincr(string key) { var rediskey = tokey(key); return client.getstringincr(rediskey); } /// <summary> /// 计数器 - 减少 /// </summary> /// <param name="key"></param> /// <returns></returns> public static bool stringdecrement(string key, long value = 1) { var rediskey = tokey(key); try { client.stringdecrement(rediskey, value); return true; } catch (exception e) { loghelper.logger.fatal($"计数器-减少错误,原因:{e.message}"); return false; } } /// <summary> /// 删除 /// </summary> /// <param name="key"></param> /// <returns></returns> public static bool delete(string key) { var rediskey = tokey(key); return client.delete(rediskey); } /// <summary> /// 清空 /// </summary> public static void clear() { //因为codis不支持keys之类的命令,所以只能自己记录下来,然后通过这个来清理。 var rediskey = tokey("||keys||"); var keys = client.get<list<string>>(rediskey); var notexists = new list<string>(); foreach (var key in keys) { if (client.exist(key)) client.delete(key); else notexists.add(key); } if (notexists.count > 0) { keys.removeall(s => notexists.contains(s)); client.set(rediskey, keys); } } }
到这来基本就快可以拿来测试是否可以用了。但是前提是得把 redis 给运行起来。
将上面的 redis安装包安装,并启动所安装文件夹中的 redis-server.exe 程序,若出现闪退情况,就运行 redis-cli.exe 程序,然后输入 shutdown 按下回车,重新运行 redis-server.exe 程序,就会出现这个界面。
到这来,添加一个测试方法来看看效果。借助redis可视化工具查看结果如下
测试完美成功,由于时间问题,上面 rediscache只有字符串的方法,没有加其它类型的方法。有需要的自己加咯~
在下一篇中将介绍如何在netcore中如何使用 过滤器来进行权限验证
有需要源码的在下方评论或私信~给我的svn访客账户密码下载,代码未放在github上。svn中新加上了redis安装包及可视化工具。
推荐阅读
-
从零开始搭建前后端分离的NetCore2.2(EF Core CodeFirst+Autofac)+Vue的项目框架之七使用JWT生成Token(个人见解)
-
从零开始搭建前后端分离的NetCore2.2(EF Core CodeFirst+Autofac)+Vue的项目框架之十一Swagger使用一
-
从零开始搭建前后端分离的NetCore2.2(EF Core CodeFirst+Autofac)+Vue的项目框架之十数据库基础方法的封装
-
从零开始搭建前后端分离的NetCore2.2(EF Core CodeFirst+Autofac)+Vue的项目框架之九如何进行用户权限控制
-
从零开始搭建前后端分离的NetCore2.2(EF Core CodeFirst+Autofac)+Vue的项目框架之六使用过滤器进行全局请求数据验证
-
从零开始搭建前后端分离的NetCore2.2(EF Core CodeFirst+Autofac)+Vue的项目框架之八MemoryCache与redis缓存的使用
-
从零开始搭建前后端分离的NetCore2.2(EF Core CodeFirst+Autofac)+Vue的项目框架之十一Swagger使用一
-
从零开始搭建前后端分离的NetCore2.2(EF Core CodeFirst+Autofac)+Vue的项目框架之七使用JWT生成Token(个人见解)
-
从零开始搭建前后端分离的NetCore2.2(EF Core CodeFirst+Autofac)+Vue的项目框架之十二Swagger(参数)使用二
-
从零开始搭建前后端分离的NetCore2.2(EF Core CodeFirst+Autofac)+Vue的项目框架之十数据库基础方法的封装