Redis在Asp.Net Core项目中的实战应用:从缓存到分布式锁
通过前几篇的学习,我们已经掌握了Redis的核心概念和数据持久化。但理论终究要落地到实践。今天,我们将把Redis真正集成到Asp.Net Core项目中,解决真实业务场景中的性能瓶颈和分布式难题。
在现代Web开发中,Redis早已不是可选项,而是构建高性能、高可用系统的必备组件。作为.Net开发者,掌握如何在Asp.Net Core中熟练使用Redis,是你进阶高级开发的必经之路。
一、环境准备:在Asp.Net Core中集成Redis
1. 安装必要的NuGet包
首先,在你的Asp.Net Core项目中安装最常用的Redis客户端:
# 使用Package Manager Console
Install-Package StackExchange.Redis
# 或使用.NET CLI
dotnet add package StackExchange.Redis
为什么选择StackExchange.Redis?
- 高性能且线程安全
- 支持同步和异步操作
- 活跃的社区支持和持续更新
- Microsoft官方推荐
2. 配置Redis服务
在appsettings.json中添加Redis连接字符串:
{
"ConnectionStrings": {
"Redis": "localhost:6379,password=your_password,abortConnect=false,connectTimeout=30000"
},
// 其他配置...
}
在Program.cs中注册Redis服务:
using StackExchange.Redis;
var builder = WebApplication.CreateBuilder(args);
// 添加Redis服务
builder.Services.AddSingleton<IConnectionMultiplexer>(sp =>
ConnectionMultiplexer.Connect(builder.Configuration.GetConnectionString("Redis"))
);
// 注册自定义Redis服务(推荐)
builder.Services.AddScoped<IRedisService, RedisService>();
var app = builder.Build();
3. 创建Redis服务封装
为了更好地使用Redis,我们创建一个服务封装类:
public interface IRedisService
{
Task<T> GetAsync<T>(string key);
Task SetAsync<T>(string key, T value, TimeSpan? expiry = null);
Task<bool> RemoveAsync(string key);
Task<bool> ExistsAsync(string key);
}
public class RedisService : IRedisService
{
private readonly IConnectionMultiplexer _redis;
private readonly IDatabase _database;
public RedisService(IConnectionMultiplexer redis)
{
_redis = redis;
_database = redis.GetDatabase();
}
public async Task<T> GetAsync<T>(string key)
{
var value = await _database.StringGetAsync(key);
if (value.IsNullOrEmpty)
return default;
return JsonSerializer.Deserialize<T>(value);
}
public async Task SetAsync<T>(string key, T value, TimeSpan? expiry = null)
{
var serializedValue = JsonSerializer.Serialize(value);
await _database.StringSetAsync(key, serializedValue, expiry);
}
public async Task<bool> RemoveAsync(string key)
{
return await _database.KeyDeleteAsync(key);
}
public async Task<bool> ExistsAsync(string key)
{
return await _database.KeyExistsAsync(key);
}
}
现在,我们的基础环境已经搭建完成,可以开始实战应用了!
二、缓存实战:提升系统性能的利器
缓存是Redis最经典的应用场景。让我们来看几个实际的例子。
1. 商品信息缓存
假设我们有一个电商系统,商品信息的查询非常频繁:
public interface IProductService
{
Task<Product> GetProductByIdAsync(int productId);
Task UpdateProductAsync(Product product);
}
public class ProductService : IProductService
{
private readonly IProductRepository _productRepository;
private readonly IRedisService _redisService;
private readonly ILogger<ProductService> _logger;
public ProductService(IProductRepository productRepository,
IRedisService redisService,
ILogger<ProductService> logger)
{
_productRepository = productRepository;
_redisService = redisService;
_logger = logger;
}
public async Task<Product> GetProductByIdAsync(int productId)
{
var cacheKey = $"product:{productId}";
// 1. 先查缓存
var product = await _redisService.GetAsync<Product>(cacheKey);
if (product != null)
{
_logger.LogInformation("从缓存获取商品 {ProductId}", productId);
return product;
}
// 2. 缓存不存在,查询数据库
_logger.LogInformation("缓存未命中,从数据库查询商品 {ProductId}", productId);
product = await _productRepository.GetByIdAsync(productId);
if (product == null)
return null;
// 3. 写入缓存,设置30分钟过期
await _redisService.SetAsync(cacheKey, product, TimeSpan.FromMinutes(30));
return product;
}
public async Task UpdateProductAsync(Product product)
{
// 更新数据库
await _productRepository.UpdateAsync(product);
// 删除缓存,保证数据一致性
var cacheKey = $"product:{product.Id}";
await _redisService.RemoveAsync(cacheKey);
_logger.LogInformation("更新商品 {ProductId} 并清除缓存", product.Id);
}
}
缓存策略分析:
- 读取时:先查缓存,命中则返回;未命中查数据库并回写缓存
- 更新时:先更新数据库,再删除缓存(Cache-Aside模式)
- 过期时间:设置合理的过期时间,防止数据长期不更新
2. 在Controller中使用缓存服务
[ApiController]
[Route("api/[controller]")]
public class ProductsController : ControllerBase
{
private readonly IProductService _productService;
public ProductsController(IProductService productService)
{
_productService = productService;
}
[HttpGet("{id}")]
public async Task<ActionResult<Product>> GetProduct(int id)
{
var product = await _productService.GetProductByIdAsync(id);
if (product == null)
return NotFound();
return product;
}
[HttpPut("{id}")]
public async Task<IActionResult> UpdateProduct(int id, Product product)
{
if (id != product.Id)
return BadRequest();
await _productService.UpdateProductAsync(product);
return NoContent();
}
}
三、应对缓存"三剑客":穿透、击穿、雪崩
在实际生产环境中,仅仅实现基础缓存是不够的,我们还需要应对三个经典问题。
1. 缓存穿透:查询不存在的数据
问题:恶意请求查询数据库中不存在的数据,导致请求直接打到数据库。
解决方案:缓存空对象
public async Task<Product> GetProductByIdWithNullCacheAsync(int productId)
{
var cacheKey = $"product:{productId}";
var product = await _redisService.GetAsync<Product>(cacheKey);
if (product != null)
{
// 如果是特殊的空对象标记,返回null
if (product.Id == -1)
return null;
return product;
}
product = await _productRepository.GetByIdAsync(productId);
if (product == null)
{
// 缓存空对象,设置较短的过期时间
var nullProduct = new Product { Id = -1 }; // 特殊标记
await _redisService.SetAsync(cacheKey, nullProduct, TimeSpan.FromMinutes(5));
return null;
}
await _redisService.SetAsync(cacheKey, product, TimeSpan.FromMinutes(30));
return product;
}
2. 缓存击穿:热点Key突然失效
问题:某个热点Key在失效的瞬间,大量请求同时到达数据库。
解决方案:使用互斥锁
public async Task<Product> GetProductWithMutexAsync(int productId)
{
var cacheKey = $"product:{productId}";
var mutexKey = $"mutex:product:{productId}";
// 尝试获取缓存
var product = await _redisService.GetAsync<Product>(cacheKey);
if (product != null)
return product;
// 使用Redis实现分布式锁
var lockToken = Guid.NewGuid().ToString();
var locked = await _redisService.AcquireLockAsync(mutexKey, lockToken, TimeSpan.FromSeconds(5));
if (!locked)
{
// 获取锁失败,稍后重试
await Task.Delay(100);
return await GetProductWithMutexAsync(productId);
}
try
{
// 双重检查,防止重复查询数据库
product = await _redisService.GetAsync<Product>(cacheKey);
if (product != null)
return product;
// 查询数据库
product = await _productRepository.GetByIdAsync(productId);
if (product != null)
{
await _redisService.SetAsync(cacheKey, product, TimeSpan.FromMinutes(30));
}
return product;
}
finally
{
// 释放锁
await _redisService.ReleaseLockAsync(mutexKey, lockToken);
}
}
3. 缓存雪崩:大量Key同时失效
问题:大量缓存Key在同一时间失效,导致所有请求直接访问数据库。
解决方案:设置不同的过期时间
public async Task SetWithRandomExpiryAsync<T>(string key, T value, TimeSpan baseExpiry)
{
// 在基础过期时间上增加随机偏差(±10%)
var random = new Random();
var variance = (int)(baseExpiry.TotalMinutes * 0.1); // 10% 偏差
var actualExpiry = baseExpiry.Add(TimeSpan.FromMinutes(random.Next(-variance, variance)));
await _redisService.SetAsync(key, value, actualExpiry);
}
四、分布式锁:控制分布式环境下的资源访问
在分布式系统中,我们需要控制多个服务实例对共享资源的访问。
1. 扩展Redis服务支持分布式锁
public interface IRedisService
{
// ... 其他方法
Task<bool> AcquireLockAsync(string key, string value, TimeSpan expiry);
Task<bool> ReleaseLockAsync(string key, string value);
Task<bool> ExtendLockAsync(string key, string value, TimeSpan expiry);
}
public class RedisService : IRedisService
{
// ... 其他实现
public async Task<bool> AcquireLockAsync(string key, string value, TimeSpan expiry)
{
// 使用SET NX EX命令原子性地获取锁
return await _database.StringSetAsync(key, value, expiry, When.NotExists);
}
public async Task<bool> ReleaseLockAsync(string key, string value)
{
// 使用Lua脚本保证原子性:只有锁的值匹配时才删除
var luaScript = @"
if redis.call('GET', KEYS[1]) == ARGV[1] then
return redis.call('DEL', KEYS[1])
else
return 0
end";
var result = await _database.ScriptEvaluateAsync(luaScript, new RedisKey[] { key }, new RedisValue[] { value });
return (int)result == 1;
}
public async Task<bool> ExtendLockAsync(string key, string value, TimeSpan expiry)
{
var luaScript = @"
if redis.call('GET', KEYS[1]) == ARGV[1] then
return redis.call('EXPIRE', KEYS[1], ARGV[2])
else
return 0
end";
var result = await _database.ScriptEvaluateAsync(luaScript,
new RedisKey[] { key },
new RedisValue[] { value, (int)expiry.TotalSeconds });
return (bool)result;
}
}
2. 使用分布式锁实现秒杀功能
public class SeckillService
{
private readonly IRedisService _redisService;
private readonly IOrderRepository _orderRepository;
public SeckillService(IRedisService redisService, IOrderRepository orderRepository)
{
_redisService = redisService;
_orderRepository = orderRepository;
}
public async Task<bool> ProcessSeckillAsync(int productId, int userId)
{
var lockKey = $"seckill:lock:{productId}";
var lockValue = Guid.NewGuid().ToString();
var stockKey = $"product:stock:{productId}";
try
{
// 获取分布式锁
var locked = await _redisService.AcquireLockAsync(lockKey, lockValue, TimeSpan.FromSeconds(10));
if (!locked)
return false; // 获取锁失败,稍后重试
// 检查库存
var stock = await _redisService.GetAsync<int>(stockKey);
if (stock <= 0)
return false;
// 扣减库存
await _redisService.SetAsync(stockKey, stock - 1);
// 创建订单
await _orderRepository.CreateAsync(new Order
{
ProductId = productId,
UserId = userId,
CreatedAt = DateTime.UtcNow
});
return true;
}
finally
{
// 释放锁
await _redisService.ReleaseLockAsync(lockKey, lockValue);
}
}
}
五、会话存储:实现分布式Session
在微服务架构中,我们需要在多台服务器之间共享用户会话状态。
1. 配置Redis作为分布式Session存储
在Program.cs中:
// 添加Redis分布式缓存
builder.Services.AddStackExchangeRedisCache(options =>
{
options.Configuration = builder.Configuration.GetConnectionString("Redis");
options.InstanceName = "MyApp_";
});
// 配置Session
builder.Services.AddSession(options =>
{
options.IdleTimeout = TimeSpan.FromMinutes(30);
options.Cookie.HttpOnly = true;
options.Cookie.IsEssential = true;
});
在Controller中使用:
public class AccountController : Controller
{
[HttpPost]
public async Task<IActionResult> Login(LoginModel model)
{
// 验证用户...
var user = await AuthenticateUserAsync(model);
if (user == null)
return Unauthorized();
// 存储用户信息到Session
HttpContext.Session.SetString("UserId", user.Id.ToString());
HttpContext.Session.SetString("UserName", user.UserName);
HttpContext.Session.SetInt32("UserRole", (int)user.Role);
return RedirectToAction("Index", "Home");
}
[HttpGet]
public IActionResult GetUserInfo()
{
if (!HttpContext.Session.TryGetValue("UserId", out _))
return Unauthorized();
var userInfo = new
{
UserId = HttpContext.Session.GetString("UserId"),
UserName = HttpContext.Session.GetString("UserName"),
Role = HttpContext.Session.GetInt32("UserRole")
};
return Ok(userInfo);
}
}
六、消息队列:实现异步任务处理
虽然Redis不是专业的消息队列,但对于简单的场景非常实用。
1. 基于List实现简单消息队列
public interface IMessageQueueService
{
Task PublishAsync<T>(string queueName, T message);
Task<T> ConsumeAsync<T>(string queueName, TimeSpan? timeout = null);
}
public class RedisMessageQueueService : IMessageQueueService
{
private readonly IConnectionMultiplexer _redis;
private readonly IDatabase _database;
public RedisMessageQueueService(IConnectionMultiplexer redis)
{
_redis = redis;
_database = redis.GetDatabase();
}
public async Task PublishAsync<T>(string queueName, T message)
{
var serializedMessage = JsonSerializer.Serialize(message);
await _database.ListLeftPushAsync(queueName, serializedMessage);
}
public async Task<T> ConsumeAsync<T>(string queueName, TimeSpan? timeout = null)
{
var value = await _database.ListRightPopAsync(queueName);
if (value.IsNullOrEmpty)
{
if (timeout.HasValue)
{
// 可以实现阻塞版本的消费
// 这里简化处理,返回默认值
await Task.Delay(timeout.Value);
}
return default;
}
return JsonSerializer.Deserialize<T>(value);
}
}
2. 使用消息队列处理耗时任务
public class EmailService
{
private readonly IMessageQueueService _messageQueue;
private readonly ILogger<EmailService> _logger;
public EmailService(IMessageQueueService messageQueue, ILogger<EmailService> logger)
{
_messageQueue = messageQueue;
_logger = logger;
}
// 发送邮件到队列(非阻塞)
public async Task SendWelcomeEmailAsync(string email, string userName)
{
var emailMessage = new EmailMessage
{
To = email,
Subject = "欢迎注册",
Body = $"亲爱的 {userName},欢迎使用我们的服务!"
};
await _messageQueue.PublishAsync("email_queue", emailMessage);
_logger.LogInformation("欢迎邮件已加入队列,收件人: {Email}", email);
}
}
// 后台服务处理队列中的邮件
public class EmailBackgroundService : BackgroundService
{
private readonly IMessageQueueService _messageQueue;
private readonly IEmailSender _emailSender;
private readonly ILogger<EmailBackgroundService> _logger;
public EmailBackgroundService(IMessageQueueService messageQueue,
IEmailSender emailSender,
ILogger<EmailBackgroundService> logger)
{
_messageQueue = messageQueue;
_emailSender = emailSender;
_logger = logger;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
try
{
var message = await _messageQueue.ConsumeAsync<EmailMessage>("email_queue");
if (message != null)
{
await _emailSender.SendEmailAsync(message.To, message.Subject, message.Body);
_logger.LogInformation("邮件发送成功: {To}", message.To);
}
else
{
// 队列为空,等待一段时间
await Task.Delay(1000, stoppingToken);
}
}
catch (Exception ex)
{
_logger.LogError(ex, "处理邮件队列时发生错误");
await Task.Delay(5000, stoppingToken); // 错误时等待更长时间
}
}
}
}
七、性能优化与最佳实践
1. 连接复用
确保在整个应用程序中复用IConnectionMultiplexer实例:
// 在Program.cs中注册为Singleton
builder.Services.AddSingleton<IConnectionMultiplexer>(sp =>
ConnectionMultiplexer.Connect(builder.Configuration.GetConnectionString("Redis"))
);
2. 使用Pipeline批量操作
public async Task<bool> SetMultipleAsync(Dictionary<string, object> keyValuePairs, TimeSpan? expiry = null)
{
var batch = _database.CreateBatch();
var tasks = new List<Task>();
foreach (var kvp in keyValuePairs)
{
var serializedValue = JsonSerializer.Serialize(kvp.Value);
tasks.Add(batch.StringSetAsync(kvp.Key, serializedValue, expiry));
}
batch.Execute();
await Task.WhenAll(tasks);
return tasks.All(t => t.IsCompletedSuccessfully);
}
3. 合理的序列化选择
// 对于简单类型,考虑使用更高效的序列化方式
public async Task SetStringAsync(string key, string value, TimeSpan? expiry = null)
{
// 对于字符串,直接存储,避免JSON序列化开销
await _database.StringSetAsync(key, value, expiry);
}
public async Task<string> GetStringAsync(string key)
{
return await _database.StringGetAsync(key);
}
总结
通过本篇的实战演练,我们掌握了在Asp.Net Core项目中集成和使用Redis的完整方案:
- 环境搭建:配置StackExchange.Redis客户端
- 缓存应用:商品信息缓存及缓存策略
- 问题解决:应对缓存穿透、击穿、雪崩的完整方案
- 分布式锁:实现秒杀等并发控制场景
- 会话存储:配置分布式Session
- 消息队列:实现异步任务处理
- 性能优化:连接复用、批量操作等最佳实践
关键收获:
- Redis在Asp.Net Core中的集成非常简单直接
- 合理使用缓存可以大幅提升系统性能
- 分布式锁是解决并发问题的利器
- 选择合适的序列化方式对性能有重要影响
现在,你可以自信地在你的Asp.Net Core项目中使用Redis来解决实际的性能瓶颈和分布式协调问题了!
欢迎在评论区分享你在集成过程中遇到的问题和解决方案!