从零开始搭建前后端分离的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 ConfigureServices(IServiceCollection services) { services.AddMemoryCache(); //.... 省略部分代码 }
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中如何使用 过滤器来进行权限验证
有需要源码的可通过此 GitHub 链接拉取 觉得还可以的给个 start 哦,谢谢!svn中新加上了Redis安装包及可视化工具。
学习本是一个不断模仿、练习、创新、超越的过程。 由于博主能力有限,文中可能存在描述不正确,欢迎指正、补充! 感谢您的阅读,麻烦动动手指点个推荐哟。
-------------------------------------------------------------------------------------------------------------------------------------------------------------