第三节:基于Redis来实现分布式锁的封装和多种应用
一. 说明
1 目标
(1) 利用redis封装分布式锁
(2) 支持直接调用(加锁和释放锁) 和 特性的形式
(3) 支持重试机制
2 核心原理
(1) 加锁
使用CSRedisCore包中的 SetAsync(key, value, (int)expiration.TotalSeconds, RedisExistence.Nx),实现加锁。
配置RedisExistence.Nx选项,表示只有在键不存在时才设置成功,即互斥锁。
补充:为什么不使用SetNx指令呢?
A. setnx不支持设置过期时间,还需要配合expire指令,高并发下两个指令分开执行存在原子性问题
B set配置RedisExistence.Nx,合并了上述两个指令为一个原子操作
注:同一个业务,加锁的key相同,value可以设置为guid,这样保证释放锁释放的是自己的。
(2) 释放锁
使用lua脚本,需要同时传入key和value, 确保redis中key获取的value和传入的value相同,再删除,防止删掉其它客户端设置的锁。
补充:释放锁的时候为什么还需要传入value,只传key不行吗?
因为业务中逻辑是:加锁→执行业务→释放锁,假设锁的过期时间是5s,执行业务耗费7s也就是说,业务还未执行完,锁已经释放了。
此时其它进程就能获取锁了,而之前进程执行完业务,去释放锁的时候,如果单纯的根据key来释放,就会把别的线程加的锁给释放了,
造成业务出错,而同时验证key+value,可以保证删除的是该线程自己加的锁。
(3) 重试机制
设置重试次数和间隔时间,本质就是在获取锁失败的时候,先判断是否达到重试次数,没有的话,等待一段时间,再次获取锁
这里封装了TryAcquireLockAsync方法,支持重试。
封装了DistributedLockAttribute特性,也支持重试
3 注意事项
(1) 手动加锁的时候,value设置guid唯一值,保证释放的时候释放的是自己锁,不会释放其它线程的
(2) 特性加锁的时候,在封装的里面,value已经设置为guid了
二. 实操
1 封装DistributedLockService类,里面有加锁(支持重试)、释放锁方法
/// <summary>
/// 基于redis封装分布式锁
/// </summary>
public class DistributedLockService
{
    private readonly CSRedisClient _redisClient;
    public DistributedLockService(CSRedisClient redisClient)
    {
        _redisClient = redisClient;
    }
    /// <summary>
    /// 获取分布式锁
    /// </summary>
    /// <param name="key">锁的键名,用于标识不同的锁</param>
    /// <param name="value">锁的值,通常为唯一标识,如 UUID,用于验证锁的持有者</param>
    /// <param name="expiration">锁的过期时间,避免锁一直被占用</param>
    /// <returns>如果成功获取锁,返回 true;否则返回 false</returns>
    public async Task<bool> TryAcquireLockAsync(string key, string value, TimeSpan expiration)
    {
        // 使用 Redis 的 Set 命令,Nx 选项确保只有在键不存在时才设置成功
        return await _redisClient.SetAsync(key, value, (int)expiration.TotalSeconds, RedisExistence.Nx);
    }
    /// <summary>
    /// 获取分布式锁,支持重试机制
    /// </summary>
    /// <param name="key">锁的键名,用于标识不同的锁</param>
    /// <param name="value">锁的值,通常为唯一标识,如 UUID,用于验证锁的持有者</param>
    /// <param name="expiration">锁的过期时间,避免锁一直被占用</param>
    /// <param name="retryCount">重试次数</param>
    /// <param name="retryInterval">重试间隔</param>
    /// <returns>如果成功获取锁,返回 true;否则返回 false</returns>
    public async Task<bool> TryAcquireLockAsync(string key, string value, TimeSpan expiration, int retryCount = 0, TimeSpan? retryInterval = null)
    {
        int currentRetry = 0;
        while (true)
        {
            // 使用 Redis 的 Set 命令,Nx 选项确保只有在键不存在时才设置成功
            bool lockAcquired = await _redisClient.SetAsync(key, value, (int)expiration.TotalSeconds, RedisExistence.Nx);
            if (lockAcquired)
            {
                return true;
            }
            if (currentRetry >= retryCount)
            {
                return false;
            }
            if (retryInterval.HasValue)
            {
                await Task.Delay(retryInterval.Value);
            }
            currentRetry++;
        }
    }
    /// <summary>
    /// 释放分布式锁
    /// </summary>
    /// <param name="key">锁的键名,用于定位要释放的锁</param>
    /// <param name="value">锁的值,用于验证当前释放操作是否由锁的持有者发起</param>
    /// <returns>如果成功释放锁,返回 true;否则返回 false</returns>
    public async Task<bool> ReleaseLockAsync(string key, string value)
    {
        var script = @"
            if redis.call('get', KEYS[1]) == ARGV[1] then
                return redis.call('del', KEYS[1])
            else
                return 0
            end";
        //删除成功返回1,否则返回0
        var result = Convert.ToInt32(await _redisClient.EvalAsync(script, key, [value]));
        return result == 1;
    }
}
2 封装DistributedLockAttribute 和 DistributedLockRetryAttribute 特性类,可以直接加在action上,支持重试。
注:构造函数中DistributedLockService类的创建,不能采用构造注入的方式,涉及生命周期先后的问题
采用 GetRequiredService 方式,另外program中需要配置 DistributedLockAttribute.SetServiceProvider(app.Services);
DistributedLockAttribute代码
查看代码
 /// <summary>
/// 分布式锁特性类(基于redis)---不支持重试机制
/// 可应用于控制器方法,实现自动获取和释放分布式锁
/// </summary>
[AttributeUsage(AttributeTargets.Method, AllowMultiple = false)]
public class DistributedLockRetryAttribute : ActionFilterAttribute
{
    private readonly string _lockKey;
    private readonly string _lockValue;
    private readonly TimeSpan _expiration;
    private readonly DistributedLockService _distributedLockService;
    private static IServiceProvider _serviceProvider;
    public static void SetServiceProvider(IServiceProvider serviceProvider)
    {
        _serviceProvider = serviceProvider;
    }
    /// <summary>
    /// 构造函数,初始化分布式锁特性
    /// </summary>
    /// <param name="lockKey">锁的键名,用于标识不同的锁</param>
    /// <param name="expirationSeconds">锁的过期时间(秒),避免锁一直被占用</param>
    public DistributedLockRetryAttribute(string lockKey, int expirationSeconds)
    {
        _lockKey = lockKey;
        // 生成唯一的锁值
        _lockValue = Guid.NewGuid().ToString(); //唯一性的锁值,保证释放的时候释放的是自己的
        // 将秒数转换为 TimeSpan
        _expiration = TimeSpan.FromSeconds(expirationSeconds);
        // 创建分布式锁服务实例
        if (_serviceProvider == null)
        {
            throw new InvalidOperationException("Service provider is not set. Call SetServiceProvider method first.");
        }
        _distributedLockService = _serviceProvider.GetRequiredService<DistributedLockService>();
    }
    /// <summary>
    /// 异步执行控制器方法前后的操作,实现锁的获取和释放
    /// </summary>
    /// <param name="context">操作执行上下文,包含请求和响应的相关信息</param>
    /// <param name="next">下一个操作执行委托,用于调用控制器方法</param>
    /// <returns>表示异步操作的任务</returns>
    public override async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
    {
        // 尝试获取锁
        var lockAcquired = await _distributedLockService.TryAcquireLockAsync(_lockKey, _lockValue, _expiration);
        if (lockAcquired)
        {
            try
            {
                // 执行控制器方法
                await next();
            }
            finally
            {
                // 无论控制器方法执行结果如何,都释放锁
                await _distributedLockService.ReleaseLockAsync(_lockKey, _lockValue);
            }
        }
        else
        {
            // 锁获取失败,返回 423 状态码表示资源被锁定
            //context.Result = new StatusCodeResult(423);
            context.Result = new ContentResult() { StatusCode = 423, Content = "锁获取失败,资源被锁定" };
            return;
        }
    }
}
DistributedLockRetryAttribute代码
查看代码
 
/// <summary>
/// 分布式锁特性类(基于redis)---支持重试机制
/// 可应用于控制器方法,实现自动获取和释放分布式锁
/// </summary>
[AttributeUsage(AttributeTargets.Method, AllowMultiple = false)]
public class DistributedLockAttribute : ActionFilterAttribute
{
    private readonly string _lockKey;
    private readonly string _lockValue;
    private readonly TimeSpan _expiration;
    private readonly int _retryCount;         // 重试次数(默认0次,不重试)
    private readonly int _retryIntervalMs;    // 重试间隔(毫秒)
    private readonly DistributedLockService _distributedLockService;
    private static IServiceProvider _serviceProvider;
    public static void SetServiceProvider(IServiceProvider serviceProvider)
    {
        _serviceProvider = serviceProvider;
    }
    /// <summary>
    /// 分布式锁特性(支持重试)
    /// </summary>
    /// <param name="lockKey">锁的键名</param>
    /// <param name="expirationSeconds">锁的过期时间(秒)</param>
    /// <param name="retryCount">重试次数(默认0)</param>
    /// <param name="retryIntervalMs">重试间隔(毫秒,默认100)</param>
    public DistributedLockAttribute(string lockKey, int expirationSeconds, int retryCount = 0, int retryIntervalMs = 100)
    {
        _lockKey = lockKey;
        _lockValue = Guid.NewGuid().ToString();  //唯一性的锁值,保证释放的时候释放的是自己的
        _expiration = TimeSpan.FromSeconds(expirationSeconds);
        _retryCount = retryCount;
        _retryIntervalMs = retryIntervalMs;
        _distributedLockService = _serviceProvider.GetRequiredService<DistributedLockService>();
    }
    /// <summary>
    /// 异步执行控制器方法前后的操作,实现锁的获取和释放
    /// </summary>
    /// <param name="context">操作执行上下文,包含请求和响应的相关信息</param>
    /// <param name="next">下一个操作执行委托,用于调用控制器方法</param>
    /// <returns>表示异步操作的任务</returns>
    public override async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
    {
        // 尝试获取锁
        int currentRetry = 0;
        bool lockAcquired = false;
        // 重试逻辑:主逻辑 + 重试循环
        do
        {
            lockAcquired = await _distributedLockService.TryAcquireLockAsync(_lockKey, _lockValue, _expiration);
            if (!lockAcquired && currentRetry < _retryCount)
            {
                currentRetry++;
                await Task.Delay(_retryIntervalMs); // 异步等待重试间隔
            }
        } 
        while (!lockAcquired && currentRetry <= _retryCount); // 满足重试条件时循环
        if (lockAcquired)
        {
            try
            {
                // 执行控制器方法
                await next();
            }
            finally
            {
                // 无论控制器方法执行结果如何,都释放锁
                await _distributedLockService.ReleaseLockAsync(_lockKey, _lockValue);
            }
        }
        else
        {
            // 锁获取失败,返回 423 状态码表示资源被锁定
            //context.Result = new StatusCodeResult(423);
            context.Result = new ContentResult() { StatusCode = 423, Content = "锁获取失败,资源被锁定" };
            return;
        }
    }
}
3 分别测试 手动加锁、手动加锁支持重试、特性加锁、特性加锁支持重试
三. 测试
(1) 统一条件:锁过期时间设置10s、模拟业务5秒,允许重试3次,每次间隔1s。
(2) 不含重试的两个action,线程1点击,3s后线程2点击,结果线程1获取锁成功,线程2直接返回加锁失败
(3) 含有重试的两个action, 线程1点击,3s后线程2点击,结果线程1获取锁成功,线程2等待3秒后,也获取锁成功
查看代码
    #region 01-手动加锁的测试方法--不含重试
   /// <summary>
   /// 手动加锁的测试方法--不含重试
   /// </summary>
   /// <returns>表示异步操作的任务,返回操作结果的响应</returns>
   [HttpPost]
   public async Task<IActionResult> TestLockManually()
   {
       string lockKey = "manual-test-lock-key";
       // 生成唯一的锁值
       string lockValue = Guid.NewGuid().ToString();
       TimeSpan expiration = TimeSpan.FromSeconds(10);
       // 尝试获取锁
       var lockAcquired = await distributedLockService.TryAcquireLockAsync(lockKey, lockValue, expiration);
       if (lockAcquired)
       {
           try
           {
               // 模拟一些耗时操作
               await Task.Delay(5000);
               return Json(new { status = "ok", msg = "手动加锁,操作完成" });
           }
           finally
           {
               // 释放锁
               await distributedLockService.ReleaseLockAsync(lockKey, lockValue);
           }
       }
       else
       {
           // 锁获取失败,返回 423 状态码表示资源被锁定
           //return StatusCode(423, "手动加锁失败,资源被锁定");
           return Json(new { status = 423, msg = "手动加锁失败,资源被锁定" });
       }
   }
   #endregion
   #region 02-手动加锁的测试方法--支持重试
   /// <summary>
   /// 手动加锁的测试方法--支持重试
   /// </summary>
   /// <returns>表示异步操作的任务,返回操作结果的响应</returns>
   [HttpPost]
   public async Task<IActionResult> TestLockManuallyWithRetry()
   {
       string lockKey = "manual-test-lock-key";
       // 生成唯一的锁值
       string lockValue = Guid.NewGuid().ToString();
       TimeSpan expiration = TimeSpan.FromSeconds(10);
       // 尝试获取锁
       var lockAcquired = await distributedLockService.TryAcquireLockAsync(lockKey, lockValue, expiration, 3, TimeSpan.FromSeconds(1));
       if (lockAcquired)
       {
           try
           {
               // 模拟一些耗时操作
               await Task.Delay(5000);
               return Json(new { status = "ok", msg = "手动加锁,操作完成" });
           }
           finally
           {
               // 释放锁
               await distributedLockService.ReleaseLockAsync(lockKey, lockValue);
           }
       }
       else
       {
           // 锁获取失败,返回 423 状态码表示资源被锁定
           //return StatusCode(423, "手动加锁失败,资源被锁定");
           return Json(new { status = 423, msg = "手动加锁失败,资源被锁定" });
       }
   }
   #endregion
   #region 03-使用特性加锁的测试方法--不含重试
   /// <summary>
   /// 使用特性加锁的测试方法--不含重试
   /// </summary>
   /// <returns>表示异步操作的任务,返回操作结果的响应</returns>
   [HttpPost]
   [DistributedLock("test-lock-key", 10)]
   public async Task<IActionResult> TestLockWithAttribute()
   {
       // 模拟一些耗时操作
       await Task.Delay(5000);
       return Json(new { status = "ok", msg = "使用特性加锁,操作完成" });
   }
   #endregion
   #region 04-使用特性加锁的测试方法--支持重试
   /// <summary>
   /// 使用特性加锁的测试方法--支持重试
   /// </summary>
   /// <returns>表示异步操作的任务,返回操作结果的响应</returns>
   [HttpPost]
   [DistributedLock("test-lock-key", expirationSeconds: 10, retryCount: 3, retryIntervalMs: 1 * 1000)]
   public async Task<IActionResult> TestLockWithRetry()
   {
       await Task.Delay(5000);
       return Json(new { status = "ok", msg = "带重试的锁,操作完成" });
   } 
   #endregion
!
- 作 者 : Yaopengfei(姚鹏飞)
 - 博客地址 : http://www.cnblogs.com/yaopengfei/
 - 声 明1 : 如有错误,欢迎讨论,请勿谩骂^_^。
 - 声 明2 : 原创博客请在转载时保留原文链接或在文章开头加上本人博客地址,否则保留追究法律责任的权利。
 
                    
                
                
            
        
浙公网安备 33010602011771号