目录
4.2、DistributedLockFactory(工厂类)
4.3、TestDistributedLockController(控制层)
1、前言
提到分布式锁,那一定绕不开Redisson,在深入Redisson源码时发现它使用了大量的lua脚本,为什么要使用lua脚本呢?答案就是它能够保证Redis操作的原子性;受到Redisson的启发,本文将带领大家一步步的通过lua脚本实现可重入分布式锁
还有两篇关于分布式锁的博客供大家参考
1.1、通过SETNX实现分布式锁Redis实现分布式锁(SETNX)_mlwsmqq的博客-CSDN博客本文详细介绍了什么是分布式锁、分布式锁的特征、应用场景;一步一步的手动实现分布式锁,分析其中需要特别注意的地方,带着大家理清其中的思路;相信对大家会有所帮助https://blog.csdn.net/mlwsmqq/article/details/127723729
1.2、Redisson分布式锁详解
2、数据结构
本文采用Redis的hash数据结构,分布式锁的名称作为key、分布式锁的值作为field、重入次数作为value,效果如下图所示:
3、Lua脚本剖析
3.1、加锁
- 判断锁是否存在(exists),不存在则直接设置锁及过期时间
- 如果锁存在则判断是否是自己的锁(hexists),是自己的则重入,hincrby key field increment,并设置锁的过期时间;否则返回0表示加锁失败
if redis.call('exists',KEYS[1]) == 0
then
redis.call('hset',KEYS[1],ARGV[1],1)
redis.call('expire',KEYS[1],ARGV[2])
return 1
elseif redis.call('hexists',KEYS[1],ARGV[1]) == 1
then
redis.call('hincrby',KEYS[1],ARGV[1],1)
redis.call('expire',KEYS[1],ARGV[2])
return 1
else
return 0
end
其实上面的写法并不是最简便的,由于hincrby命令也可以实现hset命令的效果(向Redis插入值),所以可简化为如下所示:
if redis.call('exists',KEYS[1]) == 0 or redis.call('hexists',KEYS[1],ARGV[1]) == 1
then
redis.call('hincrby',KEYS[1],ARGV[1],1)
redis.call('expire',KEYS[1],ARGV[2])
return 1
else
return 0
end
3.2、解锁
- 判断自己的锁是否存在(hexists),不存在则返回nil
- 如果自己的锁存在,则减1(hincrby -1),判断减1后的值是否为0,为0则释放锁(del)并返回1;不为0(说明锁被重入,不删除锁),返回0
if redis.call('hexists',KEYS[1],ARGV[1]) == 0
then
return nil
elseif redis.call('hincrby',KEYS[1],ARGV[1],-1) == 0
then
return redis.call('del',KEYS[1])
else
return 0
end
3.3、重置锁的过期时间
看门狗机制内使用,目的是为了重置锁的过期时间
判断自己的锁是否存在,存在就重置过期时间
if redis.call('hexists',KEYS[1],ARGV[1]) == 1
then
return redis.call('expire',KEYS[1],ARGV[2])
else
return 0
end
4、代码实现
4.1、DistributedLock(核心实现类)
package com.example.learningexpansion.controller.testRedis.distributedLockByRedis;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import java.util.Arrays;
import java.util.Objects;
import java.util.Timer;
import java.util.TimerTask;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
/**
* @Auther:admin
* @Date:2022/12/9 16:40
*/
@Slf4j
public class DistributedLock implements Lock {
private Timer timer = new Timer();
private StringRedisTemplate stringRedisTemplate;
private String lockName;
private String uuid;
private Long expire = 30L;
public DistributedLock(StringRedisTemplate stringRedisTemplate, String lockName, String uuid) {
this.stringRedisTemplate = stringRedisTemplate;
this.lockName = lockName;
this.uuid = uuid;
}
@Override
public void lock() {
tryLock();
}
@Override
public boolean tryLock() {
try {
return tryLock(-1L, TimeUnit.SECONDS);
} catch (Exception e) {
log.error("tryLock exception:", e);
}
return false;
}
// 加锁
@Override
public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
if (-1L != time) {
expire = unit.toSeconds(time);
}
String script = "if redis.call('exists',KEYS[1]) == 0 or redis.call('hexists',KEYS[1],ARGV[1]) == 1 " +
"then " +
"redis.call('hincrby',KEYS[1],ARGV[1],1) " +
"redis.call('expire',KEYS[1],ARGV[2]) " +
"return 1 " +
"else " +
"return 0 " +
"end";
String id = getId();
// 加锁失败,循环尝试获取锁
while (!stringRedisTemplate.execute(new DefaultRedisScript<>(script, Boolean.class), Arrays.asList(lockName), id, String.valueOf(expire))) {
TimeUnit.MILLISECONDS.sleep(100);
}
// 有效期为默认时间时才启动看门狗线程
if (-1L == time) {
resetExpire(id);
log.info("启动看门狗线程!");
}
return true;
}
// 解锁
@Override
public void unlock() {
String script = "if redis.call('hexists',KEYS[1],ARGV[1]) == 0 " +
"then " +
"return nil " +
"elseif redis.call('hincrby',KEYS[1],ARGV[1],-1) == 0 " +
"then " +
"return redis.call('del',KEYS[1]) " +
"else " +
"return 0 " +
"end";
Long execute = stringRedisTemplate.execute(new DefaultRedisScript<>(script, Long.class), Arrays.asList(lockName), getId());
if (Objects.isNull(execute)) {
throw new IllegalMonitorStateException("this lock doesn't belong to you");
}
// 停止看门狗
timer.cancel();
log.info("释放锁成功,停止看门狗线程!");
}
// 拼接线程ID和UUID组成唯一标识
public String getId() {
return Thread.currentThread().getId() + ":" + uuid;
}
// 重置过期时间(延迟delay毫秒后开始执行任务,之后每间隔period毫秒执行一次任务)
private void resetExpire(String id) {
String script = "if redis.call('hexists',KEYS[1],ARGV[1]) == 1 " +
"then " +
"return redis.call('expire',KEYS[1],ARGV[2]) " +
"else " +
"return 0 " +
"end";
long time = expire * 1000 / 3;
timer.schedule(new TimerTask() {
@Override
public void run() {
Boolean result = stringRedisTemplate.execute(new DefaultRedisScript<>(script, Boolean.class), Arrays.asList(lockName), id, String.valueOf(expire));
log.info("重置过期时间结果:{}", result);
}
}, time, time);
}
@Override
public void lockInterruptibly() throws InterruptedException {
}
@Override
public Condition newCondition() {
return null;
}
}
4.2、DistributedLockFactory(工厂类)
package com.example.learningexpansion.controller.testRedis.distributedLockByRedis;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import java.util.UUID;
/**
* @Auther:admin
* @Date:2022/12/9 16:43
*/
@Component
public class DistributedLockFactory {
@Resource
private StringRedisTemplate stringRedisTemplate;
private String uuid;
public DistributedLockFactory() {
this.uuid = UUID.randomUUID().toString().replaceAll("-","").toString();
}
public DistributedLock getRedisLock(String lockName){
return new DistributedLock(stringRedisTemplate,lockName,uuid);
}
}
4.3、TestDistributedLockController(控制层)
package com.example.learningexpansion.controller.testRedis.distributedLockByRedis;
import com.example.learningexpansion.utils.ResultUtils;
import com.example.learningexpansion.vo.ResultVO;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
import java.util.concurrent.TimeUnit;
/**
* @Auther:admin
* @Date:2022/12/9 17:26
*/
@Slf4j
@RequestMapping("/testDistributedLockController")
@Api(tags = "可重入分布式锁")
@RestController
public class TestDistributedLockController {
private static final String STOCK = "stock";
@Resource
private StringRedisTemplate stringRedisTemplate;
@Resource
private DistributedLockFactory factory;
private void deduct(){
// 查询库存(偷个懒,库存需手动插入)
String cache = stringRedisTemplate.opsForValue().get(STOCK);
if (StringUtils.isNotBlank(cache)){
Integer stock = Integer.valueOf(cache);
if (stock > 0){
// 减库存
stringRedisTemplate.opsForValue().set(STOCK,String.valueOf(--stock));
log.info("扣减库存成功!");
}
}
}
@GetMapping("/test")
@ApiOperation("测试(不重入)")
public ResultVO<Object> test(@RequestParam String lockName){
DistributedLock redisLock = factory.getRedisLock(lockName);
redisLock.lock();
try {
TimeUnit.SECONDS.sleep(40);
deduct();
}catch (Exception e){
log.error("test exception:",e);
return ResultUtils.error("失败!");
}finally {
redisLock.unlock();
}
return ResultUtils.success();
}
@GetMapping("/testReentrant")
@ApiOperation("测试(重入)")
public ResultVO<Object> testReentrant(@RequestParam String lockName){
DistributedLock redisLock = factory.getRedisLock(lockName);
redisLock.lock();
try {
reentrant(lockName);
deduct();
}catch (Exception e){
log.error("testReentrant exception:",e);
return ResultUtils.error("失败!");
}finally {
redisLock.unlock();
}
return ResultUtils.success();
}
private void reentrant(String lockName){
DistributedLock redisLock = factory.getRedisLock(lockName);
redisLock.lock();
try {
log.info("重入成功了!");
}catch (Exception e){
log.error("reentrant exception:",e);
}finally {
redisLock.unlock();
}
}
}
5、效果演示
以下演示均需手动插入库存缓存(stock),偷个懒!
5.1、常规使用演示(使用test方法)
使用8701、8702端口同时启动两个服务,传入相同的参数,睡眠6秒模拟执行业务逻辑,快速向两个服务各调用一次
8701服务结果:
2023-01-09 15:28:05.627 INFO 14440 --- [nio-8701-exec-3] c.e.l.c.t.d.DistributedLock : 启动看门狗线程!
2023-01-09 15:28:05.627 INFO 14440 --- [nio-8701-exec-3] .e.l.c.t.d.TestDistributedLockController : http-nio-8701-exec-3:加锁成功
2023-01-09 15:28:11.634 INFO 14440 --- [nio-8701-exec-3] .e.l.c.t.d.TestDistributedLockController : 扣减库存成功!
2023-01-09 15:28:11.636 INFO 14440 --- [nio-8701-exec-3] c.e.l.c.t.d.DistributedLock : 释放锁成功,停止看门狗线程!
8702服务效果:
2023-01-09 15:28:11.695 INFO 15732 --- [nio-8702-exec-1] c.e.l.c.t.d.DistributedLock : 启动看门狗线程!
2023-01-09 15:28:11.696 INFO 15732 --- [nio-8702-exec-1] .e.l.c.t.d.TestDistributedLockController : http-nio-8702-exec-1:加锁成功
2023-01-09 15:28:17.703 INFO 15732 --- [nio-8702-exec-1] .e.l.c.t.d.TestDistributedLockController : 扣减库存成功!
2023-01-09 15:28:17.704 INFO 15732 --- [nio-8702-exec-1] c.e.l.c.t.d.DistributedLock : 释放锁成功,停止看门狗线程!
从上述日志可看出:8701服务先拿到锁,执行完业务释放锁后8702服务才能拿到锁,达到了分布式锁想要的效果
5.2、重入演示(使用testReentrant方法)
在一次请求中加锁、解锁各两次,在第二次加锁后打断点看看缓存中的值是多少
缓存中的值:
可以看到value(重入次数)变成2,代表锁被重入,跟预期一致
看看打印的日志:
2023-01-09 15:46:12.129 INFO 14320 --- [nio-8701-exec-1] c.e.l.c.t.d.DistributedLock : 启动看门狗线程!
2023-01-09 15:46:12.129 INFO 14320 --- [nio-8701-exec-1] .e.l.c.t.d.TestDistributedLockController : http-nio-8701-exec-1:加锁成功
2023-01-09 15:46:12.131 INFO 14320 --- [nio-8701-exec-1] c.e.l.c.t.d.DistributedLock : 启动看门狗线程!
2023-01-09 15:46:12.131 INFO 14320 --- [nio-8701-exec-1] .e.l.c.t.d.TestDistributedLockController : http-nio-8701-exec-1:加锁成功
2023-01-09 15:46:12.131 INFO 14320 --- [nio-8701-exec-1] .e.l.c.t.d.TestDistributedLockController : 重入成功了!
2023-01-09 15:46:12.133 INFO 14320 --- [nio-8701-exec-1] c.e.l.c.t.d.DistributedLock : 释放锁成功,停止看门狗线程!
2023-01-09 15:46:12.141 INFO 14320 --- [nio-8701-exec-1] .e.l.c.t.d.TestDistributedLockController : 扣减库存成功!
2023-01-09 15:46:12.143 INFO 14320 --- [nio-8701-exec-1] c.e.l.c.t.d.DistributedLock : 释放锁成功,停止看门狗线程!
从日志可看出:加锁、解锁、看门狗启动/停止都进行了两次,跟预期一致
5.3、看门狗机制演示
把睡眠时间改为40s,观察看门狗是否生效
2023-01-09 15:56:29.969 INFO 12180 --- [nio-8701-exec-4] c.e.l.c.t.d.DistributedLock : 启动看门狗线程!
2023-01-09 15:56:29.969 INFO 12180 --- [nio-8701-exec-4] .e.l.c.t.d.TestDistributedLockController : http-nio-8701-exec-4:加锁成功
2023-01-09 15:56:39.971 INFO 12180 --- [ Timer-0] c.e.l.c.t.d.DistributedLock : 重置过期时间结果:true
2023-01-09 15:56:49.972 INFO 12180 --- [ Timer-0] c.e.l.c.t.d.DistributedLock : 重置过期时间结果:true
2023-01-09 15:56:59.973 INFO 12180 --- [ Timer-0] c.e.l.c.t.d.DistributedLock : 重置过期时间结果:true
2023-01-09 15:57:09.973 INFO 12180 --- [ Timer-0] c.e.l.c.t.d.DistributedLock : 重置过期时间结果:true
2023-01-09 15:57:09.976 INFO 12180 --- [nio-8701-exec-4] .e.l.c.t.d.TestDistributedLockController : 扣减库存成功!
2023-01-09 15:57:09.977 INFO 12180 --- [nio-8701-exec-4] c.e.l.c.t.d.DistributedLock : 释放锁成功,停止看门狗线程!
从上述日志可看出在执行业务逻辑期间看门狗线程不断的延长锁的过期时间,使得业务完整执行,在此期间锁没有失效或被其它线程获得,说明看门狗是发挥出作用啦,跟预期一致
温馨提示:本文主要阐述通过Lua脚本实现可重入分布式锁的思路,代码实现上不尽完善,如果大家需要用到分布式锁可以考虑使用Redisson或zookeeper
有任何错误,欢迎大家指正!
转载请注明出处!转载请注明出处!
若本文对大家有所启示,请动动小手点赞和收藏哦!!!