新春佳节,祝大家新春快乐,人人都有口罩,希望这次疫情尽快过去!对国家领导和抗战在疫情一线的义务工作者送上我的祝福于感激!
分布式锁学习笔记
来源博客:吴大山
本文的代码实现,GitHub地址:redis分布式锁-模拟多线程并发竞争锁
1.分布式锁一般三种实现方式:
- 数据库乐观锁;
- 基于Redis的分布式锁;(本文讲解的)
- 基于ZooKeeper的分布式锁。
2.分布式锁必须满足的四个条件:
- 互斥性:集群情况下只有一台机器上的服务能获取到redis的锁。
- 不会发送死锁:如果一个服务在持有锁期间发送崩溃,没有主动释放锁,也应该保证这个锁不会变成死锁,要有失效时间保证后续其他服务都能正常加锁。
- 容错性:如果你的redis是集群部署的,要能够保证在集群机器绝大多数都正常的情况下,服务能够正常加锁解锁
- 解铃还需系铃人:服务自己加的锁只能自己解,不能别的服务能解自己的锁
3.正确使用redis分布式锁 先看如何使用
/**
* 尝试获取分布式锁
* @param jedis redis客户端
* @param lockKey 锁
* @param requestId 请求标识//不一定要是请求id,只要是能标识出是谁加锁的,方便后面释放锁
* @param expireTime 过期时间
* @return
*/
public static boolean tryGetDistributedLock(Jedis jedis, String lockKey, String requestId, int expireTime) {
String result = jedis.set(lockKey, requestId, "NX", "PX", expireTime);
return "OK".equals(result);
}
上面就是正确使用redis锁的代码,可以看出,加锁其实就是调用了set方法,方法后面有5个参数,从源码来看
public String set(final String key, final String value, final String nxxx, final String expx, final int time)
其实说到底,redis分布式锁只是存储在redis中的一条记录而已,所以必要的key和value还是需要的。 第一个参数"key": key在redis中肯定是唯一的,所以可以用来当锁
第二个参数"value": 既然用key来当锁,为什么需要value呢,因为为了满足解铃还需系铃人这个要求,所以value值必须满足能标识出当前锁是谁加的锁,所以可以使用请求id之类的
第三个参数"nxxx":有两个值可选
| 参数值 | 解释 |
|---|---|
| nx | not exists,代表只有当前key不存在时,才会set key和value到redis |
| xx | is exists,代表只有当前key存在时,才会set key和value到redis |
第四个参数"expx":有两个值可选
| 参数值 | 解释 |
|---|---|
| ex | seconds 秒 |
| px | milliseconds 毫秒 |
第五个参数"time":当前这个锁的过期时间,多久后没有释放锁就自动过期 这个参数有两个类型,int和long,到源码底层都是把这个字段值转换成String类型的,当expx参数是px时,可能int的长度不能到达要求,所有有了long。
最后,这个set方法返回值是String类型,成功返回"OK",失败返回空。
4.错误使用redis分布式锁加锁
- 使用jedis.setnx()和jedis.expire()组合实现加锁
jedis.setnx()就是set if not exist,expire就是给这个key添加一个过期时间,如果在setnx()到expire执行之前程序出现崩溃,就会因为没有设置过期时间从而导致死锁
5.正确释放redis分布式锁
/**
*释放锁
* @param jedis
* @param lockKey 锁
* @param requestId 请求标识//不一定要是请求id,只要是能标识出是谁加锁的,方便后面释放锁
*/
public static boolean releaseDistributedLock(Jedis jedis, String lockKey, String requestId){
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
Object result = jedis.eval(script, Collections.singletonList(lockKey), Collections.singletonList(requestId));
if (RELEASE_SUCCESS.equals(result)) {
return true;
}
return false;
}
实现解锁我个人认为最重要的是要保证“解铃还需系铃人”这个条件。上面解锁的方法主要使用了两个方法
- 使用lua脚本
- 使用eval方法删除锁
我们设想下正确的思路应该怎么删除一个锁,首先肯定是先查询这个锁存不存在,然后判断当前执行删除锁的客户端是不是加锁的那个客户端,是,删除掉这个锁。从上面的分析看,到最后删除锁之前还有其他一些判断操作,如果这个时候出现问题,就会导致释放锁失败,只能等待锁过期或者发生死锁。所以使用lua脚本保证原子性(redis将整个eval方法当成一条命令去执行),eval方法其实就是执行的lua脚本。
6.错误释放redis分布式锁
- 使用del方法直接删除锁
这样做是做到了解锁的功能,但是不能保证释放锁的就是加锁的客户端,换言之就是我加的锁,谁都可以删除
- 不使用lua脚本,判断当前锁可不可以删除,可以就删除。
这个看似没有问题,但是不能保证原子性,只要在删除之前的程序出现问题就会导致解锁失败。这个不能保证原子性操作
总结: 如果你的项目中Redis是多机部署的,那么可以尝试使用Redisson实现分布式锁,这是Redis官方提供的Java组件
ps:一些额外的补充 事务的四种特性 原子性:事务不可以被分隔,事务执行时不可以分批次执行,必须保证一次执行到底。事务是一个不可再分割的工作单位,事务中的操作要么都发生,要么都不发生。
一致性:一致性是指在事务开始之前和事务结束以后,数据库的完整性约束没有被破坏。这是说数据库事务不能破坏关系数据的完整性以及业务逻辑上的一致性。
隔离性:隔离性是指并发的事务是相互隔离的。即一个事务内部的操作及正在操作的数据必须封锁起来,不被企图进行修改的事务看到 。
持久性:持久性是指在事务完成以后,该事务所对数据库所作的更改便持久的保存在数据库之中,并不会被回滚。 即使出现了任何事故比如断电等,事务一旦提交,则持久化保存在数据库中。