学习资料:
90分钟完全吃透Redis分布式锁!(C#/.NETCore/.NET Core)B0562_哔哩哔哩_bilibili
0-2是概念
3-4是代码演示
1、什么是分布式锁
普通锁机制:
悲观锁和乐观锁
分布式锁:
为了解决多个进程操作共享资源出现竞争,需要保证数据库的安全性
阻塞锁就是一直等到拿到锁
非阻塞锁就是拿不到锁就不拿了
2、分布式锁能解决什么问题?
为了解决多个进程操作共享资源出现竞争,需要保证数据库的安全性
3、分布式锁有哪些常见实现方式(举例说明)
悲观锁、乐观锁:什么是悲观锁和乐观锁 - 知乎
4、redis分布式锁如何实现?会出现哪些业务问题?
问题1:死锁(程序抛异常,释放资源没成功,导致于1成功了但是key没释放,其人都还在等待)
解决方案1:设置超时,确实解决了死锁问题,但是出现新的问题:误解锁
(误解锁:误删的情况,1释放失败后,然后超时自动释放锁,然后2就会创建锁,此时1回来删1自己的锁的时候可能会把2的锁删除,以此类推3456的锁都会新建然后都被其他人误删除掉)
怎么解决误解锁的问题:
那就谁创建谁删除,每个客户端的锁都有自己的唯一标识。
设计锁重入机制(当客户端创建锁的时候查到了锁,就不用获取锁了,直接跳过锁的机制,直接操作数据库)
但是依然有问题。
只要在多线程操作中出现两行(多行)代码操作redis就会有原子问题
解决方案:
Lua脚本(一个脚本中所有的操作都是原子的)
5、如何使用正确姿势操作redis分布式锁?
可以看视频里的解释,然后还有模仿秒杀的源码:
地址:百度网盘 请输入提取码
提取码:cnmd
分布式锁代码(与源码一致):
#region 秒杀业务测试
private static readonly string redisConnectionStr = "127.0.0.1:6379,connectTimeout=5000,allowAdmin=false,defaultDatabase=1";
/// <summary>
/// 秒杀业务
/// </summary>
private static void TestSeckillDemo()
{
//模拟线程数
var thredNumber = 20;
//秒杀库存数
var stockNumber = 3;//秒杀成功队列key
var key = "order_queue";
//分布式锁key
var nxKey = "orderNX";var csredis = new CSRedisClient(redisConnectionStr);
csredis.Del(key);
var isEnd = false;// 创建秒杀执行信号量集合
List<Task> taskList = new List<Task>();
// 添加计时器
Stopwatch stopwatch = new Stopwatch();
// 开启
stopwatch.Start();
for (int i = 0; i < thredNumber; i++)
{
int number = i;
taskList.Add(Task.Run(() =>
{
Thread.Sleep(50);if (isEnd)
{
Console.WriteLine($"线程{Thread.CurrentThread.ManagedThreadId} - 用户{number} 秒杀失败,抢完了。");
}//设置客户端的标识,用于加锁解锁
var nxSelfMarkvalue = $"thred{Thread.CurrentThread.ManagedThreadId}_user{number}";
//当前线程用户加锁
var setnxResult = csredis.RedisLock(nxKey, nxSelfMarkvalue, 1000);
if (setnxResult)
{
var len = csredis.LLen(key);//获取列表长度
//成功的队列长度>=库存 (库存不足)
if (len >= stockNumber)
{
isEnd = true;
stopwatch.Stop();
Console.WriteLine($"线程{Thread.CurrentThread.ManagedThreadId} - 用户{number} 秒杀失败,抢完了。");
//其实库存不足了,也不用解锁了。如果这里再做解锁操作,其他线程会出现再次加锁,但是返回的还是库存不足。加不加都行
}
else
{
var value = $"线程{Thread.CurrentThread.ManagedThreadId}-用户{number}";
csredis.LPush(key, value);//名单添加到成功队列
//当前线程用户解锁 (nxSelfMarkvalue,防止误解锁)
csredis.RedisUnLock(nxKey, nxSelfMarkvalue);
Console.WriteLine($"线程{Thread.CurrentThread.ManagedThreadId} - 用户{number} 秒杀成功。");
}
}
else
{
Console.WriteLine($"线程{Thread.CurrentThread.ManagedThreadId} - 用户{number} 系统繁忙,请稍后再试。 秒杀失败。");
}
}));
}
// 等待所有秒杀列表中任务结束
Task.WaitAll(taskList.ToArray());
var lenALL = csredis.LLen(key);
Console.WriteLine($"\r\n秒杀成功人数:{lenALL} 人,用时:{stopwatch.ElapsedMilliseconds} 毫秒.");
Console.WriteLine($"\r\n是否超售:{(lenALL > stockNumber ? "是" : "否")}");
Console.WriteLine("\r\n秒杀成功人员名单:");
for (int i = 0; i < stockNumber; i++)
{
Console.WriteLine(csredis.RPop(key));
}
}
#endregion
redis的扩展方法,一个加锁一个解锁的操作
/// <summary>
/// Redis分布式锁
/// </summary>
public static class RedisDistributedLockExtension
{#region 推荐使用
/// <summary>
/// 加锁毫秒级
/// </summary>
/// <param name="client">redis客户端连接</param>
/// <param name="key">锁key</param>
/// <param name="value">锁值</param>
/// <param name="expireMilliSeconds">缓存时间 单位/毫秒 默认1000毫秒</param>
/// <returns></returns>
public static bool RedisLock(this CSRedisClient client, string key, object value, int expireMilliSeconds = 1000)
{
var script = @"local isNX = redis.call('SETNX', KEYS[1], ARGV[1])
if isNX == 1 then
redis.call('PEXPIRE', KEYS[1], ARGV[2])
return 1
end
return 0";return client.Eval(script, key, value, expireMilliSeconds)?.ToString() == "1";
}/// <summary>
/// 解锁
/// </summary>
/// <param name="client">redis客户端连接</param>
/// <param name="key">锁key</param>
/// <param name="selfMark">对应加锁客户端标识</param>
/// <returns></returns>
public static bool RedisUnLock(this CSRedisClient client, string key, string selfMark)
{
var script = @"local getLock = redis.call('GET', KEYS[1])
if getLock == ARGV[1] then
redis.call('DEL', KEYS[1])
return 1
end
return 0";return client.Eval(script, key, selfMark)?.ToString() == "1";
}#endregion
}