1.前言
对于Redis实现分布式锁的几种方案这个话题,展开之前我想先简单聊聊什么是分布式锁,分布式锁的使用场景,除了Redis外还有什么技术实现分布式锁等一系列内容。
1.1分布式锁
说大一点,就是在现在发展越来越迅速的大背景下,去中心化分布式系统越来越普及,在我们实际的生产开发当中,有一种不可避免的场景就是多个进程互斥的对其资源的使用,为了保证数据不重复,要求在同一时刻,同一任务只在一个节点上运行,且保证在多进程下的数据安全,分布式锁就十分重要了。
1.2分布式锁的几种方案
方式有很多种,根据技术角度的不同
有基于MySQL的方式,通过表的唯一索引,通过insert和delete就可以实现加锁和解锁的效果;
有基于zookeeper的方式,通过创建临时有序节点,判断创建的节点序号是否最小。若是,则表示获取到锁,不是,则watch /lock目录下序号比自身小的前一个节点,解锁只需要删除节点;
有基于Redis的方式。通过执行setnx,若成功再执行expire添加过期时间的方式加锁,解锁执行delete命令。
方式有很多,不一一列举了。
1.3Redis分布式锁需要满足的条件
- 互斥性。在任意时刻,只有一个客户端能持有锁。
- 不发生死锁。即使有一个客户端在持有锁的期间崩溃而没有主动解锁也能保证后续其他客户端能加锁。
- 同一性。加锁和解锁必须是同一个客户端,客户端自己不能把别人加的锁给解了,即不能误解锁。
- 容错性。只要大多数Redis节点正常运行,客户端就能够获取和释放锁。
2.Redis实现分布式锁的几种方案
可以通过以下方式实现(包括但不限于):
- SETNX + EXPIRE
- SETNX + value(系统时间+过期时间)
- 通过开源框架-Redisson
简单来说说,用Java代码演示:
2.1 SETNX + EXPIRE
setnx
(SET IF NOT EXISTS)+ expire
命令。先用setnx
来抢锁,如果抢到锁,再用expire
给锁设置一个过期时间,这样持有锁超时时释放锁,防止锁忘记释放。但此时setnx
和expire
两个命令无法保证原子性,例如:
if(jedis.setnx(key_resource_id,lock_value) == 1){ **//加锁** expire(key_resource_id,100); **//设置过期时间** try { **//业务代码块** }catch() { }finally { jedis.del(key_resource_id); //释放锁 } }
2.2 SETNX + value(系统时间+过期时间)
可以把过期时间放到setnx
的value值里面。如果加锁失败,再拿出value值校验一下即可。加锁代码如下:
long expires = System.currentTimeMillis() + expireTime; **//系统时间+设置的过期时间** String expiresStr = String.valueOf(expires); **// 如果当前锁不存在,则加锁成功** if (jedis.setnx(key_resource_id, expiresStr) == 1) { return true; } **// 如果锁已经存在,获取锁的过期时间** String currentValueStr = jedis.get(key_resource_id); **// 如果获取到的过期时间,小于系统当前时间,表示已经过期** if (currentValueStr != null && Long.parseLong(currentValueStr) < System.currentTimeMillis()) { **// 锁已过期,获取上一个锁的过期时间,并设置现在锁的过期时间** String oldValueStr = jedis.getSet(key_resource_id, expiresStr); if (oldValueStr != null && oldValueStr.equals(currentValueStr)) { **// 考虑多线程并发的情况,只有一个线程的设置值和当前值相同,它才可以加锁** return true; } } **//其他情况均返回加锁失败** return false;
2.3 通过开源框架-Redisson
那么此时就要去想了,如果已经超过了加锁的过期时间,可是业务还没执行完成,这个时候怎么做呢?是把过期时间延长吗?显然不合理,可以通过开源框架-Redisson优化这个问题,简单来说,Redisson就是当一个线程获得锁以后,给该线程开启一个定时守护线程,每隔一段时间检查锁是否还存在,存在则对锁的过期时间延长,防止锁过期提前释放。假设两个线程争夺统一公共资源:线程A获取锁,并通过哈希算法选择节点,执行Lua脚本加锁,同时其看门狗机制会启动一个watch dog
(后台线程),每隔10秒检查线程,如果线程A还持有锁,那么就会不断的延长锁key的生存时间。线程B获得锁失败,就会订阅解锁消息,当获取锁到剩余过期时间后,调用信号量方法阻塞住,直到被唤醒或等待超时。一旦线程A释放了锁,就会广播解锁消息。于是,解锁消息的监听器会释放信号量,获取锁被阻塞的线程B就会被唤醒,并重新尝试获取锁。
Redisson 支持单点模式、主从模式、哨兵模式、集群模式,假设现为单点模式:
//构造Config Config config = new Config(); config.useSingleServer().setAddress("redis://ip:port").setPassword("Password.~#").setDatabase(0); //构造RedissonClient RedissonClient redissonClient = Redisson.create(config); //获取锁实例 RLock rLock = redissonClient.getLock(lockKey); try { //获取锁,waitTimeout为最大等待时间,超过这个值,则认为获取锁失败。leaseTime为锁的持有时间** boolean res = rLock.tryLock((long)waitTimeout, (long)leaseTime, TimeUnit.SECONDS); if (res) { //业务块 } } catch (Exception e) { }finally{ //解锁 rLock.unlock(); }