如何使用Redis解决分布式缓存
在现代分布式系统架构中,缓存技术已成为提升系统性能、降低数据库负载的关键手段。本文将深入探讨分布式缓存的必要性、业界主流解决方案,以及如何在.NET技术栈中基于Microsoft官方推荐的最佳实践来实现Redis分布式缓存。
一、分布式缓存的必要性分析
1.1 分布式系统架构的技术背景
随着云计算和微服务架构的普及,现代应用程序通常部署在多台服务器上,形成分布式系统。在这种架构下,传统的本地缓存(如内存缓存)面临着严峻的挑战:
单机缓存的局限性: -
数据孤岛问题:每台服务器的缓存独立存在,无法共享数据 -
数据一致性挑战:不同服务器上的缓存数据可能不一致 -
资源浪费:相同的数据在多台服务器上重复缓存 -
扩展性受限:无法动态扩展缓存容量
根据Microsoft官方文档,当应用托管在云或服务器场中时,需要使用分布式缓存来存储数据,以确保所有应用实例都能访问相同的缓存数据。
1.2 分布式缓存解决的关键问题
1.2.1 数据库负载过高
在高并发场景下,大量请求直接访问数据库会导致数据库负载过高,甚至引发数据库崩溃。分布式缓存通过在内存中存储热点数据,大幅减少对数据库的直接访问。
技术原理: -
缓存热点数据:将频繁访问的数据存储在分布式缓存中 -
减少数据库查询:大多数请求从缓存获取数据,避免数据库查询 -
降低数据库压力:减少数据库连接数、查询次数和CPU使用率
1.2.2 系统响应延迟
数据库查询通常涉及磁盘I/O操作,响应时间较长(通常在几十毫秒到几百毫秒)。而分布式缓存基于内存存储,响应时间通常在微秒级别。
性能对比: - 数据库查询:50-500ms -
分布式缓存查询:1-10ms - 性能提升:5-500倍
1.2.3 数据一致性挑战
在分布式系统中,多台服务器需要保持数据一致性。本地缓存无法实现跨服务器的数据共享,而分布式缓存提供了统一的数据存储层。
一致性保证: - 所有服务器访问同一缓存实例 -
数据更新后,所有服务器立即获取最新数据 -
避免因缓存不一致导致的业务错误
1.3
分布式缓存相比本地缓存的核心优势
| 数据共享 |
仅限单机 |
跨服务器共享 |
| 一致性 |
各服务器独立 |
全局一致 |
| 容量 |
受限于单机内存 |
可横向扩展 |
| 可用性 |
单机故障即失效 |
高可用集群 |
| 适用场景 |
单机应用 |
分布式系统 |
根据Microsoft官方文档,分布式缓存的主要优势在于: -
跨实例共享:所有应用实例都能访问相同的缓存数据 -
高可用性:支持故障转移和集群部署 -
可扩展性:可根据负载动态扩展缓存容量
二、业界主流分布式缓存解决方案概述
2.1 Redis
2.1.1 核心设计思路
Redis(Remote Dictionary
Server)是一个开源的内存数据结构存储系统,可用作数据库、缓存和消息代理。其核心设计思路包括:
数据结构丰富: -
支持多种数据类型:字符串(String)、哈希(Hash)、列表(List)、集合(Set)、有序集合(Sorted
Set) -
支持位图(Bitmap)、超日志(HyperLogLog)、地理位置(Geo)等高级数据结构
- 支持事务、Lua脚本、发布订阅等高级功能
高性能设计: - 单线程模型:避免多线程竞争和锁开销 -
内存存储:所有数据存储在内存中,访问速度快 -
I/O多路复用:使用epoll等机制实现高并发处理
持久化支持: - RDB(Redis
Database):定期将数据快照保存到磁盘 - AOF(Append Only
File):记录所有写操作,重启时重放 - 混合持久化:结合RDB和AOF的优势
2.1.2 适用场景
Redis适用于以下场景: -
缓存系统:作为分布式缓存存储热点数据 -
会话存储:存储用户会话信息 -
排行榜:利用有序集合实现实时排行榜 -
计数器:利用原子操作实现计数功能 -
消息队列:利用发布订阅功能实现消息传递 -
分布式锁:利用SETNX命令实现分布式锁
2.1.3 技术特点
优势: - 数据结构丰富,支持复杂业务场景 -
性能优秀,单机可处理10万+ QPS - 支持主从复制和集群模式,高可用 -
支持Lua脚本,支持复杂逻辑 - 社区活跃,生态完善
劣势: - 单线程模型,CPU利用率相对较低 -
内存成本较高,需要足够的内存资源 - 大对象存储效率不如Memcached
2.2 Memcached
2.2.1 核心设计思路
Memcached是一个高性能的分布式内存对象缓存系统,其核心设计思路包括:
简单键值存储: - 仅支持简单的键值对存储 -
不支持复杂的数据结构 - 数据最大支持1MB
多线程模型: - 采用多线程处理请求 -
基于I/O多路复用技术 - 主线程接收请求后,分发给子线程处理
LRU淘汰策略: - 采用最近最少使用(LRU)算法淘汰缓存
- 当内存不足时,自动淘汰最久未使用的数据 - 支持设置过期时间
2.2.2 适用场景
Memcached适用于以下场景: -
简单缓存:仅需存储简单的键值对数据 -
读多写少:适合缓存读取频繁、更新较少的数据 -
大对象缓存:对大对象的存储效率较高 -
已有系统:许多老系统已经使用Memcached
2.2.3 技术特点
优势: - 简单易用,学习成本低 -
多线程模型,CPU利用率高 - 对大对象存储效率高 -
内存管理简单,不易内存泄漏
劣势: - 数据结构单一,不支持复杂业务场景 -
不支持持久化,重启后数据丢失 - 不支持主从复制和集群模式 -
功能相对简单,扩展性有限
2.3 分布式数据库缓存
2.3.1 核心设计思路
分布式数据库缓存是指利用数据库自身的缓存能力来实现分布式缓存,主要包括:
SQL Server缓存: - 利用SQL Server的内存优化表 -
使用SQL Server的查询计划缓存 - 利用SQL Server的Service
Broker实现缓存通知
MongoDB缓存: - 利用MongoDB的内存存储引擎 -
利用MongoDB的TTL索引实现过期 - 利用MongoDB的Change
Stream实现缓存失效
2.3.2 适用场景
分布式数据库缓存适用于以下场景: -
已有数据库:已经使用分布式数据库的系统 -
数据一致性要求高:需要缓存和数据库强一致性的场景 -
减少系统复杂度:不想引入额外的缓存系统
2.3.3 技术特点
优势: - 无需额外部署缓存系统 -
数据一致性好,缓存和数据库同步 - 减少系统复杂度
劣势: - 性能不如专用缓存系统 - 数据库负载可能增加 -
功能相对有限
2.4 方案对比总结
| Redis |
丰富 |
优秀 |
复杂业务场景、高性能要求 |
|
| Memcached |
简单 |
良好 |
简单缓存、大对象存储 |
|
| 分布式数据库缓存 |
依赖数据库 |
一般 |
已有数据库、一致性要求高 |
|
三、.NET技术栈下分布式缓存的最佳实践
3.1 技术架构设计
根据Microsoft官方文档,.NET
Core提供了IDistributedCache接口来实现分布式缓存。该接口定义了分布式缓存的基本操作,包括获取、设置、删除、刷新等。
3.1.1 架构层次
在.NET应用中,分布式缓存的架构层次如下:
┌─────────────────────────────────────┐
│ 应用层(Controllers/Services) │
└──────────────┬──────────────────┘
│
┌──────────────▼──────────────────┐
│ IDistributedCache 接口层 │
│ - GetAsync │
│ - SetAsync │
│ - RemoveAsync │
└──────────────┬──────────────────┘
│
┌──────────────▼──────────────────┐
│ Redis 实现 │
│ - StackExchange.Redis │
│ - Microsoft.Extensions.Caching │
│ .StackExchangeRedis │
└──────────────┬──────────────────┘
│
┌──────────────▼──────────────────┐
│ Redis 服务器 │
│ - 单机模式 │
│ - 主从模式 │
│ - 集群模式 │
└─────────────────────────────────┘
3.1.2 技术选型
根据Microsoft官方文档,.NET Core提供了多种Redis实现:
Microsoft.Extensions.Caching.StackExchangeRedis: -
官方推荐的Redis实现 - 基于StackExchange.Redis库 -
支持连接池、重试、故障转移 - 与依赖注入深度集成
StackExchange.Redis: - 最流行的Redis .NET客户端 -
功能丰富,性能优秀 - 支持所有Redis命令 - 社区活跃,文档完善
CSRedisCore: - 高性能的Redis .NET客户端 - 支持Redis
Cluster - 支持Redis Sentinel - 支持异步操作
推荐选择:根据Microsoft官方文档,推荐使用Microsoft.Extensions.Caching.StackExchangeRedis,因为它是官方推荐的实现,与.NET
Core深度集成。
3.2 核心API使用方法
3.2.1 IDistributedCache接口
IDistributedCache接口定义了以下核心方法:
public interface IDistributedCache
{
// 获取缓存值
byte[] Get(string key);
Task<byte[]> GetAsync(string key, CancellationToken token = default);
// 设置缓存值
void Set(string key, byte[] value, DistributedCacheEntryOptions options);
Task SetAsync(string key, byte[] value, DistributedCacheEntryOptions options,
CancellationToken token = default);
// 刷新缓存过期时间
void Refresh(string key);
Task RefreshAsync(string key, CancellationToken token = default);
// 删除缓存值
void Remove(string key);
Task RemoveAsync(string key, CancellationToken token = default);
}
3.2.2
DistributedCacheEntryOptions
DistributedCacheEntryOptions类用于配置缓存选项:
public class DistributedCacheEntryOptions
{
// 绝对过期时间:缓存项在指定时间后过期
DateTimeOffset? AbsoluteExpiration { get; set; }
// 相对过期时间:缓存项在指定时间后过期(相对于当前时间)
TimeSpan? AbsoluteExpirationRelativeToNow { get; set; }
// 滑动过期时间:缓存项在指定时间内未被访问则过期
TimeSpan? SlidingExpiration { get; set; }
}
3.3 配置步骤
3.3.1 安装NuGet包
根据Microsoft官方文档,首先需要安装以下NuGet包:
# 安装Redis分布式缓存实现
dotnet add package Microsoft.Extensions.Caching.StackExchangeRedis
# 如果使用Azure Redis Cache
dotnet add package Microsoft.Extensions.Caching.StackExchangeRedis
3.3.2 配置依赖注入
在Program.cs中配置Redis分布式缓存:
using Microsoft.Extensions.Caching.StackExchangeRedis;
using Microsoft.Extensions.Caching.Distributed;
var builder = WebApplication.CreateBuilder(args);
// 配置Redis分布式缓存
builder.Services.AddStackExchangeRedisCache(options =>
{
// Redis连接字符串
options.Configuration = "localhost:6379";
// 实例名称(可选)
options.InstanceName = "MyApp_";
// 配置选项(可选)
options.ConfigurationOptions = new StackExchange.Redis.ConfigurationOptions
{
AbortOnConnectFail = false,
ConnectRetry = 3,
ConnectTimeout = 5000,
SyncTimeout = 5000
};
});
var app = builder.Build();
Azure Redis Cache配置示例:
builder.Services.AddStackExchangeRedisCache(options =>
{
options.Configuration = "myredis.redis.cache.windows.net:6380,password=...";
options.InstanceName = "MyApp_";
});
3.3.3 注入IDistributedCache
在服务或控制器中注入IDistributedCache:
public class ProductService
{
private readonly IDistributedCache _cache;
private readonly AppDbContext _dbContext;
public ProductService(IDistributedCache cache, AppDbContext dbContext)
{
_cache = cache;
_dbContext = dbContext;
}
}
3.4 代码实现示例
3.4.1 基本缓存操作
获取和设置缓存:
public async Task<Product> GetProductAsync(int productId)
{
// 生成缓存键
string cacheKey = $"product:{productId}";
// 尝试从缓存获取
byte[] cachedData = await _cache.GetAsync(cacheKey);
if (cachedData != null)
{
// 缓存命中,反序列化数据
return JsonSerializer.Deserialize<Product>(cachedData);
}
// 缓存未命中,从数据库获取
Product product = await _dbContext.Products
.FirstOrDefaultAsync(p => p.Id == productId);
if (product != null)
{
// 将数据序列化并写入缓存
byte[] serializedData = JsonSerializer.SerializeToUtf8Bytes(product);
var options = new DistributedCacheEntryOptions
{
// 设置绝对过期时间:1小时后过期
AbsoluteExpirationRelativeToNow = TimeSpan.FromHours(1)
};
await _cache.SetAsync(cacheKey, serializedData, options);
}
return product;
}
删除缓存:
public async Task UpdateProductAsync(Product product)
{
// 更新数据库
_dbContext.Products.Update(product);
await _dbContext.SaveChangesAsync();
// 删除缓存
string cacheKey = $"product:{product.Id}";
await _cache.RemoveAsync(cacheKey);
}
3.4.2 封装缓存辅助类
为了简化缓存操作,可以封装一个缓存辅助类:
public class CacheHelper
{
private readonly IDistributedCache _cache;
private readonly ILogger<CacheHelper> _logger;
public CacheHelper(IDistributedCache cache, ILogger<CacheHelper> logger)
{
_cache = cache;
_logger = logger;
}
public async Task<T> GetOrCreateAsync<T>(
string key,
Func<Task<T>> factory,
TimeSpan? expiration = null)
{
// 尝试从缓存获取
byte[] cachedData = await _cache.GetAsync(key);
if (cachedData != null)
{
_logger.LogDebug("Cache hit for key: {Key}", key);
return JsonSerializer.Deserialize<T>(cachedData);
}
_logger.LogDebug("Cache miss for key: {Key}", key);
// 缓存未命中,调用工厂方法获取数据
T value = await factory();
// 将数据写入缓存
byte[] serializedData = JsonSerializer.SerializeToUtf8Bytes(value);
var options = new DistributedCacheEntryOptions
{
AbsoluteExpirationRelativeToNow = expiration ?? TimeSpan.FromHours(1)
};
await _cache.SetAsync(key, serializedData, options);
return value;
}
public async Task RemoveAsync(string key)
{
await _cache.RemoveAsync(key);
_logger.LogDebug("Cache removed for key: {Key}", key);
}
public async Task SetAsync<T>(string key, T value, TimeSpan? expiration = null)
{
byte[] serializedData = JsonSerializer.SerializeToUtf8Bytes(value);
var options = new DistributedCacheEntryOptions
{
AbsoluteExpirationRelativeToNow = expiration ?? TimeSpan.FromHours(1)
};
await _cache.SetAsync(key, serializedData, options);
_logger.LogDebug("Cache set for key: {Key}", key);
}
}
使用缓存辅助类:
public class ProductService
{
private readonly CacheHelper _cache;
private readonly AppDbContext _dbContext;
public ProductService(CacheHelper cache, AppDbContext dbContext)
{
_cache = cache;
_dbContext = dbContext;
}
public async Task<Product> GetProductAsync(int productId)
{
string cacheKey = $"product:{productId}";
return await _cache.GetOrCreateAsync(
cacheKey,
async () => await _dbContext.Products
.FirstOrDefaultAsync(p => p.Id == productId),
TimeSpan.FromHours(1)
);
}
}
3.5 缓存策略设计
3.5.1 缓存键设计
缓存键命名规范:
public static class CacheKeys
{
// 产品缓存
public static string Product(int id) => $"product:{id}";
// 产品列表缓存
public static string ProductList(string filter) => $"product:list:{filter}";
// 用户缓存
public static string User(int id) => $"user:{id}";
// 分类缓存
public static string Category(int id) => $"category:{id}";
}
缓存键设计原则: - 使用冒号分隔不同层级 -
包含业务实体类型 - 包含唯一标识符 - 避免特殊字符和空格
3.5.2 过期策略选择
绝对过期: - 适用于数据更新频率较低的场景 -
适用于数据一致性要求不高的场景 - 示例:产品详情、文章内容
var options = new DistributedCacheEntryOptions
{
AbsoluteExpirationRelativeToNow = TimeSpan.FromHours(1)
};
滑动过期: - 适用于热点数据 -
适用于访问频率不均衡的场景 - 示例:用户会话、购物车
var options = new DistributedCacheEntryOptions
{
SlidingExpiration = TimeSpan.FromMinutes(30)
};
混合过期: - 结合绝对过期和滑动过期 -
适用于需要兼顾性能和一致性的场景 - 示例:推荐列表、搜索结果
var options = new DistributedCacheEntryOptions
{
AbsoluteExpirationRelativeToNow = TimeSpan.FromHours(1),
SlidingExpiration = TimeSpan.FromMinutes(30)
};
3.5.3 缓存预热
在系统启动时预先加载热点数据到缓存:
public class CacheWarmupService : BackgroundService
{
private readonly IDistributedCache _cache;
private readonly AppDbContext _dbContext;
private readonly ILogger<CacheWarmupService> _logger;
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
_logger.LogInformation("Starting cache warmup...");
// 预热热点产品
var hotProducts = await _dbContext.Products
.Where(p => p.IsHot)
.ToListAsync(stoppingToken);
foreach (var product in hotProducts)
{
string cacheKey = CacheKeys.Product(product.Id);
byte[] serializedData = JsonSerializer.SerializeToUtf8Bytes(product);
var options = new DistributedCacheEntryOptions
{
AbsoluteExpirationRelativeToNow = TimeSpan.FromHours(2)
};
await _cache.SetAsync(cacheKey, serializedData, options);
}
_logger.LogInformation("Cache warmup completed: {Count} products", hotProducts.Count);
}
}
3.6 数据一致性保障
3.6.1 缓存更新策略
Cache-Aside模式(旁路缓存):
public async Task<Product> UpdateProductAsync(Product product)
{
// 步骤1:更新数据库
_dbContext.Products.Update(product);
await _dbContext.SaveChangesAsync();
// 步骤2:删除缓存
string cacheKey = CacheKeys.Product(product.Id);
await _cache.RemoveAsync(cacheKey);
// 步骤3:下次读取时重新加载缓存
return product;
}
Write-Through模式(写穿透):
public async Task<Product> UpdateProductAsync(Product product)
{
// 步骤1:更新数据库
_dbContext.Products.Update(product);
await _dbContext.SaveChangesAsync();
// 步骤2:更新缓存
string cacheKey = CacheKeys.Product(product.Id);
byte[] serializedData = JsonSerializer.SerializeToUtf8Bytes(product);
var options = new DistributedCacheEntryOptions
{
AbsoluteExpirationRelativeToNow = TimeSpan.FromHours(1)
};
await _cache.SetAsync(cacheKey, serializedData, options);
return product;
}
Write-Behind模式(写回):
public class ProductService
{
private readonly IDistributedCache _cache;
private readonly AppDbContext _dbContext;
private readonly Channel<Product> _updateChannel;
public ProductService(IDistributedCache cache, AppDbContext dbContext)
{
_cache = cache;
_dbContext = dbContext;
_updateChannel = Channel.CreateUnbounded<Product>();
// 启动后台任务处理缓存更新
_ = Task.Run(ProcessCacheUpdates);
}
public async Task UpdateProductAsync(Product product)
{
// 步骤1:更新数据库
_dbContext.Products.Update(product);
await _dbContext.SaveChangesAsync();
// 步骤2:将更新请求放入队列
await _updateChannel.Writer.WriteAsync(product);
}
private async Task ProcessCacheUpdates()
{
await foreach (var product in _updateChannel.Reader.ReadAllAsync())
{
// 异步更新缓存
string cacheKey = CacheKeys.Product(product.Id);
byte[] serializedData = JsonSerializer.SerializeToUtf8Bytes(product);
var options = new DistributedCacheEntryOptions
{
AbsoluteExpirationRelativeToNow = TimeSpan.FromHours(1)
};
await _cache.SetAsync(cacheKey, serializedData, options);
}
}
}
3.6.2 缓存失效策略
主动失效:
public async Task DeleteProductAsync(int productId)
{
// 删除数据库
var product = await _dbContext.Products.FindAsync(productId);
if (product != null)
{
_dbContext.Products.Remove(product);
await _dbContext.SaveChangesAsync();
}
// 删除缓存
string cacheKey = CacheKeys.Product(productId);
await _cache.RemoveAsync(cacheKey);
// 删除相关缓存(如产品列表)
string listCacheKey = CacheKeys.ProductList("all");
await _cache.RemoveAsync(listCacheKey);
}
批量失效:
public async Task UpdateCategoryAsync(Category category)
{
// 更新数据库
_dbContext.Categories.Update(category);
await _dbContext.SaveChangesAsync();
// 批量删除相关缓存
var tasks = new List<Task>();
// 删除分类详情缓存
tasks.Add(_cache.RemoveAsync(CacheKeys.Category(category.Id)));
// 删除分类下的产品列表缓存
tasks.Add(_cache.RemoveAsync(CacheKeys.ProductList($"category:{category.Id}")));
await Task.WhenAll(tasks);
}
3.7 缓存穿透、击穿、雪崩防护
3.7.1 缓存穿透防护
问题描述:大量请求查询不存在的数据,导致每次请求都绕过缓存直接打到数据库。
解决方案1:空值缓存
public async Task<Product> GetProductAsync(int productId)
{
string cacheKey = CacheKeys.Product(productId);
// 尝试从缓存获取
byte[] cachedData = await _cache.GetAsync(cacheKey);
if (cachedData != null)
{
// 检查是否为空值标记
if (cachedData.Length == 0)
{
return null; // 返回空值
}
return JsonSerializer.Deserialize<Product>(cachedData);
}
// 从数据库获取
Product product = await _dbContext.Products
.FirstOrDefaultAsync(p => p.Id == productId);
if (product == null)
{
// 缓存空值,设置较短的过期时间
await _cache.SetAsync(cacheKey, Array.Empty<byte>(),
new DistributedCacheEntryOptions
{
AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(5)
});
}
else
{
// 缓存实际数据
byte[] serializedData = JsonSerializer.SerializeToUtf8Bytes(product);
await _cache.SetAsync(cacheKey, serializedData,
new DistributedCacheEntryOptions
{
AbsoluteExpirationRelativeToNow = TimeSpan.FromHours(1)
});
}
return product;
}
解决方案2:布隆过滤器
public class BloomFilterCache
{
private readonly IDistributedCache _cache;
private readonly IBloomFilter _bloomFilter;
public async Task<Product> GetProductAsync(int productId)
{
string cacheKey = CacheKeys.Product(productId);
// 步骤1:检查布隆过滤器
if (!_bloomFilter.Contains(productId))
{
// 布隆过滤器判断数据不存在,直接返回null
return null;
}
// 步骤2:尝试从缓存获取
byte[] cachedData = await _cache.GetAsync(cacheKey);
if (cachedData != null)
{
return JsonSerializer.Deserialize<Product>(cachedData);
}
// 步骤3:从数据库获取
Product product = await _dbContext.Products
.FirstOrDefaultAsync(p => p.Id == productId);
if (product != null)
{
// 添加到布隆过滤器
_bloomFilter.Add(productId);
// 缓存数据
byte[] serializedData = JsonSerializer.SerializeToUtf8Bytes(product);
await _cache.SetAsync(cacheKey, serializedData);
}
return product;
}
}
3.7.2 缓存击穿防护
问题描述:某个热点缓存过期时,大量请求同时穿透到数据库。
解决方案:互斥锁
public class ProductService
{
private readonly IDistributedCache _cache;
private readonly IConnectionMultiplexer _redis;
private readonly AppDbContext _dbContext;
public async Task<Product> GetProductAsync(int productId)
{
string cacheKey = CacheKeys.Product(productId);
string lockKey = $"lock:{cacheKey}";
// 尝试从缓存获取
byte[] cachedData = await _cache.GetAsync(cacheKey);
if (cachedData != null)
{
return JsonSerializer.Deserialize<Product>(cachedData);
}
// 获取分布式锁
var db = _redis.GetDatabase();
bool lockAcquired = await db.StringSetAsync(lockKey, "1",
TimeSpan.FromSeconds(10),
When.NotExists);
if (lockAcquired)
{
try
{
// 双重检查:可能在等待期间其他请求已经填充了缓存
cachedData = await _cache.GetAsync(cacheKey);
if (cachedData != null)
{
return JsonSerializer.Deserialize<Product>(cachedData);
}
// 从数据库获取数据
Product product = await _dbContext.Products
.FirstOrDefaultAsync(p => p.Id == productId);
if (product != null)
{
// 缓存数据
byte[] serializedData = JsonSerializer.SerializeToUtf8Bytes(product);
await _cache.SetAsync(cacheKey, serializedData,
new DistributedCacheEntryOptions
{
AbsoluteExpirationRelativeToNow = TimeSpan.FromHours(1)
});
}
return product;
}
finally
{
// 释放锁
await db.KeyDeleteAsync(lockKey);
}
}
else
{
// 等待锁释放后重新获取缓存
await Task.Delay(100);
return await GetProductAsync(productId);
}
}
}
3.7.3 缓存雪崩防护
问题描述:大量缓存同时过期,导致所有请求都穿透到数据库。
解决方案1:随机过期时间
public async Task<Product> GetProductAsync(int productId)
{
string cacheKey = CacheKeys.Product(productId);
// 尝试从缓存获取
byte[] cachedData = await _cache.GetAsync(cacheKey);
if (cachedData != null)
{
return JsonSerializer.Deserialize<Product>(cachedData);
}
// 从数据库获取数据
Product product = await _dbContext.Products
.FirstOrDefaultAsync(p => p.Id == productId);
if (product != null)
{
// 在基础过期时间上增加随机偏移量
var baseExpiration = TimeSpan.FromHours(1);
var randomOffset = TimeSpan.FromSeconds(Random.Shared.Next(0, 300)); // 0-5分钟随机偏移
byte[] serializedData = JsonSerializer.SerializeToUtf8Bytes(product);
await _cache.SetAsync(cacheKey, serializedData,
new DistributedCacheEntryOptions
{
AbsoluteExpirationRelativeToNow = baseExpiration + randomOffset
});
}
return product;
}
解决方案2:多级缓存
结合本地缓存和分布式缓存:
public class ProductService
{
private readonly IDistributedCache _distributedCache;
private readonly IMemoryCache _memoryCache;
private readonly AppDbContext _dbContext;
public async Task<Product> GetProductAsync(int productId)
{
string cacheKey = CacheKeys.Product(productId);
// 步骤1:检查本地缓存
if (_memoryCache.TryGetValue(cacheKey, out Product product))
{
return product;
}
// 步骤2:检查分布式缓存
byte[] cachedData = await _distributedCache.GetAsync(cacheKey);
if (cachedData != null)
{
product = JsonSerializer.Deserialize<Product>(cachedData);
// 回填到本地缓存
_memoryCache.Set(cacheKey, product,
new MemoryCacheEntryOptions
{
AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(10)
});
return product;
}
// 步骤3:从数据库获取
product = await _dbContext.Products
.FirstOrDefaultAsync(p => p.Id == productId);
if (product != null)
{
// 同时写入两级缓存
byte[] serializedData = JsonSerializer.SerializeToUtf8Bytes(product);
await _distributedCache.SetAsync(cacheKey, serializedData,
new DistributedCacheEntryOptions
{
AbsoluteExpirationRelativeToNow = TimeSpan.FromHours(1)
});
_memoryCache.Set(cacheKey, product,
new MemoryCacheEntryOptions
{
AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(10)
});
}
return product;
}
}
3.8 性能优化
3.8.1 批量操作
public async Task<Dictionary<int, Product>> GetProductsAsync(IEnumerable<int> productIds)
{
var tasks = productIds.Select(async id =>
{
string cacheKey = CacheKeys.Product(id);
byte[] cachedData = await _cache.GetAsync(cacheKey);
if (cachedData != null)
{
return (id, JsonSerializer.Deserialize<Product>(cachedData));
}
// 从数据库获取
Product product = await _dbContext.Products
.FirstOrDefaultAsync(p => p.Id == id);
if (product != null)
{
byte[] serializedData = JsonSerializer.SerializeToUtf8Bytes(product);
await _cache.SetAsync(cacheKey, serializedData,
new DistributedCacheEntryOptions
{
AbsoluteExpirationRelativeToNow = TimeSpan.FromHours(1)
});
}
return (id, product);
});
var results = await Task.WhenAll(tasks);
return results.ToDictionary(r => r.id, r => r.Item2);
}
3.8.2 序列化优化
使用高性能序列化器:
public class ProductSerializer
{
public static byte[] Serialize(Product product)
{
// 使用MemoryPack等高性能序列化器
return MemoryPackSerializer.Serialize(product);
}
public static Product Deserialize(byte[] data)
{
return MemoryPackSerializer.Deserialize<Product>(data);
}
}
public async Task<Product> GetProductAsync(int productId)
{
string cacheKey = CacheKeys.Product(productId);
byte[] cachedData = await _cache.GetAsync(cacheKey);
if (cachedData != null)
{
return ProductSerializer.Deserialize(cachedData);
}
Product product = await _dbContext.Products
.FirstOrDefaultAsync(p => p.Id == productId);
if (product != null)
{
byte[] serializedData = ProductSerializer.Serialize(product);
await _cache.SetAsync(cacheKey, serializedData);
}
return product;
}
3.8.3 连接池优化
builder.Services.AddStackExchangeRedisCache(options =>
{
options.Configuration = "localhost:6379";
options.InstanceName = "MyApp_";
options.ConfigurationOptions = new StackExchange.Redis.ConfigurationOptions
{
// 连接超时
ConnectTimeout = 5000,
// 同步超时
SyncTimeout = 5000,
// 连接重试次数
ConnectRetry = 3,
// 连接失败时是否中止
AbortOnConnectFail = false,
// 连接池配置
EndPoints = { "localhost:6379" },
DefaultDatabase = 0,
Password = "",
Ssl = false,
// 连接池大小
MaxDirectMultiplexConnections = 100,
KeepAlive = 180,
ConnectRetry = 3,
ReconnectRetryPolicy = new ExponentialRetry(5000)
};
});
3.9 监控和日志
3.9.1 缓存命中率监控
public class CacheMonitor
{
private readonly IDistributedCache _cache;
private readonly ILogger<CacheMonitor> _logger;
private long _cacheHits;
private long _cacheMisses;
public double HitRate => _cacheHits / (double)(_cacheHits + _cacheMisses);
public async Task<T> GetOrCreateAsync<T>(
string key,
Func<Task<T>> factory,
TimeSpan? expiration = null)
{
byte[] cachedData = await _cache.GetAsync(key);
if (cachedData != null)
{
Interlocked.Increment(ref _cacheHits);
_logger.LogDebug("Cache hit for key: {Key}", key);
return JsonSerializer.Deserialize<T>(cachedData);
}
Interlocked.Increment(ref _cacheMisses);
_logger.LogDebug("Cache miss for key: {Key}", key);
T value = await factory();
byte[] serializedData = JsonSerializer.SerializeToUtf8Bytes(value);
await _cache.SetAsync(key, serializedData,
new DistributedCacheEntryOptions
{
AbsoluteExpirationRelativeToNow = expiration ?? TimeSpan.FromHours(1)
});
return value;
}
public void LogStatistics()
{
_logger.LogInformation(
"Cache statistics - Hits: {Hits}, Misses: {Misses}, Hit Rate: {HitRate:P2}",
_cacheHits, _cacheMisses, HitRate);
}
}
3.9.2 性能监控
public class CachePerformanceMonitor
{
private readonly IDistributedCache _cache;
private readonly ILogger<CachePerformanceMonitor> _logger;
public async Task<T> GetOrCreateAsync<T>(
string key,
Func<Task<T>> factory,
TimeSpan? expiration = null)
{
var stopwatch = Stopwatch.StartNew();
byte[] cachedData = await _cache.GetAsync(key);
if (cachedData != null)
{
stopwatch.Stop();
_logger.LogDebug(
"Cache hit for key: {Key}, Latency: {Latency}ms",
key, stopwatch.ElapsedMilliseconds);
return JsonSerializer.Deserialize<T>(cachedData);
}
stopwatch.Restart();
T value = await factory();
stopwatch.Stop();
_logger.LogDebug(
"Cache miss for key: {Key}, DB Query Latency: {Latency}ms",
key, stopwatch.ElapsedMilliseconds);
stopwatch.Restart();
byte[] serializedData = JsonSerializer.SerializeToUtf8Bytes(value);
await _cache.SetAsync(key, serializedData,
new DistributedCacheEntryOptions
{
AbsoluteExpirationRelativeToNow = expiration ?? TimeSpan.FromHours(1)
});
stopwatch.Stop();
_logger.LogDebug(
"Cache set for key: {Key}, Write Latency: {Latency}ms",
key, stopwatch.ElapsedMilliseconds);
return value;
}
}
四、总结
Redis作为业界领先的分布式缓存解决方案,在.NET技术栈中有着广泛的应用。通过本文的介绍,我们了解到:
4.1 分布式缓存的核心价值
- 解决数据库负载问题:通过缓存热点数据,大幅减少数据库查询
- 提升系统响应速度:内存访问比数据库访问快几个数量级
- 保证数据一致性:跨服务器共享缓存数据,避免数据不一致
- 提升系统可扩展性:支持水平扩展,应对高并发场景
4.2 .NET技术栈的最佳实践
- 使用官方推荐方案:采用
Microsoft.Extensions.Caching.StackExchangeRedis
- 合理设计缓存策略:根据业务场景选择合适的过期策略
- 防护缓存问题:实现穿透、击穿、雪崩的防护机制
- 优化性能:使用批量操作、高性能序列化器、连接池优化
- 监控和日志:建立完善的监控体系,持续优化缓存效果
4.3 实施建议
- 渐进式实施:从核心功能开始,逐步扩展到全系统
- 充分测试:在测试环境充分验证缓存策略和性能
- 持续优化:根据监控数据持续优化缓存配置
- 文档完善:建立完善的缓存使用文档和规范
通过合理使用Redis分布式缓存,可以显著提升.NET应用的性能和可靠性,为系统的稳定运行提供有力保障。
参考文档
Microsoft官方文档
Redis官方文档
技术博客和文章
技术对比文章