这篇博文讲介绍如何一步步构建一个基于Redis的分布式锁。会从最原始的版本开始,然后根据问题进行调整,最后完成一个较为合理的分布式锁。
本篇文章会将分布式锁的实现分为两部分,一个是单机环境,另一个是集群环境下的Redis锁实现。在介绍分布式锁的实现之前,先来了解下分布式锁的一些信息。
2 分布式锁 2.1 什么是分布式锁?分布式锁是控制分布式系统或不同系统之间共同访问共享资源的一种锁实现,如果不同的系统或同一个系统的不同主机之间共享了某个资源时,往往需要互斥来防止彼此干扰来保证一致性。
2.2 分布式锁需要具备哪些条件互斥性:在任意一个时刻,只有一个客户端持有锁。
无死锁:即便持有锁的客户端崩溃或者其他意外事件,锁仍然可以被获取。
容错:只要大部分Redis节点都活着,客户端就可以获取和释放锁
2.4 分布式锁的实现有哪些?数据库
Memcached(add命令)
Redis(setnx命令)
Zookeeper(临时节点)
等等
3 单机Redis的分布式锁 3.1 准备工作 3.1.1 定义常量类 public class LockConstants { public static final String OK = "OK"; /** NX|XX, NX -- Only set the key if it does not already exist. XX -- Only set the key if it already exist. **/ public static final String NOT_EXIST = "NX"; public static final String EXIST = "XX"; /** expx EX|PX, expire time units: EX = seconds; PX = milliseconds **/ public static final String SECONDS = "EX"; public static final String MILLISECONDS = "PX"; private LockConstants() {} } 3.1.2 定义锁的抽象类抽象类RedisLock实现java.util.concurrent包下的Lock接口,然后对一些方法提供默认实现,子类只需实现lock方法和unlock方法即可。代码如下
public abstract class RedisLock implements Lock { protected Jedis jedis; protected String lockKey; public RedisLock(Jedis jedis,String lockKey) { this(jedis, lockKey); } public void sleepBySencond(int sencond){ try { Thread.sleep(sencond*1000); } catch (InterruptedException e) { e.printStackTrace(); } } @Override public void lockInterruptibly(){} @Override public Condition newCondition() { return null; } @Override public boolean tryLock() { return false; } @Override public boolean tryLock(long time, TimeUnit unit){ return false; } } 3.2 最基础的版本1先来一个最基础的版本,代码如下
public class LockCase1 extends RedisLock { public LockCase1(Jedis jedis, String name) { super(jedis, name); } @Override public void lock() { while(true){ String result = jedis.set(lockKey, "value", NOT_EXIST); if(OK.equals(result)){ System.out.println(Thread.currentThread().getId()+"加锁成功!"); break; } } } @Override public void unlock() { jedis.del(lockKey); } }LockCase1类提供了lock和unlock方法。
其中lock方法也就是在reids客户端执行如下命令
而unlock方法就是调用DEL命令将键删除。
好了,方法介绍完了。现在来想想这其中会有什么问题?
假设有两个客户端A和B,A获取到分布式的锁。A执行了一会,突然A所在的服务器断电了(或者其他什么的),也就是客户端A挂了。这时出现一个问题,这个锁一直存在,且不会被释放,其他客户端永远获取不到锁。如下示意图
可以通过设置过期时间来解决这个问题
3.3 版本2-设置锁的过期时间 public void lock() { while(true){ String result = jedis.set(lockKey, "value", NOT_EXIST,SECONDS,30); if(OK.equals(result)){ System.out.println(Thread.currentThread().getId()+"加锁成功!"); break; } } }类似的Redis命令如下
SET lockKey value NX EX 30注:要保证设置过期时间和设置锁具有原子性
这时又出现一个问题,问题出现的步骤如下
客户端A获取锁成功,过期时间30秒。
客户端A在某个操作上阻塞了50秒。
30秒时间到了,锁自动释放了。
客户端B获取到了对应同一个资源的锁。
客户端A从阻塞中恢复过来,释放掉了客户端B持有的锁。
示意图如下
这时会有两个问题
过期时间如何保证大于业务执行时间?
如何保证锁不会被误删除?
先来解决如何保证锁不会被误删除这个问题。
这个问题可以通过设置value为当前客户端生成的一个随机字符串,且保证在足够长的一段时间内在所有客户端的所有获取锁的请求中都是唯一的。
版本2的完整代码:Github地址
3.4 版本3-设置锁的value