分布式锁的本质就是要在redis中占位,当有别的线程进来的时候发现位置已经被占用就只好放弃或者稍后再试。 使用命令 setnx 指令来实现只允许被一个客服端占位,先来的先占,用完后在执行 del 指令
shell
setnx lock:codehole true
... do something
del lock:codehole- 问题1: 如果程序拿到了锁后在执行任务的过程中出现了异常,可能导致 del 指令没有被执行,这就会陷入到死锁,所有线程都不会再获取到锁。 为了解决这个问题,需要再给这个锁加个过期时间,等到期后锁被自动释放
shell
setnx lock:codehole true
expire lock:codehole 5 # 5秒后过期
... do something
del lock:codehole- 问题2: 由于 setnx 与 expire 是两个独立的命令,如果在执行了 setnx 之后系统出现了宕机就会导致 expire 无法被执行,也会造成死锁; 造成这个问题的根本原因就是 这两个命令不是原子操作,为了解决这个问题 redis 扩展了 set 命令
shell
set lock:codehole true ex 5 nx # ex 5 表示5秒过期,nx 表示仅在键不存在时设置
... do something
del lock:codehole以上的一条set命令就可以设置值和过期时间,原子操作
- 问题3: 由于锁设置了过期时间,如果任务执行的时间很长超过了设置的过期时间,那么锁会因为过期被删除掉,第二个线程重新持有这把锁, 紧接着第一个线程执行完了任务后就调用了del释放了锁,第三个线程又会在第二个线程执行任务的时候拿到锁。
解决的方案就是在 执行 set 指令的时候把value参数设置成随机值,把这个随机值存放到ThreadLocal中, 当释放锁的时候通过判断当前的锁的value是不是与ThreadLocal中存放值相同,如果相同就是本线程加的锁,可以释放,否则就不能释放。 但是 匹配value 与 del 不是一个原子操作,Redis也没有提供对应的指令,所以只能通过 lua 脚步来处理,因为lua脚步可以保证多个连续的指令是原子执行。
java
if redis.call("get",KEYS[1] == ARGV[1]) then
return redis.call("del",KEYS[1])问题4:在集群的环境下,分布式锁也会又缺陷,不是绝对的安全,比如哨兵模式下,主节点挂掉了,从节点会取而代之, 如果第一个客户端在主节点申请了一把锁,但是这把锁还没有来得及同步到从节点,主节点就宕机了,这时候从节点就会变成主节点, 而此时新的节点没有锁,所以当新的客户端来申请锁时,就会立即成功,导致系统中同一把锁被两个客服端持有。 不过这种情况仅仅发生在主从切换的情况下,时间很短,业务系统多数能够容忍。 如果特别在意这个情况,可以考虑使用
Redlock, 不过也是又代价的,就是需要更多的redis实例,性能也会下降。可重入性 可重入性是指线程在持有锁的情况下再次请求加锁,如果一个锁支持同一个线程多次加锁,那么这个锁就是可重入。 Redis的分布式锁要实现可重入可以使用 ThreadLocal 变量存储当前持有锁的计数
java
public class RedisWithReentrantLock {
private ThreadLocal<Map> lockers = new ThreadLocal<>();
private Jedis jedis;
public RedisWithReentrantLock(Jedis jedis) {
this.jedis = jedis;
}
private boolean _lock(String key) {
return jedis.set(key, "", "nx", "ex", 5L) != null;
}
private void _unlock(String key) {
jedis.del(key);
}
private Map<String, Integer> currentLockers() {
Map<String, Integer> refs = lockers.get();
if (refs != null) {
return refs;
}
lockers.set(new HashMap<>());
return lockers.get();
}
public boolean lock(String key) {
Map refs = currentLockers();
Integer refCnt = refs.get(key);
if (refCnt != null) {
refs.put(key, refCnt + 1);
return true;
}
boolean ok = this._lock(key);
if (!ok) {
return false;
}
refs.put(key, 1);
return true;
}
public boolean unlock(String key) {
Map refs = currentLockers();
Integer refCnt = refs.get(key);
if (refCnt == null) {
return false;
}
refCnt -= 1;
if (refCnt > 0) {
refs.put(key, refCnt);
} else {
refs.remove(key);
this._unlock(key);
}
return true;
}
public static void main(String[] args) {
Jedis jedis = new Jedis();
RedisWithReentrantLock redis = new RedisWithReentrantLock(jedis);
System.out.println(redis.lock("codehole"));
System.out.println(redis.lock("codehole"));
System.out.println(redis.unlock("codehole"));
System.out.println(redis.unlock("codehole"));
}
}原文链接: http://herman7z.site