Redis实战:用缓存为数据库减负(二)

     上一篇记录了redis 的搭建、配置、服务注册  , 本篇来记录将redis 接入项目中的部分业务,在业务快速迭代过程中,数据库连接数和磁盘 IO 逐渐成为性能瓶颈,   并发高峰时,连接池耗尽导致请求排队,以及重复的数据库查询把磁盘 IO 打满,RT 飙高。因此引入 Redis 作为“高速缓存层”,目标是:

  1. 减少数据库连接数——把热点查询拦截在缓存层;
  2. 降低磁盘 IO——把读操作从磁盘搬到内存;
  3. 提升吞吐量——单实例 Redis QPS ≈ 10 万,远高于 MySQL。

第一步, 将redis 的nuget包 安装到项目中,我这里选择的时公共层Comm,方便后续在redis帮助类中书写基本方法

image

 第二步,编写基础的redis帮助类,如果是单机,直接食用即可,支持重连,空数据、连接失败、异常都会返回null,业务只需要根据是否为空来判断即可,不会阻塞原有业务,如果项目部署采用是分布式,要考虑到集群或者哨兵模式

 public static class APIRedisHelper
 {
     #region 私有成员
     // 连接字符串(保持你的原逻辑)
     private static readonly Lazy<ConnectionMultiplexer?> _connLazy = new(() =>
     {
         try
         {
             return ConnectionMultiplexer.Connect(GetConnectionString());
         }
         catch
         {
             // 首次连接失败后先返回 null
             return null;
         }
     });

     // volatile 保证可见性;用于重连后替换实例
     private static volatile ConnectionMultiplexer? _current;
     private static readonly SemaphoreSlim _lock = new(1, 1);

     private static IDatabase? Db => _current?.GetDatabase() ?? _connLazy.Value?.GetDatabase();

     public static TimeSpan ExpireTime = TimeSpan.FromDays(60);

     private static string? GetConnectionString()
     {
         return new ConfigurationBuilder()
                .SetBasePath(Directory.GetCurrentDirectory())
                .AddJsonFile("appsettings.json")
                .Build()["Connection:APIRedisConnectionString"]?.Trim();
     }

     /// <summary>
     /// 获取当前可用的 Database 实例,如果已断开则尝试重连一次。
     /// 整个重连过程是异步的,但调用方不会阻塞(快速失败)。
     /// </summary>
     private static async Task<IDatabase?> GetDbAsync()
     {
         var old = _current ?? _connLazy.Value;
         if (old is null || !old.IsConnected)
         {
             // 双重检查锁,确保只会有一个线程去重连
             await _lock.WaitAsync().ConfigureAwait(false);
             try
             {
                 // 再次判断,防止并发重连
                 var again = _current ?? _connLazy.Value;
                 if (again is null || !again.IsConnected)
                 {
                     again?.Dispose(); // 释放旧连接
                     try
                     {
                         var mux = await ConnectionMultiplexer.ConnectAsync(GetConnectionString()).ConfigureAwait(false);
                         _current = mux; // 覆盖实例
                     }
                     catch (Exception ex)
                     {
                         // 记录日志后仍返回 null(保持你的异常处理风格)
                         NLogHelper.WriteError($"Redis 重连失败: {ex.Message}-{ex.StackTrace}");
                         _current = null;
                     }
                 }
             }
             finally
             {
                 _lock.Release();
             }
         }
         return (_current ?? _connLazy.Value)?.GetDatabase();
     }

     private static async Task<T> SafeExecuteAsync<T>(Func<IDatabase, Task<T>> redisFunc, T defaultValue = default!)
     {
         try
         {
             var db = await GetDbAsync().ConfigureAwait(false);
             return db is null ? defaultValue : await redisFunc(db).ConfigureAwait(false);
         }
         catch (Exception ex)
         {
             NLogHelper.WriteError($"Redis操作失败: {ex.Message}-{ex.StackTrace}");
             return defaultValue;
         }
     }

     private static T SafeExecute<T>(Func<IDatabase, T> redisFunc, T defaultValue = default!)
     {
         try
         {
             return Db is null ? defaultValue : redisFunc(Db);
         }
         catch (Exception ex)
         {
             NLogHelper.WriteError($"Redis操作失败: {ex.Message}-{ex.StackTrace}");
             return defaultValue;
         }
     }
     #endregion

     #region 部分Key 命名
     public static string GetHTMKey(string key) => $"htm_{key}";
     public static string GetHTCKey(string key) => $"htc_{key}";
     public static string GetHTPKey(string key) => $"htp_{key}";
     public static string GetHTBKey(string key) => $"htb_{key}";
     #endregion

     #region 基础方法
     public static Task<bool> SetAsync(string key, string value, TimeSpan? expiry = null) =>
         SafeExecuteAsync(db => db.StringSetAsync(key, value, expiry ?? ExpireTime), false);

     public static Task<string?> GetAsync(string key) =>
         SafeExecuteAsync(async db => (string?)await db.StringGetAsync(key));

     public static Task<bool> DeleteAsync(string key) =>
         SafeExecuteAsync(db => db.KeyDeleteAsync(key), false);

     public static Task<bool> ExistsAsync(string key) =>
         SafeExecuteAsync(db => db.KeyExistsAsync(key), false);

     public static Task<bool> SetExpiryAsync(string key, TimeSpan expiry) =>
         SafeExecuteAsync(db => db.KeyExpireAsync(key, expiry), false);
     #endregion
 }

使用了redis之后,就要考虑到数据的强一致性,以及防止缓存被击穿,前者一般采用双删的方式来保证数据一致,后者我一般通过动态更新有效期来浅显的控制,当然也可以结合提前续期,以及增加互斥锁来防止进一步被击穿,主要看项目体量和薪资的多少来设计具体的哪种方案, 业务的简单调用如下

        private async Task<KiaserData> GetKiaserData(string addr)
        {
            try
            {
                var data = await APIRedisHelper.GetAsync($"{APIRedisHelper.GetHTMKey(addr)}");
                if (data == null)
                {
                    string str = GetKiaserData(addr);
                    var redis = GetHTMObject(str);
                    if (redis != null)
                    {
                        var task = Task.Factory.StartNew(async () =>
                        {
                            redis.Addr = addr;
                            await APIRedisHelper.SetAsync($"{APIRedisHelper.GetHTMKey(addr)}", JsonConvert.SerializeObject(redis));
                        });
                    }

                    return redis;
                }
                return JsonConvert.DeserializeObject<KiaserData>(data);
            }
            catch (Exception ex)
            {
                NLogHelper.WriteError($"获取Kiase数据请求异常,{addr}-{ex.Message}", ParamConst.KiaserHome);
            }
            return null;

        }

 这里小小的提供一个redis 的可视化工具,不是新的,只是个人用习惯了

Medis :https://github.com/sinajia/medis/releases/tag/win

image

 

posted @ 2025-09-03 17:45  郎中令  阅读(8)  评论(0)    收藏  举报