VB.net 2010 视频教程 VB.net 2010 视频教程 python基础视频教程
SQL Server 2008 视频教程 c#入门经典教程 Visual Basic从门到精通视频教程
当前位置:
首页 > 编程开发 > c#编程 >
  • C#网络编程之分布式缓存(Redis、Memcached)

第38章 分布式缓存
38.1 分布式缓存(Redis、Memcached)
一、我踩过的缓存坑:从“Memcached内存碎片炸了”到“Redis缓存一致性翻车”
做电商首页缓存时,我先用Memcached存热门商品列表,结果运行3个月后内存使用率飙到95%,但实际缓存数据只有50%——Slab分配机制导致严重内存碎片,重启Memcached才解决;后来换成Redis,用“先更新缓存再更数据库”的逻辑,结果并发vb.net教程C#教程python教程SQL教程access 2010教程
更新时缓存和数据库数据不一致,用户看到的价格和实际价格差了20块;再后来又遇到缓存雪崩,所有首页缓存同时过期,10万请求瞬间打MySQL,DB直接挂了。这节我把这些踩坑经验揉进去,用大白话讲透Redis和Memcached的核心原理,结合C#实战代码逐行拆解,拓展生产级优化技巧,让你的缓存既高效又稳如狗。
二、Redis:全能型缓存选手,90%场景的首选
Redis是开源的内存数据库,核心优势是功能全、能持久、高可用、支持复杂数据结构,几乎能覆盖所有分布式缓存场景(商品详情、用户会话、分布式锁、排行榜)。
核心原理(大白话类比)
把Redis比作“智能储物柜”:
1.内存存储:柜子在你面前(内存),存取东西比去仓库(数据库)快1000倍;
2.持久化:柜子有自动拍照功能(RDB快照)和全程录像功能(AOF日志),断电后能恢复里面的东西;
3.数据结构:柜子有不同格子,能放盒子(哈希)、排队的快递(列表)、分类的零食(集合)、按价格排序的商品(有序集合);
4.高可用:柜子有备用柜(从节点),主柜坏了备用柜自动顶上;还能拼多个柜子成大柜子(集群),存更多东西;
5.分布式:大柜子按商品类型分片,比如零食放1号柜,电子产品放2号柜,存取互不影响。
我踩过的坑:一开始为了省内存,把Redis的持久化关了,结果机房跳闸Redis重启,缓存全丢,10万请求打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();
	// 1. 初始化布隆过滤器:预计存100万商品ID,误判率0.01%
	_bloomFilter = new BloomFilter<string>(1000000, 0.0001);
	// 2. 预热布隆过滤器:把所有存在的商品ID导入(实际从DB批量查)
	_ = PreheatBloomFilterAsync();
	}
	
	/// <summary>
	/// 预热布隆过滤器(异步不阻塞启动)
	/// </summary>
	private async Task PreheatBloomFilterAsync()
	{
	Console.WriteLine("开始预热布隆过滤器...");
	// 模拟从DB查100万商品ID
	for (int i = 1; i <= 1000000; i++)
	{
	_bloomFilter.Add($"product_{i}");
	// 每10万条打个日志,避免卡着以为死了
	if (i % 100000 == 0)
	{
	Console.WriteLine($"已预热{i}条商品ID");
	}
	}
	Console.WriteLine("布隆过滤器预热完成");
	}
	
	/// <summary>
	/// 获取商品详情(解决穿透、击穿问题)
	/// </summary>
	public async Task<string> GetProductDetailAsync(string productId)
	{
	#region 缓存穿透处理:布隆过滤器拦截不存在的商品ID
	if (!_bloomFilter.Contains(productId))
	{
	Console.WriteLine($"[拦截穿透] 商品ID {productId} 不存在,直接返回");
	return null;
	}
	#endregion
	
	string cacheKey = $"product:detail:{productId}";
	string cacheValue = await _redisDb.StringGetAsync(cacheKey);
	
	#region 缓存命中:直接返回
	if (!string.IsNullOrEmpty(cacheValue))
	{
	Console.WriteLine($"[缓存命中] {cacheKey}");
	return cacheValue;
	}
	#endregion
	
	#region 缓存击穿处理:Redis互斥锁,只有一个线程查DB
	string lockKey = $"lock:product:{productId}";
	string lockValue = Guid.NewGuid().ToString("N"); // 唯一值防误删锁
	// 加锁5秒,避免线程挂了锁一直存在
	bool lockAcquired = await _redisDb.StringSetAsync(lockKey, lockValue, TimeSpan.FromSeconds(5), When.NotExists);
	
	if (!lockAcquired)
	{
	// 其他线程等100ms重试,避免打满CPU
	await Task.Delay(100);
	return await GetProductDetailAsync(productId);
	}
	#endregion
	
	try
	{
	#region 缓存未命中:查DB
	string dbValue = await QueryProductFromDbAsync(productId);
	if (string.IsNullOrEmpty(dbValue))
	{
	// 数据库也不存在,缓存空值5分钟,避免重复穿透
	await _redisDb.StringSetAsync(cacheKey, "", TimeSpan.FromMinutes(5));
	Console.WriteLine($"[缓存空值] {cacheKey} 数据库不存在,缓存空值");
	return null;
	}
	#endregion
	
	#region 缓存雪崩处理:随机过期时间(30-60分钟)
	int expireMinutes = new Random().Next(30, 61);
	await _redisDb.StringSetAsync(cacheKey, dbValue, TimeSpan.FromMinutes(expireMinutes));
	Console.WriteLine($"[缓存更新] {cacheKey} 从DB加载,过期时间 {expireMinutes} 分钟");
	#endregion
	
	return dbValue;
	}
	finally
	{
	#region 释放锁: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 });
	#endregion
	}
	}
	
	/// <summary>
	/// 更新商品详情(解决缓存一致性:写穿策略)
	/// </summary>
	public async Task UpdateProductDetailAsync(string productId, string productDetail)
	{
	// 1. 先更DB:保证数据持久化
	await UpdateProductInDbAsync(productId, productDetail);
	// 2. 再删缓存:避免并发更新导致缓存不一致
	string cacheKey = $"product:detail:{productId}";
	await _redisDb.KeyDeleteAsync(cacheKey);
	Console.WriteLine($"[缓存删除] {cacheKey} 商品更新,删除缓存");
	}
	
	#region 模拟DB操作
	private async Task<string> QueryProductFromDbAsync(string productId)
	{
	await Task.Delay(100); // 模拟DB查询耗时100ms
	// 模拟不存在的商品ID
	if (productId == "product_9999999") return null;
	return $"【商品详情】ID:{productId},价格:99.9元,库存:1000件";
	}
	
	private async Task UpdateProductInDbAsync(string productId, string productDetail)
	{
	await Task.Delay(50); // 模拟DB更新耗时50ms
	Console.WriteLine($"[DB更新] 商品 {productId} 详情已更新");
	}
	#endregion
	}
	
	// 测试代码
	class Program
	{
	static async Task Main(string[] args)
	{
	var redis = ConnectionMultiplexer.Connect("localhost:6379");
	var cacheService = new RedisCacheService(redis);
	
	// 测试缓存命中
	string hitDetail = await cacheService.GetProductDetailAsync("product_123");
	Console.WriteLine(hitDetail);
	
	// 测试缓存更新
	await cacheService.UpdateProductDetailAsync("product_123", "【商品详情】ID:product_123,价格:89.9元,库存:999件");
	string updatedDetail = await cacheService.GetProductDetailAsync("product_123");
	Console.WriteLine(updatedDetail);
	
	// 测试缓存穿透
	string invalidDetail = await cacheService.GetProductDetailAsync("product_9999999");
	Console.WriteLine(invalidDetail ?? "商品不存在");
	}
	}

核心代码逻辑拆解:
1.缓存穿透:用布隆过滤器拦截不存在的商品ID,避免请求打MySQL;
2.缓存击穿:Redis互斥锁保证只有一个线程查DB,其他线程等待重试;
3.缓存雪崩:随机过期时间(30-60分钟),避免所有缓存同时失效;
4.缓存一致性:先更DB再删缓存,下次请求自动加载最新数据到缓存;
5.空值缓存:数据库不存在的商品,缓存空值5分钟,避免恶意请求反复穿透。
我踩过的坑:一开始用“先更缓存再更DB”的逻辑,结果两个线程并发更新时,线程1更了缓存,线程2更了DB,导致缓存数据比DB旧——换成“先更DB再删缓存”后,一致性问题彻底解决!
拓展知识:Redis生产级必备技能
1.持久化机制选择:
1.RDB:定期快照,适合备份和灾备,性能开销小,数据丢失最多5分钟;
2.AOF:记录每个写操作,数据完整性高,性能开销大,建议用“everysec”模式(每秒同步一次);
3.混合持久化:Redis 4.0+支持,先写RDB再写AOF日志,兼顾性能和完整性;
2.高可用方案:
1.哨兵模式:1主N从,哨兵监控主节点,主节点挂了自动切换到从节点,适合中小规模;
2.集群模式:N主N从,数据分片存储,支持水平扩展,适合大规模(TB级数据);
3.缓存淘汰策略:
1.LRU:删除最近最少使用的键,适合热点稳定场景(如商品详情);
2.LFU:删除最不经常使用的键,适合热点变化快场景(如新闻首页);
3.TTL:删除最早过期的键,适合有明确过期时间的场景(如验证码);
4.高级数据结构实战:
1.哈希:存用户信息,HMSET user:1000 name "张三" age 20,比存字符串更省内存;
2.有序集合:做商品销量排行榜,ZADD product:rank 1000 product_123 900 product_456,支持按销量排序;
3.位图:做用户签到,SETBIT user:sign:1000 1 1(1号签到),BITCOUNT user:sign:1000统计签到天数。
三、Memcached:轻量级缓存选手,高并发简单场景首选
Memcached是开源的内存缓存系统,核心优势是轻量级、高性能、分布式哈希,适合简单缓存场景(如静态页面、热点商品列表)。
核心原理(大白话类比)
把Memcached比作“便利店货架”:
1.内存存储:货架在收银台旁边,拿东西比去仓库快;
2.分布式哈希:多个便利店按商品ID分片,比如ID以1开头的放1号店,以2开头的放2号店;
3.不持久化:货架上的商品只在营业时间存在,打烊(重启)后清空;
4.简单数据结构:货架只放整包商品(字符串),不拆包(复杂结构);
5.自动淘汰:货架满了,自动把最久没人买的商品扔掉(LRU)。
我踩过的坑:一开始用Memcached存用户会话,结果Memcached重启会话全丢,用户被迫重新登录——需要持久化的场景别用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()
	{
	// 1. 配置Memcached客户端
	var config = new MemcachedClientConfiguration();
	config.AddServer("localhost", 11211); // Memcached服务器地址
	config.Protocol = MemcachedProtocol.Binary; // 二进制协议比文本协议快30%
	config.SocketPool.ReceiveTimeout = TimeSpan.FromSeconds(1); // 读超时1秒
	config.SocketPool.ConnectionTimeout = TimeSpan.FromSeconds(1); // 连接超时1秒
	config.SocketPool.MinPoolSize = 10; // 最小连接数
	config.SocketPool.MaxPoolSize = 100; // 最大连接数,根据并发量调整
	
	// 2. 初始化客户端
	_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($"[缓存命中] {key}");
	}
	else
	{
	Console.WriteLine($"[缓存未命中] {key}");
	}
	return value;
	}
	
	/// <summary>
	/// 设置缓存
	/// </summary>
	public async Task<bool> SetAsync(string key, string value, TimeSpan expiration)
	{
	// StoreMode.Set:不管key是否存在都设置
	// StoreMode.Add:只有key不存在时设置
	// StoreMode.Replace:只有key存在时设置
	bool success = await _memcachedClient.StoreAsync(StoreMode.Set, key, value, expiration);
	Console.WriteLine(success ? $"[缓存设置成功] {key} 过期时间:{expiration}" : $"[缓存设置失败] {key}");
	return success;
	}
	
	/// <summary>
	/// 删除缓存
	/// </summary>
	public async Task<bool> DeleteAsync(string key)
	{
	bool success = await _memcachedClient.RemoveAsync(key);
	Console.WriteLine(success ? $"[缓存删除成功] {key}" : $"[缓存删除失败] {key}");
	return success;
	}
	}
	
	// 测试代码
	class Program
	{
	static async Task Main(string[] args)
	{
	var cacheService = new MemcachedCacheService();
	string key = "product:list:hot";
	string value = "热门商品:product_123, product_456, product_789";
	
	// 设置缓存30分钟
	await cacheService.SetAsync(key, value, TimeSpan.FromMinutes(30));
	
	// 获取缓存
	string cachedValue = await cacheService.GetAsync(key);
	Console.WriteLine(cachedValue);
	
	// 删除缓存
	await cacheService.DeleteAsync(key);
	}
	}

核心代码逻辑拆解:
1.二进制协议:比文本协议性能提升30%,生产环境必须用;
2.连接池配置:最小连接数10,最大连接数100,根据并发量调整,避免连接打满;
3.存储模式:Set、Add、Replace三种模式,根据业务场景选择,比如Add适合初始化缓存,Replace适合更新缓存。
我踩过的坑:一开始把最大连接数设为1000,结果Memcached的连接数打满,导致新请求无法连接——最大连接数不要超过Memcached的maxconn配置(默认1024),建议设为800以内!
拓展知识:Memcached生产级必备技能
1.分布式哈希:用一致性哈希算法,节点增减时只有1/N的数据需要迁移,减少缓存失效;
2.内存管理:Slab分配机制,把内存分成固定大小的块,避免内存碎片,但会有少量内存浪费;
3.缓存淘汰策略:只支持LRU,当内存满时自动淘汰最近最少使用的键;
4.性能优化:
1.用二进制协议;
2.压缩大值(如JSON字符串);
3.避免存大对象(超过1MB),影响性能。
四、Redis vs Memcached:特性对比与场景选择

特性 Redis Memcached
数据结构 支持字符串、哈希、列表、集合、有序集合等 仅支持字符串
持久化 支持RDB、AOF、混合持久化 不支持
高可用 哨兵模式、集群模式,自动故障转移 分布式哈希,需手动故障转移
性能 高(万级QPS) 极高(十万级QPS)
内存利用率 高(支持压缩、复杂结构) 中(Slab分配有内存碎片)
适用场景 复杂场景(商品详情、用户会话、分布式锁) 简单场景(静态页面、热点列表缓存)
学习曲线 陡(功能多) 平缓(功能少)
选择建议    
需要复杂数据结构/持久化/高可用:选Redis;    
简单缓存、超高并发、低配服务器:选Memcached;    
预算充足、追求一站式解决方案:选Redis(现在大部分公司都用Redis替代Memcached)。    
五、分布式缓存进阶问题与最佳实践    
  1. 缓存一致性策略
    写穿策略:先更DB,再删缓存(推荐),适合一致性要求高的场景;
    写回策略:先更缓存,再异步更DB,适合性能要求高、一致性要求低的场景(如日志);
    异步更新:用RabbitMQ异步更新缓存,避免阻塞业务操作,适合高并发场景。
  2. 缓存命中率优化
    热点数据缓存:只缓存访问量前20%的热点数据,避免浪费内存;
    缓存粒度合理:缓存整个对象(如商品详情),不要缓存单个字段,减少缓存次数;
    缓存预热:系统启动时加载热点数据到缓存,避免用户请求时缓存未命中;
    监控告警:缓存命中率低于90%时告警,及时排查问题。
  3. 生产级踩坑总结
    1.缓存命中率低:一开始缓存了所有数据,包括冷数据,命中率只有50%——只缓存热点数据后,命中率提升到95%;
    2.内存溢出:Redis内存设为16GB,服务器物理内存只有16GB,导致Redis触发OOM被系统kill——Redis内存要设为物理内存的70%以内;
    3.缓存雪崩:所有缓存过期时间都是30分钟,导致同时失效——改成随机过期时间(30-60分钟)后,问题解决;
    4.Memcached内存碎片:运行3个月后内存使用率95%,实际缓存数据只有50%——重启Memcached或调整Slab大小解决。
    六、总结
    Redis是全能型缓存选手,适合90%的分布式场景;Memcached是轻量级选手,适合简单高并发场景。选择合适的缓存,解决穿透、击穿、雪崩、一致性四大核心问题,能让你的系统性能提升10倍以上,同时保证数据稳定。
    下一节我们会学习分布式消息队列:RabbitMQ、Kafka的实战,解决分布式系统中的异步通信和解耦问题。

 本站原创,转载请注明出处:


相关教程