引言
在多线程并发的情况下,我们可以使用锁来保证一个代码块在同一时间内只能由一个线程访问。比如Java的synchronized关键字和Reentrantlock类等等。
这样子可以保证在同一个JVM进程内的多个线程同步执行。如果在分布式的集群环境中,如何保证不同节点的线程同步执行呢?
分布式锁需要解决的问题
- 互斥性:任意时刻只能有一个客户端拥有锁,不能同时多个客户端获取
- 安全性:锁只能被持有该锁的用户删除,而不能被其他用户删除
- 死锁:获取锁的客户端因为某些原因而宕机,而未能释放锁,其他客户端无法获取此锁,需要有机制来避免该类问题的发生
- 容错:当部分节点宕机,客户端仍能获取锁或者释放锁
分布式锁的实现方式
1.Memcached分布式锁
利用Memcached的add命令。此命令是原子性操作,只有在key不存在的情况下,才能add成功,也就意味着线程得到了锁。
2. Redis分布式锁
和Memcached的方式类似,利用Redis的setnx命令。此命令同样是原子性操作,只有在key不存在的情况下,才能set成功。(setnx命令并不完善,后续会介绍替代方案)
3.Zookeeper分布式锁
利用Zookeeper的顺序临时节点,来实现分布式锁和等待队列。Zookeeper设计的初衷,就是为了实现分布式锁服务的。
redis分布式锁的实现
1.加锁
SET key value [EX seconds] [PX milliseconds] [NX|XX]
- EX second :设置键的过期时间为second秒
- PX millisecond :设置键的过期时间为millisecond毫秒
- NX :只在键不存在时,才对键进行设置操作
- XX:只在键已经存在时,才对键进行设置操作
- SET操作成功完成时,返回OK ,否则返回nil
以下是伪代码:
String result = redisService.set(lockKey, lockValue, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, expireTime);
if ("OK".equals(result)) {
//执行独占资源逻辑
doSomething();
}
2.解锁
redisService.del(lockKey);
3,加锁解锁分析
在加锁的同时设置超时时间,并且该操作是原子的,解决了互斥、死锁的问题,但是在释放锁的时候,不能保证是释放当前线程加的锁。假如某线程成功得到了锁,并且设置的超时时间是30秒。如果某些原因导致线程A执行的很慢很慢,过了30秒都没执行完,这时候锁过期自动释放,线程B得到了锁。随后,线程A执行完了任务,线程A接着执行del指令来释放锁。但这时候线程B还没执行完,线程A实际上删除的是线程B加的锁。
4.如何避免错误释放锁
可以在del释放锁之前做一个判断,验证当前的锁是不是自己加的锁。
至于具体的实现,可以在加锁的时候把当前的线程ID当做value,并在删除之前验证key对应的value是不是自己线程的ID。
- 加锁
String threadId = Thread.currentThread().getId()
redisService.set(key,threadId ,30,NX)
- 解锁
if(threadId .equals(redisService.get(key))){
redisService.del(key)
}
5.为锁“续航”
在实际的项目中,有可能遇到网络问题,导致某个操作在规定时间不能完成 ,如何防止这种情况下key超时过期呢?我们可以启动一个后台线程,在检查到key快要过期的时候,自动给key重新设置超时时间,达到“续航”的目的。
expire(key,30)
参考:
https://blog.csdn.net/kongmin_123/article/details/82080962
https://blog.csdn.net/qq_38542085/article/details/91818742