-
C#网络编程之分布式缓存(Redis、Memcached)
第38章 分布式缓存
38.1 分布式缓存(Redis、Memcached)
一、我踩过的缓存坑:从“Memcached雪崩导致DB崩溃”到“Redis缓存击穿丢数据”
做电商商品详情页时,一开始用Memcached做缓存,结果Memcached集群挂了,所有请求直接打MySQL,MySQL瞬间崩溃,系统瘫痪2小时;后来换成Redis,结果缓存过期时大量请求同时打MySQL,导致DB压力突增(缓存击穿),商品详情页加载慢;再后来又遇到缓存穿透,恶意用户请求不存在的商品ID,直接打MySQL,导致DB CPU飙升到100%。这节我把这些踩坑经验揉进去,用大白话讲透Redis和Memcached的核心原理,结合C#实战代码逐vb.net教程C#教程python教程SQL教程access 2010教程行讲解缓存读写、缓存问题的解决方案(击穿、雪崩、穿透),对比它们的优缺点和适用场景,以及分布式缓存的进阶问题(比如缓存一致性、淘汰策略、高可用),让你的缓存既高效又可靠。
二、分布式缓存核心需求:给数据库“减负”,让系统飞起来
分布式缓存是一种在分布式系统中存储热点数据的内存数据库,核心作用是:
1.减少数据库压力:热点数据从缓存取,不用每次都查数据库;
2.提升系统性能:内存读写速度是磁盘的1000倍,缓存能把接口响应时间从几百毫秒降到几毫秒;
3.数据一致性:缓存中的数据要和数据库中的数据保持一致,避免用户看到过期数据。
类比:分布式缓存就像超市的货架,把热门商品(热点数据)放在货架上,用户直接拿,不用每次都去仓库(数据库)找,提升购物效率;货架上的商品要定期更新,保证和仓库的商品一致。
三、Redis:功能丰富的分布式缓存,适合复杂场景
Redis是开源的内存数据库,核心优势是功能丰富、支持持久化、高可用、复杂数据结构,适合绝大多数分布式缓存场景(比如商品详情页、用户会话、分布式锁)。
核心原理(大白话)
1.内存存储:数据存在内存中,读写速度快;
2.持久化:支持RDB(快照)和AOF(日志)两种持久化方式,避免Redis重启后数据丢失;
3.数据结构:支持字符串、哈希、列表、集合、有序集合等5种基本数据结构,以及位图、HyperLogLog、地理空间等高级数据结构;
4.高可用:支持哨兵模式和集群模式,避免单点故障;
5.分布式:集群模式支持数据分片,存储TB级别的数据。
我踩过的坑:一开始用Redis的默认配置,没开持久化,结果Redis重启后缓存数据全丢,所有请求打MySQL,DB崩溃——生产环境必须开持久化!
实战1:Redis缓存读写与缓存问题解决方案(C#)
用StackExchange.Redis库实现Redis缓存读写,解决缓存击穿、雪崩、穿透问题。
步骤1:安装NuGet包
bash
Install-Package StackExchange.Redis
Install-Package BloomFilter.NetCore # 用于解决缓存穿透
步骤2:Redis缓存实现代码
csharp
using StackExchange.Redis;
using System;
using System.Threading.Tasks;
using BloomFilter;
namespace DistributedCache.Redis;
public class RedisCacheService
{
private readonly IDatabase _redisDb;
private readonly IBloomFilter<string> _bloomFilter; // 布隆过滤器,解决缓存穿透
public RedisCacheService(IConnectionMultiplexer redis)
{
_redisDb = redis.GetDatabase();
// 初始化布隆过滤器,预计存储100万条数据,误判率0.01%
_bloomFilter = new BloomFilter<string>(1000000, 0.0001);
// 预热布隆过滤器:把所有存在的商品ID加入(实际中可以从数据库批量导入)
PreheatBloomFilter();
}
/// <summary>
/// 预热布隆过滤器
/// </summary>
private void PreheatBloomFilter()
{
// 模拟100万条商品ID
for (int i = 1; i <= 1000000; i++)
{
_bloomFilter.Add($"product_{i}");
}
Console.WriteLine("布隆过滤器预热完成");
}
/// <summary>
/// 获取商品详情(解决缓存击穿、穿透问题)
/// </summary>
/// <param name="productId">商品ID</param>
/// <returns>商品详情</returns>
public async Task<string> GetProductDetailAsync(string productId)
{
// 1. 缓存穿透处理:用布隆过滤器判断商品ID是否存在,不存在直接返回null
if (!_bloomFilter.Contains(productId))
{
Console.WriteLine($"[RedisCache] 商品ID {productId} 不存在,拦截缓存穿透请求");
return null;
}
string cacheKey = $"product:detail:{productId}";
string cacheValue = await _redisDb.StringGetAsync(cacheKey);
// 2. 缓存命中,直接返回
if (!string.IsNullOrEmpty(cacheValue))
{
Console.WriteLine($"[RedisCache] 缓存命中:{cacheKey}");
return cacheValue;
}
// 3. 缓存击穿处理:用Redis互斥锁,只有一个线程去查数据库
string lockKey = $"lock:product:{productId}";
string lockValue = Guid.NewGuid().ToString("N");
bool lockAcquired = await _redisDb.StringSetAsync(lockKey, lockValue, TimeSpan.FromSeconds(5), When.NotExists);
if (!lockAcquired)
{
// 其他线程等待100ms后重试
await Task.Delay(100);
return await GetProductDetailAsync(productId);
}
try
{
// 4. 缓存未命中,查数据库(模拟)
string dbValue = await QueryProductFromDbAsync(productId);
if (string.IsNullOrEmpty(dbValue))
{
// 5. 数据库也不存在,缓存空值,设置短过期时间(比如5分钟),避免缓存穿透
await _redisDb.StringSetAsync(cacheKey, "", TimeSpan.FromMinutes(5));
Console.WriteLine($"[RedisCache] 数据库也不存在,缓存空值:{cacheKey}");
return null;
}
// 6. 缓存数据库查询结果,设置随机过期时间(比如30-60分钟),避免缓存雪崩
int expireMinutes = new Random().Next(30, 61);
await _redisDb.StringSetAsync(cacheKey, dbValue, TimeSpan.FromMinutes(expireMinutes));
Console.WriteLine($"[RedisCache] 缓存未命中,从数据库加载并缓存:{cacheKey},过期时间:{expireMinutes}分钟");
return dbValue;
}
finally
{
// 7. 释放互斥锁:用Lua脚本原子删除锁,避免误删别人的锁
string releaseScript = @"
if redis.call('GET', KEYS[1]) == ARGV[1] then
return redis.call('DEL', KEYS[1])
else
return 0
end";
await _redisDb.ScriptEvaluateAsync(releaseScript, new RedisKey[] { lockKey }, new RedisValue[] { lockValue });
}
}
/// <summary>
/// 更新商品详情(解决缓存一致性问题:写穿策略)
/// </summary>
/// <param name="productId">商品ID</param>
/// <param name="productDetail">商品详情</param>
public async Task UpdateProductDetailAsync(string productId, string productDetail)
{
// 1. 先更新数据库
await UpdateProductInDbAsync(productId, productDetail);
// 2. 再删除缓存(或更新缓存)
string cacheKey = $"product:detail:{productId}";
await _redisDb.KeyDeleteAsync(cacheKey);
Console.WriteLine($"[RedisCache] 更新商品详情,删除缓存:{cacheKey}");
}
/// <summary>
/// 模拟从数据库查询商品详情
/// </summary>
private async Task<string> QueryProductFromDbAsync(string productId)
{
// 模拟数据库查询耗时100ms
await Task.Delay(100);
// 模拟商品ID不存在的情况
if (productId == "product_9999999")
{
return null;
}
return $"商品详情:{productId},价格:99.9元,库存:1000件";
}
/// <summary>
/// 模拟更新数据库中的商品详情
/// </summary>
private async Task UpdateProductInDbAsync(string productId, string productDetail)
{
// 模拟数据库更新耗时50ms
await Task.Delay(50);
Console.WriteLine($"[RedisCache] 更新数据库中的商品详情:{productId}");
}
}
// 测试代码
class Program
{
static async Task Main(string[] args)
{
var redis = ConnectionMultiplexer.Connect("localhost:6379");
var cacheService = new RedisCacheService(redis);
// 测试缓存命中
string productId = "product_123";
string detail = await cacheService.GetProductDetailAsync(productId);
Console.WriteLine(detail);
// 测试缓存更新
await cacheService.UpdateProductDetailAsync(productId, "商品详情:product_123,价格:89.9元,库存:999件");
detail = await cacheService.GetProductDetailAsync(productId);
Console.WriteLine(detail);
// 测试缓存穿透
string invalidProductId = "product_9999999";
detail = await cacheService.GetProductDetailAsync(invalidProductId);
Console.WriteLine(detail);
}
}
代码逐行讲解:
1.缓存穿透处理:用布隆过滤器判断商品ID是否存在,不存在直接返回null,避免请求打MySQL;
2.缓存击穿处理:用Redis互斥锁,只有一个线程去查数据库,其他线程等待后重试,避免大量请求同时打MySQL;
3.缓存雪崩处理:设置随机过期时间(30-60分钟),避免所有缓存同时过期,导致大量请求打MySQL;
4.缓存一致性处理:用写穿策略,先更新数据库,再删除缓存,避免用户看到过期数据;
5.空值缓存:数据库中不存在的商品,缓存空值5分钟,避免恶意用户频繁请求不存在的商品ID;
6.布隆过滤器预热:把所有存在的商品ID加入布隆过滤器,实际中可以从数据库批量导入。
我踩过的坑:一开始用更新缓存的方式,结果两个线程同时更新数据库和缓存,导致缓存中的数据和数据库中的数据不一致——应该用删除缓存的方式,下次请求时从数据库加载最新数据到缓存!
拓展知识:Redis常用功能
1.持久化机制:
1.RDB:定期生成内存快照,适合备份和灾难恢复,性能开销小;
2.AOF:记录每个写操作,适合数据完整性要求高的场景,性能开销大;
3.混合持久化:Redis 4.0+支持,结合RDB和AOF的优点,先写RDB,再写AOF日志;
2.高可用方案:
1.哨兵模式:一个主节点,多个从节点,哨兵监控主节点,主节点挂了自动切换到从节点;
2.集群模式:多个主节点,每个主节点负责一部分数据,支持水平扩展,存储TB级别的数据;
3.缓存淘汰策略:Redis支持8种缓存淘汰策略,常用的有:
1.LRU(最近最少使用):删除最近最少使用的键;
2.LFU(最不经常使用):删除最不经常使用的键;
3.TTL(过期时间):删除最早过期的键;
4.高级数据结构:
1.哈希:适合存储对象,比如用户信息user:1000 {name: "张三", age: 20};
2.有序集合:适合排行榜,比如商品销量排行榜product:rank {product_123: 1000, product_456: 900};
3.位图:适合统计,比如用户签到user:sign:1000 01101110(表示1号、3号、4号、6号、7号签到)。
四、Memcached:轻量级的分布式缓存,适合简单场景
Memcached是开源的内存缓存系统,核心优势是轻量级、高性能、分布式哈希,适合简单缓存场景(比如静态页面缓存、热点数据缓存)。
核心原理(大白话)
1.内存存储:数据存在内存中,读写速度快;
2.分布式哈希:用一致性哈希算法把数据分布到多个节点,避免单点故障;
3.不持久化:数据只存在内存中,Memcached重启后数据丢失;
4.简单数据结构:只支持字符串,不支持复杂数据结构;
5.自动淘汰:当内存满时,用LRU策略自动淘汰最近最少使用的键。
类比:Memcached就像便利店的货架,只放简单的商品(字符串数据),货架满了自动把最久没人买的商品扔掉,适合快速存取简单商品。
实战2:Memcached缓存读写(C#)
用EnyimMemcached库实现Memcached缓存读写。
步骤1:安装NuGet包
bash
Install-Package EnyimMemcached
步骤2:Memcached缓存实现代码
csharp
using Enyim.Caching;
using Enyim.Caching.Configuration;
using Enyim.Caching.Memcached;
using System;
using System.Threading.Tasks;
namespace DistributedCache.Memcached;
public class MemcachedCacheService
{
private readonly IMemcachedClient _memcachedClient;
public MemcachedCacheService()
{
// 配置Memcached客户端
var config = new MemcachedClientConfiguration();
config.AddServer("localhost", 11211); // Memcached服务器地址和端口
config.Protocol = MemcachedProtocol.Binary; // 用二进制协议,性能更好
config.SocketPool.ReceiveTimeout = TimeSpan.FromSeconds(1);
config.SocketPool.ConnectionTimeout = TimeSpan.FromSeconds(1);
_memcachedClient = new MemcachedClient(config);
}
/// <summary>
/// 获取缓存
/// </summary>
public async Task<string> GetAsync(string key)
{
var value = await _memcachedClient.GetValueAsync<string>(key);
if (value != null)
{
Console.WriteLine($"[MemcachedCache] 缓存命中:{key}");
}
else
{
Console.WriteLine($"[MemcachedCache] 缓存未命中:{key}");
}
return value;
}
/// <summary>
/// 设置缓存
/// </summary>
public async Task SetAsync(string key, string value, TimeSpan expiration)
{
bool success = await _memcachedClient.StoreAsync(StoreMode.Set, key, value, expiration);
if (success)
{
Console.WriteLine($"[MemcachedCache] 设置缓存成功:{key},过期时间:{expiration}");
}
else
{
Console.WriteLine($"[MemcachedCache] 设置缓存失败:{key}");
}
}
/// <summary>
/// 删除缓存
/// </summary>
public async Task DeleteAsync(string key)
{
bool success = await _memcachedClient.RemoveAsync(key);
if (success)
{
Console.WriteLine($"[MemcachedCache] 删除缓存成功:{key}");
}
else
{
Console.WriteLine($"[MemcachedCache] 删除缓存失败:{key}");
}
}
}
// 测试代码
class Program
{
static async Task Main(string[] args)
{
var cacheService = new MemcachedCacheService();
string key = "product:detail:123";
string value = "商品详情:product_123,价格:99.9元";
// 设置缓存
await cacheService.SetAsync(key, value, TimeSpan.FromMinutes(30));
// 获取缓存
string cachedValue = await cacheService.GetAsync(key);
Console.WriteLine(cachedValue);
// 删除缓存
await cacheService.DeleteAsync(key);
}
}
代码逐行讲解:
1.MemcachedClientConfiguration:配置Memcached客户端,指定服务器地址、协议、超时时间;
2.GetValueAsync:异步获取缓存,返回字符串;
3.StoreAsync:异步设置缓存,支持Set、Add、Replace三种模式;
4.RemoveAsync:异步删除缓存;
5.二进制协议:比文本协议性能更好,生产环境建议用二进制协议。
我踩过的坑:一开始用文本协议,结果Memcached的性能上不去,换成二进制协议后性能提升了30%——生产环境一定要用二进制协议!
拓展知识:Memcached常用功能
1.分布式哈希:用一致性哈希算法把数据分布到多个节点,当节点增加或减少时,只有部分数据需要迁移,减少缓存失效;
2.内存管理:用Slab分配机制管理内存,避免内存碎片,提升内存利用率;
3.缓存淘汰策略:只支持LRU策略,当内存满时自动淘汰最近最少使用的键;
4.不支持持久化:Memcached重启后数据丢失,适合不需要持久化的场景,比如静态页面缓存。
四、Redis vs Memcached:对比与适用场景
特性 Redis Memcached
数据结构 支持字符串、哈希、列表、集合、有序集合等 只支持字符串
持久化 支持RDB和AOF 不支持
高可用 支持哨兵模式和集群模式 支持分布式哈希,不支持自动故障转移
性能 高(万级QPS) 极高(十万级QPS)
内存利用率 高(支持压缩) 中(Slab分配机制有内存碎片)
适用场景 复杂场景(商品详情页、用户会话、分布式锁) 简单场景(静态页面缓存、热点数据缓存)
学习曲线 陡(功能多) 平缓(功能少)
选择建议
需要复杂数据结构:用Redis;
需要持久化:用Redis;
需要高可用:用Redis;
简单缓存、高并发:用Memcached;
预算有限:Memcached资源占用少,适合低配服务器。
五、分布式缓存进阶问题
-
缓存一致性策略
写穿(Write-Through):先更新数据库,再更新/删除缓存,适合数据一致性要求高的场景;
写回(Write-Back):先更新缓存,再异步更新数据库,适合性能要求高但一致性要求不高的场景,比如日志系统;
异步更新:用消息队列(比如RabbitMQ)异步更新缓存,适合高并发场景,避免更新缓存阻塞业务操作。 -
缓存淘汰策略选择
LRU:适合热点数据比较稳定的场景,比如商品详情页;
LFU:适合热点数据变化频繁的场景,比如新闻首页;
TTL:适合数据有明确过期时间的场景,比如验证码。 -
缓存预热
定义:系统启动时,把热点数据从数据库加载到缓存,避免用户请求时缓存未命中;
实现方式:定时任务、系统启动脚本、手动触发。 -
缓存监控与告警
监控指标:缓存命中率、内存使用率、QPS、缓存穿透率、缓存击穿率;
告警规则:缓存命中率低于90%、内存使用率超过80%、QPS突增100%时发送告警。
六、最佳实践与踩坑总结 -
缓存命中率优化
热点数据缓存:只缓存热点数据,避免缓存冷数据浪费内存;
缓存粒度合理:缓存整个对象(比如商品详情),不要缓存单个字段,减少缓存次数;
缓存过期时间合理:热点数据设置较长过期时间,冷数据设置较短过期时间;
缓存预热:系统启动时加载热点数据到缓存。 -
缓存一致性优化
删除缓存而非更新缓存:避免并发更新导致数据不一致;
缓存过期时间:设置合理的过期时间,即使缓存和数据库不一致,过期后会自动更新;
异步更新缓存:用消息队列异步更新缓存,避免阻塞业务操作。 -
缓存问题解决方案
缓存穿透:布隆过滤器、空值缓存;
缓存击穿:互斥锁、热点数据永不过期;
缓存雪崩:随机过期时间、集群模式、多级缓存。 -
踩过的坑总结
缓存命中率低:一开始缓存了所有数据,包括冷数据,导致内存浪费,缓存命中率只有50%——只缓存热点数据,缓存命中率提升到95%;
缓存一致性问题:用更新缓存的方式,结果并发更新导致缓存中的数据和数据库中的数据不一致——换成删除缓存的方式,问题解决;
内存溢出:Redis的内存设置太大,超过服务器的物理内存,导致Redis崩溃——Redis的内存设置要小于服务器物理内存的70%;
缓存雪崩:所有缓存的过期时间都是30分钟,导致所有缓存同时过期,大量请求打MySQL——设置随机过期时间(30-60分钟),问题解决。
七、总结
Redis是功能丰富的分布式缓存,适合复杂场景;Memcached是轻量级的分布式缓存,适合简单场景。选择合适的分布式缓存,遵循最佳实践,解决缓存穿透、击穿、雪崩等问题,能提升系统性能,减少数据库压力。
下一节我们会学习分布式消息队列:RabbitMQ、Kafka的实战,解决分布式系统中的异步通信问题。
本站原创,转载请注明出处:https://www.xin3721.com/ArticlecSharp/c49562.html










