Redis分布式锁的简单实现
Posted 风在哪
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Redis分布式锁的简单实现相关的知识,希望对你有一定的参考价值。
Redis分布式锁
随着业务发展的需要,原来单机部署的系统演化成分布式集群系统之后,由于分布式系统多线程、多进程并且分布在不同的机器上,使得原来单机部署情况下的并发控制策略失效,单纯的Java API并不能提供分布式锁的能力,为了解决这个问题就需要一种跨JVM的互斥机制来控制共享资源的访问,这就是分布式锁要解决的问题。
也就是说,当我们使用分布式锁时,该锁多分布式系统内的所有应用都有效。
分布式锁的主流实现方案有多种,包括基于数据库实现的分布式锁、基于缓存(如redis)实现(性能最高)、基于zookeeper实现(可靠性最高)。本节主要讲基于redis实现的分布式锁。
setnx命令实现分布式锁
setnx key value
setnx只有当key不存在的情况下,才将key的值设置为value,若key已经存在,那么setnx命令不做任何操作。
例如:
当我们初次调用setnx命令设置lock的值时,lock还不存在,所以可以设置成功,后续再调用setnx设置相同的key,也就是lock时,却无法设置成功,这就是setnx的作用。
其实setnx就是[SET if Not eXists(如果不存在则SET)]的简写。
那么加锁之后我们如何释放锁呢,此时就需要调用del命令删除key,以此来释放锁。
也就是将对应的key删除掉就释放了相应的锁了。
所以,redis实现分布式锁需要setnx和del这两个指令来完成加锁和解锁。
相关问题
1、当我们加锁以后忘记释放锁,那么其他应用将一直无法获取到锁,这是就产生了问题。那么这个问题怎么解决呢?
我们可以在加锁之后设置锁的存活时间,存活时间到达之后,锁会自动失效,我们无需手动删除key。
例如:
此时我们就可以放心使用分布式锁了。
2、上面虽然实现了加锁以及设置过期时间,但是如果我们加锁以后,设置过期时间之前,redis突然宕机了,没有执行expire指令,当redis重启以后,该锁还是会一直存在,所以我们需要使用原子操作来加锁并且设置过期时间,那么可以通过一条指令搞定:
set lock yes nx ex 20
这样我们就可以放心使用redis分布式锁啦!
redis分布式锁优化-UUID防误删
首先我们来看看redis分布式锁的流程:
- 加锁并设置过期时间
- 进行业务处理
- del操作释放锁
假如此时我们的A服务在第2步由于机器的原因卡住了,并且卡顿的时间超过了我们设置的过期时间。此时,B服务获得了相同的分布式锁。
此时A服务从卡顿状态恢复,并且处理完业务,继续执行了del操作,那么此时del操作删除的是B服务的分布式锁,这会导致一系列的问题。
这种问题如何解决呢,其实解决的方式很简单:
- 加锁并设置过期时间,加锁时需要设置锁的值为UUID(其实不是UUID也可以,只要不同的服务产生的值不同即可,主要作用就是标识不同的加锁对象)
- 进行业务处理
- 首先获取锁的值,判断是否与自身的UUID值相等,如果相等则证明这是我们加的分布式锁,可以进行删除,否则就不能删除,因为这是其他服务加的锁。
具体代码很简单,这里不再展示啦!
redis分布式锁优化-原子性操作实现锁的删除操作
其实我们使用UUID删除分布式锁的话还是存在一定的问题:
当A服务判断了分布式锁的UUID与自身的UUID值相等,那就准备进行删除操作,但是此时,分布式锁正好过期,B服务获得了这个分布式锁,A服务还是会进行del操作删除分布式锁,那么又成了删除B服务的分布式锁。这里又产生了问题,那么我们如何解决呢?
我们可以通过原子性操作来解决这个问题。
通过如下代码可以使用Lua脚本实现原子操作:
@GetMapping("testLockLua")
public void testLockLua() {
//1 声明一个uuid ,将做为一个value 放入我们的key所对应的值中
String uuid = UUID.randomUUID().toString();
//2 定义一个锁:lua 脚本可以使用同一把锁,来实现删除!
String locKey = "lock"; // 锁住的是每个商品的数据
// 3 获取锁
Boolean lock = redisTemplate.opsForValue().setIfAbsent(locKey, uuid, 3, TimeUnit.SECONDS);
// 第一种: lock 与过期时间中间不写任何的代码。
// redisTemplate.expire("lock",10, TimeUnit.SECONDS);//设置过期时间
// 如果true
if (lock) {
// 执行的业务逻辑开始
// 获取缓存中的num 数据
Object value = redisTemplate.opsForValue().get("num");
// 如果是空直接返回
if (StringUtils.isEmpty(value)) {
return;
}
// 不是空 如果说在这出现了异常! 那么delete 就删除失败! 也就是说锁永远存在!
int num = Integer.parseInt(value + "");
// 使num 每次+1 放入缓存
redisTemplate.opsForValue().set("num", String.valueOf(++num));
/*使用lua脚本来锁*/
// 定义lua 脚本
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
// 使用redis执行lua执行
DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>();
redisScript.setScriptText(script);
// 设置一下返回值类型 为Long
// 因为删除判断的时候,返回的0,给其封装为数据类型。如果不封装那么默认返回String 类型,
// 那么返回字符串与0 会有发生错误。
redisScript.setResultType(Long.class);
// 第一个要是script 脚本 ,第二个需要判断的key,第三个就是key所对应的值。
redisTemplate.execute(redisScript, Arrays.asList(locKey), uuid);
} else {
// 其他线程等待
try {
// 睡眠
Thread.sleep(1000);
// 睡醒了之后,调用方法。
testLockLua();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
总结
综上所述,要想完美的使用redis分布式锁,我们需要如下几步:
- 加锁并设置过期时间,设置锁的值为UUID,防止误删
- 进行业务的处理
- 删除时使用Lua脚本进行事务的原子性操作,防止误删其他服务的锁
参考:
以上是关于Redis分布式锁的简单实现的主要内容,如果未能解决你的问题,请参考以下文章
分布式锁的两种实现方式(基于redis和基于zookeeper)