第三节:基于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 : 原创博客请在转载时保留原文链接或在文章开头加上本人博客地址,否则保留追究法律责任的权利。
 
posted @ 2025-05-07 15:08  Yaopengfei  阅读(98)  评论(1)    收藏  举报