现在实现分布式锁的技术有以下三种:
一,基于数据库
在数据库中创建一个表,表中包含方法名等字段,并在方法名字段上创建唯一索引,想要执行某个方法,就使用这个方法名向表中插入数据,成功插入则获取锁,执行完成后删除对应的行数据释放锁。
这种实现方式很简单,但是对于分布式锁应该具备的条件来说,它有一些问题需要解决及优化。
因为是基于数据库实现的,数据库的可用性和性能将直接影响分布式锁的可用性及性能,所以,数据库需要双机部署、数据同步、主备切换;
不具备可重入的特性,因为同一个线程在释放锁之前,行数据一直存在,无法再次成功插入数据,所以,需要在表中新增一列,用于记录当前获取到锁的机器和线程信息,在再次获取锁的时候,先查询表中机器和线程信息是否和当前机器和线程相同,若相同则直接获取锁;
没有锁失效机制,因为有可能出现成功插入数据后,服务器宕机了,对应的数据没有被删除,当服务恢复后一直获取不到锁,所以,需要在表中新增一列,用于记录失效时间,并且需要有定时任务清除这些失效的数据;
不具备阻塞锁特性,获取不到锁直接返回失败,所以需要优化获取逻辑,循环多次去获取。
优点:借助数据库,方案简单。
缺点:在实际实施的过程中会遇到各种不同的问题,为了解决这些问题,实现方式将会越来越复杂;依赖数据库需要一定的资源开销,性能问题需要考虑。
二,基于redis
基于redis的实现分布式锁主要由以下几种:
一,使用redis命令watch和multi结合使用,这种方式性能较低,一般不用。
二,使用redis的setnx命令实现。
以下代码来源于:https://github.com/josiahcarlson/redis-in-action/blob/master/java/src/main/java/Chapter06.java
获取锁:
public String acquireLockWithTimeout(
Jedis conn, String lockName, long acquireTimeout, long lockTimeout)
{
String identifier = UUID.randomUUID().toString();//随机的唯一标识
String lockKey = "lock:" + lockName;
int lockExpire = (int)(lockTimeout / 1000);//确保传给expire的都是整数
long end = System.currentTimeMillis() + acquireTimeout;
while (System.currentTimeMillis() < end) {
if (conn.setnx(lockKey, identifier) == 1){//这个判断语句是获取锁并设置过期时间
conn.expire(lockKey, lockExpire);
return identifier;
}
if (conn.ttl(lockKey) == -1) {//这个判断检查过期时间,并在有需要时对其进行更新
conn.expire(lockKey, lockExpire);
}
try {
Thread.sleep(1);
}catch(InterruptedException ie){
Thread.currentThread().interrupt();
}
}
// null indicates that the lock was not acquired
return null;
}
释放锁:
public boolean releaseLock(Jedis conn, String lockName, String identifier) {
String lockKey = "lock:" + lockName;
while (true){
conn.watch(lockKey);//检查进程是否还存在锁
if (identifier.equals(conn.get(lockKey))){//这个判断语句是释放锁
Transaction trans = conn.multi();
trans.del(lockKey);
List<Object> results = trans.exec();
if (results == null){
continue;
}
return true;
}
conn.unwatch();
break;
}
return false;
}
三,基于redlock算法的
具体可以参考:https://juejin.im/post/5cc165816fb9a03202221dd5#heading-6
https://www.cnblogs.com/sheldon-lou/p/11039795.html
简单的redis主从架构遇到的问题
为了避免单点故障,我们给Redis做一个Master/Slave的主从架构,一个Master,一台Slave。下面就会碰到这么一个问题。下面是使用场景。
- 客户端A在Master上获取到一个锁。
- Master把这个数据同步到Slave的时候挂了(因为Master和Slave之间同步是异步的)。
- Slave变成了Master。
- 客户端B通过相同的key,和value获取到锁。分布式锁失效
所以redis的作者为解决上面的问题,就相应提出了redlock算法。
redlock算法思想:
假设有5个独立的Redis节点(注意这里的节点可以是5个Redis单master实例,也可以是5个Redis Cluster集群,但并不是有5个主节点的cluster集群):
- 获取当前Unix时间,以毫秒为单位
- 依次尝试从5个实例,使用相同的key和具有唯一性的value(例如UUID)获取锁,当向Redis请求获取锁时,客户端应该设置一个网络连接和响应超时时间,这个超时时间应用小于锁的失效时间,例如你的锁自动失效时间为10s,则超时时间应该在5~50毫秒之间,这样可以避免服务器端Redis已经挂掉的情况下,客户端还在死死地等待响应结果。如果服务端没有在规定时间内响应,客户端应该尽快尝试去另外一个Redis实例请求获取锁
- 客户端使用当前时间减去开始获取锁时间(步骤1记录的时间)就得到获取锁使用的时间,当且仅当从大多数(N/2+1,这里是3个节点)的Redis节点都取到锁,并且使用的时间小于锁失败时间时,锁才算获取成功。
- 如果取到了锁,key的真正有效时间等于有效时间减去获取锁所使用的时间(步骤3计算的结果)
- 如果某些原因,获取锁失败(没有在至少N/2+1个Redis实例取到锁或者取锁时间已经超过了有效时间),客户端应该在所有的Redis实例上进行解锁(即便某些Redis实例根本就没有加锁成功,防止某些节点获取到锁但是客户端没有得到响应而导致接下来的一段时间不能被重新获取锁)