本文来讲分布式锁的两种实现,一种是用Redis实现,另一个种使用Zookeeper实现。
1. 分布式系统
分布式系统(distributed system)是建立在网络之上的软件系统。正是因为软件的特性,所以分布式系统具有高度的内聚性和透明性。因此,网络和分布式系统之间的区别更多的在于高层软件(特别是操作系统),而不是硬件。
2. CAP理论
- C:Consistency一致性
- A:Availability可用性
- P:Partition tolerance分区容错性
CAP理论是指,在一个分布式系统中,这三个要素最多只能同时实现两点,不可能三者兼顾。
既然是分布式系统,那么在系统中会存在很多节点,这些节点通过网络进行通信。如果网络发生故障,比如A节点和B节点的网络无法通信了,那么系统中就存在A分区和B分区,那么分区容错性就是指当出现了分区问题,整个系统还需要继续运行。所以一般在设计分布式框架或系统时,一定要会满足P,会在AP和CP之间选择,本文介绍的Redis就是满足AP,而Zookeeper是满足CP。
3. Redis分布式锁
Redis提供了setnx指令来实现分布式锁。
3.1 代码实现
String lockKey = "lockKey";
String clientId = UUID.randomUUID().toString();
try {
//设置锁超时时间
Boolean result = stringRedisTemplate.opsForValue().setIfAbsent(lockKey, clientId,30, TimeUnit.MILLISECONDS);
//锁还没有被释放,直接返回
if (!result){
return "error";
}
int account = Integer.parseInt(stringRedisTemplate.opsForValue().get("account"));
if (account > 0){
int realAccount = account - 1;
stringRedisTemplate.opsForValue().set("account",realAccount + "");
System.out.println("扣减成功,剩余库存:" + realAccount + "");
}else {
System.out.println("扣减失败,库存不足");
}
}finally {
//判断该锁是不是当前线程加的
if (clientId.equals(stringRedisTemplate.opsForValue().get(lockKey))){
//业务代码执行完,释放锁
stringRedisTemplate.delete(lockKey);
}
}
return "end";
为每一个线程设置线程ID,并将该ID作为锁Key信息的value值,在删除锁的时候,进行判断,如果是本线程自己加的锁,可以删除,如果不是,不能删除。
具体思路如下:
- 现获取锁的key
- 为当前线程设置线程ID
- 在try代码块里,设置锁并设置超时时间
- 获取锁的状态,如果没有设置成功,返回
- 拿到设置锁成功的状态后,获取具体业务的value值
- value减一,并更新缓存
- 在finally代码块中判断,线程ID是不是加锁的ID
- 释放锁
3.2 存在问题
- 如果业务代码执行的时间大于锁的过期时间,没有对锁进行续期
- 由于Redis一般是集群部署的,所以会出现由于主节点挂掉的话,从节点会取而代之。这时候如果客户端在主节点上申请成功了一把锁,但是这把锁还没有来得及同步到从节点,主节点突然挂了,然后从节点变为主节点,这个新的节点内部没有这个锁,所以当另一个客户端过来请求加锁时,立即就批准了,这样就导致系统中同样一把锁被两个客户端同时持有,不安全性由此产生。
4. Zookeeper分布式锁
4.1 原理
获取锁: 1.首先,在Zookeeper当中创建一个持久节点ParentLock。当第一个客户端想要获得锁时,需要在ParentLock这个节点下面创建一个临时顺序节点 Lock1。
Client1查找ParentLock下面所有的临时顺序节点并排序,判断自己所创建的节点Lock1是不是顺序最靠前的一个。如果是第一个节点,则成功获得锁。
2.再有一个客户端 Client2 前来获取锁,则在ParentLock下载再创建一个临时顺序节点Lock2。Client2查找ParentLock下面所有的临时顺序节点并排序,判断自己所创建的节点Lock2是不是顺序最靠前的一个,结果发现节点Lock2并不是最小的。
于是,Client2向排序仅比它靠前的节点Lock1注册Watcher,用于监听Lock1节点是否存在。这意味着Client2抢锁失败,进入了等待状态。
3.又有一个客户端Client3前来获取锁,则在ParentLock下载再创建一个临时顺序节点Lock3。 Client3查找ParentLock下面所有的临时顺序节点并排序,判断自己所创建的节点Lock3是不是顺序最靠前的一个,结果同样发现节点Lock3并不是最小的。
于是,Client3向排序仅比它靠前的节点Lock2注册Watcher,用于监听Lock2节点是否存在。这意味着Client3同样抢锁失败,进入了等待状态。
释放锁:
任务完成,客户端显示释放 任务执行过程中,客户端崩溃
4.2 存在问题
- 由于Zookeeper中ZAB协议(下次介绍)的存在,每次加锁/释放锁都会由leader同步给集群的所有节点,导致性能不高
- 当加锁过程中leader节点挂了,由于Zookeper要先选举一个leader,才能对外提供服务,这个时候服务是不可用的,既,满足的是CP。