1. 什么是分布式锁
概念: 防止两个节点在同一条数据上面操作,比如多个节点机器对同一个订单操作不同的流程有可能会导致该笔订单最后状态出现错误,造成损失。
2.分布锁的特点
- 互斥性:和我们本地锁一样互斥性是最基本,但是分布式锁需要保证在不同节点的不同线程的互斥。
- 可重入性:同一个节点上的同一个线程如果获取了锁之后那么也可以再次获取这个锁。
- 锁超时:和本地锁一样支持锁超时,防止死锁。
- 高效,高可用:加锁和解锁需要高效,同时也需要保证高可用防止分布式锁失效,可以增加降级。
- 支持阻塞和非阻塞:和ReentrantLock一样支持lock和trylock以及tryLock(long timeOut)。
- 支持公平锁和非公平锁(可选):公平锁的意思是按照请求加锁的顺序获得锁,非公平锁就相反是无序的。这个一般来说实现的比较少。
3. 使用redis实现分布锁
思路 :
就是使用redis的setnx()函数,然后通过获取锁和释放锁来实现,首先请求的获取锁,执行操作后释放锁,后后续请求在执行同样的操作.setnx()方法作用就是SET IF NOT EXIST 若不存在就设置值返回1,否则0,expire()方法就是给锁加一个过期时间通过setnx尝试设置某个key的值,成功(当前没有这个锁)则返回,就是获得锁一定不要把两个命令(NX EX)分开执行,如果在 NX 之后程序出现问题就有可能产生死锁。
PS: Redis2.6.12 改进了set方法,可以同时设置value和expire
//加锁
name 锁的名称
requestId 请求标志(解锁时候的依据)生成:UUID.randomUUID().toString()
timeout 等待获取锁的时间 ,为0表示失败后直接返回不等待
expire 锁的最大生存时间,如果超过时间没有释放锁,就会被强制释放
waitintervalus 获取锁失败挂起后重试时间间隔
jedis.set(name ,requestId,timeout,expire,waitintervalus);
private static final String SET_IF_NOT_EXIST = "NX";
private static final String SET_WITH_EXPIRE_TIME = "PX";
public boolean tryLock(String key, String request) {
String result = this.jedis.set(LOCK_PREFIX + key, request, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, 10 * TIME);
i
if (LOCK_MSG.equals(result)){
return true ;
}else {
return false ;
}
}
unlock(name,requestId )
首先判断锁是否存在,若是存在即删
public boolean unlock(String key,String request){
//lua script
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
Object result = null ;
if (jedis instanceof Jedis){
result = ((Jedis)this.jedis).eval(script, Collections.singletonList(LOCK_PREFIX + key), Collections.singletonList(request));
}else if (jedis instanceof JedisCluster){
result = ((JedisCluster)this.jedis).eval(script, Collections.singletonList(LOCK_PREFIX + key), Collections.singletonList(request));
}else {
//throw new RuntimeException("instance is error") ;
return false ;
}
if (UNLOCK_MSG.equals(result)){
return true ;
}else {
return false ;
}
}
RedLock问题
描述:我们想象一个这样的场景当机器A申请到一把锁之后,如果Redis主宕机了,这个时候从机并没有同步到这一把锁,那么机器B再次申请的时候就会再次申请到这把锁,为了解决这个问题Redis作者提出了RedLock红锁的算法,在Redission中也对RedLock进行了实现。
1.首先生成多个Redis集群的Rlock,并将其构造成RedLock。
2. 依次循环对三个集群进行加锁
3.如果循环加锁的过程中加锁失败,那么需要判断加锁失败的次数是否超出了最大值,这里的最大值是根据集群的个数,比如三个那么只允许失败一个,五个的话只允许失败两个,要保证多数成功
4. 加锁的过程中需要判断是否加锁超时,有可能我们设置加锁只能用3ms,第一个集群加锁已经消耗了3ms了。那么也算加锁失败
5.加锁失败的话,那么就会进行解锁操作,解锁会对所有的集群在请求一次解锁
6.可以看见RedLock基本原理是利用多个Redis集群,用多数的集群加锁成功,减少Redis某个集群出故障,造成分布式锁出现问题的概率
4.zookpper实现分布锁
zk节点描述:
- 有序节点:假如当前有一个父节点为/lock,我们可以在这个父节点下面创建子节点;zookeeper提供了一个可选的有序特性,例如我们可以创建子节点“/lock/node-”并且指明有序,那么zookeeper在生成子节点时会根据当前的子节点数量自动添加整数序号,也就是说如果是第一个创建的子节点,那么生成的子节点为/lock/node-0000000000,下一个节点则为/lock/node-0000000001,依次类推。
- 临时节点:客户端可以建立一个临时节点,在会话结束或者会话超时后,zookeeper会自动删除该节点。
- 事件监听:在读取数据时,我们可以同时对节点设置事件监听,当节点数据或结构变化时,zookeeper会通知客户端。当前zookeeper有如下四种事件:1)节点创建;2)节点删除;3)节点数据修改;4)子节点变更。
zk分布锁实现思路:
- 1.客户端连接zookeeper,并在/lock下创建临时的且有序的子节点,第一个客户端对应的子节点为/lock/lock-0000000000,第二个为/lock/lock-0000000001,以此类推。
- 2.客户端获取/lock下的子节点列表,判断自己创建的子节点是否为当前子节点列表中序号最小的子节点,如果是则认为获得锁,否则监听/lock的子节点变更消息,获得子节点变更通知后重复此步骤直至获得锁;
- 3.执行业务代码;
- 4.完成业务流程后,删除对应的子节点释放锁。
配置依赖jar包:
dependency>
<groupId>org.apache.curator</groupId>
<artifactId>curator-recipes</artifactId>
<version>2.9.1</version>
dependency
1. 创建Curator客户端、启动
RetryPolicy retryPolicy = new ExponentialBackoffRetry(1000, 3); // 重连的策略
CuratorFramework client = CuratorFrameworkFactory.newClient("127.0.0.1:2181", retryPolicy);
client.start();
interProcessMutex = new InterProcessMutex(client, this.path);
interProcessMutex.acquire //获取锁
interProcessMutex.release //释放锁
关于zk锁超时问题:
Zookeeper不需要配置锁超时,由于我们设置节点是临时节点,我们的每个机器维护着一个ZK的session,通过这个session,ZK可以判断机器是否宕机。如果我们的机器挂掉的话,那么这个临时节点对应的就会被删除,所以我们不需要关心锁超时。
参考:https://juejin.im/post/5bbb0d8df265da0abd3533a5#heading-20