第三节:基于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号