忘忧的小站

  • 首页
  • 文章归档
  • 日志
  • 关于页面

  • 搜索
异常处理 AutoWrapper 入门 NoSql 数据库 sqlserver 1 分布式索引 索引 全文搜索 Lucene.Net GPS 音视频 过滤 AOP 时区 升级 ABP.Zero 数据备份 linux 阿里云盘 aliyunpan 面试题 Signalr S 汉字 css html 前端 拼音键盘 在线键盘 uniapp .Net Core XMLRPC Serilog LOKI Nlog 分布式日志 加密 总结 人生 Asp.Net Core Swagger Web Element-plus Quasar 匹配 JavaScript 正则 .Net 后台 架构师 Redis EF CORE MySQL 自考 英语 集群 Jenkins CI/DI 内网穿透 代理 ABP 学习 后端 软考

Redis入门:Redis在项目中的实战应用

发表于 2025-04-05 | 分类于 NoSql | 0 | 阅读次数 16722

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的完整方案:

  1. 环境搭建:配置StackExchange.Redis客户端
  2. 缓存应用:商品信息缓存及缓存策略
  3. 问题解决:应对缓存穿透、击穿、雪崩的完整方案
  4. 分布式锁:实现秒杀等并发控制场景
  5. 会话存储:配置分布式Session
  6. 消息队列:实现异步任务处理
  7. 性能优化:连接复用、批量操作等最佳实践

关键收获:

  • Redis在Asp.Net Core中的集成非常简单直接
  • 合理使用缓存可以大幅提升系统性能
  • 分布式锁是解决并发问题的利器
  • 选择合适的序列化方式对性能有重要影响

现在,你可以自信地在你的Asp.Net Core项目中使用Redis来解决实际的性能瓶颈和分布式协调问题了!

欢迎在评论区分享你在集成过程中遇到的问题和解决方案!

  • 本文作者: 忘忧
  • 本文链接: /archives/2945
  • 版权声明: 本博客所有文章除特别声明外,均采用CC BY-NC-SA 3.0 许可协议。转载请注明出处!
# Redis # NoSql
Redis入门:Redis的持久化与高可用
Redis入门:进阶知识与运维管理
  • 文章目录
  • 站点概览
忘忧

忘忧

君子藏器于身,待时而动,何不利之有

59 日志
9 分类
67 标签
RSS
Github E-mail StackOverflow
Creative Commons
0%
© 2025 忘忧
由 Halo 强力驱动