Redis分布式锁
可通过redis中提供的指令setnx(SET if not exists的简写) key value实现,也可通过mysql的唯一约束来实现,redis的核心部分是单线程运行的,用了setnx命令之后,只能有一个客户端对某一个key设置值,在没有过期或删除key的时候其他客户端是不能设置这个key的,但是redis的setnx不好控制锁的有效时长问题(锁超时问题:一个线程获取到锁之后,线程阻塞了,无法释放锁,其它线程也就拿不到锁了),现在都是使用redis的一个框架redisson来实现分布式锁。
redisson框架会根据hash算法从分布式节点中选择一个节点通过执lua脚本进行加锁,当锁住的一个业务还没有执行完成的时候,redisson引入看门狗机制,看门狗watch dog每隔一段时间就会检查当前业务是否还持有锁,如果活没干完锁快过期了就给锁续期,当业务执行完成之后释放锁就可以了
在高并发情况下,一个业务可能会很快执行完成,客户端1持有锁的时候,客户端2访问不会马上拒绝,它会自旋不断尝试获取锁.客户端1释放锁之后,客户端2可以马上抢锁持有锁,性能也得到了提升
并且redisson实现分布式锁支持锁重入(setnx目前不支持,即同一线程多次获取锁的场景,可能会死锁),避免了死锁的产生.重入会在内部判断是否是当前线程持有的锁,如果是就会计数加一,释放锁就会计数减一.存储数据的时候采用hash结构,redis自身外圈的key可以按照自己的业务进行指定,hash结构自身内圈的key是当前线程的唯一标识,value是当前线程重入的次数.
但是redisson实现分布式锁还是会存在主从一致性问题,比如当线程1加锁成功后,master主节点数据会异步复制到slave从节点,但当持有redis的锁master主节点宕机后,根据哨兵机制选举一个slave从节点为新的master主节点,这时出现一个线程2,再次加速,会在新的master主节点上加锁成功,这时候会出现两个节点同时持有一把锁问题.
虽然可以利用redisson提供的红锁来解决这个问题,但是红锁需要在多个节点都添加锁(红锁要求是在多个redis实例上创建锁,并且大多数(数量过半)都成功创建锁),性能很低且运维成本非常高,因此官方也暂时废弃了红锁
非要做到数据强一致性,可以使用zookeeper实现分布式锁
public class DistributedLock {
private ZooKeeper zooKeeper;
private String lockPath;
private String currentLockNode;
public DistributedLock(ZooKeeper zooKeeper, String lockPath) {
this.zooKeeper = zooKeeper;
this.lockPath = lockPath;
}
public void lock() throws KeeperException, InterruptedException {
// 创建短暂有序节点
currentLockNode = zooKeeper.create(lockPath + "/lock_", null,
ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL_SEQUENTIAL);
while (true) {
List children = zooKeeper.getChildren(lockPath, false);
Collections.sort(children);
if (currentLockNode.equals(lockPath + "/" + children.get(0))) {
// 当前节点是最小的节点,获取到了锁
return;
} else {
// 监听前一个节点的删除事件
String previousNode = children.get(Collections.binarySearch(children,
currentLockNode.substring(lockPath.length() + 1)) - 1);
Stat stat = zooKeeper.exists(lockPath + "/" + previousNode, true);
if (stat != null) {
synchronized (this) {
wait();
}
}
}
}
}
public void unlock() throws KeeperException, InterruptedException {
zooKeeper.delete(currentLockNode, -1);
}
}