分布式锁需要具备的条件:
-
互斥(必须):同一时刻,分布式部署的应用中,同一个方法/资源只能被一台机器上的一个线程占用。
-
锁失效保护(必须):出现客户端断电等异常情况,锁仍然能被其他客户端获取,防止死锁。
-
可重入(可选):同一个线程在没有释放锁之前,如果想再次操作,可以直接获得锁。
-
阻塞/非阻塞(可选):若没有获取到锁,重新获取。
-
高可用、高性能(可选):获取释放锁最好是原子操作,获取释放锁的性能要好
分布式锁主流的实现方案:
1. 基于数据库实现分布式锁:
首先在数据库中创建一张表:
SQL
CREATE TABLE `myLock` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT ‘主键’,
`method_name` varchar(100) NOT NULL DEFAULT ‘’ COMMENT ‘锁定的方法名’,
`value` varchar(1024) NOT NULL DEFAULT ‘锁信息’,
PRIMARY KEY (`id`),
UNIQUE KEY `uidx_method_name` (`method_name `) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT=‘锁定中的方法’;
如果要锁住某方法,只要执行:
SQL
insert into myLock(method_name, value) values (‘m1’, ‘1’);
method_name做了唯一性约束,一个方法只能存在一条记录,执行完这个方法后,再释放锁:
SQL
delete from myLock where method_name =‘m1’;
这就使用数据库实现了一个简单的分布式锁,但是对比分布式锁的条件,只满足了可斥性。
-
针对锁失效问题,新增一个超时字段,在加锁时设置。然后做一个定时任务,负责轮询判断已经存在的锁的创建时间和当前系统时间之间的差是否已经超过超时时间,如果已经超过就删除。
-
针对不可重入问题,可以再新增一个字段,记录当前获取锁的线程的机器和线程信息,相同的线程再次访问时,就可以识别放行了。
-
针对非阻塞,写一个while死循环,失败了不断递归,直到获取锁成功为止。
-
针对单点问题,可以配置主从数据库。
虽然数据库实现分布式锁非常简单,但是数据库建立连接查询是一个非常耗性能的操作,基本不会使用
2. 基于缓存(Redis等)
基本原理如图:
Redis实现分布式锁
Java
public void testLock() {
// 1. 从redis中获取锁,setnx
String uuid = UUID.randomUUID().toString();
Boolean lock = this.redisTemplate.opsForValue().setIfAbsent(“lock”, uuid, 2, TimeUnit.SECONDS);
if (lock) {
//自己的业务代码
//TODO
// 2. 释放锁 del(使用lua脚本使释放锁是原子性操作)
// if(uuid.equals((String)redisTemplate.opsForValue().get(“lock”))) {
// this.redisTemplate.delete(“lock”);
// }
String script = “if redis.call(‘get’, KEYS[1]) == ARGV[1] then return tostring(redis.call(‘del’,KEYS[1])) else return 0 end”;
this.redisTemplate.execute(new DefaultRedisScript<>(script), Collections.singletonList(“lock”), uuid);
} else {
// 3. 每隔1秒钟回调一次,再次尝试获取锁
try {
Thread.sleep(500);
testLock();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
-
使用setnx实现互斥
-
设置过期时间实现防锁失效保护
-
利用ThreadLocal实现,获取锁后将Redis中的value保存在ThreadLocal中,同一线程再次尝试获取锁的时候就先将 ThreadLocal 中的 值 与 Redis 的 value 比较,如果相同则表示这把锁所以该线程,即实现可重入锁。
-
加锁失败后递归调用这个方法可以实现阻塞
-
可以使用lua脚本进行删除,使释放锁的操作是原子性的。
这种方式存在一些问题:
过期时间太长会影响系统性能,太短有可能会业务未执行完时释放锁
在Redis集群状态下会存在问题:
- 客户端A从master获取到锁
- 在master将锁同步到slave之前,master宕掉了。
- slave节点被晋级为master节点
- 客户端B取得了同一个资源被客户端A已经获取到的另外一个锁。
进阶1:使用Redisson实现分布式锁
1,加锁机制:
如果该客户端面对的是一个redis cluster集群,他首先会根据hash节点选择一台机器,线程去获取锁,获取成功: 执行lua脚本,保存数据到redis数据库。
2.watch dog自动延期机制:
Redisson设计了看门狗机制,线程1 业务还没有执行完时到达过期时间,线程1 还想持有锁的话,就会启动一个watch dog后台线程,不断的延长锁key的生存时间。
进阶2:Redlock
Redis集群部署时,在master将锁同步到slave之前,master宕掉会造成风险,Redlock可以大大减少这种风险出现的概率,Redlock是redis官方提出的一种分布式锁的算法。Redlock流程为:
- 顺序向集群节点请求加锁
- 根据一定的超时时间来推断是不是跳过该节点
- 2N + 1个节点加锁成功并且花费时间小于锁的有效期
- 认定加锁成功
- 如果获取失败,客户端应该在所有的Redis实例上进行解锁(即便某些Redis实例根本就没有加锁成功,防止某些节点获取到锁但是客户端没有得到响应而导致接下来的一段时间不能被重新获取锁)。
- 释放锁时也类似
3. 基于Zookeeper
解释: 左边的整个区域表示一个Zookeeper集群,locker是Zookeeper的一个持久节点,node_1、node_2、node_3是locker这个持久节点下面的临时顺序节点。client_1、client_2、client_n表示多个客户端,Service表示需要互斥访问的共享资源。
下面描述使用zookeeper实现分布式锁的算法流程,假设锁空间的根节点为/lock:
- 客户端连接zookeeper,并在/locker下创建临时的且有序的子节点,第一个获取锁的客户端对应的子节点为/locker/node_1,第二个为/locker/node_2,以此类推。
- 客户端获取/locker下的子节点列表,判断自己创建的子节点是否为当前子节点列表中序号最小的子节点,如果是则认为获得锁,否则创建监听器监听/locker的子节点变更消息,这个被关注的节点删除时,则客户端的监听器收到相应通知,此时再次判断自己创建的节点是否是locker子节点中序号最小的,如果是则获取到了锁,如果不是则重复以上步骤直至获得锁;
- 执行业务代码;
- 完成业务流程后,删除对应的子节点释放锁。
-
利用了zookeeper的有序节点来实现互斥性,最小序号只能有一个
-
利用临时节点在会话结束或超时后会被删除实现锁失效保护
-
利用监听器实现阻塞
-
客户端在创建节点的时候,把当前客户端的主机信息和线程信息直接写入到节点中,下次想要获取锁的时候和当前最小的节点中的数据比对一下就可以了。如果和自己的信息一样,那么自己直接获取到锁,如果不一样就再创建一个临时的顺序节点,参与排队,实现可重入。
-
利用zookeeper集群实现高可用
问题:zookeeper********会不会出现Redis集群那样的数据同步问题呢?
zookeeper宕机可能发生在Follwer也可能发生在Cilent,分别分析这两种情况:
-
client给Follwer写数据,此时Follwer宕机了,client建立节点失败,获取锁失败。
-
client给Follwer写数据,Follwer将请求转发给Leader,Leader宕机了,会出现不一致的问题么?不可能,这种时候,Zookeeper会选取新的leader,也就是刚才的Follwer,继续上面的提到的写流程。
-
client1给Follwer1写数据,client2同时Follwer2写数据,两个客户端会不会同时得到锁?也不会出现同时获取到锁的情况,因为只有Leader和Follwer都确认写入成功才会回应客户端,Follwer1和Follwer2只会有一个成功。
-
client给Leader写数据,此时Leader宕机了,client建立节点失败,获取锁失败。
综上,采用Zookeeper作为分布式锁,你要么就获取不到锁,一旦获取到了,必定节点的数据是一致的,不会出现redis那种异步同步导致数据丢失的问题。
每一种分布式锁解决方案都有各自的优缺点:
1. 性能:redis最高
2. 可靠性:zookeeper最高
博主介绍:上海交大毕业,大厂资深Java后端工程师,
《Java全套学习资料》作者,
专注于系统架构设计和高并发解决方案
阿里云开发社区乘风者计划专家博主,
CSDN平台Java领域优质创作者
常年分享Java技术干货、项目实战经验,
并为大学生和初学者提供项目实战与就业指导
擅长:分布式系统、SpringCloud、SpringBoot、Vue、MySQL、Redis、Docker等项目开发和设计
/**
* @author[vx] vip1024p(备注java)
* @【描述:浏览器打开】docs.qq.com/doc/DUkVoZHlPeElNY0Rw
*/
public class Hello {
public static void main(String[] args) {
System.out.println("Hello!!!");
}
}