FreeRedis分布式锁实现以及使用
前言
最近公司的小伙伴在准备面试题,随时准备跑路。听到他们正在讨论分布式锁相关知识,便也立即加入了群聊(我也想溜溜球了)。于是有了今天这篇小作文,记录一下知识点,也希望能帮助其他的小伙伴共同学习,共同进步。
场景
本文中的演示 DEMO
, 以下订单减库存为例。
无锁裸奔表现
示例代码:
先来模拟一个库存服务呗!
/// <summary>
/// 模拟库存服务
/// </summary>
public class StockService
{
private static RedisClient cli = new RedisClient("127.0.0.1:6379");
/// <summary>
/// 减库存操作
/// </summary>
/// <param name="goodsCount">商品数</param>
/// <returns></returns>
public bool ReduceStock(int goodsCount)
{
var stockCount = cli.Get<int>("StockCount");
if (stockCount > 0 && stockCount >= goodsCount)
{
stockCount -= goodsCount;
cli.Set("StockCount", stockCount, 10);
Console.WriteLine($"线程Id:{Thread.CurrentThread.ManagedThreadId},抢购成功!库存数:{stockCount}");
return true;
}
Console.WriteLine($"线程Id:{Thread.CurrentThread.ManagedThreadId},抢购失败!");
return false;
}
}
模拟500个并发请求,开始测试。
static void Main(string[] args)
{
var stockService = new StockService();
// 初始化库存
var cli = new RedisClient("127.0.0.1:6379");
cli.Set("StockCount", 10, 10);
// 模拟 500 个并发
Parallel.For(0, 500, (i) => { Task.Run(() => { stockService.ReduceStock(1); }); });
}
执行完成后,结果如下图所示:
我们的库存只有 10 个,截图可见,至少有 29 个请求抢购成功了,出现了超卖的现象。
上分布式锁表现
针对无锁情况下出现的并发问题,如果是单体应用,用 lock
可以解决,但不适用于分布式应用。FreeRedis 中已有现成实现的分布式锁,我们先来看看是如何使用的吧!
修改一下订单服务代码:
/// <summary>
/// 模拟库存服务
/// </summary>
public class StockService
{
private static RedisClient cli = new RedisClient("127.0.0.1:6379");
private static readonly string _distributedLockKey = "DISTRIBUTEDLOCKKEY";
/// <summary>
/// 减库存操作
/// </summary>
/// <param name="goodsCount">商品数</param>
/// <returns></returns>
public bool ReduceStock(int goodsCount)
{
// 取锁
var lockObj = cli.Lock(_distributedLockKey, 1);
if (lockObj != null)
{
var stockCount = cli.Get<int>("StockCount");
if (stockCount > 0 && stockCount >= goodsCount)
{
stockCount -= goodsCount;
cli.Set("StockCount", stockCount, 10);
Console.WriteLine($"线程Id:{Thread.CurrentThread.ManagedThreadId},抢购成功!库存数:{stockCount}");
lockObj.Unlock(); // 解锁
return true;
}
Console.WriteLine($"线程Id:{Thread.CurrentThread.ManagedThreadId},抢购失败!");
lockObj.Unlock(); // 解锁
}
return false;
}
}
执行结果如下所示:
从输出结果中可以看出,库存有序的扣除中,确实只有 10 个请求是抢购成功。
看看 FreeRedis 实现的分布式锁
通过上面示例可以看见,分布式锁的使用无非就是 Lock
和 UnLock
的操作。我这里直接用编辑器调试进去看了,就不是上 GitHub
上下载代码看了。体验不好,还请担待。
上锁
- 循环检测获取锁操作是否过期,过期直接返回
Null
, 否则继续步骤二 SetNx
设置值,如果成功,创建分布式锁对象,否则线程等待一会,继续第一步,如此循环
为啥不可以设置唯一值呢?在没有启动自动续时(看门狗机制),业务执行时间超过了锁的过期时间时,会引发问题。
- 比如说现在
请求1
,请求2
,请求3
同时过来,请求1
先抢到了锁,开始执行。 - 但是
请求1
的业务执行时间比较长,锁已经过期失效了,业务还没有执行完成。这时请求2
获取到锁,执行自己的业务。就出现了请求1
和请求2
并发执行了 - 当
请求1
执行完自己的业务的时候,执行解锁操作,因为键值都一样,会误把请求2
的锁给释放掉,导致故障
通过设置值的唯一,当删除缓存的时候,还需要判断一下值是不是一致,来防止误释放其他锁。
看门狗机制
- 定时执行
Refresh
方法 - 通过
lua
脚本设置新的过期时间,不成功的话(已解锁),删除定时器
解锁
- 通过
lua
脚本匹配键
和值
都一样的key, 才能删除