为什么使用分布式锁
场景
在分布式系统中,java 中 synchronized 锁只是 JVM 级别的,也就是进程级别。因此,当同一个服务,启动多次出现多个节点时,在不同进程中,相同的同步代码块使用 synchronized,并不能达到想要的同步效果,也就是这个关键字管不到别的进程。
此时,前端如果出现高并发场景,系统通过负载均衡,将同一个接口的请求,分发到不同节点中处理业务,这些请求都分别能拿到各自所在进程的锁,去处理业务,从而导致问题出现。
解决方案
在需要同步的代码块前面加 redis 分布式锁,同步代码块执行完毕后,释放锁。从而达到同步操作的目的。
具体解决方案
认识 redis 分布式锁实现的关键命令
SET key value [EX seconds] [PX milliseconds] [NX|XX]
参数解释:
EX seconds : 将键的过期时间设置为 seconds 秒。
PX milliseconds : 将键的过期时间设置为 milliseconds 毫秒。PSETEX key milliseconds value 。
NX : 只在键不存在时, 才对键进行设置操作。 执行 SET key value NX 的效果等同于执行 SETNX key value 。
XX : 只在键已经存在时, 才对键进行设置操作。
注意:
众所周知,中括号参数是可选的,而一个中括号中出现管道符 “|” 时,表示符号两边的参数只能选一个。
实际应用
在 java 中对应的 redis api 如下,使用此接口设置的 key 不能重复。也就是同时多个线程 set 同一个 key 时,只有第一个线程能成功,接口返回 true,后续的线程都将返回 false。
boolean result = redisTemplate.opsForValue().setIfAbsent(lockKey, value, timeout, timeUnit);
所以,通过上面接口就能实现同步功能中的加锁操作,只要线程执行这段代码返回的是 false,就直接 return,后续具体业务逻辑代码不允许执行。
而解锁方式,就是直接删除这个作为锁使用的 key
redisTemplate.delete(lockKey)
大致代码如下:
// 加锁
boolean result = redisTemplate.opsForValue().setIfAbsent(lockKey, value, timeout, timeUnit); if (!result) { log.warn("已经有线程在执行此段代码,其他线程不允许调用"); return; } // 执行业务代码 ... ... 业务代码 ...
// 业务代码执行完毕,删除锁
redisTemplate.delete(lockKey)
上面这种简单加锁可能出现的问题
锁失效引起的问题
高并发情况下,假设上面加锁代码中 lockKey 这个变量值为 "lock",timeout 值为 10,timeUtil 为 TimeUnit.SECONDS 也就是秒。此时线程 A 执行加锁操作后,再执行完同步块中的业务代码需要花 15 秒钟。在线程 A 还未执行完毕,执行到第 10 秒的时候,锁失效时间到了,被 redis 清除了。
然后线程 B 来了,执行加锁接口返回 true,也就是因为线程 A 锁失效了,所以线程 B 又能成功加锁了。
线程 B 执行业务代码需要 10 秒,在线程 B 执行到第 5 秒时,线程 A 因为之前已经执行 10 秒,此时又经过 5 秒后,业务逻辑代码执行完毕,执行到删除锁的代码,因为大家使用的是值相同的 lockKey,所以线程 A 成功的删除了线程 B添加的锁。
这时,线程 C 也来了,发现没锁,它也加锁,然后执行业务代码,它的锁又被 B 清除了......,以此类推,会出现锁永远失效的情况。
锁失效引起的问题 解决方案
解决执行业务逻辑时,线程加的锁因到了失效时间,被 redis 清除的问题:在执行业务代码前,开个线程,使用定时器,定时更新 lockKey 失效时间。例如上面设置10 秒失效,定时器这边取失效时间的 1/3 (Redisson 也是取1/3时间),作为定时去更新失效时间的时间间隔,只要判断这个 lockKey 还存在,就去更新,避免在线程还未执行完业务代码,锁就是失效了。
解决锁被其他线程删除的问题:lockKey 使用不同的值,没个线程都各自使用一个唯一的值(如:UUID)作为线程唯一标识。在删除 lockKey 的时候,使用当前前程唯一标识 redis 中缓存的 lockKey 做对比,只有值相同的情况下,才允许删除。
须知:
上面解决方案实现起来,代码不少,所以可以使用 Redisson 来实现加锁,解锁的操作。Redisson 内部就使用了上面的解决方案,来避免锁失效引起的问题。
使用 Redision 解决大致代码
// 引入 Redisson @Autowrite Redisson redisson; ************************ // 使用 Redisson String lockKey = "lock"; RLock redissonLock = redisson.getLock(lockKey) // 加锁操作。只有无锁的线程能执行下面代码加锁,其他线程都会被阻塞,所以不需要前面的 if 判断来处理 redissonLock.lock(30, TimeUnit.SECONDS); // 执行业务代码 ... ... 业务代码 ... // 业务代码执行完毕,释放锁(一定要在 finally 中执行) redissonLock.unlock();
主节点宕机,锁没来得及向从节点同步问题
基于上面 redisson 的加锁场景,在 redis 集群中,如果刚加的锁,主节点还未来的及向从节点同步,就挂了。此时,从节点在选举出新的主节点后,里面并没有刚刚加的锁,这个问题该如何解决。?
分布式锁优化问题
基于上面 redisson 的场景,加的锁该如何优化,提升效率?
标签:加锁,代码,Redis,线程,失效,执行,lockKey,分布式 From: https://www.cnblogs.com/zhousjcn/p/17498065.html