-
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.双重检查缓存:在获取锁后再次检查缓存,防止其他线程已经写入缓存;
基础知识拓展
-
Redis核心概念
数据结构:Redis支持字符串、哈希、列表、集合、有序集合等多种数据结构;
持久化:Redis支持RDB和AOF两种持久化方式,确保数据不会丢失;
主从复制:Redis支持主从复制,实现数据备份和读写分离;
集群:Redis Cluster实现分布式存储和高可用,支持水平扩展; - 缓存更新策略
| 策略 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|
| Cache-Aside(旁路缓存) | 读多写少的场景,如商品详情页 | 简单易实现,无需修改数据库操作 | 缓存与数据库可能存在短暂不一致 |
| Write-Through(写穿缓存) | 写多读少的场景,如订单数据 | 缓存与数据库强一致 | 写性能较低,需要修改数据库操作 |
| Write-Behind(写回缓存) | 高并发写场景,如日志收集 | 写性能高,异步更新数据库 | 缓存与数据库可能存在不一致,数据丢失风险 |
| Cache-Invalidate(缓存失效) | 数据更新不频繁的场景,如配置数据 | 简单易实现 | 下次查询时会有缓存穿透风险 |
- 缓存常见问题与解决方案
| 问题 | 描述 | 解决方案 |
|---|---|---|
| 缓存穿透 | 查询不存在的数据,请求直接打到数据库 | 缓存空值、布隆过滤器、接口参数校验 |
| 缓存击穿 | 热点数据缓存过期,大量请求直接打到数据库 | 热点数据永不过期、分布式互斥锁、提前刷新缓存 |
| 缓存雪崩 | 大量缓存同时过期,大量请求直接打到数据库 | 随机过期时间、多级缓存、集群部署 |
| 缓存一致性 | 缓存数据与数据库数据不一致 | 实时更新缓存、定期同步缓存、使用分布式事务 |
-
Redis性能优化技巧
连接池:使用连接池管理Redis连接,避免频繁创建和销毁连接;
批量操作:使用MGET、MSET等批量操作减少网络开销;
数据压缩:对大体积数据进行压缩后存储,减少内存占用和网络传输;
合理设置过期时间:根据数据更新频率设置合适的过期时间,避免缓存雪崩;
读写分离:使用主从复制实现读写分离,提升系统吞吐量; - .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应用!
本站原创,转载请注明出处:










