Redis分布式锁前世今生
1.redis锁前世即基于单Redis节点的分布式锁,诸如setkey value px milliseconds nx
前世者,必将经历种种磨砺,才能稍微符合一些主流。推荐自测非常好用的redis工具(http://try.redis.io/)
第一劫:
dos命令版本
1)setnx job "hello"
如果当前job存在,则返回0表明赋值不成功。
如果当前job不存在,则返回1,表明赋值成功。
2)del job
单独删除操作命令
Java代码版本
1)redisCacheTemplate.opsForValue().set("key","123");(springboot+Redis)
如果当前key存在则覆盖,不存在则继续添加
2)redisCacheTemplate.delete("key");
删除存在的key操作
劫语:无论是dos命令操作,还是通过Java代码实现都不难发现,一个线程占用当前资源时候,如果请求执行因为某些原因意外推出了,导致独占的锁一直没有释 放,那么这个锁将一直存在。以至于以后缓存得不到任何的更新。
第二劫:
劫语应对:既然占有的锁一直释放不了,我们可以通过锁添加失效时间
Java命令版本
1)redisCacheTemplate.opsForValue().setIfAbsent("key",UUID.randomUUID());
(redis封装的函数setIfAbsent(),追踪至底层代码,实际就是connection.setNX(rawKey, rawValue)一个原子性操作)
如果当前key存在,则赋值不成功,如果不存在的话,则赋值成功
2)redisCacheTemplate.expire("key",60,TimeUnit.SECONDS);
并且给当前锁设置失效时间.设置超时时间需要合理评估,过长或者过短都是问题。
劫语:有效的锁定了key,并且设置了失效时间,但是setnx方法只能由一个线程占有,如果其中执行逻辑比较缓慢,缓慢到超过设置的失效时间,另外一个线程获 取key,执行到中间执行逻辑代码,出现冲突。
第三劫:
劫语应对:既然出现了由于中间逻辑执行缓慢情况,可以通过LUA脚本来加长当前key失效时间。
LUA脚本
1)伪代码
if redis.call("get",KEYS[1]) == ARGV[1] then
redis.call("set",KEYS[1],ex=3000)
else
getDLock();
如果获取当前锁还没失效,则增加当前锁失效时间,如果已经失效,则重新获取锁
劫语:就目前情况依旧不能解决两个线程同时操作独占资源情况。
第四劫:(普遍单机操作应用方法setkey value px milliseconds)
劫语应对:既然占有锁和锁添加超时时间,会存在一个执行,一个没有执行情况。我们就把他们封装城一个事务操作处理。
dos命令版本
1)setex mykey 60 redis
如果当前mykey没有值,则赋值redis,并且声明超时时间为60s(单位为second)
2)setex mykey 60 java(报错)
如果当前mykey存在值,并且没有超过超时时间,则赋值失败。
3)setex mykey 60 cainiao(返回1)
如果当前mykey存在值,并且超时时间已过,则赋值成功。
java命令版本
目前redis操作jar包中,已经对获取key值,还有设置失效方法封装成一个操作。
1)redisCacheTemplate.opsForValue().set("key", UUID.randomUUID(),60,TimeUnit.SECONDS);
追踪其底层代码,你就会发现,其实就是原子性操作
connection.setEx(rawKey, TimeoutUtils.toSeconds(timeout, unit), rawValue);
劫语:当单机REDIS服务停用,当前分布式锁方案仍旧存在问题。
第五劫:(普遍单机操作应用方法setNX+LUA(释放锁))
java命令版本
1)setnx方式获取锁,并且设置超时时间
public static boolean lock(String key,String uuid,int expire){
if(null == key){
return false;
}
try {
Jedis jedis = getJedisPool().getResource();
String res = jedis.set(key,uuid,"NX","EX",expire);
jedis.close();
return res!=null && res.equals("OK");
} catch (Exception e) {
return false;
}
}
需要注意事项:uuid值,需要唯一标识。否则会导致 “信号错误”,释放了不该释放的锁
A----->获取锁,占用资源 B-------->尝试获取失败,继续尝试
A----->执行公共资源(未执行完),锁失效 B-------->尝试获取锁成功,执行公共资源
A----->执行完成,释放锁(A B锁一起释放)B-------->B还没操作成功
2)LUA释放锁结构,需要判断当前锁,是否为需要释放的锁,这就是为何声明锁唯一的原因
static String luaScript = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del',KEYS[1]) else return 0 end";
public static boolean releaseLock(String key ,String lockValue){
if(key == null || lockValue == null) {
return false;
}
try {
Jedis jedis = getJedisPool().getResource();
Object res =jedis.eval(luaScript,Collections.singletonList(key),Collections.singletonList(lockValue));
jedis.close();
return res!=null && res.equals(lockReleaseOK);
} catch (Exception e) {
return false;
}
}
劫语:当单机REDIS服务停用,当前分布式锁方案仍旧存在问题。
2.redis锁今生,基于redlock封装而成的redission框架
对于前世redis分布式锁各种解决方案,无论是成熟单机方案 1.SETNX+LUA 2.SET unique_value nx px milliseconds ,都局限性很大
所以Martin发布了一种算法redlock来进行集群(完全互相独立,不存在主从复制或者其他集群协调机制)操作分布式锁
算法如下:
1.获取当前Unix时间,以毫秒为单位。
2.依次尝试从5个实例,使用相同的key和具有唯一性的value(例如UUID)获取锁。并且设置一个超时时间(一般是5-50毫秒,远小于失效时间)
3.客户端当前时间减去开始获取锁时间(第一个redis实例开始)作为获取锁消耗总时间。当且仅当redis集群中有一多半锁获取到(n/2+1),并且获取锁总时间小于锁设置的失效时间,才任务该线程获取到分布式锁。
4.获取到锁以后,锁的有效时间更改为,最起初设置的锁失效时间-获取锁总消耗时间
5.如果获取锁失败,应该在redis集群中进行解锁
Reddisson框架有效的实现了对redlock的封装。
1)项目中引入
<!--分布式锁-->
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.11.1</version>
</dependency>
2)项目中使用
/**
* redisson配置
*/
@Configuration
public class RedissonConfig {
@Value("${spring.redis.host}")
private String host;
@Value("${spring.redis.port}")
private String port;
@Bean
public RedissonClient getRedisson(){
Config config = new Config();
config.useSingleServer().setAddress("redis://" + host + ":" + port);
return Redisson.create(config);
}
}
3)代码中引用
public VersionT getOneVersion() {
VersionT versionT = (VersionT) redisCacheTemplate.opsForValue().get("bzversion");
RLock rLock = redissonClient.getLock("redissonLock:" + Thread.currentThread().getName()); //分布式锁,避免大量请求一瞬间请求到数据库,造成缓存击穿
try {
rLock.tryLock(500,10000, TimeUnit.SECONDS); // 锁失效时间设置10秒,锁响应时间设置50毫秒
if (versionT == null) {
VersionT version = versionDao.getOneVersion();
redisCacheTemplate.opsForValue().set("bzversion", version);
return version;
}
} catch (Exception e) {
System.out.println("缓存版本号失败" + e.getMessage());
} finally {
rLock.unlock();
}
return versionT;
}
4)源码中分析(redission获取锁源码解析)
public boolean tryLock(long waitTime, long leaseTime, TimeUnit unit) throws InterruptedException {
long time = unit.toMillis(waitTime); // 单实例获取锁响应时间
long current = System.currentTimeMillis();
long threadId = Thread.currentThread().getId();
Long ttl = this.tryAcquire(leaseTime, unit, threadId); // 获取分布式锁
if (ttl == null) {
return true;
} else {
time -= System.currentTimeMillis() - current; // 超过定义响应时间,返回获取锁失败
if (time <= 0L) {
this.acquireFailed(threadId);
return false;
} else {
current = System.currentTimeMillis();
RFuture<RedissonLockEntry> subscribeFuture = this.subscribe(threadId);
if (!this.await(subscribeFuture, time, TimeUnit.MILLISECONDS)) {
if (!subscribeFuture.cancel(false)) {
subscribeFuture.onComplete((res, e) -> {
if (e == null) {
this.unsubscribe(subscribeFuture, threadId);
}
});
}
this.acquireFailed(threadId);
return false;
} else {
try {
time -= System.currentTimeMillis() - current;
if (time <= 0L) {
this.acquireFailed(threadId);
boolean var20 = false;
return var20;
} else {
boolean var16;
do {
long currentTime = System.currentTimeMillis();
ttl = this.tryAcquire(leaseTime, unit, threadId);
if (ttl == null) {
var16 = true;
return var16;
}
time -= System.currentTimeMillis() - currentTime;
if (time <= 0L) {
this.acquireFailed(threadId);
var16 = false;
return var16;
}
currentTime = System.currentTimeMillis();
if (ttl.longValue() >= 0L && ttl.longValue() < time) {
this.getEntry(threadId).getLatch().tryAcquire(ttl.longValue(), TimeUnit.MILLISECONDS);
} else {
this.getEntry(threadId).getLatch().tryAcquire(time, TimeUnit.MILLISECONDS);
}
time -= System.currentTimeMillis() - currentTime;
} while(time > 0L);
this.acquireFailed(threadId);
var16 = false;
return var16;
}
} finally {
this.unsubscribe(subscribeFuture, threadId);
}
}
}
}
}
5) <T> RFuture<T> tryLockInnerAsync(long leaseTime, TimeUnit unit, long threadId, RedisStrictCommand<T> command) {
this.internalLockLeaseTime = unit.toMillis(leaseTime);
return this.commandExecutor.evalWriteAsync(this.getName(), LongCodec.INSTANCE, command, "if (redis.call('exists', KEYS[1]) == 0) then redis.call('hset', KEYS[1], ARGV[2], 1); redis.call('pexpire', KEYS[1], ARGV[1]); return nil; end; if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then redis.call('hincrby', KEYS[1], ARGV[2], 1); redis.call('pexpire', KEYS[1], ARGV[1]); return nil; end; return redis.call('pttl', KEYS[1]);", Collections.singletonList(this.getName()), new Object[]{this.internalLockLeaseTime, this.getLockName(threadId)});
}
跟踪到这里,就会发现,通过LUA脚本实现了锁判断,锁重入等操作。
if (redis.call('exists', KEYS[1]) == 0)
then redis.call('hset', KEYS[1], ARGV[2], 1); // 获取锁
redis.call('pexpire', KEYS[1], ARGV[1]); // 设置key失效时间
return nil;
end;
if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then redis.call('hincrby', KEYS[1], ARGV[2], 1); // redis重入锁
redis.call('pexpire', KEYS[1], ARGV[1]); // 设置key失效时间
return nil;
end;
return redis.call('pttl', KEYS[1]); // 以毫秒为单位返回 key 的剩余过期时间
redlock已经属于现在较为稳定的reids分布式锁,但是redlock的作者Martin以及antirez就这个算法不足进行了激烈的讨论,以及引发分布式阵营的对垒。
他们主要纠结的问题点在于:
1.时钟发生跳跃
2.长时间的GC pause或者长时间的网络延迟
其实对于时钟跳跃情况1.服务器更新时间插件 2.运维同学手动更改服务器时间 这两种情况虽然很极端,但是确实会造成redlock的失效。
对于第二种情况,无论是长时间的GC pause还是长时间的网络延迟,其实在redlock算法第四步做了校验,那就是最起初设置的失效时间如果小于集群环境下获取redis锁消耗的总时间,则会进行获取锁失败操作。
参考文献 https://mp.weixin.qq.com/s?__biz=MzA4NTg1MjM0Mg==&mid=2657261514&idx=1&sn=47b1a63f065347943341910dddbb785d&chksm=84479e13b3301705ea29c86f457ad74010eba8a8a5c12a7f54bcf264a4a8c9d6adecbe32ad0b&scene=21#wechat_redirect
https://yq.aliyun.com/articles/674394
https://www.cnblogs.com/demingblog/p/9542124.html
Redis分布式锁前世今生的更多相关文章
- 利用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分布式 ...
- redis分布式锁实践
分布式锁在多实例部署,分布式系统中经常会使用到,这是因为基于jvm的锁无法满足多实例中锁的需求,本篇将讲下redis如何通过Lua脚本实现分布式锁,不同于网上的redission,完全是手动实现的 我 ...
- Redis分布式锁的try-with-resources实现
Redis分布式锁的try-with-resources实现 一.简介 在当今这个时代,单体应用(standalone)已经很少了,java提供的synchronized已经不能满足需求,大家自然 而 ...
随机推荐
- RPC——看这一篇就…显然不够
引言 RPC blablabla…… RPC 知识点 扩展 有给老婆解释的如:https://www.jianshu.com/p/2accc2840a1b
- jQuery实现点击div外的区域,来隐藏指定节点
<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <script sr ...
- DataStructuresAndAlogorithm--红黑树
简介 为了理解红黑树(red-black tree)是什么,首先需要知道二叉树. 定义1:二叉树是结点的有限集合,该集合或者为空集,或者是由一个根和两棵互不相交的,称为该根的左子树和右子树的二叉树组成 ...
- python常用代码、问题汇总
1.生成dataframe数据 5.读取带 ','分隔符的txt文件 4.DataFrame格式数据处理中报错 2.安装库时出现如下错误: 3.得到股票交易日数据 1.生成dataframe数据 im ...
- 进度5_家庭记账本App_数据库的添加和查看
今天继续在昨天的基础上完成了家庭记账单的在数据库中的添加和查看功能 在之前的基础上舍弃了Fragment,重新在百度上找到了学习资料,并且自我完成了实践 首先在之前的基础上创建CostListAdap ...
- bzoj 4260REBXOR
什么什么trie树??呵呵呵,,,, 一直在困惑怎么处理哪连续一段最大..看了题解迷惑了好久.. 然后突然发现,是xor啊,,在trie树里找到以前得插入的前缀和,然后找到与现在前缀和每一位都不同的, ...
- 二、在SAP中创建一个程序
一.我们来到SE38 二.添加一个程序的名字,需要以Y或者Z开头,点击创建就可以了 三.我们输入hello Sap,然后选择可执行程序,然后保存 四.创建对象目录时,可以选择把这个加入到包中,或者选择 ...
- Spring注解 @Autowired
@Autowired可以对成员变量.方法和构造函数进行标注,来完成自动装配的工作,这里必须明确:@Autowired是根据类型进行自动装配的,如果需要按名称进行装配,则需要配合@Qualifier使用
- css笔记01
CSS样式(Cascading Style Sheets) 表格布局缺陷: 嵌套太多,一旦顺序错乱页面达不到预期效果 表格布局页面不灵活,动一块整个布局全都要变 语法: 在style标签中 ...
- 读书笔记 - js高级程序设计 - 第七章 函数表达式
闭包 有权访问另一个函数作用域中的变量的函数 匿名函数 函数没有名字 少用闭包 由于闭包会携带包含它的函数的作用域,因此会比其它函数占用更多的内存.过度使用闭包可能会导致内存占用过多,我们建议读者 ...