VB.net 2010 视频教程 VB.net 2010 视频教程 python基础视频教程
SQL Server 2008 视频教程 c#入门经典教程 Visual Basic从门到精通视频教程
当前位置:
首页 > 编程开发 > c#编程 >
  • C#中关于缓存策略——Redis分布式缓存

第十部分:性能优化与内存管理
实例101:缓存策略——Redis分布式缓存
实例介绍
之前做电商商品详情页时,每次请求都要查询数据库,数据库QPS达到1000,响应时间超过500ms,系统经常出现超时。通过引入Redis分布式缓存后,商品详情页的响应时间降到10ms以内,数据库QPS降到100,性能提升50倍。这vb.net教程C#教程python教程SQL教程access 2010教程节就带你从零学习Redis分布式缓存,包括缓存基本概念、Redis安装配置、缓存读写操作、缓存更新策略、缓存穿透/击穿/雪崩解决方案,覆盖分布式缓存的核心场景。
需求分析
Redis分布式缓存要解决“数据库压力大、响应时间长、系统吞吐量低”的问题,具体需求如下:
1.缓存读写操作:实现商品数据的缓存读写,减少数据库查询;
2.缓存更新策略:掌握缓存更新的常见策略,如过期时间、主动更新;
3.缓存问题解决:解决缓存穿透、击穿、雪崩等常见问题;
4.分布式缓存:实现多服务器共享缓存,支持分布式系统;
5.性能监控:掌握Redis性能监控和调优方法;
代码实现
前置条件:.NET 8 SDK;Visual Studio 2022;Redis 7.0;StackExchange.Redis NuGet包;
场景1:缓存读写操作——减少数据库查询
使用Redis缓存商品数据,减少数据库查询次数,提升系统响应速度。
步骤1:Redis连接配置(RedisCacheService.cs)
csharp

	using StackExchange.Redis;
	using System;
	using System.Text.Json;
	
	namespace RedisDistributedCache;
	
	public class RedisCacheService
	{
	// Redis连接多路复用器(单例模式,避免频繁创建连接)
	private readonly IConnectionMultiplexer _redisMultiplexer;
	private readonly IDatabase _redisDb;
	
	public RedisCacheService(string connectionString)
	{
	// 创建Redis连接多路复用器
	_redisMultiplexer = ConnectionMultiplexer.Connect(connectionString);
	// 获取Redis数据库(默认数据库0)
	_redisDb = _redisMultiplexer.GetDatabase();
	}
	
	// 写入缓存
	public async Task SetCacheAsync<T>(string key, T value, TimeSpan? expiration = null)
	{
	if (value == null)
	{
	throw new ArgumentNullException(nameof(value));
	}
	
	// 将对象序列化为JSON字符串
	var json = JsonSerializer.Serialize(value);
	// 写入Redis缓存,设置过期时间(默认1小时)
	await _redisDb.StringSetAsync(key, json, expiration ?? TimeSpan.FromHours(1));
	}
	
	// 读取缓存
	public async Task<T?> GetCacheAsync<T>(string key)
	{
	// 从Redis缓存读取JSON字符串
	var json = await _redisDb.StringGetAsync(key);
	if (json.IsNull)
	{
	return default; // 缓存不存在,返回默认值
	}
	
	// 将JSON字符串反序列化为对象
	return JsonSerializer.Deserialize<T>(json);
	}
	
	// 删除缓存
	public async Task DeleteCacheAsync(string key)
	{
	await _redisDb.KeyDeleteAsync(key);
	}
	
	// 检查缓存是否存在
	public async Task<bool> ExistsAsync(string key)
	{
	return await _redisDb.KeyExistsAsync(key);
	}
	}

步骤2:商品服务实现(ProductService.cs)
csharp

	using System;
	using System.Collections.Generic;
	using System.Linq;
	using System.Threading.Tasks;
	using Microsoft.EntityFrameworkCore;
	
	namespace RedisDistributedCache;
	
	public class ProductService
	{
	private readonly ProductDbContext _dbContext;
	private readonly RedisCacheService _redisCache;
	private const string ProductCachePrefix = "product:";
	
	public ProductService(ProductDbContext dbContext, RedisCacheService redisCache)
	{
	_dbContext = dbContext;
	_redisCache = redisCache;
	}
	
	// 获取商品详情(先查缓存,缓存不存在再查数据库)
	public async Task<Product?> GetProductAsync(int productId)
	{
	var cacheKey = $"{ProductCachePrefix}{productId}";
	
	// 1. 先查缓存
	var product = await _redisCache.GetCacheAsync<Product>(cacheKey);
	if (product != null)
	{
	Console.WriteLine($"从缓存获取商品:{productId}");
	return product;
	}
	
	// 2. 缓存不存在,查数据库
	product = await _dbContext.Products
	.FirstOrDefaultAsync(p => p.ProductId == productId);
	if (product == null)
	{
	return null;
	}
	
	// 3. 将数据库查询结果写入缓存
	await _redisCache.SetCacheAsync(cacheKey, product, TimeSpan.FromHours(2));
	Console.WriteLine($"从数据库获取商品并写入缓存:{productId}");
	
	return product;
	}
	
	// 更新商品信息(同时更新数据库和缓存)
	public async Task UpdateProductAsync(Product product)
	{
	// 1. 更新数据库
	_dbContext.Products.Update(product);
	await _dbContext.SaveChangesAsync();
	
	// 2. 更新缓存(或删除缓存,下次查询时自动加载)
	var cacheKey = $"{ProductCachePrefix}{product.ProductId}";
	await _redisCache.SetCacheAsync(cacheKey, product, TimeSpan.FromHours(2));
	// 或者删除缓存:await _redisCache.DeleteCacheAsync(cacheKey);
	}
	}
	
	public class ProductDbContext : DbContext
	{
	public DbSet<Product> Products { get; set; }
	
	public ProductDbContext(DbContextOptions<ProductDbContext> options) : base(options)
	{
	}
	}
	
	public class Product
	{
	public int ProductId { get; set; }
	public string ProductName { get; set; } = string.Empty;
	public decimal Price { get; set; }
	public string Description { get; set; } = string.Empty;
	public int Stock { get; set; }
	public DateTime CreateTime { get; set; }
	}

场景2:缓存穿透解决——缓存空值或布隆过滤器
解决缓存穿透问题,即查询不存在的数据,导致请求直接打到数据库。
步骤1:缓存空值解决缓存穿透(CachePenetrationFix.cs)
csharp

	using System;
	using System.Threading.Tasks;
	using Microsoft.EntityFrameworkCore;
	
	namespace RedisDistributedCache;
	
	public class ProductService
	{
	// ... 构造函数省略 ...
	
	// 解决缓存穿透:缓存空值
	public async Task<Product?> GetProductWithNullCacheAsync(int productId)
	{
	var cacheKey = $"{ProductCachePrefix}{productId}";
	
	// 1. 先查缓存
	var product = await _redisCache.GetCacheAsync<Product>(cacheKey);
	if (product != null)
	{
	Console.WriteLine($"从缓存获取商品:{productId}");
	return product;
	}
	
	// 2. 缓存不存在,查数据库
	product = await _dbContext.Products
	.FirstOrDefaultAsync(p => p.ProductId == productId);
	
	// 3. 如果数据库也不存在,缓存空值(设置较短的过期时间,如5分钟)
	if (product == null)
	{
	await _redisCache.SetCacheAsync(cacheKey, null, TimeSpan.FromMinutes(5));
	Console.WriteLine($"缓存空值:{productId}");
	return null;
	}
	
	// 4. 将数据库查询结果写入缓存
	await _redisCache.SetCacheAsync(cacheKey, product, TimeSpan.FromHours(2));
	Console.WriteLine($"从数据库获取商品并写入缓存:{productId}");
	
	return product;
	}
	}

步骤2:布隆过滤器解决缓存穿透(BloomFilterExample.cs)
csharp

	using System;
	using System.Threading.Tasks;
	using Microsoft.EntityFrameworkCore;
	using BloomFilter;
	
	namespace RedisDistributedCache;
	
	public class ProductService
	{
	private readonly IBloomFilter<int> _productBloomFilter;
	
	public ProductService(ProductDbContext dbContext, RedisCacheService redisCache, IBloomFilter<int> productBloomFilter)
	{
	_dbContext = dbContext;
	_redisCache = redisCache;
	_productBloomFilter = productBloomFilter;
	}
	
	// 解决缓存穿透:使用布隆过滤器
	public async Task<Product?> GetProductWithBloomFilterAsync(int productId)
	{
	// 1. 先查布隆过滤器,如果不存在,直接返回null
	if (!_productBloomFilter.Contains(productId))
	{
	Console.WriteLine($"布隆过滤器判断商品不存在:{productId}");
	return null;
	}
	
	var cacheKey = $"{ProductCachePrefix}{productId}";
	
	// 2. 查缓存
	var product = await _redisCache.GetCacheAsync<Product>(cacheKey);
	if (product != null)
	{
	Console.WriteLine($"从缓存获取商品:{productId}");
	return product;
	}
	
	// 3. 查数据库
	product = await _dbContext.Products
	.FirstOrDefaultAsync(p => p.ProductId == productId);
	if (product == null)
	{
	return null;
	}
	
	// 4. 写入缓存
	await _redisCache.SetCacheAsync(cacheKey, product, TimeSpan.FromHours(2));
	Console.WriteLine($"从数据库获取商品并写入缓存:{productId}");
	
	return product;
	}
	
	// 初始化布隆过滤器(在系统启动时执行)
	public async Task InitializeBloomFilterAsync()
	{
	var productIds = await _dbContext.Products.Select(p => p.ProductId).ToListAsync();
	_productBloomFilter.AddRange(productIds);
	}
	}

场景3:缓存击穿解决——热点数据永不过期或互斥锁
解决缓存击穿问题,即热点数据缓存过期,导致大量请求直接打到数据库。
步骤1:互斥锁解决缓存击穿(CacheBreakdownFix.cs)
csharp

	using System;
	using System.Threading.Tasks;
	using Microsoft.EntityFrameworkCore;
	using StackExchange.Redis;
	
	namespace RedisDistributedCache;
	
	public class ProductService
	{
	// ... 构造函数省略 ...
	
	// 解决缓存击穿:使用Redis分布式互斥锁
	public async Task<Product?> GetProductWithLockAsync(int productId)
	{
	var cacheKey = $"{ProductCachePrefix}{productId}";
	var lockKey = $"lock:product:{productId}";
	
	// 1. 先查缓存
	var product = await _redisCache.GetCacheAsync<Product>(cacheKey);
	if (product != null)
	{
	Console.WriteLine($"从缓存获取商品:{productId}");
	return product;
	}
	
	// 2. 缓存不存在,获取分布式互斥锁
	var lockTaken = await _redisDb.LockTakeAsync(lockKey, Environment.MachineName, TimeSpan.FromSeconds(10));
	if (!lockTaken)
	{
	// 获取锁失败,等待100ms后重试
	await Task.Delay(100);
	return await GetProductWithLockAsync(productId);
	}
	
	try
	{
	// 3. 再次查缓存(防止其他线程已经写入缓存)
	product = await _redisCache.GetCacheAsync<Product>(cacheKey);
	if (product != null)
	{
	Console.WriteLine($"从缓存获取商品(锁内):{productId}");
	return product;
	}
	
	// 4. 查数据库
	product = await _dbContext.Products
	.FirstOrDefaultAsync(p => p.ProductId == productId);
	if (product == null)
	{
	return null;
	}
	
	// 5. 写入缓存
	await _redisCache.SetCacheAsync(cacheKey, product, TimeSpan.FromHours(2));
	Console.WriteLine($"从数据库获取商品并写入缓存(锁内):{productId}");
	
	return product;
	}
	finally
	{
	// 6. 释放分布式互斥锁
	await _redisDb.LockReleaseAsync(lockKey, Environment.MachineName);
	}
	}
	}

逐行讲解
场景1:Redis缓存核心代码
1.ConnectionMultiplexer:Redis连接多路复用器,单例模式,避免频繁创建连接,提升性能;
2.StringSetAsync/StringGetAsync:Redis字符串类型操作,用于存储和读取JSON序列化后的对象;
3.过期时间:设置缓存过期时间,避免缓存数据与数据库数据不一致;
4.缓存读写流程:先查缓存,缓存不存在再查数据库,然后将数据库结果写入缓存;
场景2:缓存穿透解决核心代码
1.缓存空值:查询不存在的数据时,将空值写入缓存,设置较短的过期时间,避免后续请求直接打到数据库;
2.布隆过滤器:在系统启动时将所有存在的商品ID写入布隆过滤器,查询时先判断布隆过滤器,如果不存在直接返回null,避免查询数据库;
场景3:缓存击穿解决核心代码
1.分布式互斥锁:使用Redis的LockTakeAsync和LockReleaseAsync实现分布式互斥锁,确保只有一个线程查询数据库并写入缓存,其他线程等待后从缓存获取数据;
2.双重检查缓存:在获取锁后再次检查缓存,防止其他线程已经写入缓存;
基础知识拓展

  1. Redis核心概念
    数据结构:Redis支持字符串、哈希、列表、集合、有序集合等多种数据结构;
    持久化:Redis支持RDB和AOF两种持久化方式,确保数据不会丢失;
    主从复制:Redis支持主从复制,实现数据备份和读写分离;
    集群:Redis Cluster实现分布式存储和高可用,支持水平扩展;
  2. 缓存更新策略
策略 适用场景 优点 缺点
Cache-Aside(旁路缓存) 读多写少的场景,如商品详情页 简单易实现,无需修改数据库操作 缓存与数据库可能存在短暂不一致
Write-Through(写穿缓存) 写多读少的场景,如订单数据 缓存与数据库强一致 写性能较低,需要修改数据库操作
Write-Behind(写回缓存) 高并发写场景,如日志收集 写性能高,异步更新数据库 缓存与数据库可能存在不一致,数据丢失风险
Cache-Invalidate(缓存失效) 数据更新不频繁的场景,如配置数据 简单易实现 下次查询时会有缓存穿透风险
  1. 缓存常见问题与解决方案
问题 描述 解决方案
缓存穿透 查询不存在的数据,请求直接打到数据库 缓存空值、布隆过滤器、接口参数校验
缓存击穿 热点数据缓存过期,大量请求直接打到数据库 热点数据永不过期、分布式互斥锁、提前刷新缓存
缓存雪崩 大量缓存同时过期,大量请求直接打到数据库 随机过期时间、多级缓存、集群部署
缓存一致性 缓存数据与数据库数据不一致 实时更新缓存、定期同步缓存、使用分布式事务
  1. Redis性能优化技巧
    连接池:使用连接池管理Redis连接,避免频繁创建和销毁连接;
    批量操作:使用MGET、MSET等批量操作减少网络开销;
    数据压缩:对大体积数据进行压缩后存储,减少内存占用和网络传输;
    合理设置过期时间:根据数据更新频率设置合适的过期时间,避免缓存雪崩;
    读写分离:使用主从复制实现读写分离,提升系统吞吐量;
  2. .NET中Redis客户端对比
客户端 特点 适用场景
StackExchange.Redis 官方推荐,性能高,支持异步,功能丰富 大多数.NET项目
ServiceStack.Redis 功能丰富,支持ORM,性能高 企业级项目,需要ORM支持
Redis.NET 轻量级,简单易用 小型项目,快速开发

总结
Redis分布式缓存的核心是减少数据库查询次数,提升系统响应速度和吞吐量。关键要点:
1.缓存读写流程:先查缓存,缓存不存在再查数据库,然后将数据库结果写入缓存;
2.缓存更新策略:根据业务场景选择合适的缓存更新策略,如Cache-Aside、Write-Through;
3.缓存问题解决:掌握缓存穿透、击穿、雪崩的解决方案,如缓存空值、布隆过滤器、分布式互斥锁;
4.Redis性能优化:使用连接池、批量操作、数据压缩等技巧提升Redis性能;
5.缓存一致性:确保缓存数据与数据库数据的一致性,避免数据不一致问题;
比如这个Redis分布式缓存,引入后商品详情页的响应时间从500ms降到10ms以内,数据库QPS从1000降到100,性能提升50倍。掌握Redis分布式缓存,你就能构建高性能、高并发的.NET应用!

 本站原创,转载请注明出处:https://www.xin3721.com/ArticlecSharp/c49501.html


相关教程