免费图片制作网站模板,兰州网站备案谁家做,网站的关键词在哪设置,广州网站优化推广文章目录 1. 分布式锁1.1 基本原理和实现方式对比synchronized锁在集群模式下的问题多jvm使用同一个锁监视器分布式锁概念分布式锁须满足的条件分布式锁的实现 1.2 基于Redis的分布式锁获取锁释放锁操作示例 基于Redis实现分布式锁初级版本ILock接口SimpleRedisLock使用示… 文章目录 1. 分布式锁1.1 基本原理和实现方式对比synchronized锁在集群模式下的问题多jvm使用同一个锁监视器分布式锁概念分布式锁须满足的条件分布式锁的实现 1.2 基于Redis的分布式锁获取锁释放锁操作示例 基于Redis实现分布式锁初级版本ILock接口SimpleRedisLock使用示例 Redis分布式锁误删问题锁超时释放问题描述解决方式 改进Redis的分布式锁SimpleRedisLock改进版 Redis分布式锁误删问题2原子性问题Lua脚本解决多条命令原子性问题Redis的Lua脚本Redis提供的调用函数redis.call(..)函数调用及传参示例 lua脚本改进Redis的分布式锁lua脚本解决redis命令原子性问题SimpleRedisLock改进版测试 总结 2. redisson2.1 setnx实现的分布式锁存在的问题2.2 Redisson简介2.3 redisson快速入门引入依赖配置Redisson客户端使用Redisson的分布式锁 2.4 redisson的可重入原理可重入原理分析获取锁的lua脚本释放锁的lua脚本redisson获取锁释放锁源码 2.5 redisson的锁重试和锁超时解决方式图解代码总结 2.6 联锁分布式锁主从一致性问题redisson解决主从一致问题联锁使用示例配置3个RedissonClient使用RedissonMultiLock 总结 1. 分布式锁
1.1 基本原理和实现方式对比
synchronized锁在集群模式下的问题
在集群模式下synchronized的锁失效了synchronized只能保证单个jvm内部的多个线程之间的互斥而没有办法让集群下的多个jvm进程之间互斥如果要解决这个问题就要用到分布式锁。synchornized就是利用jvm内部的锁监视器来控制线程的在jvm的内部因为各线程共享同1个锁监视器所以只会有1个线程获取锁可以实现线程间的互斥。但是当有多个jvm进程之后就会有多个锁监视器就会有多个线程获取到锁这样就没有办法实现多jvm进程之间的互斥了。因此集群模式下就不能使用jvm内部的锁监视器了。 多jvm使用同一个锁监视器
我们要让多个jvm使用同一个锁监视器这个锁监视器一定是1个在jvm外部的多个jvm进程都可以看到的。这样多个jvm进程中的线程中只会有1个线程能够获取到这把锁。这样就可以实现集群模式下多jvm进程中的各线程互斥了。
如下图在线程1获取到jvm进程外的锁监视器当它获取到该锁成功之后就可以执行业务查询订单如果订单不存在则插入新订单然后释放锁。假设在这个过程中线程3也来获取这个jvm进程外的锁监视器因为线程1已经拿到了这个锁监视器因此就会失败然后一直等待这把锁。等到线程1执行完业务并释放锁之后线程3才会获取锁成功此时来查询订单的话肯定能查到线程1插入的订单就不会插入新的订单了这样就避免了安全问题的发生了。 分布式锁概念
分布式锁满足分布式系统或集群模式下多进程可见并且互斥的锁。
分布式锁的核心思想就是让大家都使用同一把锁只要大家使用的是同一把锁那么我们就能锁住线程不让线程进行让程序串行执行这就是分布式锁的核心思路
分布式锁须满足的条件 可见性多个线程都能看到相同的结果注意这个地方说的可见性并不是并发编程中指的内存可见性只是说多个进程之间都能感知到变化的意思 互斥互斥是分布式锁的最基本的条件使得程序串行执行 高可用程序不易崩溃时时刻刻都保证较高的可用性 高性能由于加锁本身就让性能降低所有对于分布式锁本身需要他就较高的加锁性能和释放锁性能 安全性安全也是程序中必不可少的一环
除了上述基本条件还有一些特性比如是否可重入、获取锁时是阻塞的还是非阻塞的、公平锁或者非公平锁 分布式锁的实现
分布式锁的核心是实现多进程之间互斥而满足这一点的方式有很多常见的有三种 mysql分布式锁实现mysql数据库具备事务机制在事务执行的时候或者说在执行写操作的时候mysql会自动分配1个互斥的锁这样一来在多个事务之间是互斥的只有1个事务能够执行。可以利用这个原理来实现分布式锁。我们在业务执行前先去mysql里申请1个互斥锁然后执行我们的业务业务执行完之后去提交事务这样锁就释放了。当业务抛出异常后它会自动的触发回滚这样锁也释放了。
redis分布式锁实现利用redis中的setnx命令只有当redis中的key不存在时这个命令才会执行成功。如果已经存在则会执行失败。因此当多个线程去执行setnx时只会有1个能够成功其它都会失败。这样就实现了互斥。
zookeeper分布式锁实现利用zk内部的节点机制zk内部可以创建节点同时节点具备唯一性和有序性并且还可以创建临时节点。唯一性指的是创建的节点不能重复。有序性指的是每次创建的节点的id都是递增的。可以利用有序性来实现互斥假设很多线程在zk中创建节点这样每个线程创建的节点的id都是递增的我们约定节点的id最小的那个它是算获取锁成功这样就实现了互斥因为最小的只有1个。如果要释放锁则可以删除自己创建的节点这样一来它就不是最小的了另外的节点就变成最小的了。也可以使用唯一性每个线程创建的节点名称都是一样的这样只会有1个能够创建成功。
1.2 基于Redis的分布式锁
获取锁释放锁
实现分布式锁时需要实现的两个基本方法 获取锁 互斥确保只能有一个线程获取锁非阻塞尝试一次成功返回true失败返回false阻塞式获取锁失败之后等待一直到能够获取到锁或者到指定超时时间为止相对于非阻塞式比较耗CPU实现起来比较复杂 # 添加锁利用setnx的互斥特性
SETNX lock thread1# 添加锁过期时间避免服务宕机引起的死锁
EXPIRE lock 10由于上述命令不具备原子性可以使用[help set]查看set命令的详细使用。 # 添加锁NX是互斥、EX是设置超时时间
SET lock thread1 NX EX 10释放锁 手动释放超时释放 操作示例 基于Redis实现分布式锁初级版本
ILock接口
需求定义一个类实现下面接口利用Redis实现分布式锁功能。
public interface ILock {/*** 尝试获取锁* param timeoutSec 锁持有的超时时间过期后自动释放* return true代表获取锁成功; false代表获取锁失败*/boolean tryLock(long timeoutSec);/*** 释放锁*/void unlock();
}SimpleRedisLock
redis分布式锁初级版本实现
public class SimpleRedisLock implements ILock {// 业务的名称, 即锁的名称private String name;private StringRedisTemplate stringRedisTemplate;// 锁统一前缀private static final String KEY_PREFIX lock:;// 传入业务名称和stringRedisTemplatepublic SimpleRedisLock(String name, StringRedisTemplate stringRedisTemplate) {this.name name;this.stringRedisTemplate stringRedisTemplate;}Overridepublic boolean tryLock(long timeoutSec) {// 获取线程标示String threadId Thread.currentThread().getId();// 获取锁Boolean success stringRedisTemplate.opsForValue().setIfAbsent(KEY_PREFIX name,threadId,// 可以指定超时时间(这里实际上是假设获取锁成功之后, 同时指定锁的过期时间。// 并不是阻塞式获取锁失败后的等待时间)timeoutSec,TimeUnit.SECONDS);// 避免自动拆装箱出现null的情况(防止success为null)return Boolean.TRUE.equals(success);}Overridepublic void unlock() {// 释放锁stringRedisTemplate.delete(KEY_PREFIX name);}}使用示例
上面的redis分布式锁初级版本SimpleRedisLock的使用示例
public Result testRedis() {Long voucherId 100L;Long userId UserHolder.getUser().getId();// 对每个用户使用锁控制并发访问SimpleRedisLock lock new SimpleRedisLock(order userId, stringRedisTemplate);// 尝试获取锁, 并指定如果获取锁成功时, 设置的锁的过期时间boolean isLock lock.tryLock(5000);// 判断是否获取锁成功if (!isLock) {// 获取锁失败, 返回错误或重试return Result.fail(不允许重复下单);}try {IVoucherOrderService proxy (IVoucherOrderService) AopContext.currentProxy();return proxy.createVoucherOrder(voucherId);} finally {// 释放锁lock.unlock();}}Redis分布式锁误删问题锁超时释放
问题描述
问题描述如下图所示使用上面的SimpleRedisLock线程1先获取到锁锁设置了过期时间然后执行业务可是由于在执行业务的过程中阻塞了导致锁过期释放了。此时线程2来拿锁因为锁已经释放了所以线程2能够成功拿到锁然后线程2开始执行自己的业务。恰巧在这个时候线程1又开始执行了线程1就去释放锁把锁给删掉了。此时线程3又来获取锁因为锁已经被删了所以线程3能够成功拿到锁。但现在问题是线程2也在执行业务线程3也在执行业务这样又出现了并发问题。原因在于线程1删除了不属于自己的锁线程1拿到锁之后由于锁超时而被自动释放掉从而让线程2拿到了锁而线程1超时阻塞之后恢复运行删除了线程2的锁
解决方式
上面问题出现的根本原因在于线程1持有锁但锁由于超时而释放锁被其它线程争抢了但线程1恢复运行后删除了已经被其它线程获取的锁。也就是线程1删除了当前已经不属于自己的锁了。因此线程1在释放锁时需要判断一下当前持有这把锁的线程是不是自己。如果是才能删除如果不是则不能删除。同时还需要在获取锁的时候存入当前线程自己的标识这样才能在释放锁的时候才能判断当前持有这把锁的线程是不是自己。 改进Redis的分布式锁
需求修改之前的分布式锁实现满足
在获取锁时存入线程标示可以用UUID表示在释放锁时先获取锁中的线程标示判断是否与当前线程标示一致 如果一致则释放锁如果不一致则不释放锁 SimpleRedisLock改进版
如下实现但也存在问题
public class SimpleRedisLock implements ILock {// 业务的名称, 即锁的名称private String name;private StringRedisTemplate stringRedisTemplate;// 锁统一前缀private static final String KEY_PREFIX lock:;// 引入uuid, 用于区分多个jvm因为线程id是递增的, 防止多个jvm的线程id出现重复的情况private static final String ID_PREFIX UUID.randomUUID().toString(true) -;// 传入业务名称和stringRedisTemplatepublic SimpleRedisLock(String name, StringRedisTemplate stringRedisTemplate) {this.name name;this.stringRedisTemplate stringRedisTemplate;}Overridepublic boolean tryLock(long timeoutSec) {// 获取线程标示String threadId Thread.currentThread().getId();// 获取锁Boolean success stringRedisTemplate.opsForValue().setIfAbsent(KEY_PREFIX name,ID_PREFIX threadId,// 可以指定超时时间(这里实际上是假设获取锁成功之后, 同时指定锁的过期时间。// 并不是阻塞式获取锁失败后的等待时间)timeoutSec,TimeUnit.SECONDS);// 避免自动拆装箱出现null的情况(防止success为null)return Boolean.TRUE.equals(success);}Overridepublic void unlock() {// 获取线程标示String threadId ID_PREFIX Thread.currentThread().getId();// 获取锁中的标示String id stringRedisTemplate.opsForValue().get(KEY_PREFIX name);// 判断标示是否一致if(threadId.equals(id)) {// 释放锁stringRedisTemplate.delete(KEY_PREFIX name);}}}Redis分布式锁误删问题2原子性问题
前面我们在释放锁的时候添加了1个判断线程在释放锁之前会执行1个判断来判断当前持有这把锁的线程是不是当前线程通过判断锁标识来实现。如果是才去释放锁如果不是则不去释放锁。但这样仍然存在问题假设线程1获取到锁之后执行完业务然后也进行了判断当前持有锁是不是自己这个时候的确是自己线程1判断完成之后开始去释放锁。比如恰巧这个时候发生了FULL-GC所有代码都被阻塞并且时间还比较长超过了锁的过期时间锁被释放了此时线程2就可以拿到锁线程2拿到锁之后就开始执行自己的业务但是这个时候线程1就去释放锁了线程1又一次释放了当前不属于自己的锁又发生了误删的问题。假设此时线程3过来拿锁因为锁已经被线程1给释放掉了因此线程3就拿到了锁开始执行业务此时发现线程2和线程3都在执行业务了它们并没有被并发控制。
出现这个问题的根本原因在于判断锁标识是否是自己 和 释放锁 是2个动作不具备原子性。 因此如果要解决这个问题判断锁标识 与 删除锁 必须是原子操作不能被间隔。我们可以使用lua脚本来解决这个问题。
Lua脚本解决多条命令原子性问题
Redis提供了Lua脚本功能在一个脚本中编写多条Redis命令确保多条命令执行时的原子性。
Lua是一种编程语言它的基本语法大家可以参考网站https://www.runoob.com/lua/lua-tutorial.html
Redis的Lua脚本
Redis提供的调用函数
这里重点介绍Redis提供的调用函数语法如下
# 执行redis命令
redis.call(命令名称, key, 其它参数, ...)示例
例如我们要执行set name jack则脚本是这样
# 执行 set name jack
redis.call(set, name, jack)例如我们要先执行set name Rose再执行get name则脚本如下
# 先执行 set name jack
redis.call(set, name, jack)# 再执行 get name
local name redis.call(get, name)# 返回
return nameredis.call(…)函数调用及传参
写好脚本以后需要用Redis命令来调用脚本调用脚本的常见命令如下 例如我们要执行 redis.call(‘set’, ‘name’, ‘jack’) 这个脚本语法如下 如果脚本中的key、value不想写死可以作为参数传递。key类型参数会放入KEYS数组其它参数会放入ARGV数组在脚本中可以从KEYS和ARGV数组获取这些参数 示例 lua脚本改进Redis的分布式锁
lua脚本解决redis命令原子性问题
前面我们提到由于redis执行命令判断锁标识是否是自己 和 释放锁 是2个动作不具备原子性现在使用lua脚本解决执行多个redis命令原子性问题。
释放锁的业务流程是这样的
获取锁中的线程标示判断是否与指定的标示当前线程标示一致如果一致则释放锁删除如果不一致则什么都不做
初步写
-- 锁的key
local key lock:order:5-- 当前线程标识
local threadId xxxx-33-- 获取锁中的线程标识 get key
local id redis.call(get, key)-- 比较线程标识 与 锁中的标识 是否一致
if(id threadId) then-- 释放锁 del keyreturn redis.call(del, key)
endreturn 0将变量替换为从数组中取
-- 锁的key
local key KEYS[1]-- 当前线程标识
local threadId ARGV[1]-- 获取锁中的线程标识 get key
local id redis.call(get, key)-- 比较线程标识 与 锁中的标识 是否一致
if(id threadId) then-- 释放锁 del keyreturn redis.call(del, key)
endreturn 0简化
-- 这里的 KEYS[1] 就是锁的key这里的ARGV[1] 就是当前线程标示
-- 获取锁中的标示判断是否与当前线程标示一致
if (redis.call(GET, KEYS[1]) ARGV[1]) then-- 一致则删除锁成功删除锁, 则返回1return redis.call(DEL, KEYS[1])end-- 不一致则直接返回
return 0SimpleRedisLock改进版
unlock.lua
-- 比较线程标示与锁中的标示是否一致
if(redis.call(get, KEYS[1]) ARGV[1]) then-- 释放锁 del keyreturn redis.call(del, KEYS[1])
end
return 0SimpleRedisLock
public class SimpleRedisLock implements ILock {// 业务的名称, 即锁的名称private String name;private StringRedisTemplate stringRedisTemplate;// 锁统一前缀private static final String KEY_PREFIX lock:;// 引入uuid, 用于区分多个jvm因为线程id是递增的, 防止多个jvm的线程id出现重复的情况private static final String ID_PREFIX UUID.randomUUID().toString(true) -;//提前加载好unlock.lua脚本//其中泛型为返回值private static final DefaultRedisScriptLong UNLOCK_SCRIPT;static {UNLOCK_SCRIPT new DefaultRedisScript();// 指定类路径下的unlock.lua文件UNLOCK_SCRIPT.setLocation(new ClassPathResource(unlock.lua));// 指定返回值类型UNLOCK_SCRIPT.setResultType(Long.class);}// 传入业务名称和stringRedisTemplatepublic SimpleRedisLock(String name, StringRedisTemplate stringRedisTemplate) {this.name name;this.stringRedisTemplate stringRedisTemplate;}Overridepublic boolean tryLock(long timeoutSec) {// 获取线程标示String threadId Thread.currentThread().getId();// 获取锁Boolean success stringRedisTemplate.opsForValue().setIfAbsent(KEY_PREFIX name,ID_PREFIX threadId,// 可以指定超时时间(这里实际上是假设获取锁成功之后, 同时指定锁的过期时间。// 并不是阻塞式获取锁失败后的等待时间)timeoutSec,TimeUnit.SECONDS);// 避免自动拆装箱出现null的情况(防止success为null)return Boolean.TRUE.equals(success);}Overridepublic void unlock() {/*// 获取线程标示String threadId ID_PREFIX Thread.currentThread().getId();// 获取锁中的标示String id stringRedisTemplate.opsForValue().get(KEY_PREFIX name);// 判断标示是否一致if(threadId.equals(id)) {// 释放锁stringRedisTemplate.delete(KEY_PREFIX name);}*/// 调用lua脚本这里会由redis保证判断和删除这2个操作的原子性//由原来的在代码中判断线程标识与锁中的标识, 如果标识一致, 则删除。改为执行lua脚本命令, 让这2个命令具备原子性stringRedisTemplate.execute(UNLOCK_SCRIPT, // lua脚本Collections.singletonList(KEY_PREFIX name), // 锁的keyID_PREFIX Thread.currentThread().getId() // 线程标识);// 这里在释放锁。如果当前锁是自己的, 则会释放成功; 如果当前锁不是自己的, 则不会释放。// 并且判断锁是否是自己的与释放锁是具备原子性的。}}测试
将应用启动2次idea勾选允许并行运行发送2个请求分别到这2个应用上先让其中1个应用获取到锁然后走删除锁的逻辑这里打上断点然后让锁超时此时让第2个应用获取到锁第2个应用能够成功拿到锁。此时让第1个应用执行释放锁的逻辑发现第1个应用没有删除第2个应用拿到的锁因为此时锁已经不是第1个应用的。因此锁是没有被误删的。同时由于redis提供的lua脚本功能让判断锁标识与释放锁具备原子性不会出现线程安全的漏洞。
总结
基于Redis的分布式锁实现思路
利用set nx ex获取锁并设置过期时间保存线程标示释放锁时先判断线程标示是否与自己一致一致则删除锁
特性
利用set nx满足互斥性利用set ex保证故障时锁依然能释放避免死锁提高安全性利用Redis集群保证高可用和高并发特性
2. redisson
2.1 setnx实现的分布式锁存在的问题
基于setnx实现的分布式锁存在下面的问题 2.2 Redisson简介
Redisson是一个在Redis的基础上实现的Java驻内存数据网格In-Memory Data Grid。它不仅提供了一系列的分布式的Java常用对象还提供了许多分布式服务其中就包含了各种分布式锁的实现。 官网地址 https://redisson.org GitHub地址 https://github.com/redisson/redisson
redisson的wiki文档https://github.com/redisson/redisson/wiki/1.-%E6%A6%82%E8%BF%B0
2.3 redisson快速入门
引入依赖
dependency groupIdorg.redisson/groupIdartifactIdredisson/artifactIdversion3.13.6/version
/dependency配置Redisson客户端
Configuration
public class RedissonConfig {Beanpublic RedissonClient redissonClient(){// 配置Config config new Config();config.useSingleServer().setAddress(redis://192.168.150.101:6379).setPassword(123321);// 创建RedissonClient对象return Redisson.create(config);}
}使用Redisson的分布式锁
Resource
private RedissonClient redissonClient;Test
void testRedisson() throws InterruptedException { // 获取锁可重入指定锁的名称 RLock lock redissonClient.getLock(anyLock);// 尝试获取锁可以空参, 也可以有参// 有参的参数分别是waitTime - 获取锁的最大等待时间期间会重试// leaseTime - 锁自动释放时间// timeUnit - 时间单位 boolean isLock lock.tryLock(1, 10, TimeUnit.SECONDS);// 判断释放获取成功 if (isLock) {try {System.out.println(执行业务);} finally {// 释放锁 lock.unlock();}}
}2.4 redisson的可重入原理
可重入原理分析
前面我们时使用redis提供的set命令并且指定nx命令参数当key不存在时执行set命令和ex命令参数当key设置成功时指定key的过期时间。如下图method1方法在第一次获取锁时由于key不存在获取锁会成功。当method1调用method2时method2方法也要取获取锁由于在method1已经获取了锁此时再用setnx命令获取就会失败因此这种方式获取的锁是不可重入的。 为了实现锁的可重入我们参考jdk中可重入锁的实现原理。我们在获取锁的时候不仅要在锁中记录当前线程标识还需要记录当前的重入次数。显然 这个时候使用redis的string数据结构就不满足需求了。因此可以采用hash数据结构注意hash结构未提供setnx ex等命令参数如下图所示KEY记录锁标识field记录线程标识value记录锁的重入次数。在获取锁时先判断锁是否存在如果已经存在先不立刻失败而是查看当前锁中的线程标识是否是自己如果是自己则重入次数加1如果不是自己才失败。再说释放锁在释放锁的时候先去查看当前这个锁的线程标识是否是自己如果是才能释放不是的话就不能释放并且释放的时候并不是直接删除而是把锁重入次数减1如果减为0了才把这把锁给删掉如果还不为0说明还有其它业务就要重置有效期给后面的业务留够充足的时间。 获取锁的lua脚本 释放锁的lua脚本 redisson获取锁释放锁源码 2.5 redisson的锁重试和锁超时解决方式
图解 代码
public class RedissonLock extends RedissonExpirable implements RLock {// ...// waitTime - 最大锁等待时间// leaseTime - 获取锁成功时, 设置的失效时间// 1. 锁重试问题: // waitTime就是指定的等待时间,// 里面用到了redis的发布订阅机制, redisson在释放锁的lua脚本中, 释放锁时会发布消息, 这里会订阅消息// 不断计算剩余等待时间 redis发布订阅 实现锁重试Overridepublic 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 tryAcquire(waitTime, leaseTime, unit, threadId);// lock acquiredif (ttl null) {return true;}time - System.currentTimeMillis() - current;if (time 0) {acquireFailed(waitTime, unit, threadId);return false;}current System.currentTimeMillis();RFutureRedissonLockEntry subscribeFuture subscribe(threadId);if (!subscribeFuture.await(time, TimeUnit.MILLISECONDS)) {if (!subscribeFuture.cancel(false)) {subscribeFuture.onComplete((res, e) - {if (e null) {unsubscribe(subscribeFuture, threadId);}});}acquireFailed(waitTime, unit, threadId);return false;}try {time - System.currentTimeMillis() - current;if (time 0) {acquireFailed(waitTime, unit, threadId);return false;}while (true) {long currentTime System.currentTimeMillis();ttl tryAcquire(waitTime, leaseTime, unit, threadId);// lock acquiredif (ttl null) {return true;}time - System.currentTimeMillis() - currentTime;if (time 0) {acquireFailed(waitTime, unit, threadId);return false;}// waiting for messagecurrentTime System.currentTimeMillis();if (ttl 0 ttl time) {subscribeFuture.getNow().getLatch().tryAcquire(ttl, TimeUnit.MILLISECONDS);} else {subscribeFuture.getNow().getLatch().tryAcquire(time, TimeUnit.MILLISECONDS);}time - System.currentTimeMillis() - currentTime;if (time 0) {acquireFailed(waitTime, unit, threadId);return false;}}} finally {unsubscribe(subscribeFuture, threadId);}}// ...// 2. 锁超时问题: // 当leaseTime为-1时, 才会开启看门狗机制, 每隔一段时间就去重置有效期。在释放锁的时候, 会取消看门狗这个任务。private T RFutureLong tryAcquireAsync(long waitTime, long leaseTime, TimeUnit unit, long threadId) {if (leaseTime ! -1) {return tryLockInnerAsync(waitTime, leaseTime, unit, threadId, RedisCommands.EVAL_LONG);}RFutureLong ttlRemainingFuture tryLockInnerAsync(waitTime,commandExecutor.getConnectionManager().getCfg().getLockWatchdogTimeout(),TimeUnit.MILLISECONDS, threadId, RedisCommands.EVAL_LONG);ttlRemainingFuture.onComplete((ttlRemaining, e) - {if (e ! null) {return;}// lock acquiredif (ttlRemaining null) {// 获取锁成功之后, 会自动续约scheduleExpirationRenewal(threadId);}});return ttlRemainingFuture;}private void scheduleExpirationRenewal(long threadId) {ExpirationEntry entry new ExpirationEntry();// 获取的entryName实际上是跟锁是一对一的ExpirationEntry oldEntry EXPIRATION_RENEWAL_MAP.putIfAbsent(getEntryName(), entry);if (oldEntry ! null) {oldEntry.addThreadId(threadId);} else {entry.addThreadId(threadId);// 续约renewExpiration();}}// 续约方法private void renewExpiration() {ExpirationEntry ee EXPIRATION_RENEWAL_MAP.get(getEntryName());if (ee null) {return;}Timeout task commandExecutor.getConnectionManager().newTimeout(new TimerTask() {Overridepublic void run(Timeout timeout) throws Exception {ExpirationEntry ent EXPIRATION_RENEWAL_MAP.get(getEntryName());if (ent null) {return;}Long threadId ent.getFirstThreadId();if (threadId null) {return;}RFutureBoolean future renewExpirationAsync(threadId);future.onComplete((res, e) - {if (e ! null) {log.error(Cant update lock getName() expiration, e);return;}if (res) {// 完成之后, 继续续约, 重置有效期//无限延续下去, 因此在释放锁的时候, 会取消这个续约任务renewExpiration();}});}// 看门狗默认时间为30s, 因此, 这里是10s}, internalLockLeaseTime / 3, TimeUnit.MILLISECONDS); ee.setTimeout(task);}protected RFutureBoolean renewExpirationAsync(long threadId) {// 重置锁的有效期return evalWriteAsync(getName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN,if (redis.call(hexists, KEYS[1], ARGV[2]) 1) then redis.call(pexpire, KEYS[1], ARGV[1]); return 1; end; return 0;,Collections.singletonList(getName()),internalLockLeaseTime, getLockName(threadId));}
}总结
Redisson分布式锁原理
可重入利用hash结构记录线程id和重入次数可重试利用信号量和PubSub功能实现等待、唤醒获取锁失败的重试机制超时续约利用watchDog每隔一段时间releaseTime / 3重置超时时间
2.6 联锁
分布式锁主从一致性问题
为了提高redis的可用性在生产中往往会搭建redis主从模式它由多台redis组成只不过它们的角色会有不同会有一台作为主节点其它的作为从节点。主从的职责也会不一样往往会做读写分离主节点用来处理所有发向redis的写的操作增删改从节点处理redis的读的操作。既然数据都写在了主节点那么从节点没有数据又是怎么处理读的请求呢因此主节点和从节点需要做数据的同步。主节点会不断的把数据同步给从节点来确保主节点和从节点的数据是一致的。但是主从同步会有一定的延迟所以数据同步也会有延迟尽管延迟很短但它客观存在。redisson分布式锁主从一致性问题正是由这个延迟导致的。
当1个java应用来获取锁执行1个写操作命令来获取锁此时主节点执行成功保存了该数据。而后主节点会向从节点同步数据恰在此时主节点发生故障同步尚未完成这个时候redis中会有哨兵监控集群状态当发现主节点宕机后会在从节点中选出1台作为新的主节点但因为之前的主节点尚未把数据同步过来也就是锁已经丢失了。当java应用再来访问这个新的主节点时就会发现锁已经没有了锁失效了假设此时其它线程也来获取锁因为所以经丢失了所以其它线程也能够获取到锁了此时就发生了并发的安全问题。 redisson解决主从一致问题
既然主从关系是导致出现主从一致问题的原因干脆就不要主从了所有的节点都看作是独立的redis节点相互之间没有任何关系都可以做读写。此时获取锁的方式就变了之前只需要向master执行写操作获取锁就可以了但现在必须依次的向多个redis节点都去执行写操作获取锁都保存了这个锁标识才算获取锁成功。因为现在没有主从所以没有主从一致性问题。假设有一台redis节点宕机了但此时redis仍然是可用的只要有节点还存活着redis的锁仍然有效。 为了继续提高redis的可用性我们也可以给每个redis节点建立主从关系如下图所示。假设其中1个redis节点发生故障假设它并未完成同步那么它的slave上就没有锁的标识同时这个slave也会成为新的主节点但它没有锁标识。此时1个线程过来拿锁是获取不了的因为必须每1个节点都拿到锁才算拿到锁成功。尽管这个主节点能拿到成功但其它节点仍保存了锁标识因此只要有1个节点存活者那么其它线程就不可能拿到锁就不会出现锁失效的问题。这样的方案保留了主从同步机制确保了整个redis集群高可用的特性同时也避免了主从一致引发的锁失效问题这种方案在redis中叫MultiLock即联锁。 联锁使用示例
配置3个RedissonClient
Configuration
public class RedissonConfig {Beanpublic RedissonClient redissonClient1(){// 配置Config config new Config();config.useSingleServer().setAddress(redis://192.168.150.101:6379).setPassword(123321);// 创建RedissonClient对象return Redisson.create(config);}Beanpublic RedissonClient redissonClient1(){// 配置Config config new Config();config.useSingleServer().setAddress(redis://192.168.150.101:6380).setPassword(123321);// 创建RedissonClient对象return Redisson.create(config);}Beanpublic RedissonClient redissonClient1(){// 配置Config config new Config();config.useSingleServer().setAddress(redis://192.168.150.101:6381).setPassword(123321);// 创建RedissonClient对象return Redisson.create(config);}
}使用RedissonMultiLock
Resource
private RedissonClient redissonClient1;Resource
private RedissonClient redissonClient2;Resource
private RedissonClient redissonClient3;private RLock lock;Test
public void testMultiLock(){RLock lock1 redissonClient1.getLock(order);RLock lock2 redissonClient2.getLock(order);RLock lock3 redissonClient3.getLock(order);// 创建联锁//这里使用redissonClient1 或 redissonClient2 或 redissonClient3 都是一样的lock redissonClient1.getMultiLock(lock1, lock2, lock3);
}总结
1不可重入Redis分布式锁
原理利用setnx的互斥性利用ex避免死锁释放锁时判断线程标示缺陷不可重入、无法重试、锁超时失效
2可重入的Redis分布式锁
原理利用hash结构记录线程标示和重入次数利用watchDog延续锁时间利用信号量控制锁重试等待缺陷redis宕机引起锁失效问题
3Redisson的multiLock
原理多个独立的Redis节点必须在所有节点都获取重入锁才算获取锁成功缺陷运维成本高、实现复杂