Redis实战:用缓存为数据库减负(二)
上一篇记录了redis 的搭建、配置、服务注册 , 本篇来记录将redis 接入项目中的部分业务,在业务快速迭代过程中,数据库连接数和磁盘 IO 逐渐成为性能瓶颈, 并发高峰时,连接池耗尽导致请求排队,以及重复的数据库查询把磁盘 IO 打满,RT 飙高。因此引入 Redis 作为“高速缓存层”,目标是:
-
减少数据库连接数——把热点查询拦截在缓存层;
-
降低磁盘 IO——把读操作从磁盘搬到内存;
-
提升吞吐量——单实例 Redis QPS ≈ 10 万,远高于 MySQL。
第一步, 将redis 的nuget包 安装到项目中,我这里选择的时公共层Comm,方便后续在redis帮助类中书写基本方法

第二步,编写基础的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

本文来自博客园,作者:郎中令,世人皆大笑,举手揶揄之,文未佳,却己创,转载请注明原文链接:https://www.cnblogs.com/Sientuo/p/19072089

浙公网安备 33010602011771号