本章主要说一下Redis
- Redis操作优化
一.基础类的配置工作
1.我想信许多人(许多neter人)操作redis依然用的是StackExchange.Redis,这个neget包,并没有用国内现在一些大佬们推出了包
RedisOptions主要是redis连接的一个配置类
实现代码如下:
public class RedisOptions { ////// 数据库地址 /// public string RedisHost { get; set; } /// /// 数据库用户名 /// public string RedisName { get; set; } /// /// 数据库密码 /// public string RedisPass { get; set; } /// /// 库 /// public int RedisIndex { get; set; } }
RedisServiceExtensions,算是redis操作的核心类,主要封装了redis的crud以及sub,pub
public static class RedisServiceExtensions { #region 初始化参数 private static readonly int _DefulatTime = 600; //默认有效期 private static RedisOptions config; private static ConnectionMultiplexer connection; private static IDatabase _db; private static ISubscriber _sub; #endregion public static IServiceCollection AddRedisCacheService( this IServiceCollection serviceCollection, FuncredisOptions = null ) { var _redisOptions = new RedisOptions(); _redisOptions = redisOptions?.Invoke(_redisOptions) ?? _redisOptions; config = _redisOptions; connection = ConnectionMultiplexer.Connect(GetSystemOptions()); _db = connection.GetDatabase(config.RedisIndex); _sub = connection.GetSubscriber(); return serviceCollection; } #region 系统配置 /// /// 获取系统配置 /// /// private static ConfigurationOptions GetSystemOptions() { var options = new ConfigurationOptions { AbortOnConnectFail = false, AllowAdmin = true, ConnectRetry = 10, ConnectTimeout = 5000, KeepAlive = 30, SyncTimeout = 5000, EndPoints = { config.RedisHost }, ServiceName = config.RedisName, }; if (!string.IsNullOrWhiteSpace(config.RedisPass)) { options.Password = config.RedisPass; } return options; } #endregion //============ #region 获取缓存 /// /// 读取缓存 /// /// 键 /// 数据类型/NULL public static object Get(string key) { return Get<object>(key); } /// /// 读取缓存 /// /// 泛型 /// 键 /// 数据类型/NULL public static T Get (string key) { var value = _db.StringGet(key); return (value.IsNull ? default(T) : JsonTo (value).Value); } #endregion #region 异步获取缓存 /// /// 异步读取缓存 /// /// 键 /// object/NULL public static async Task<object> GetAsync(string key) { return await GetAsync<object>(key); } /// /// 异步读取缓存 /// /// 泛型 /// 键 /// 数据类型/NULL public static async Task GetAsync (string key) { var value = await _db.StringGetAsync(key); return (value.IsNull ? default(T) : JsonTo (value).Value); } #endregion #region 同步转异步添加[I/O密集] /// /// 添加缓存 /// /// 键 /// 数据 /// 是否永久保存[true:是,false:保存10分钟] public static bool Insert(string key, object data, bool never = false) { return InsertAsync(key, data, never).Result; } /// /// 添加缓存 /// /// 泛型 /// 键 /// 数据 /// 是否永久保存[true:是,false:保存10分钟] /// 添加结果 public static bool Insert (string key, T data, bool never = false) { return InsertAsync (key, data, never).Result; } /// /// 添加缓存 /// /// 键 /// 数据 /// 保存时间[单位:秒] /// 添加结果 public static bool Insert(string key, object data, int time) { return InsertAsync(key, data, time).Result; } /// /// 添加缓存 /// /// 泛型 /// 键 /// 数据 /// 保存时间[单位:秒] /// 添加结果 public static bool Insert (string key, T data, int time) { return InsertAsync (key, data, time).Result; } /// /// 添加缓存 /// /// 键 /// 数据 /// 缓存时间 /// 添加结果 public static bool Insert(string key, object data, DateTime cachetime) { return InsertAsync(key, data, cachetime).Result; } /// /// 添加缓存 /// /// 泛型 /// 键 /// 数据 /// 缓存时间 /// 添加结果 public static bool Insert (string key, T data, DateTime cachetime) { return InsertAsync (key, data, cachetime).Result; } #endregion #region 异步添加 /// /// 添加缓存[异步] /// /// 键 /// 数据 /// 是否永久保存[true:是,false:保存10分钟] /// 添加结果 public static async Task<bool> InsertAsync(string key, object data, bool never = false) { return await _db.StringSetAsync(key, ToJson(data), (never ? null : new TimeSpan?(TimeSpan.FromSeconds(_DefulatTime)))); } /// /// 添加缓存[异步] /// /// 泛型 /// 键 /// 数据 /// 是否永久保存[true:是,false:保存10分钟] /// 添加结果 public static async Task<bool> InsertAsync (string key, T data, bool never = false) { return await _db.StringSetAsync(key, ToJson (data), (never ? null : new TimeSpan?(TimeSpan.FromSeconds(_DefulatTime)))); } /// /// 添加缓存[异步] /// /// 键 /// 数据 /// 保存时间[单位:秒] /// 添加结果 public static async Task<bool> InsertAsync(string key, object data, int time) { return await _db.StringSetAsync(key, ToJson(data), new TimeSpan?(TimeSpan.FromSeconds(time))); } /// /// 添加缓存[异步] /// /// 泛型 /// 键 /// 数据 /// 保存时间[单位:秒] /// 添加结果 public static async Task<bool> InsertAsync (string key, T data, int time) { return await _db.StringSetAsync(key, ToJson (data), new TimeSpan?(TimeSpan.FromSeconds(time))); } /// /// 添加缓存[异步] /// /// 键 /// 数据 /// 缓存时间 /// 添加结果 public static async Task<bool> InsertAsync(string key, object data, DateTime cachetime) { return await _db.StringSetAsync(key, ToJson(data), new TimeSpan?(cachetime - DateTime.Now)); } /// /// 添加缓存[异步] /// /// 泛型 /// 键 /// 数据 /// 缓存时间 /// 添加结果 public static async Task<bool> InsertAsync (string key, T data, DateTime cachetime) { return await _db.StringSetAsync(key, ToJson (data), new TimeSpan?(cachetime - DateTime.Now)); } #endregion #region 验证缓存 /// /// 验证缓存是否存在 /// /// 键 /// 验证结果 public static bool Exists(string key) { return _db.KeyExists(key); } #endregion #region 异步验证缓存 /// /// 验证缓存是否存在 /// /// 键 /// 验证结果 public static async Task<bool> ExistsAsync(string key) { return await _db.KeyExistsAsync(key); } #endregion #region 移除缓存 /// /// 移除缓存 /// /// 键 /// 移除结果 public static bool Remove(string key) { return _db.KeyDelete(key); } #endregion #region 异步移除缓存 /// /// 移除缓存 /// /// 键 /// 移除结果 public static async Task<bool> RemoveAsync(string key) { return await _db.KeyDeleteAsync(key); } #endregion #region 队列发布 /// /// 队列发布 /// /// 通道名 /// 数据 /// 是否有消费者接收 public static bool Publish(Models.RedisChannels Key, object data) { return _sub.Publish(Key.ToString(), ToJson(data)) > 0 ? true : false; } #endregion #region 队列接收 /// /// 注册通道并执行对应方法 /// /// 数据类型 /// 通道名 /// 方法 public static void Subscribe (Models.RedisChannels Key, DoSub doSub) where T : class { var _subscribe = connection.GetSubscriber(); _subscribe.Subscribe(Key.ToString(), delegate (RedisChannel channel, RedisValue message) { T t = Recieve (message); doSub(t); }); } #endregion #region 退订队列通道 /// /// 退订队列通道 /// /// 通道名 public static void UnSubscribe(Models.RedisChannels Key) { _sub.Unsubscribe(Key.ToString()); } #endregion #region 数据转换 /// /// JSON转换配置文件 /// private static JsonSerializerSettings _jsoncfg = new JsonSerializerSettings { ReferenceLoopHandling = ReferenceLoopHandling.Ignore, NullValueHandling = NullValueHandling.Ignore, Formatting = Formatting.None }; /// /// 封装模型转换为字符串进行存储 /// /// 值 /// private static string ToJson(object value) { return ToJson<object>(value); } /// /// 封装模型转换为字符串进行存储 /// /// 泛型 /// 值 /// private static string ToJson (T value) { return JsonConvert.SerializeObject(new Models.RedisData { Value = value }, _jsoncfg); } /// /// 缓存字符串转为封装模型 /// /// /// /// private static Models.RedisData JsonTo (string value) { return JsonConvert.DeserializeObject >(value, _jsoncfg); } private static T Recieve (string cachevalue) { T result = default(T); bool flag = !string.IsNullOrWhiteSpace(cachevalue); if (flag) { var cacheObject = JsonConvert.DeserializeObject >(cachevalue, _jsoncfg); result = cacheObject.Value; } return result; } #endregion #region 方法委托 /// /// 委托执行方法 /// /// public delegate void DoSub(object d); #endregion }
二.在starup里注入
AddRedisCacheService是我在RedisServiceExtensions里放的拓展方法,这里用来注入redis的配置,RedisOptionKey是我在预编译变量里放置的key,
对应appsetting.json里配置文件的key
如图:
这里我们通过上一章的ConfigLocator很轻松的就拿到了redisOptions强类型的值
到这里我们基本配置就完成,再说明一下,redisconfig的配置,redisName代表用户名,redisHost代表链接库地址,redisPass代表密码
reisIndex代表库名
三.初级测试
测试代码如下:
public IActionResult Index() { var key = "Test_Redis_Key"; var result= RedisServiceExtensions.Insert(key,"good"); var value = RedisServiceExtensions.Get(key); return View(); }
测试结果:
result=true表示写入成功
value=good恰好是我们刚才写的good
初级对reids的测试就是这么简单,客户端连接数据库我就不演示了
四.redis高级测试
高级测试我主要测试pub和sub并且要给大家演示出timeout那个问题,我争取将其复现了!
首先强调一点pub和sub是有通道的,通道大家不陌生吧!
如图:
程序启动,我们先订阅一个通道,本此测试我订阅两个通道,根据有说服力!
subscribe是一个泛型方法,泛型约束的是执行方法的参数类型,有两个入参,一个是通道名称,一个是要执行的委托方法
代码如下:注意我这里将其做成拓展方法,并且开另一个线程执行
////// 注册通道并执行对应方法 /// /// 数据类型 /// /// 通道名 /// 方法 public static IServiceCollection Subscribe (this IServiceCollection serviceCollection,Models.RedisChannels Key, DoSub doSub) where T : class { Task.Run(() => { var _subscribe = connection.GetSubscriber(); _subscribe.Subscribe(Key.ToString(), delegate (RedisChannel channel, RedisValue message) { T t = Recieve (message); doSub(t); }); }); return serviceCollection; }
RedisService.SubscribeDoSomething,RedisService.MemberChannel_SubscribeDoSomething是两个委托方法,我给第一个通道pub,他就执行一次SubscribeDoSomething
给第二个通道pub,他就执行一次MemberChannel_SubscribeDoSomething
这两个方法实现如下:
public class RedisService { public static void SubscribeDoSomething(object query) { int num = 0; Log4Net.Info($"TestPubSub_通道订阅_{num}"); num += 1; } public static void MemberChannel_SubscribeDoSomething(object query) { query= query as string; int num = 0; Log4Net.Info($"MemberChannel_SubscribeDoSomething_{query}_{num}"); num += 1; } }
接下来我还是在控制器里调用,进行pub
public IActionResult Index() { //发布TestPubSub var result = RedisServiceExtensions.Publish( Infrastructrue.Caches.Redis.Models.RedisChannels.TestPubSub,null); //发布MemberRegister var result2 = RedisServiceExtensions.Publish(Infrastructrue.Caches.Redis.Models.RedisChannels.MemberRegister, "哥就是哥_不一样的烟火..."); return View(); }
测试结果:
日志上成功记录下来,也就是说,pub出去的东西,sub到,然后执行写了日志!
接下来我让控制器循环执行100次pub,我们让第二个通道的pub100次,第一个通道就pub一次
代码如下:
public IActionResult Index() { //发布TestPubSub var result = RedisServiceExtensions.Publish(Infrastructrue.Caches.Redis.Models.RedisChannels.TestPubSub, null); for (int i = 0; i < 100; i++) { //发布MemberRegister var result2 = RedisServiceExtensions.Publish(Infrastructrue.Caches.Redis.Models.RedisChannels.MemberRegister, "哥就是哥_不一样的烟火..."); } return View(); }
哈哈,太happy了,一次把我要说的那个问题------------timeout的问题测出来了!
如图:
详细信息如下:遇到的肯定是这个问题想都不用想
Timeout performing PUBLISH MemberRegister (5000ms), inst: 24, qs: 0, in: 0, serverEndpoint: 39.107.202.142:6379, mgr: 9 of 10 available, clientName: GY, IOCP: (Busy=0,Free=1000,Min=4,Max=1000), WORKER: (Busy=5,Free=32762,Min=4,Max=32767), v: 2.0.513.63329 (Please take a look at this article for some common client-side issues that can cause timeouts: https://stackexchange.github.io/StackExchange.Redis/Timeouts)
在看一下我异常处理中间件拦截下来的用Log4net记录的日志
这个问题的根本原因在于我们配置的redis默认等待时间,今天早上我修改了一些,断定问题出现在连接时间上connectTime,sysncTimeOut
//刚刚对这里又做了优化,彻底测出了timeout那个问题,另外配置完全放到了json里,通过强类型model,在拓展里配置
redisOption修改成如下:
public class RedisOptions { ////// 数据库地址 /// public string RedisHost { get; set; } /// /// 数据库用户名 /// public string RedisName { get; set; } /// /// 数据库密码 /// public string RedisPass { get; set; } /// /// 库 /// public int RedisIndex { get; set; } /// /// 异步连接等待时间 /// public int ConnectTimeout { get; set; } = 600; /// /// 同步连接等待时间 /// public int SyncTimeout { get; set; } = 600; /// /// 最大连接数 /// public int KeepAlive { get; set; } = 30; /// /// 连接重试次数 /// public int ConnectRetry { get; set; } = 10; /// /// 获取或设置是否应显式通知连接/配置超时通过TimeoutException /// public bool AbortOnConnectFail { get; set; } = true; /// /// 是否允许管理员操作 /// public bool AllowAdmin { get; set; } = true; }
其余没有什么大的变化!
最后想说,StackExchange.Redis 这个包没什么问题,我依然推荐用这个包,至少目前我真没有发现无法解决的问题!!!
我现在用的是等待二十秒不行,如果改才600等待10分钟,你的timeout应该就不会出现了!(如有不对请斧正)
这一篇就这样吧,下一篇我会唠唠这个系统的权限管理模块的实现
- 下章管理系统模块实现