Lua脚本实现可重入分布式锁

目录

1、前言

1.1、通过SETNX实现分布式锁

1.2、Redisson分布式锁详解

2、数据结构

3、Lua脚本剖析

3.1、加锁

3.2、解锁

3.3、重置锁的过期时间

4、代码实现

4.1、DistributedLock(核心实现类)

4.2、DistributedLockFactory(工厂类)

4.3、TestDistributedLockController(控制层)

5、效果演示

5.1、常规使用演示(使用test方法)

5.2、重入演示(使用testReentrant方法)

5.3、看门狗机制演示


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分布式锁详解

Redisson分布式锁详解(非公平、公平、红锁、联锁)_mlwsmqq的博客-CSDN博客本文讲解了Redisson框架提供的分布式锁(公平/非公平)、红锁、联锁的基本使用及效果演示,帮助大家快速熟悉分布式锁,相信一定对大家有所收益,欢迎观看!https://blog.csdn.net/mlwsmqq/article/details/128469771

2、数据结构

        本文采用Redis的hash数据结构,分布式锁的名称作为key、分布式锁的值作为field、重入次数作为value,效果如下图所示:

        

3、Lua脚本剖析

3.1、加锁

  1.  判断锁是否存在(exists),不存在则直接设置锁及过期时间
  2. 如果锁存在则判断是否是自己的锁(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、解锁

  1.  判断自己的锁是否存在(hexists),不存在则返回nil
  2. 如果自己的锁存在,则减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  

        有任何错误,欢迎大家指正!

        转载请注明出处!转载请注明出处!

        若本文对大家有所启示,请动动小手点赞和收藏哦!!!

  • 0
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
好的,我可以为您解答这个问题。在Java中,我们可以使用Redis作为分布式锁实现。其中,使用RedisLua脚本可以保证原子性操作。下面是一个使用RedisTemplate执行Lua脚本实现分布式锁的示例代码: ```java public class DistributedLock { private RedisTemplate redisTemplate; public DistributedLock(RedisTemplate redisTemplate) { this.redisTemplate = redisTemplate; } public boolean acquire(String lockKey, String requestId, int expireTime) { String script = "if redis.call('setnx', KEYS[1], ARGV[1]) == 1 then return redis.call('expire', KEYS[1], ARGV[2]) else return 0 end"; List<String> keys = Collections.singletonList(lockKey); List<String> args = Arrays.asList(requestId, String.valueOf(expireTime)); Object result = redisTemplate.execute(new DefaultRedisScript<>(script, Long.class), keys, args); return result != null && (Long) result == 1; } public boolean release(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"; List<String> keys = Collections.singletonList(lockKey); List<String> args = Collections.singletonList(requestId); Object result = redisTemplate.execute(new DefaultRedisScript<>(script, Long.class), keys, args); return result != null && (Long) result == 1; } } ``` 在上面的代码中,我们使用了RedisTemplate的execute方法执行了两个Lua脚本。acquire方法用于获取分布式锁,release方法用于释放分布式锁。其中,acquire方法的Lua脚本用于在Redis中创建一个指定的key,如果该key不存在,则将其设置为指定的值,并设置过期时间,该操作是原子性的。如果该key已经存在,则返回0。release方法的Lua脚本用于删除指定的key,如果该key的值等于指定的值,则删除该key,否则返回0。 这样,我们就可以使用上述代码来实现分布式锁了。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值