使用Redis作为分布式锁的一些注意点
Redis实现分布式锁
最近看分布式锁的过程中看到一篇不错的文章,特地的加工一番自己的理解:
Redis分布式锁实现的三个核心要素:
1.加锁
最简单的方法是使用setnx命令。key是锁的唯一标识,按业务来决定命名,value为当前线程的线程ID。
比如想要给一种商品的秒杀活动加锁,可以给key命名为 “lock_sale_ID” 。而value设置成什么呢?我们可以姑且设置成1。加锁的伪代码如下:
setnx(key,1)当一个线程执行setnx返回1,说明key原本不存在,该线程成功得到了锁,当其他线程执行setnx返回0,说明key已经存在,该线程抢锁失败。
2.解锁
有加锁就得有解锁。当得到锁的线程执行完任务,需要释放锁,以便其他线程可以进入。释放锁的最简单方式是执行del指令,伪代码如下:
del(key)释放锁之后,其他线程就可以继续执行setnx命令来获得锁。
3.锁超时
锁超时是什么意思呢?如果一个得到锁的线程在执行任务的过程中挂掉,来不及显式地释放锁,这块资源将会永远被锁住,别的线程再也别想进来。
所以,setnx的key必须设置一个超时时间,以保证即使没有被显式释放,这把锁也要在一定时间后自动释放。setnx不支持超时参数,所以需要额外的指令,伪代码如下:
expire(key, 30)综合起来,我们分布式锁实现的第一版伪代码如下:
if(setnx(key,1) == 1){
expire(key,30)
try {
do something ......
}catch()
{
}
finally {
del(key)
} }
因为上面的伪代码中,存在着三个致命问题:
1. setnx和expire的非原子性
设想一个极端场景,当某线程执行setnx,成功得到了锁:
setnx刚执行成功,还未来得及执行expire指令,节点1 Duang的一声挂掉了。
if(setnx(key,1) == 1){
//此处挂掉了.....
expire(key,30)
try {
do something ......
}catch()
{
}
finally {
del(key)
} }
这样一来,这把锁就没有设置过期时间,变得“长生不老”,别的线程再也无法获得锁了。
怎么解决呢?setnx指令本身是不支持传入超时时间的,Redis 2.6.12以上版本为set指令增加了可选参数,伪代码如下:set(key,1,30,NX),这样就可以取代setnx指令。
2. 超时后使用del 导致误删其他线程的锁
又是一个极端场景,假如某线程成功得到了锁,并且设置的超时时间是30秒。
如果某些原因导致线程A执行的很慢很慢,过了30秒都没执行完,这时候锁过期自动释放,线程B得到了锁。
随后,线程A执行完了任务,线程A接着执行del指令来释放锁。但这时候线程B还没执行完,线程A实际上删除的是线程B加的锁。
怎么避免这种情况呢?可以在del释放锁之前做一个判断,验证当前的锁是不是自己加的锁。
至于具体的实现,可以在加锁的时候把当前的线程ID当做value,并在删除之前验证key对应的value是不是自己线程的ID。
加锁:
String threadId = Thread.currentThread().getId()
set(key,threadId ,30,NX)
doSomething..... 解锁:
if(threadId .equals(redisClient.get(key))){
del(key)
}
但是,这样做又隐含了一个新的问题,if判断和释放锁是两个独立操作,不是原子性。
我们都是追求极致的程序员,所以这一块要用Lua脚本来实现:
String luaScript = 'if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end';
redisClient.eval(luaScript , Collections.singletonList(key), Collections.singletonList(threadId));
这样一来,验证和删除过程就是原子操作了。
3. 出现并发的可能性
还是刚才第二点所描述的场景,虽然我们避免了线程A误删掉key的情况,但是同一时间有A,B两个线程在访问代码块,仍然是不完美的。
怎么办呢?我们可以让获得锁的线程开启一个守护线程,用来给快要过期的锁“续航”。
当过去了29秒,线程A还没执行完,这时候守护线程会执行expire指令,为这把锁“续命”20秒。守护线程从第29秒开始执行,每20秒执行一次。
当线程A执行完任务,会显式关掉守护线程。
另一种情况,如果节点1 忽然断电,由于线程A和守护线程在同一个进程,守护线程也会停下。这把锁到了超时的时候,没人给它续命,也就自动释放了。
memcache实现分布式锁
首页top 10, 由数据库加载到memcache缓存n分钟
微博中名人的content cache, 一旦不存在会大量请求不能命中并加载数据库
需要执行多个IO操作生成的数据存在cache中, 比如查询db多次
问题
在大并发的场合,当cache失效时,大量并发同时取不到cache,会同一瞬间去访问db并回设cache,可能会给系统带来潜在的超负荷风险。我们曾经在线上系统出现过类似故障。
解决方法
if (memcache.get(key) == null) {
// 3 min timeout to avoid mutex holder crash
if (memcache.add(key_mutex, 3 * 60 * 1000) == true) {
value = db.get(key);
memcache.set(key, value);
memcache.delete(key_mutex);
} else { sleep(50);
retry();
}
}
在load db之前先add一个mutex key, mutex key add成功之后再去做加载db, 如果add失败则sleep之后重试读取原cache数据。为了防止死锁,mutex key也需要设置过期时间。伪代码如下
Zookeeper实现分布式缓存
Zookeeper的数据存储结构就像一棵树,这棵树由节点组成,这种节点叫做Znode
。
Znode
分为四种类型:
- 1.
持久节点
(PERSISTENT)
默认的节点类型。创建节点的客户端与zookeeper断开连接后,该节点依旧存在 。
- 2.
持久节点顺序节点
(PERSISTENT_SEQUENTIAL)
所谓顺序节点,就是在创建节点时,Zookeeper根据创建的时间顺序给该节点名称进行编号:
- 3.
临时节点
(EPHEMERAL)
和持久节点相反,当创建节点的客户端与zookeeper断开连接后,临时节点会被删除:
- 4.
临时顺序节点
(EPHEMERAL_SEQUENTIAL)
顾名思义,临时顺序节点结合和临时节点和顺序节点的特点:在创建节点时,Zookeeper根据创建的时间顺序给该节点名称进行编号;当创建节点的客户端与zookeeper断开连接后,临时节点会被删除。
Zookeeper分布式锁恰恰应用了临时顺序节点。具体如何实现呢?让我们来看一看详细步骤:
- 获取锁
首先,在Zookeeper当中创建一个持久节点ParentLock
。当第一个客户端想要获得锁时,需要在ParentLock
这个节点下面创建一个临时顺序节点 Lock1
。
之后,Client1
查找ParentLock
下面所有的临时顺序节点并排序,判断自己所创建的节点Lock1
是不是顺序最靠前的一个。如果是第一个节点,则成功获得锁。
这时候,如果再有一个客户端 Client2
前来获取锁,则在ParentLock
下载再创建一个临时顺序节点Lock2
。
Client2
查找ParentLock
下面所有的临时顺序节点并排序,判断自己所创建的节点Lock2
是不是顺序最靠前的一个,结果发现节点Lock2
并不是最小的。
于是,Client2
向排序仅比它靠前的节点Lock1
注册Watcher
,用于监听Lock1
节点是否存在。这意味着Client2
抢锁失败,进入了等待状态。
这时候,如果又有一个客户端Client3
前来获取锁,则在ParentLock
下载再创建一个临时顺序节点Lock3
。
Client3
查找ParentLock
下面所有的临时顺序节点并排序,判断自己所创建的节点Lock3
是不是顺序最靠前的一个,结果同样发现节点Lock3
并不是最小的。
于是,Client3
向排序仅比它靠前的节点Lock2
注册Watcher
,用于监听Lock2
节点是否存在。这意味着Client3
同样抢锁失败,进入了等待状态。
这样一来,Client1
得到了锁,Client2
监听了Lock1
,Client3
监听了Lock2
。这恰恰形成了一个等待队列,很像是Java当中ReentrantLock
所依赖的AQS(AbstractQueuedSynchronizer)
。
- 释放锁
释放锁分为两种情况:
1.任务完成,客户端显示释放
当任务完成时,Client1
会显示调用删除节点Lock1
的指令。
2.任务执行过程中,客户端崩溃
获得锁的Client1
在任务执行过程中,如果Duang的一声崩溃,则会断开与Zookeeper服务端的链接。根据临时节点的特性,相关联的节点Lock1
会随之自动删除。
由于Client2
一直监听着Lock1
的存在状态,当Lock1
节点被删除,Client2
会立刻收到通知。这时候Client2
会再次查询ParentLock
下面的所有节点,确认自己创建的节点Lock2
是不是目前最小的节点。如果是最小,则Client2
顺理成章获得了锁。
同理,如果Client2
也因为任务完成或者节点崩溃而删除了节点Lock2
,那么Cient3
就会接到通知。
最终,Client3
成功得到了锁。
Zookeeper和Redis分布式锁的比较
下面的表格总结了Zookeeper和Redis分布式锁的优缺点:
使用Redis作为分布式锁的一些注意点的更多相关文章
- 基于redis 实现分布式锁的方案
在电商项目中,经常有秒杀这样的活动促销,在并发访问下,很容易出现上述问题.如果在库存操作上,加锁就可以避免库存卖超的问题.分布式锁使分布式系统之间同步访问共享资源的一种方式 基于redis实现分布式锁 ...
- 用Redis构建分布式锁-RedLock(真分布)
在不同进程需要互斥地访问共享资源时,分布式锁是一种非常有用的技术手段. 有很多三方库和文章描述如何用Redis实现一个分布式锁管理器,但是这些库实现的方式差别很大,而且很多简单的实现其实只需采用稍微增 ...
- 用Redis实现分布式锁 与 实现任务队列(转)
这一次总结和分享用Redis实现分布式锁 与 实现任务队列 这两大强大的功能.先扯点个人观点,之前我看了一篇博文说博客园的文章大部分都是分享代码,博文里强调说分享思路比分享代码更重要(貌似大概是这个意 ...
- 利用多写Redis实现分布式锁原理与实现分析(转)
利用多写Redis实现分布式锁原理与实现分析 一.关于分布式锁 关于分布式锁,可能绝大部分人都会或多或少涉及到. 我举二个例子:场景一:从前端界面发起一笔支付请求,如果前端没有做防重处理,那么可能 ...
- Redis实现分布式锁
http://redis.io/topics/distlock 在不同进程需要互斥地访问共享资源时,分布式锁是一种非常有用的技术手段. 有很多三方库和文章描述如何用Redis实现一个分布式锁管理器,但 ...
- 基于redis的分布式锁
<?php /** * 基于redis的分布式锁 * * 参考开源代码: * http://nleach.com/post/31299575840/redis-mutex-in-php * * ...
- Redis实现分布式锁与任务队列
Redis实现分布式锁 与 实现任务队列 这一次总结和分享用Redis实现分布式锁 与 实现任务队列 这两大强大的功能.先扯点个人观点,之前我看了一篇博文说博客园的文章大部分都是分享代码,博文里强调说 ...
- 使用Redis实现分布式锁
在天猫.京东.苏宁等等电商网站上有很多秒杀活动,例如在某一个时刻抢购一个原价1999现在秒杀价只要999的手机时,会迎来一个用户请求的高峰期,可能会有几十万几百万的并发量,来抢这个手机,在高并发的情形 ...
- 基于Redis实现分布式锁(1)
转自:http://blog.csdn.net/ugg/article/details/41894947 背景在很多互联网产品应用中,有些场景需要加锁处理,比如:秒杀,全局递增ID,楼层生成等等.大部 ...
- redis咋么实现分布式锁,redis分布式锁的实现方式,redis做分布式锁 积极正义的少年
前言 分布式锁一般有三种实现方式:1. 数据库乐观锁:2. 基于Redis的分布式锁:3. 基于ZooKeeper的分布式锁.本篇博客将介绍第二种方式,基于Redis实现分布式锁.虽然网上已经有各种介 ...
随机推荐
- log4j日志输出到日志文件中和控制台中 +log4j配置详解
1.引入log4j的jar包 https://mvnrepository.com/,可以找到log4j的jar和依赖. 2.创建log4j.properties,并配置log4j #设置日志的级别 , ...
- Python中使用PyCharm为函数及参数增加注释
在函数名中键入数遍光标,左上角亮起小灯泡,点击小灯泡 选中第二行内容 在"""后添加函数注释,以及参数注释 然后再引用函数时,选中函数,Ctrl q 即可显示函数以及参 ...
- HBase之HRegionServer启动(含与HMaster交互)
在我的博文<HBase——HMaster启动之一>.<HBase——HMaster启动之二>中已经详细介绍过HMaster在启动过程中调用的各种方法.下面,单就HRegionS ...
- Windows下安装及使用NVM
Windows下安装及使用NVM 所谓nvm就是一个可以让你在同一台机器上安装和切换不同版本node的工具.这里是一篇安装及使用教程. 第一步:下载nvm 可以到github上下载最新版本https: ...
- Jexus~mono中使用StackExchange.redis的问题
在windows平台的vs里,添加包包时,可以选择StackExchange.redis,而如果你的应该程序需要部署到linux的mono环境上,使用这个StackExchange.redis是不行的 ...
- Jenkins 忘记admin密码拯救方法
突然有一日发现自己忘记了jenkins的管理员密码,因为我一直登录的是另外一个非管理员账户.如果出现必须要使用管理员账户操作的,比如用户管理那里的,必须要管理员账号吧,这就尴尬了. 很方的我打开安装j ...
- 使用Docker发布DNC项目
项目结构 可以忽略中间三个Console项目 ApiCenter 是一个WebAPI项目,引用了NLog.MQ项目 ApiCenter使用5001端口 public static IWebHost B ...
- TypeError: Cannot red property 'style' of null 错误解决
错误信息如下: JSP代码如下: <c:if test ="${not empty excelErrors}"> <div id="excelError ...
- 如何走上更高平台分享传递干货知识:(开通个人Github面向开源及私有软件项目的托管平台:https://github.com/zlslch/)(图文详解)(博主推荐)
不多说,直接上干货! https://github.com/ 欢迎大家,加入我的微信公众号:大数据躺过的坑 人工智能躺过的坑 同时,大家可以关注我的个人博客: http ...
- ES6进阶之路
1.说出至少5个ES6的新特性,并简述它们的作用. . let关键字,用于声明只在块级作用域起作用的变量. . const关键字,用于声明一个常量. . 结构赋值,一种新的变量赋值方式.常用于交换变量 ...