分布式锁是在分布式系统中用于控制多个进程对共享资源进行互斥访问的一种锁机制。在分布式系统中,由于进程分布在不同的服务器上,因此需要一种机制来保证进程之间的互斥访问。
分布式锁是在分布式系统中,用于控制多个进程对共享资源进行互斥访问的一种锁机制。
分布式锁的目的是在分布式系统中实现同步互斥,避免因多个进程同时访问共享资源而导致资源竞争和数据不一致的问题。
1.互斥性:同一时刻只能有一个进程持有锁。2.避免死锁:锁的设置和释放都要有固定的规则,避免死锁的发生。3.有效性:锁的设置和释放必须保证可靠性。4.速度:锁的设置和释放的速度要尽可能快,避免影响业务性能。
分布式锁常用于以下场景:
1.分布式任务调度2.分布式计数器3.分布式限流器4.数据库事务的分布式锁5.共享的全局资源的互斥访问
在分布式系统中,可以使用数据库的乐观锁和悲观锁来实现分布式锁。
乐观锁基于版本号实现。每次加锁前,检查当前版本是否与获取到锁时的版本号一致,若一致,则加锁成功,否则加锁失败。
悲观锁是在加锁前先获取资源的锁,直到操作完成后才释放锁。常见的实现方式是数据库中使用SELECT...FOR UPDATE语句来获取锁。
在分布式系统中,由于锁通常只是临时的,因此基于缓存实现分布式锁的方式具有更高的性能。常用的缓存工具有Redis和ZooKeeper。
Redis可以实现分布式锁的原理是利用Redis的SET命令的特性,当且仅当key不存在时插入该key,若key已存在,则该命令将返回失败。利用SET命令的特性,可以将key作为锁的唯一标识,每次加锁时尝试插入该key,若插入成功,则加锁成功;若插入失败,则加锁失败。同时通过对key设置过期时间来防止死锁的发生。
ZooKeeper实现分布式锁的原理是在ZooKeeper中创建一个临时顺序节点,每个客户端获取锁时都会先创建一个顺序节点,然后获取所有节点中的最小节点,若该节点为自己所创建的节点,则加锁成功;否则,则监听自己前面的节点,当其他客户端释放锁时,会触发监听事件,待自己所监听的节点被删除时,则加锁成功。
利用SET命令的特性,通过将key作为锁的唯一标识,每次加锁时尝试插入该key,若插入成功,则加锁成功;若插入失败,则加锁失败。同时通过对key设置过期时间来防止死锁的发生。
加锁方法:
public boolean lock(String key, long expire) {
long expires = System.currentTimeMillis() + expire * 1000 + 1;
String expiresStr = String.valueOf(expires);
if (redisTemplate.opsForValue().setIfAbsent(key, expiresStr)) {
return true;
}
String currentValue = redisTemplate.opsForValue().get(key);
if (currentValue != null && Long.parseLong(currentValue) < System.currentTimeMillis()) {
String oldValue = redisTemplate.opsForValue().getAndSet(key, expiresStr);
if (oldValue != null && oldValue.equals(currentValue)) {
return true;
}
}
return false;
}
该方法实现了如下功能:
1.判断是否能够加锁(即key是否存在),若可以则加锁成功。2.若加锁失败,则判断该锁是否已经超时,若该锁已经超时,则尝试重新加锁。
解锁方法:
public boolean unlock(String key) {
redisTemplate.opsForValue().getOperations().delete(key);
return true;
}
该方法实现了将锁删除,并释放锁的过程。
网络抖动可能导致加锁时的结果不一致。因此需要对每次加锁尝试的时间进行处理,在加锁时增加1毫秒的时间。
在每次加锁时,需要给锁设置过期时间,以防止出现死锁的情况。过期时间的设置需要根据业务场景的需要进行调整。
Redis单节点的故障可能导致锁的失效,因此可以使用Redis Sentinel或者Redis Cluster来实现Redis的高可用性。
在ZooKeeper中创建一个临时顺序节点,每个客户端获取锁时都会先创建一个顺序节点,然后获取所有节点中的最小节点,若该节点为自己所创建的节点,则加锁成功;否则,则监听自己前面的节点,当其他客户端释放锁时,会触发监听事件,待自己所监听的节点被删除时,则加锁成功。
创建锁节点方法:
public void createLockNode(String lockName) {
try {
if (zk.exists("/" + lockName, true) == null) {
zk.create("/" + lockName, new byte[0], ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
}
} catch (Exception e) {
e.printStackTrace();
}
}
获取锁方法:
public boolean acquire(String lockName) {
boolean locked = false;
try {
//创建临时顺序节点
String path = zk.create("/" + lockName + "/lock-", new byte[0], ZooDefs.Ids.OPEN_ACL_UNSAFE,
CreateMode.EPHEMERAL_SEQUENTIAL);
//获取所有顺序节点
List<String> list = zk.getChildren("/" + lockName, false);
//对所有顺序节点进行排序
Collections.sort(list);
//获取最小的顺序节点
String minNode = list.get(0);
//若当前节点是最小的顺序节点,则获取锁成功
if (path.equals("/" + lockName + "/" + minNode)) {
locked = true;
} else {
//当前节点不是最小的顺序节点,则监听自己前一位的节点
String previousNode = list.get(Collections.binarySearch(list, minNode) - 1);
Stat stat = zk.exists("/" + lockName + "/" + previousNode, true);
if (stat != null) {
latch.await();
locked = true;
}
}
} catch (Exception e) {
e.printStackTrace();
}
return locked;
}
释放锁方法:
public boolean release(String lockName) {
try {
String id = Thread.currentThread().getName();
zk.delete("/" + lockName + "/" + id, -1);
zk.close();
return true;
} catch (Exception e) {
e.printStackTrace();
}
return false;
}
该方法实现了将锁删除,并释放锁的过程。
分布式计数器是通过分布式锁实现的,每次对计数器进行加/减操作前,需要获取分布式锁。
分布式任务调度是通过对分布式锁的维护来实现的。每当某个任务的执行时间到来时,尝试获取分布式锁,若获取锁,则执行任务;否则,等待下一次执行时间。
分布式限流器是通过分布式锁和Redis原子操作实现的。每次请求过来时,先获取分布式锁,然后根据业务需要修改计数器(如在固定时间段内只允许访问10次),同时设置过期时间,在过期时间到来时自动清零计数器。
分布式锁是在分布式系统中用于保证共享资源的互斥访问的一种锁机制。分布式锁可以基于数据库或缓存实现,并且常用的缓存工具有Redis和ZooKeeper。分布式锁的常见应用场景包括分布式任务调度、分布式计数器和分布式限流器等。