什么是分布式锁(进程之间)?
分布式锁是控制分布式系统或不同系统之间共同访问共享资源的一种锁的实现,如果不同的系统或同一个系统的不同主机之间共享了某个资源时,往往需要互斥来防止彼此干扰,进而保证一致性
分布式锁需要解决的问题
- 互斥性
任意时刻只能有一个客户端获取锁,不能同时有两个客户端获取到锁 - 安全性
锁只能被持有该锁的客户端删除,不能由其他客户端删除 - 死锁
获取锁的客户端因为某些原因而宕机而未能释放锁,其他客户端再也无法获取到该锁而导致了死锁,此时需要有机制来避免这个问题的发生 - 容错
当部分节点,比如redis节点宕机的时候,客户端任然能够获取锁和释放锁
如何通过Redis来实现
//如果key不存在,则创建key并且赋值
//setnx:set if not exist,操作是原子级别的
//时间复杂度是O(1)
//返回值:设置成功返回1,设置失败返回0
setnx key value
setnx的判断机制个原子操作初期被用来实现分布式锁
在执行某段代码逻辑的时候,先尝试使用setnx对某个key设值,如果设值成功,则证明此时没有别的进程在执行该段代码,或者说占用该独占资源,这个时候我们的进程就可以顺利的执行代码逻辑,如果执行失败,则证明此时有别的程序或进程占用该资源,那么当前进程就需要等待直到setnx成功
如何解决setnx设置的key长期有效问题
问题:如果setnx成功设置了某个key,该key就长期存在了,那么后续进程如何能再次获得到锁,那么就需要给该key设置一个过期时间
但是 setnx并不同时支持expire指令,就需要单独使用expire指定某个key的过期时间
// 设置key的过期,当key过期时,会被自动删除
expire key seconds
通过setnx和expire实现分布式锁的伪代码
//获得一个执行redis的服务,比如Jedis
RedisService redisService = SpringUtils.getBean(RedisService.class);
//通过setnx去设置某个用作判断的key,返回status
long status = redisService.setnx(key,"1");
//去循环判断
while(status == 0){
//可以添加一个线程休眠时间,隔一段时间去尝试一次
TimeUtils.sleep(time);
//通过setnx去设置某个用作判断的key,返回status
status = redisService.setnx(key,"1");
}
//如果 status为 0,则说明该key存在,有别的进程正在执行该资源,那么当前进程只能阻塞,直到获取status为1为止
if(status == 1){
//设置给key的过期时间
redisService.expire(key,seconds);
//执行独占资源逻辑
doWork();
}
上述实现方式,存在的问题:
出现该问题的原因是虽然setnx和expire都是原子性的操作,但是其组合到一起使用就无法满足原子性了,违背了操作的初衷及利用redis操作的原子性
解决: 从Redis2.6.12版本开始,可以使用set原子操作将setnx和expire组合到一块去执行了,具体如下:
//set成功时返回OK,否则返回nil
set key value [EX seconds] [PX milliseconds] [NX] [XX]
//EX seconds : 设置键的过期时间为seconds秒
//PX milliseconds : 设置键的过期时间为millseconds毫秒
//NX : 只在键不存在时,才对键进行设置操作,相当于 setnx
//XX : 只在键已经存在时,才对键进行设置操作
伪代码:
//获得一个执行redis的服务,比如Jedis
RedisService redisService = SpringUtils.getBean(RedisService.class);
//locke即用来判断的key,resultId为key的值,一般可以设置为线程Id
String result = redisService.set(lockKey,requestId,SET_IF_NOT_EXSIT,SET_WITH_EXPIRE_TIME,expireTime);
//去循环判断
while("nil".equals(result)){
//可以添加一个线程休眠时间,隔一段时间去尝试一次
TimeUtils.sleep(time);
//通过setnx去设置某个用作判断的key,返回status
result = redisService.set(lockKey,requestId,SET_IF_NOT_EXSIT,SET_WITH_EXPIRE_TIME,expireTime);
}
//如果result为nil,则说明该key存在,有别的进程正在执行该资源,那么当前进程只能阻塞,直到获取result为ok为止
if("OK".equals(result)){
//执行独占资源的代码逻辑
dowork();
}