在 .NET Core中如何使用 Redis 创建分布式锁

在 .NET Core WebApi 中使用 Redis 创建分布式锁可以通过 StackExchange.Redis 库来实现。分布式锁用于确保在分布式系统中,同一时间只有一个进程可以执行某段代码。

1. 场景描述

在支付系统中,可能会出现以下并发问题:

  • 用户同时发起多次支付请求,导致重复扣款。
  • 多个请求同时处理同一个订单,导致数据不一致。

通过分布式锁,可以确保同一时间只有一个请求能够执行关键操作(如扣款)。


2. 实现步骤

2.1 安装 StackExchange.Redis

首先,安装 Redis 客户端库:

复制代码
dotnet add package StackExchange.Redis

2.2 配置 Redis 连接

appsettings.json 中添加 Redis 连接字符串:

复制代码
{
  "ConnectionStrings": {
    "Redis": "localhost:6379"
  }
}

2.3 创建分布式锁工具类

创建一个工具类来封装 Redis 分布式锁的逻辑:

复制代码
using StackExchange.Redis;
using System;
using System.Threading.Tasks;

public class RedisDistributedLock
{
    private readonly IDatabase _redisDatabase;
    private readonly string _lockKey;
    private readonly string _lockValue;
    private readonly TimeSpan _expiry;

    public RedisDistributedLock(IDatabase redisDatabase, string lockKey, string lockValue, TimeSpan expiry)
    {
        _redisDatabase = redisDatabase;
        _lockKey = lockKey;
        _lockValue = lockValue;
        _expiry = expiry;
    }

    public async Task<bool> AcquireLockAsync()
    {
        // 尝试设置锁,仅当键不存在时才成功
        return await _redisDatabase.StringSetAsync(_lockKey, _lockValue, _expiry, When.NotExists);
    }

    public async Task ReleaseLockAsync()
    {
        // 使用 Lua 脚本确保只有锁的持有者才能释放锁
        var luaScript = @"
            if redis.call('GET', KEYS[1]) == ARGV[1] then
                return redis.call('DEL', KEYS[1])
            else
                return 0
            end";

        await _redisDatabase.ScriptEvaluateAsync(luaScript, new RedisKey[] { _lockKey }, new RedisValue[] { _lockValue });
    }
}

2.4 在 Web API 中使用分布式锁

在 Web API 的控制器中使用分布式锁来确保支付操作的原子性。

2.4.1 注册 Redis 服务

Startup.csProgram.cs 中注册 Redis 服务:

复制代码
// 添加 Redis 服务
builder.Services.AddSingleton<IConnectionMultiplexer>(sp =>
ConnectionMultiplexer.Connect(builder.Configuration.GetConnectionString("Redis")));
2.4.2 创建支付控制器

Controllers 文件夹中创建一个 PaymentController,并在其中使用分布式锁:

复制代码
using Microsoft.AspNetCore.Mvc;
using StackExchange.Redis;
using System;
using System.Threading.Tasks;

    [ApiController]
    [Route("api/[controller]")]
    public class PaymentController : ControllerBase
    {
        private readonly IDatabase _redisDatabase;

        public PaymentController(IConnectionMultiplexer redis)
        {
            _redisDatabase = redis.GetDatabase();
        }

        [HttpPost("pay")]
        public async Task<IActionResult> ProcessPayment([FromBody] PaymentRequest request)
        {
            // 创建分布式锁
            var lockKey = $"PaymentLock:{request.OrderId}"; // 锁的键,基于订单 ID
            var lockValue = Guid.NewGuid().ToString(); // 锁的值,确保唯一性
            var expiry = TimeSpan.FromSeconds(10); // 锁的过期时间

            var distributedLock = new RedisDistributedLock(_redisDatabase, lockKey, lockValue, expiry);

            try
            {
                // 尝试获取锁
                if (await distributedLock.AcquireLockAsync())
                {
                    Console.WriteLine("已获取锁,正在处理付款...");

                    // 模拟支付处理
                    bool paymentSuccess = await ProcessPaymentAsync(request.UserId, request.OrderId, request.Amount);

                    if (paymentSuccess)
                    {
                        return Ok(new { Message = "付款成功!" });
                    }
                    else
                    {
                        return BadRequest(new { Message = "付款失败!" });
                    }
                }
                else
                {
                    return Conflict(new { Message = "正在处理此订单的另一个付款请求..." });
                }
            }
            finally
            {
                // 释放锁
                await distributedLock.ReleaseLockAsync();
            }
        }

      
    }

3. 代码说明

3.1 分布式锁的实现

  • AcquireLockAsync: 使用 RedisSET key value NX EX 命令尝试获取锁。NX 表示仅在键不存在时设置,`EX 设置键的过期时间。
  • ReleaseLockAsync: 使用 Lua 脚本确保只有锁的持有者才能释放锁,避免误删其他请求的锁。

3.2 支付控制器的使用

  • 锁的键 : 使用订单 ID 作为锁的键(如 PaymentLock:202501061410455506968463210),确保同一订单的支付请求串行化。
  • 锁的值: 使用 GUID 作为锁的值,确保锁的唯一性。
  • 锁的过期时间: 设置合理的过期时间(如 10 秒),防止锁被长时间占用。

3.3 支付处理逻辑

  • ProcessPaymentAsync: 模拟支付处理逻辑,包括调用支付网关、扣减余额等操作。

4. 测试 API

4.1 启动 Web API

运行项目,启动 Web API。

4.2 发送支付请求

使用工具(如 Postman 或 curl)发送支付请求:

复制代码
POST /api/payment/pay
Content-Type: application/json

{
    "userId": "9527",
    "orderId": "202501061410455506968463210"
    }

4.3 测试并发场景

同时发送多个相同的支付请求,观察是否只有一个请求能够成功获取锁并处理支付。

null

5. 注意事项

  1. 锁的粒度:

    • 锁的粒度要适中。如果锁的粒度过大(如全局锁),可能导致性能问题;如果粒度过小,可能增加复杂性。
    • 在支付系统中,通常以订单 ID 或用户 ID 作为锁的粒度。
  2. 锁的过期时间:

    • 设置合理的过期时间,避免锁被长时间占用导致死锁。
    • 如果业务逻辑执行时间较长,可以动态延长锁的过期时间。
  3. 锁的可靠性:

    • Redis 需要高可用,否则可能导致锁失效。可以使用 Redis 集群或 Redlock 算法提高可靠性。
  4. 异常处理:

    • 确保锁的释放操作放在 finally 块中,避免因异常导致锁无法释放。
  5. 幂等性:

    • 支付系统需要支持幂等性,即使多次请求,也只会产生一次扣款。

6. 总结

在 .NET Core Web API 中使用 Redis 创建分布式锁,可以带来以下好处:

  • 解决并发问题,确保数据一致性。
  • 提高系统的可靠性和性能。
  • 简化代码逻辑,降低开发复杂度。
  • 支持高并发、分布式环境和高可用需求。

通过合理使用 Redis 分布式锁,可以构建高可靠、高性能的分布式系统,满足复杂的业务需求。

相关推荐
黑贝是条狗3 天前
对.net 的改变
.net core
小吴同学·7 天前
NET6 WebApi第5讲:中间件(源码理解,俄罗斯套娃怎么来的?);Web 服务器 (Nginx / IIS / Kestrel)、WSL、SSL/TSL
中间件·c#·.net·.netcore·.net core
坐望云起9 天前
ASP.NET Web的 Razor Pages应用,配置热重载,解决.NET Core MVC 页面在更改后不刷新
前端·后端·asp.net·mvc·.net core·razor pages
代码拾光21 天前
.NET Core 中如何实现缓存的预热?
.net core
EdisonZhou1 个月前
基于Microsoft.Extensions.AI核心库实现RAG应用
llm·aigc·.net core
时光追逐者1 个月前
一个开源且免费的 .NET CMS 和应用程序框架
开源·c#·.net·cms·.net core
EdisonZhou1 个月前
基于Microsoft.Extensions.VectorData实现语义搜索
llm·aigc·.net core
时光追逐者1 个月前
推荐几款开源免费的 .NET MAUI 组件库
microsoft·开源·c#·.net·.net core·maui
EdisonZhou1 个月前
.NET程序员AI开发基座:Microsoft.Extensions.AI
aigc·.net core