分布式锁
分布式锁应该具有:
- 互斥性:任意时刻只能有一个客户端持有锁
- 锁超时释放: 锁超时会自动释放,防止死锁
- 可重入性: 一个线程获取锁之后可以再次对请求加锁
- 高可用、高性能:加锁和解锁需要开销尽可能低,同时要保证高可用
- 安全性:锁只能被持有客户端删除,不能被其他客户端删除
为什么需要分布式锁?
在分布式系统中,多个节点(进程、服务实例)同时访问共享资源时,会面临一些并发控制的问题。这些问题包括数据一致性、竞态条件和资源冲突等。分布式锁的出现正是为了解决这些问题,确保在分布式环境下的数据访问的正确性和可靠性
分布式锁的实现方法
在分布式架构当中,单机模式下的Java对象锁已经无法起作用了(tomcat当中每个tomcat的服务器都能够持有一个锁,那么这个锁就没有意义了),所以需要一个独立的第三方来实现分布式锁,MySQL、Redis、zookeeper等等
MySQL分布式锁
- 缺点:读取IO消耗大,获取锁的线程如果崩溃了没有释放锁,将会产生死锁
实现步骤:
- 每次操作资源的时候插入一条数据(获取当前时间戳,利用定时任务反复读取表中时间是否超时,超时释放,用于防止线程崩溃后未释放。获取锁的线程应该每隔一段时间进行更新时间戳,防止自身并未崩溃但操作超时锁被另一个线程获取,当自身完成时释放了别的客户端的锁)
insert into locks(lock_key,locktimeout,...)values('stock','30',...)
2、操作完成后删除锁(根据lock_key实现可重入性,可以在表中增加一个计数器每次重入加锁则加1,释放则减1,当计数器为0时则代表操作完成,删除表数据)
Redis分布式锁
redis基于内存,所以在每次IO操作的时候比MySQL节省更多的时间,同时redis自身支持过期时间自动释放,所以利用Redis实现分布式锁会比MySQL更加合适
前置知识:
- SET key value[EX seconds][PX milliseconds][NX|XX]
- NX :表示key不存在的时候,才能set成功,也即保证只有第一个客户端请求才能获得锁,而其他客户端请求只能等其释放锁,才能获取。
- EX seconds :设定key的过期时间,时间单位是秒。
- PX milliseconds: 设定key的过期时间,单位为毫秒
- XX 仅当key存在时设置值
最简单的实现如下(伪代码):
if(jedis.set(key_resource_id, uni_request_id, "NX", "EX", 100s) == 1){ //加锁
try {
do something //业务处理
}catch(){
}
finally {
jedis.del(key_resource_id); //释放锁
}
}
目前很多企业或者项目里使用lua脚本加锁,这边也进行叙述一下
if redis.call('setnx',KEYS[1],ARGV[1]) == 1 then
redis.call('expire',KEYS[1],ARGV[2])
else
return 0
end;
lua脚本操作redis主要是因为lua脚本保证了原子性(后续删锁还会用到),在不使用set扩展命令时(NX、EX等),如果是如下代码,就可能导致加锁后 还未来得及设置过期时间,线程崩溃了导致产生了永久锁
if(jedis.setnx(key_resource_id,lock_value) == 1){ //加锁
expire(key_resource_id,100); //设置过期时间
...
最后为了保证防止其他线程误删,我们需要在finally上面进行一些操作
/*
if (uni_request_id.equals(jedis.get(key_resource_id))) {
jedis.del(lockKey); //释放锁
}
和前面的操作同理,该操作是非原子性的,所以我们要采用lua脚本进行操作
*/
if redis.call('get',KEYS[1]) == ARGV[1] then
return redis.call('del',KEYS[1])
else
return 0
end;
Zookeeper分布式锁
Zookeeper底层是类似于文件系统那样的树结构(称为ZNode),这种树状结构和基于ZNode的数据模型使得ZooKeeper非常适合用于实现分布式协调和同步的场景,例如分布式锁、选举算法等。客户端可以通过创建、读取、更新和删除ZNode来实现对共享数据和协调状态的访问和操作。
下面是ZooKeeper实现分布式锁的基本原理和步骤:
- 客户端在ZooKeeper指定的目录下创建一个有序临时节点,代表自己的请求。
- 客户端获取目录下所有的子节点,并按节点名称的顺序进行排序。
- 客户端判断自己创建的节点是否是当前最小的节点,如果是,则认为获取到了锁;如果不是,则监听比自己小的节点的删除事件。
- 如果监听的节点被删除,客户端回到第2步重新判断自己是否获得了锁。
- 客户端在完成任务后,删除自己创建的节点,释放锁。
至于ZooKeeper为什么能够实现分布式锁的原因有以下几点:
- 有序节点:ZooKeeper的节点是有序的,可以根据节点名称的顺序来实现竞争顺序。通过对节点名称进行排序,客户端可以判断自己是否是当前最小的节点,从而决定是否获得锁。
- 临时节点:ZooKeeper的临时节点是会话级别的,当创建节点的客户端会话结束时,该节点会被自动删除。利用临时节点,可以实现锁的自动释放,避免锁被长时间占用。
- Watch机制:ZooKeeper提供了Watch机制,客户端可以对节点的变化进行监听。通过监听比自己小的节点的删除事件,可以实现客户端之间的协调,确保只有最小节点的客户端获得锁。
- 总体来说,ZooKeeper通过有序节点和临时节点的特性,结合Watch机制,提供了一种简单而可靠的机制来实现分布式锁。客户端通过竞争创建有序临时节点,并监听前一个节点的删除事件,从而实现分布式环境下的互斥访问和协调操作。
具体实现可以参考 Zookeeper实现分布式锁