Redis之品鉴之旅(七)
分布式锁
1)阻塞锁:
尝试在redis中创建一个字符串结构缓存,方法传入的key,value为锁的过期时间timeout的时间戳。
若redis中没有这个key,则创建成功(即抢到锁),然后立即返回。
若已经有这个key,则先watch,然后校验value中的时间戳是否已经超过当前时间。
若已超过,则尝试使用提交事务的方式覆盖新的时间戳,事务提交成功(即抢到锁),然后立即返回;
若未超过当前时间或事务提交失败(即被别人抢到锁),
如果没有抢到锁,则进入 一个内部优化过的微循环,不断重试。
//这个是阻塞的锁
public static void Show(int i, string key, TimeSpan timeout)
{
using (var client = new RedisClient("127.0.0.1", 6379, "12345", 10))
{
//尝试在redis中创建一个字符串结构缓存,方法传入的key,value为锁的过期时间timeout的时间戳。
// 加了这句话,下面所有的代码都是单线程的执行
using (var datalock = client.AcquireLock("DataLock:" + key,timeout))
{
//库存数量
var inventory = client.Get<int>("inventoryNum");
if (inventory > 0)
{
client.Set<int>("inventoryNum", inventory - 1);
//订单数量
var orderNum = client.Incr("orderNum");
Console.WriteLine($"{i}抢购成功*****线程id:{ Thread.CurrentThread.ManagedThreadId.ToString("00")},库存:{inventory},订单数量:{orderNum}");
}
else
{
Console.WriteLine($"{i}抢购失败");
}
//client.Remove("DataLock:" + key);
Thread.Sleep(100);
}
}
}
using完成方法调用或者显式调用dispose,都会直接清除key。
AcquireLock这句话对应的redis内部源码:
public RedisLock(IRedisClient redisClient, string key, TimeSpan? timeOut)
{
this.redisClient = redisClient;
this.key = key;
//如果返回fasle,则进入一个内部优化过的微循环,不断重试
ExecUtils.RetryUntilTrue(delegate
{
TimeSpan value = timeOut ?? new TimeSpan(365, 0, 0, 0);
DateTime dateTime = DateTime.UtcNow.Add(value);
string lockString = (dateTime.ToUnixTimeMs() + 1).ToString();
//若redis中没有这个key,则创建成功(即抢到锁),然后立即返回。
if (redisClient.SetValueIfNotExists(key, lockString))
{
return true;
}
//若已经有这个key,则先watch,然后校验value中的时间戳是否已经超过当前时间
redisClient.Watch(key);
if (!long.TryParse(redisClient.Get<string>(key), out long result))
{
redisClient.UnWatch();
return false;
}
//通过检查value中时间戳来判断是否过期,并不是利用redis在key上设置expire time来通过key的过期实现,下面的代码是靠事务实现的,如果key不存在了,事务也就不能使用了,所以这个过期时间使用的是value,而不是set方法设置的过期时间。
//若未超过当前时间(即被别人抢到锁)
if (result > DateTime.UtcNow.ToUnixTimeMs())
{
redisClient.UnWatch();
return false;
}
//若已超过,则尝试使用提交事务的方式覆盖新的时间戳,事务提交成功(即抢到锁),然后立即返回;事务提交失败(即被别人抢到锁)
using (IRedisTransaction redisTransaction = redisClient.CreateTransaction())
{
redisTransaction.QueueCommand((Func<IRedisClient, bool>)((IRedisClient r) => r.Set(key, lockString)));
return redisTransaction.Commit();
}
}, timeOut);//传入的timeout还有一个作用,就是控制重试时间,重试超时后则抛异常。
}
//内部优化过的微循环,不断重试,直到
public static void RetryUntilTrue(Func<bool> action, TimeSpan? timeOut = null)
{
int num = 0;
DateTime utcNow = DateTime.UtcNow;
while (!timeOut.HasValue || DateTime.UtcNow - utcNow < timeOut.Value)
{
num++;
if (action())
{
return;
}
SleepBackOffMultiplier(num);
}
throw new TimeoutException($"Exceeded timeout of {timeOut.Value}");
}
可以看出,timeout有两个意思,1:如果成功加锁后锁的过期时间, :2:未成功加锁后阻塞等待的时间。数据锁服务通过检查value中时间戳来判断是否过期,并不是利用redis在key上设置expire time来通过key的过期实现。
2)非阻塞锁
尝试在redis中创建一个字符串结构缓存项,方法传入的key,value无意义,过期时间为传入的timeout。
若redis中没有这个key,则创建成功(即抢到锁),然后立即返回true。若已经有这个key,则立即返回false。
以上过程为全局单线程原子操作,整个过程为独占式操作。
IsLock可以检测key是否存在。
public static void Show(int i, string key, TimeSpan timeout)
{
using (var client = new RedisClient("127.0.0.1", 6379, "12345", 10))
{
// 非阻塞加锁 如果已经存在当前的key,则执行失败,然后false
// 把这个时间 timeout 设置长不就行了吗,但是你需要悠着点
// 没有完全之策 ,一般在生产环境,给一个不要超过3s就可以
bool isLocked = client.Add<string>("DataLock:" + key, key, timeout);
if (isLocked)
{
try
{
//库存数量
var inventory = client.Get<int>("inventoryNum");
if (inventory > 0)
{
client.Set<int>("inventoryNum", inventory - 1);
//订单数量
var orderNum = client.Incr("orderNum");
Console.WriteLine($"{i}抢购成功*****线程id:{ Thread.CurrentThread.ManagedThreadId.ToString("00")},库存:{inventory},订单数量:{orderNum}");
}
else
{
Console.WriteLine($"{i}抢购失败:原因,没有库存");
}
}
catch
{
throw;
}
finally
{
client.Remove("DataLock:" + key);
}
}
else
{
Console.WriteLine($"{i}抢购失败:原因:没有拿到锁");
}
}
}
注意:
timeout即成功加锁后锁的过期时间
利用redis在key上设置expire time来通过key的过期实现。
不要先用IsLock判断是否有锁再用Add加锁,因为这两个操作非原子性操作,期间会被其他操作干
针对上面的两种情况,非阻塞锁会出现库存卖不完的情况,但是性能比较高。
如果架构中采用微服务的形式,并且使用.net进行开发,肯定会选用上面两种情况实现。根据业务需求进行两种选择就可以了。
阻塞锁在asp.net core 3.1中的应用:demo
Redis之品鉴之旅(七)的更多相关文章
- Redis之品鉴之旅(一)
Redis之品鉴之旅(一) 好知识就如好酒,需要我们坐下来,静静的慢慢的去品鉴.Redis作为主流nosql数据库,在提升性能的方面是不可或缺的.下面就拿好小板凳,我们慢慢的来一一品鉴. 1)redi ...
- Redis之品鉴之旅(六)
持久化 快照的方式(RDB) 文件追加方式(AOF) 快照形式: save和bgsave能快速的备份数据.但是.........., Save命令:将内存数据镜像保存为rdb文件,由于redis是单线 ...
- Redis之品鉴之旅(五)
Redis事务 原子性:就是最小的单位 一致性:好多命令,要么全部执行成功,要么全部执行失败 隔离性:一个会话和另一个会话之间是互相隔离的 持久性:执行了就执行了,数据保存在硬盘上 典型例子:银行转账 ...
- Redis之品鉴之旅(二)
2)hash类型,上代码 using (RedisClient client = new RedisClient("127.0.0.1", 6379, "12345&qu ...
- Redis之品鉴之旅(四)
发布订阅,简单场景下的发布订阅完全可以使用. 可以简单的理解,将一个公众号视为发布者,关注公众号的人视作订阅者,公众号发布一条文章或者消息,凡事订阅公众号的都可以收到消息.一个人可以订阅多个公众号,一 ...
- Redis之品鉴之旅(三)
3)Set,可以去重的.无序的集合.可以取交集.并集.zset(sorted set),有序的.去重的集合,排序不是根据value排序,而是根据score排序. using (RedisClient ...
- redis成长之路——(七)
扩展性封装 虽说现在StackExchange.Redis免费,万一到时候和servicestack.redis一样要收费呢,所以先留一口,后续的可以再处理 实例代码点击这里查看 redis成长之路- ...
- Java8之旅(七) - 函数式备忘录模式优化递归
前言 在上一篇开始Java8之旅(六) -- 使用lambda实现Java的尾递归中,我们利用了函数的懒加载机制实现了栈帧的复用,成功的实现了Java版本的尾递归,然而尾递归的使用有一个重要的条件就是 ...
- Redis 设计与实现 (七)--事务
事务 *ACID,指数据库事务正确执行的四个基本要素的缩写.包含:原子性(Atomicity).一致性(Consistency).隔离性(Isolation).持久性(Durability) redi ...
随机推荐
- 请问在电脑里PNP是什么意思啊?
PnP(Plug and Play,即插即用)是指用户不必干预计算机的各个外围设备对系统资源的分配,而将这一繁杂的工作交给系统,由系统自身去解决底层硬件资源,包括IRQ(中断请求).I/O(输入输出端 ...
- 关于Junit中Assert已经过时
在junit4.12之后,Assert就过时了,提供了TestCase来取代: 同样在TestCase中原本比较常见的一些方法也已经取消了,例如:assertNotEquals.assertThat. ...
- Servlet的特点及运行过程
- python·那些不值钱的经验
时间:2018-11-22 整理:byzqy python读写文本文件 1 # -*- coding: utf-8 -*- 2 3 def read_file(file): 4 with open(f ...
- OpenCV 传统分割测试
github官网源文件:https://github.com/opencv/opencv/tree/master/samples/python 最好是先克隆整个仓库下来,再测试里面的:floodfil ...
- 发布 mbtiles 存储的矢量瓦片
之前我们分享过如何 在本地发布OSM矢量瓦片地图,里面介绍了生成的矢量瓦片会存放在 .mbtiles 文件中,然后用 tileserver-gl 软件发布. mbtiles 是基于sqllite数据库 ...
- Spring系列之事物是如何管理的
前言 我们都知道Spring给我们提供了很多抽象,比如我们在操作数据库的过程中,它为我们提供了事物方面的抽象,让我们可以非常方便的以事物方式操作数据库.不管你用JDBC.Mybatis.Hiberna ...
- 大厂的 SDK 写法,偷学到了!
自己动手写 SDK 的经验技巧分享 大家好,我是鱼皮. 最近因为工作需要,自己动手写了一些项目的通用 SDK.在编写的过程中,我阅读和参考了不少公司中其他大佬写的 SDK,也总结了一些开发 SDK 的 ...
- 新来的前端小姐姐问:Vue路由history模式刷新页面出现404问题
摘要:vue-router 默认 hash 模式 -- 使用 URL 的 hash 来模拟一个完整的 URL,于是当 URL 改变时,页面不会重新加载. 本文分享自华为云社区<学习Vue Rou ...
- API:获取当前用户的公网IP
在vue项目根目录下" public " 文件夹中的index.html,也就是根节点所在的文件引入JS,vue项目中静态文件需要在这里引入,用 script 标签规避跨域 < ...