jedisLock—redis分布式锁实现
一、使用分布式锁要满足的几个条件:
- 系统是一个分布式系统(关键是分布式,单机的可以使用ReentrantLock或者synchronized代码块来实现)
- 共享资源(各个系统访问同一个资源,资源的载体可能是传统关系型数据库或者NoSQL)
- 同步访问(即有很多个进程同事访问同一个共享资源。没有同步访问,谁管你资源竞争不竞争)
二、应用的场景例子
管理后台的部署架构(多台tomcat服务器+redis【多台tomcat服务器访问一台redis】+mysql【多台tomcat服务器访问一台服务器上的mysql】)就满足使用分布式锁的条件。多台服务器要访问redis全局缓存的资源,如果不使用分布式锁就会出现问题。 看如下伪代码:
- long N=0L;
- //N从redis获取值
- if(N<5){
- N++;
- //N写回redis
- }
上面的代码主要实现的功能:
从redis获取值N,对数值N进行边界检查,自加1,然后N写回redis中。 这种应用场景很常见,像秒杀,全局递增ID、IP访问限制等。以IP访问限制来说,恶意攻击者可能发起无限次访问,并发量比较大,分布式环境下对N的边界检查就不可靠,因为从redis读的N可能已经是脏数据。传统的加锁的做法(如java的synchronized和Lock)也没用,因为这是分布式环境,这个同步问题的救火队员也束手无策。在这危急存亡之秋,分布式锁终于有用武之地了。
分布式锁可以基于很多种方式实现,比如zookeeper、redis...。不管哪种方式,他的基本原理是不变的:用一个状态值表示锁,对锁的占用和释放通过状态值来标识。
这里主要讲如何用redis实现分布式锁。
三、使用redis的setNX命令实现分布式锁
1、实现的原理
Redis为单进程单线程模式,采用队列模式将并发访问变成串行访问,且多客户端对Redis的连接并不存在竞争关系。redis的SETNX命令可以方便的实现分布式锁。
2、基本命令解析
1)setNX(SET if Not eXists)
语法:
- SETNX key value
将 key 的值设为 value ,当且仅当 key 不存在。
若给定的 key 已经存在,则 SETNX 不做任何动作。
SETNX 是『SET if Not eXists』(如果不存在,则 SET)的简写
返回值:
例子:
- redis> EXISTS job # job 不存在
- (integer) 0
- redis> SETNX job "programmer" # job 设置成功
- (integer) 1
- redis> SETNX job "code-farmer" # 尝试覆盖 job ,失败
- (integer) 0
- redis> GET job # 没有被覆盖
- "programmer"
所以我们使用执行下面的命令
- SETNX lock.foo <current Unix time + lock timeout + 1>
如返回1,则该客户端获得锁,把lock.foo的键值设置为时间值表示该键已被锁定,该客户端最后可以通过DEL lock.foo来释放该锁。
如返回0,表明该锁已被其他客户端取得,这时我们可以先返回或进行重试等对方完成或等待锁超时。
2)getSET
语法:
- GETSET key value
将给定 key 的值设为 value ,并返回 key 的旧值(old value)。
当 key 存在但不是字符串类型时,返回一个错误。
返回值:
- GET key
返回值:
四、解决死锁
上面的锁定逻辑有一个问题:如果一个持有锁的客户端失败或崩溃了不能释放锁,该怎么解决?
- 我们可以通过锁的键对应的时间戳来判断这种情况是否发生了,如果当前的时间已经大于lock.foo的值,说明该锁已失效,可以被重新使用。
发生这种情况时,可不能简单的通过DEL来删除锁,然后再SETNX一次(讲道理,删除锁的操作应该是锁拥有这执行的,这里只需要等它超时即可),当多个客户端检测到锁超时后都会尝试去释放它,这里就可能出现一个竞态条件,让我们模拟一下这个场景:
- C0操作超时了,但它还持有着锁,C1和C2读取lock.foo检查时间戳,先后发现超时了。
- C1 发送DEL lock.foo
- C1 发送SETNX lock.foo 并且成功了。
- C2 发送DEL lock.foo
- C2 发送SETNX lock.foo 并且成功了。
- 这样一来,C1,C2都拿到了锁!问题大了!
幸好这种问题是可以避免的,让我们来看看C3这个客户端是怎样做的:
- C3发送SETNX lock.foo 想要获得锁,由于C0还持有锁,所以Redis返回给C3一个0
- C3发送GET lock.foo 以检查锁是否超时了,如果没超时,则等待或重试。
- 反之,如果已超时,C3通过下面的操作来尝试获得锁:
- GETSET lock.foo <current Unix time + lock timeout + 1>
- 通过GETSET,C3拿到的时间戳如果仍然是超时的,那就说明,C3如愿以偿拿到锁了。
- 如果在C3之前,有个叫C4的客户端比C3快一步执行了上面的操作,那么C3拿到的时间戳是个未超时的值,这时,C3没有如期获得锁,需要再次等待或重试。留意一下,尽管C3没拿到锁,但它改写了C4设置的锁的超时值,不过这一点非常微小的误差带来的影响可以忽略不计。
注意:为了让分布式锁的算法更稳键些,持有锁的客户端在解锁之前应该再检查一次自己的锁是否已经超时,再去做DEL操作,因为可能客户端因为某个耗时的操作而挂起,操作完的时候锁因为超时已经被别人获得,这时就不必解锁了。
五、代码实现
expireMsecs 锁持有超时,防止线程在入锁以后,无限的执行下去,让锁无法释放
timeoutMsecs 锁等待超时,防止线程饥饿,永远没有入锁执行代码的机会
注意:项目里面需要先搭建好redis的相关配置
- import org.slf4j.Logger;
- import org.slf4j.LoggerFactory;
- import org.springframework.dao.DataAccessException;
- import org.springframework.data.redis.connection.RedisConnection;
- import org.springframework.data.redis.core.RedisCallback;
- import org.springframework.data.redis.core.RedisTemplate;
- import org.springframework.data.redis.serializer.StringRedisSerializer;
- /**
- * Redis distributed lock implementation.
- *
- * @author zhengcanrui
- */
- public class RedisLock {
- private static Logger logger = LoggerFactory.getLogger(RedisLock.class);
- private RedisTemplate redisTemplate;
- private static final int DEFAULT_ACQUIRY_RESOLUTION_MILLIS = 100;
- /**
- * Lock key path.
- */
- private String lockKey;
- /**
- * 锁超时时间,防止线程在入锁以后,无限的执行等待
- */
- private int expireMsecs = 60 * 1000;
- /**
- * 锁等待时间,防止线程饥饿
- */
- private int timeoutMsecs = 10 * 1000;
- private volatile boolean locked = false;
- /**
- * Detailed constructor with default acquire timeout 10000 msecs and lock expiration of 60000 msecs.
- *
- * @param lockKey lock key (ex. account:1, ...)
- */
- public RedisLock(RedisTemplate redisTemplate, String lockKey) {
- this.redisTemplate = redisTemplate;
- this.lockKey = lockKey + "_lock";
- }
- /**
- * Detailed constructor with default lock expiration of 60000 msecs.
- *
- */
- public RedisLock(RedisTemplate redisTemplate, String lockKey, int timeoutMsecs) {
- this(redisTemplate, lockKey);
- this.timeoutMsecs = timeoutMsecs;
- }
- /**
- * Detailed constructor.
- *
- */
- public RedisLock(RedisTemplate redisTemplate, String lockKey, int timeoutMsecs, int expireMsecs) {
- this(redisTemplate, lockKey, timeoutMsecs);
- this.expireMsecs = expireMsecs;
- }
- /**
- * @return lock key
- */
- public String getLockKey() {
- return lockKey;
- }
- private String get(final String key) {
- Object obj = null;
- try {
- obj = redisTemplate.execute(new RedisCallback<Object>() {
- @Override
- public Object doInRedis(RedisConnection connection) throws DataAccessException {
- StringRedisSerializer serializer = new StringRedisSerializer();
- byte[] data = connection.get(serializer.serialize(key));
- connection.close();
- if (data == null) {
- return null;
- }
- return serializer.deserialize(data);
- }
- });
- } catch (Exception e) {
- logger.error("get redis error, key : {}", key);
- }
- return obj != null ? obj.toString() : null;
- }
- private boolean setNX(final String key, final String value) {
- Object obj = null;
- try {
- obj = redisTemplate.execute(new RedisCallback<Object>() {
- @Override
- public Object doInRedis(RedisConnection connection) throws DataAccessException {
- StringRedisSerializer serializer = new StringRedisSerializer();
- Boolean success = connection.setNX(serializer.serialize(key), serializer.serialize(value));
- connection.close();
- return success;
- }
- });
- } catch (Exception e) {
- logger.error("setNX redis error, key : {}", key);
- }
- return obj != null ? (Boolean) obj : false;
- }
- private String getSet(final String key, final String value) {
- Object obj = null;
- try {
- obj = redisTemplate.execute(new RedisCallback<Object>() {
- @Override
- public Object doInRedis(RedisConnection connection) throws DataAccessException {
- StringRedisSerializer serializer = new StringRedisSerializer();
- byte[] ret = connection.getSet(serializer.serialize(key), serializer.serialize(value));
- connection.close();
- return serializer.deserialize(ret);
- }
- });
- } catch (Exception e) {
- logger.error("setNX redis error, key : {}", key);
- }
- return obj != null ? (String) obj : null;
- }
- /**
- * 获得 lock.
- * 实现思路: 主要是使用了redis 的setnx命令,缓存了锁.
- * reids缓存的key是锁的key,所有的共享, value是锁的到期时间(注意:这里把过期时间放在value了,没有时间上设置其超时时间)
- * 执行过程:
- * 1.通过setnx尝试设置某个key的值,成功(当前没有这个锁)则返回,成功获得锁
- * 2.锁已经存在则获取锁的到期时间,和当前时间比较,超时的话,则设置新的值
- *
- * @return true if lock is acquired, false acquire timeouted
- * @throws InterruptedException in case of thread interruption
- */
- public synchronized boolean lock() throws InterruptedException {
- int timeout = timeoutMsecs;
- while (timeout >= 0) {
- long expires = System.currentTimeMillis() + expireMsecs + 1;
- String expiresStr = String.valueOf(expires); //锁到期时间
- if (this.setNX(lockKey, expiresStr)) {
- // lock acquired
- locked = true;
- return true;
- }
- String currentValueStr = this.get(lockKey); //redis里的时间
- if (currentValueStr != null && Long.parseLong(currentValueStr) < System.currentTimeMillis()) {
- //判断是否为空,不为空的情况下,如果被其他线程设置了值,则第二个条件判断是过不去的
- // lock is expired
- String oldValueStr = this.getSet(lockKey, expiresStr);
- //获取上一个锁到期时间,并设置现在的锁到期时间,
- //只有一个线程才能获取上一个线上的设置时间,因为jedis.getSet是同步的
- if (oldValueStr != null && oldValueStr.equals(currentValueStr)) {
- //防止误删(覆盖,因为key是相同的)了他人的锁——这里达不到效果,这里值会被覆盖,但是因为什么相差了很少的时间,所以可以接受
- //[分布式的情况下]:如过这个时候,多个线程恰好都到了这里,但是只有一个线程的设置值和当前值相同,他才有权利获取锁
- // lock acquired
- locked = true;
- return true;
- }
- }
- timeout -= DEFAULT_ACQUIRY_RESOLUTION_MILLIS;
- /*
- 延迟100 毫秒, 这里使用随机时间可能会好一点,可以防止饥饿进程的出现,即,当同时到达多个进程,
- 只会有一个进程获得锁,其他的都用同样的频率进行尝试,后面有来了一些进行,也以同样的频率申请锁,这将可能导致前面来的锁得不到满足.
- 使用随机的等待时间可以一定程度上保证公平性
- */
- Thread.sleep(DEFAULT_ACQUIRY_RESOLUTION_MILLIS);
- }
- return false;
- }
- /**
- * Acqurired lock release.
- */
- public synchronized void unlock() {
- if (locked) {
- redisTemplate.delete(lockKey);
- locked = false;
- }
- }
- }
调用:
- RedisLock lock = new RedisLock(redisTemplate, key, 10000, 20000);
- try {
- if(lock.lock()) {
- //需要加锁的代码
- }
- }
- } catch (InterruptedException e) {
- e.printStackTrace();
- }finally {
- //为了让分布式锁的算法更稳键些,持有锁的客户端在解锁之前应该再检查一次自己的锁是否已经超时,再去做DEL操作,因为可能客户端因为某个耗时的操作而挂起,
//操作完的时候锁因为超时已经被别人获得,这时就不必解锁了。 ————这里没有做- lock.unlock();
- }
六、一些问题
1、为什么不直接使用expire设置超时时间,而将时间的毫秒数其作为value放在redis中?
如下面的方式,把超时的交给redis处理:
- lock(key, expireSec){
- isSuccess = setnx key
- if (isSuccess)
- expire key expireSec
- }
这种方式貌似没什么问题,但是假如在setnx后,redis崩溃了,expire就没有执行,结果就是死锁了。锁永远不会超时。
2、为什么前面的锁已经超时了,还要用getSet去设置新的时间戳的时间获取旧的值,然后和外面的判断超时时间的时间戳比较呢?
因为是分布式的环境下,可以在前一个锁失效的时候,有两个进程进入到锁超时的判断。如:
C0超时了,还持有锁,C1/C2同时请求进入了方法里面
C1/C2获取到了C0的超时时间
C1使用getSet方法
C2也执行了getSet方法
假如我们不加 oldValueStr.equals(currentValueStr) 的判断,将会C1/C2都将获得锁,加了之后,能保证C1和C2只能一个能获得锁,一个只能继续等待。
注意:这里可能导致超时时间不是其原本的超时时间,C1的超时时间可能被C2覆盖了,但是他们相差的毫秒及其小,这里忽略了。
致谢:感谢您的阅读!转载请加原文链接,谢谢。转载请加上原文链接,谢谢!http://www.cnblogs.com/0201zcr/p/5942748.html
jedisLock—redis分布式锁实现的更多相关文章
- jedisLock—redis分布式锁实现(转)
一.使用分布式锁要满足的几个条件: 系统是一个分布式系统(关键是分布式,单机的可以使用ReentrantLock或者synchronized代码块来实现) 共享资源(各个系统访问同一个资源,资源的载体 ...
- Redis分布式锁 (图解-秒懂-史上最全)
文章很长,而且持续更新,建议收藏起来,慢慢读! 高并发 发烧友社群:疯狂创客圈(总入口) 奉上以下珍贵的学习资源: 疯狂创客圈 经典图书 : 极致经典 + 社群大片好评 < Java 高并发 三 ...
- 利用redis分布式锁的功能来实现定时器的分布式
文章来源于我的 iteye blog http://ak478288.iteye.com/blog/1898190 以前为部门内部开发过一个定时器程序,这个定时器很简单,就是配置quartz,来实现定 ...
- Redis分布式锁
Redis分布式锁 分布式锁是许多环境中非常有用的原语,其中不同的进程必须以相互排斥的方式与共享资源一起运行. 有许多图书馆和博客文章描述了如何使用Redis实现DLM(分布式锁管理器),但是每个库都 ...
- redis分布式锁和消息队列
最近博主在看redis的时候发现了两种redis使用方式,与之前redis作为缓存不同,利用的是redis可设置key的有效时间和redis的BRPOP命令. 分布式锁 由于目前一些编程语言,如PHP ...
- redis咋么实现分布式锁,redis分布式锁的实现方式,redis做分布式锁 积极正义的少年
前言 分布式锁一般有三种实现方式:1. 数据库乐观锁:2. 基于Redis的分布式锁:3. 基于ZooKeeper的分布式锁.本篇博客将介绍第二种方式,基于Redis实现分布式锁.虽然网上已经有各种介 ...
- spring boot redis分布式锁
随着现在分布式架构越来越盛行,在很多场景下需要使用到分布式锁.分布式锁的实现有很多种,比如基于数据库. zookeeper 等,本文主要介绍使用 Redis 做分布式锁的方式,并封装成spring b ...
- Redis分布式锁的正确实现方式
前言 分布式锁一般有三种实现方式:1. 数据库乐观锁:2. 基于Redis的分布式锁:3. 基于ZooKeeper的分布式锁.本篇博客将介绍第二种方式,基于Redis实现分布式锁.虽然网上已经有各种介 ...
- Redis分布式锁---完美实现
这几天在做项目缓存时候,因为是分布式的所以需要加锁,就用到了Redis锁,正好从网上发现两篇非常棒的文章,来和大家分享一下. 第一篇是简单完美的实现,第二篇是用到的Redisson. Redis分布式 ...
随机推荐
- HTML基础
HTML指的是超文本标记语言 (Hyper Text Markup Language), HTML不是一种编程语言,而是一种标记语言 (markup language) ,HTML使用标记标签来描述网 ...
- git revert和reset区别
1.在github上建立测试项目并克隆到本地 2.本地中新建两个文本文件 3.将a.txt commit并push到远程仓库 执行 git add a.txt, git commit -m " ...
- 多线程同步工具——LockSupport
用例1:子线程等待主线程发放许可! public static void main(String[] args) { Thread thread = new Thread(){ public void ...
- ubuntu 14.04LTS 环境下配置NFS服务
简言之,NFS(Network FileSystem,网络文件系统)用于在不同机器,不同操作系统之间通过网络互相分享各自的文件.NFS设计之初就是为了在不同的系统间使用,所以它的通讯协议设计与主机及操 ...
- CSS3与页面布局学习总结(七)——前端预处理技术(Less、Sass、CoffeeScript、TypeScript)
CSS不像其它高级语言一样支持算术运算.变量.流程控制与面向对象特性,所以CSS样式较多时会引起一些问题,如修改复杂,冗余,某些别的语言很简单的功能实现不了等.而javascript则是一种半面向对象 ...
- 【Oracle 集群】Linux下Oracle RAC集群搭建之Oracle DataBase安装(八)
Oracle 11G RAC数据库安装(八) 概述:写下本文档的初衷和动力,来源于上篇的<oracle基本操作手册>.oracle基本操作手册是作者研一假期对oracle基础知识学习的汇总 ...
- Oracle逻辑迁移某业务用户及数据
1.确定基本信息 2.源数据库导出 3.目的数据库导入 4.逻辑迁移注意事项 1.确定基本信息 确定基本信息: 源数据库所在系统类型:________ 源数据库地址:__.__.__.__ 源数据库版 ...
- jQuery-1.9.1源码分析系列(三) Sizzle选择器引擎——编译原理
这一节要分析的东东比较复杂,篇幅会比较大,也不知道我描述后能不能让人看明白.这部分的源码我第一次看的时候也比较吃力,现在重头看一遍,再分析一遍,看能否查缺补漏. 看这一部分的源码需要有一个完整的概念后 ...
- NavisWorks Api 简单使用与Gantt
相信很多朋友在做BIM项目的时候.都有客户会提出项目计划,形象进度 等需求. 那么当前最主要的问题就是计划与BIM模型的关联问题.那么我在项目中是用户用Project软件编辑计划然后手动跟三维模型关联 ...
- 《连载 | 物联网框架ServerSuperIO教程》1.4种通讯模式机制。附小文:招.NET开发,结果他转JAVA了,一切都是为了生活
参考文章: 1.SuperIO通讯框架介绍,含通信本质 2.C#跨平台物联网通讯框架ServerSuperIO(SSIO) 一.感慨 上大学的时候,没有学过C#,花了5块钱在地坛书市买了一本教程,也就 ...