当前位置: 首页 > news >正文

C#+Redis,如何有效防止缓存雪崩、穿透和击穿问题

引言

在C#应用程序开发中,Redis作为高性能的分布式缓存系统,被广泛用于提升应用性能、减轻数据库压力。然而,在享受Redis带来便利的同时,我们也需要面对缓存穿透、缓存雪崩和缓存击穿这三大经典问题。这些问题如果处理不当,可能会导致数据库压力骤增,甚至系统崩溃。本文将详细解析这三大问题的产生原因,并提供C#环境下的具体解决方案和代码实例。

缓存穿透

产生原因

缓存穿透是指查询一个缓存中不存在且数据库中也不存在的数据,导致每次请求都直接访问数据库的行为。这种情况会使缓存完全失效,所有请求都绕过缓存直接打到数据库上,可能导致数据库压力过大甚至宕机。

典型场景:

  • 恶意攻击:频繁请求不存在的ID(如负数ID)

  • 业务逻辑缺陷:未对输入参数进行有效校验

  • 误操作:查询了已被删除的数据

解决方案

1. 缓存空值

当查询一个不存在的数据时,将空值(如 null 或空字符串)缓存起来,并设置一个较短的过期时间。这样下次再查询相同数据时,可以直接从缓存中获取空值,而不需要再访问数据库。

C#代码实例:

public async Task<T> GetDataWithCacheNullAsync<T>(string key, Func<Task<T>> dbQueryFunc, TimeSpan nullValueExpireTime, TimeSpan normalExpireTime) { // 尝试从缓存获取数据 var cacheValue = await _redisDb.StringGetAsync(key);

if (!cacheValue.IsNullOrEmpty())
{// 缓存命中if (cacheValue == "NULL_VALUE"){// 空值缓存return default;}return JsonConvert.DeserializeObject<T>(cacheValue);
}
​
// 缓存未命中,查询数据库
var data = await dbQueryFunc();
​
if (data == null)
{// 数据库中也不存在,缓存空值await _redisDb.StringSetAsync(key, "NULL_VALUE", nullValueExpireTime);
}
else
{// 数据库中存在,缓存真实值await _redisDb.StringSetAsync(key, JsonConvert.SerializeObject(data), normalExpireTime);
}
​
return data;
}
2.布隆过滤器

在缓存层前加一层布隆过滤器,用于判断数据是否存在。如果布隆过滤器判断数据不存在,就直接返回,不需要查询缓存和数据库。

C#代码实例(使用StackExchange.Redis.Bloom):

public class BloomFilterService
{private readonly IDatabase _redisDb;private readonly string _filterKey = "bloom_filter";private readonly int _expectedElements = 100000;private readonly double _falsePositiveRate = 0.01;
​public BloomFilterService(IConnectionMultiplexer redis){_redisDb = redis.GetDatabase();// 初始化布隆过滤器_redisDb.Execute("BF.RESERVE", _filterKey, _falsePositiveRate, _expectedElements);}
​public async Task AddAsync(string key){await _redisDb.ExecuteAsync("BF.ADD", _filterKey, key);}
​public async Task<bool> ExistsAsync(string key){return (bool)await _redisDb.ExecuteAsync("BF.EXISTS", _filterKey, key);}
}
​
// 使用布隆过滤器防止缓存穿透
public async Task<T> GetDataWithBloomFilterAsync<T>(string key, Func<Task<T>> dbQueryFunc, TimeSpan expireTime)
{// 先检查布隆过滤器if (!await _bloomFilter.ExistsAsync(key)){return default;}
​// 尝试从缓存获取数据var cacheValue = await _redisDb.StringGetAsync(key);
​if (!cacheValue.IsNullOrEmpty()){return JsonConvert.DeserializeObject<T>(cacheValue);}
​// 缓存未命中,查询数据库var data = await dbQueryFunc();
​if (data != null){// 缓存真实值await _redisDb.StringSetAsync(key, JsonConvert.SerializeObject(data), expireTime);}
​return data;
}

缓存雪崩

产生原因

缓存雪崩是指在某个时间点,缓存中的数据大面积同时失效(或缓存服务不可用),导致大量请求直接打到数据库,从而引发数据库压力骤增,甚至崩溃的现象。

典型场景:

  • 大量缓存数据设置了相同的过期时间

  • Redis服务宕机

  • 网络故障导致缓存不可用

解决方案

1.错峰过期

给不同的缓存数据设置不同的过期时间,避免集中过期。可以在基础过期时间上增加一个随机值。

public async Task SetDataWithRandomExpireAsync<T>(string key, T data, TimeSpan baseExpireTime)
{// 生成随机偏移量(0-300秒)var random = new Random();int offset = random.Next(0, 300);TimeSpan actualExpireTime = baseExpireTime.Add(TimeSpan.FromSeconds(offset));
​await _redisDb.StringSetAsync(key, JsonConvert.SerializeObject(data), actualExpireTime);
}
2.Redis高可用集群

通过主从复制、哨兵模式或Redis Cluster等方式构建高可用集群,避免单点故障。

C#代码实例(使用StackExchange.Redis连接哨兵模式):

public class RedisSentinelService
{private readonly ConnectionMultiplexer _connection;
​public RedisSentinelService(){var options = ConfigurationOptions.Parse("sentinel1:26379,sentinel2:26379,sentinel3:26379");options.ServiceName = "mymaster";options.AllowAdmin = true;options.ConnectRetry = 3;
​_connection = ConnectionMultiplexer.Connect(options);}
​public IDatabase GetDatabase(){return _connection.GetDatabase();}
}

缓存击穿

产生原因

缓存击穿是指某个热点数据过期的瞬间,有大量并发请求同时访问该数据,导致这些请求直接打到数据库上的现象。

典型场景:

  • 热门商品详情页缓存过期

  • 秒杀活动中核心商品信息缓存失效

  • 热点新闻数据缓存过期

解决方案

1. 互斥锁

只允许一个线程重建缓存,其他线程等待。

C#代码实例:

public async Task<T> GetDataWithMutexLockAsync<T>(string key, Func<Task<T>> dbQueryFunc, TimeSpan expireTime)
{// 尝试从缓存获取数据var cacheValue = await _redisDb.StringGetAsync(key);
​if (!cacheValue.IsNullOrEmpty()){return JsonConvert.DeserializeObject<T>(cacheValue);}
​// 缓存未命中,尝试获取锁string lockKey = $"lock:{key}";bool lockAcquired = false;
​try{// 尝试获取锁,过期时间10秒lockAcquired = await _redisDb.LockTakeAsync(lockKey, Environment.MachineName, TimeSpan.FromSeconds(10));
​if (lockAcquired){// 获取锁成功,查询数据库var data = await dbQueryFunc();
​if (data != null){// 缓存真实值await _redisDb.StringSetAsync(key, JsonConvert.SerializeObject(data), expireTime);}
​return data;}else{// 获取锁失败,等待100ms后重试await Task.Delay(100);return await GetDataWithMutexLockAsync(key, dbQueryFunc, expireTime);}}finally{// 释放锁if (lockAcquired){await _redisDb.LockReleaseAsync(lockKey, Environment.MachineName);}}
}
2.逻辑过期

将过期时间存储在缓存数据中,而不是依赖Redis的过期机制。当数据逻辑过期时,异步更新缓存,不阻塞当前请求。

C#代码实例:

public class CacheDataWithLogicalExpire<T>
{public T Data { get; set; }public DateTime ExpireTime { get; set; }
}
​
public async Task<T> GetDataWithLogicalExpireAsync<T>(string key, Func<Task<T>> dbQueryFunc, TimeSpan expireTime)
{// 尝试从缓存获取数据var cacheValue = await _redisDb.StringGetAsync(key);
​if (cacheValue.IsNullOrEmpty()){return default;}
​// 反序列化缓存数据var cacheData = JsonConvert.DeserializeObject<CacheDataWithLogicalExpire<T>>(cacheValue);
​if (cacheData.ExpireTime > DateTime.Now){// 数据未过期,直接返回return cacheData.Data;}
​// 数据已过期,异步更新缓存_ = Task.Run(async () =>{string lockKey = $"lock:{key}";bool lockAcquired = await _redisDb.LockTakeAsync(lockKey, Environment.MachineName, TimeSpan.FromSeconds(10));
​if (lockAcquired){try{// 双重检查var currentCacheValue = await _redisDb.StringGetAsync(key);if (!currentCacheValue.IsNullOrEmpty()){var currentCacheData = JsonConvert.DeserializeObject<CacheDataWithLogicalExpire<T>>(currentCacheValue);if (currentCacheData.ExpireTime > DateTime.Now){return;}}
​// 查询数据库并更新缓存var data = await dbQueryFunc();if (data != null){var newCacheData = new CacheDataWithLogicalExpire<T>{Data = data,ExpireTime = DateTime.Now.Add(expireTime)};await _redisDb.StringSetAsync(key, JsonConvert.SerializeObject(newCacheData));}}finally{await _redisDb.LockReleaseAsync(lockKey, Environment.MachineName);}}});
​// 返回过期数据,不阻塞当前请求return cacheData.Data;
}

总结

缓存穿透、雪崩和击穿是Redis使用过程中常见的三大问题,它们虽然表现形式不同,但核心都是导致数据库压力过大。在C#应用中,我们可以通过以下方式有效解决这些问题:

在实际项目中,我们需要根据具体场景选择合适的解决方案。例如,对于恶意攻击导致的缓存穿透,布隆过滤器是更好的选择;对于热点数据的缓存击穿,逻辑过期可以提供更好的性能。同时,我们还需要结合监控和报警机制,及时发现和处理缓存问题,确保系统的稳定性和高性能。

  • 缓存穿透 :缓存空值或使用布隆过滤器

  • 缓存雪崩 :错峰过期或构建Redis高可用集群

  • 缓存击穿 :使用互斥锁或逻辑过期

http://www.lryc.cn/news/620242.html

相关文章:

  • 联网车辆功能安全和网络安全的挑战与当前解决方案
  • OpenBMC中的BMCWeb:架构、原理与应用全解析
  • 直播美颜SDK开发实战:高性能人脸美型的架构与实现
  • C++调试革命:时间旅行调试实战指南
  • 图像优化:使用 Next.js 的 Image 组件
  • h5bench(4)
  • linux 内核 - 内存管理概念
  • Linux 服务部署:自签 CA 证书构建 HTTPS 及动态 Web 集成
  • GO学习记录四——读取excel完成数据库建表
  • [AXI5]AXI协议中awsize和awlen在Vector Atomic地址膨胀中的作用
  • Vue3从入门到精通: 3.5 Vue3与TypeScript集成深度解析
  • FPGA的PS基础1
  • 力扣(O(1) 时间插入、删除和获取随机元素)
  • 热门手机机型重启速度对比
  • 以鼠标位置为中心进行滚动缩放
  • 力扣top100(day02-03)--链表03
  • 修复运动模糊的视频用什么软件?快速解决方案分享
  • ECCV-2018《Variational Wasserstein Clustering》
  • AI工程化闭环法(AIEC – AI Engineering Cycle) 适合TRAE CURSOR CLAUDE等工具
  • Transformer 之自注意力机制(一)
  • TF-IDF------词向量转化:从“文字”到“向量”
  • 可视化调试LangChain SQLChatMessageHistory:SQLite数据库查看全攻略
  • Java多线程进阶-从乐观锁到读写锁
  • 西门子TIA-SCL转STL指令项目案例及技巧
  • 【Python】Python 函数基本介绍(详细版)​
  • ARM 实操 流水灯 按键控制 day53
  • ACL 可以限制哪些流量?入方向和出方向怎么判断?
  • vue路由_router
  • rk3588 ubuntu20.04安装包经常出现的问题总结(chatgpt回复)
  • C++ 优选算法 力扣 209.长度最小的子数组 滑动窗口 (同向双指针)优化 每日一题 详细题解