我的博客文章https://affzl.xyz
所有服务抢占同一个公共锁,抢到后,执行业务,执行完毕释放业务。
去中心化分布式系统越来越普及,有一种不可避免的场景就是多个进程互斥的对其资源的使用,为了保证数据不重复,要求在同一时刻,同一任务只在一个节点上运行,且保证在多进程下的数据安全,分布式锁就十分重要了。
通过执行setnx,若成功再执行expire添加过期时间的方式加锁,解锁执行delete命令。
SETNX key value:将键名为 key、值为 value 的数据存储到 Redis 数据库中,但只有在该键不存在时才进行设置。如果该键已经存在,则该操作不会执行任何动作。
EXPIRE key seconds:为键名为 key 的数据设置过期时间,单位为秒。在指定的 seconds 秒数之后,Redis 会自动删除该键及其对应的值。
例如:
SETNX session:1234 some_value # 若session:1234 不存在就创建
EXPIRE session:1234 60 # session:1234 60s后过期
先用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); //释放锁
}
}
把过期时间放到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;
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();
}
set lock lock1 NX 将键名为 “lock”,值为 “lock1” 的数据存储到 Redis 数据库中,并且只有在该键不存在时才进行设置,即使用 “NX”(Not Exists)选项
直接加锁
//1、占分布式锁。去redis占坑、加锁
Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock", "1");
if (lock){
//加锁成功 执行业务
Map<String, List<Catelog2Vo>> dataFromDb = getDataFromDb();
redisTemplate.delete("lock");
return dataFromDb;
}
问题:如果因为一些原因没有解锁,会造成死锁
解决:设置一个过期时间,避免死锁
直接加锁+过期时间
EXPIRE lock 30
//1、占分布式锁。去redis占坑
Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock", "1");
if (lock){
//设置过期时间
redisTemplate.expire("lock",30, TimeUnit.SECONDS);
//加锁成功 执行业务
Map<String, List<Catelog2Vo>> dataFromDb = getDataFromDb();
redisTemplate.delete("lock");
return dataFromDb;
}
问题:原子性问题,如果在设置过期时间时挂了,即过期时间设置失败会造成死锁
解决:在加锁时设置过期时间,解决了原子性问题
加锁时设置过期时间
set lock lock1 EX 30 NX
//1、占分布式锁。去redis占坑
Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock", "1",30,TimeUnit.SECONDS);
if (lock){
//加锁成功 执行业务
Map<String, List<Catelog2Vo>> dataFromDb = getDataFromDb();
redisTemplate.delete("lock");
return dataFromDb;
}
问题:在执行完毕删锁时,该线程的lock可能已经过期,其它线程又获取了lock。可能删除别的线程中的lock
解决:获取lock对比删除这几步必须是原子操作,使用lua脚本解决
使用lua脚本
//1、占分布式锁。去redis占坑
String uuid = UUID.randomUUID().toString();
Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock", uuid,30,TimeUnit.SECONDS);
if (lock){
Map<String, List<Catelog2Vo>> dataFromDb = null;
try {
//加锁成功 执行业务
dataFromDb = getDataFromDb();
}finally {
//原子删锁
String script = "if redis.call(\"get\",KEYS[1]) == ARGV[1] \n" +
"then\n" +
"\treturn redis.call(\"del\",KEYS[1])\n" +
"else\n" +
" return 0\n" +
"end;\n";
redisTemplate.execute(new DefaultRedisScript<Integer>(script,Integer.class)
,Arrays.asList("lock"),uuid);
}
return dataFromDb;
}