使用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实现分布式锁.虽然网上已经有各种介 ...
随机推荐
- 二、activiti工作流-创建25张表
首先我们在eclipse上创建一个maven项目 然后在resources下面创建一个file,并命名问activiti.cfg.xml activiti.cfg.xml的配置内容如下 <?xm ...
- Python - References
01 - Python文档 Python:https://www.python.org/ Documentation:https://docs.python.org/ Standard Library ...
- Liunx服务管理(Centos)
RPM包安装的服务其安装文件是遵循系统默认安装位置,所以可以通过命令快速启动,但源码包的安装是统一放在一个自定义文件夹下,所有其服务要使用绝对路径,但也可以通过软连接方式,让其支持RPM包相同管理方式 ...
- 【log4j2】log4j的升级版log4j2的简单入门使用
1.jar包 log4j-api.jar + log4j-core.jar maven仓库配置: <dependency> <groupId>org.apache. ...
- 国内使用google搜索引擎
百度搜索 "谷歌访问助手",点击第一个搜索结果,如下: 或者直接点击链接:http://www.ggfwzs.com/ ,然后点击相应的浏览器下载谷歌访问助手,解压,将解压好的谷 ...
- 纯手写SpringMVC框架,用注解实现springmvc过程
闲话不多说,直接上代码! 1.第一步,首先搭建如下架构,其中,annotation中放置自己编写的注解,主要包括service controller qualifier RequestMapping ...
- Java核心技术及面试指南 流程控制方面的面试题答案
2.2.5.1 switch语句能否作用在byte上,能否作用在long上,能否作用在String上? 1 switch里可以用char,byte,short,int这些基本类型,以及它们的封装类. ...
- ionic 热更新 cordova-hot-code-push
cordova-hot-code-push ,Cordova热代码推送插件提供了在应用程序中执行基于Web的内容的自动更新的功能.使用此插件可以更新存储在项目的www文件夹中的所有内容. cordov ...
- logstash收集nginx访问日志
logstash收集nginx访问日志 安装nginx #直接yum安装: [root@elk-node1 ~]# yum install nginx -y 官方文档:http://nginx.org ...
- 总结jenkins Android自动打包遇到的坑
一.ndk-build报错 [root@hejianlai-jenkins LearnGradle]# ndk-build /usr/local/android-ndk-r8/ndk-build: / ...