基于 Redisson 和 lua 脚本实现可重入锁

        Redisson就是一个使用Redis解决分布式问题的方案的集合,是一个十分成熟的Redis框架,功能也很多,比如:分布式锁和同步器、分布式对象、分布式集合、分布式服务。。。

什么是可重入锁?为什么要用可重入锁?

        可重入锁是一种线程同步机制,它允许一个线程在释放锁之前可以再次获取同一锁。这种特性在多线程编程中非常有用,尤其是在需要递归调用的场景下。

可重入锁的特点:
  1. 重入性:当一个线程持有锁并再次尝试获取该锁时,它不会被阻塞。这与普通的互斥锁不同,后者在同一时间只允许一个线程持有锁。

  2. 递归性:可重入锁允许同一个线程在其执行过程中多次获得同一锁,只要每次获取锁时的嵌套深度不超过锁的最大重入深度。

  3. 灵活性:现代可重入锁通常提供了更多的控制功能,比如公平性和非公平性、超时等待等,这些特性使得锁的选择更加灵活,可以根据不同的应用场景进行调整。

使用可重入锁的原因:
  1. 减少锁竞争:在多级嵌套的代码结构中,使用可重入锁可以避免频繁地上下文切换和锁的争夺,从而提高程序的执行效率。

  2. 简化并发控制:对于需要在多个方法内部或不同方法之间共享资源的情况,可重入锁简化了同步控制逻辑,减少了复杂的锁管理问题。

  3. 增强代码可读性:在设计上考虑重入性可以使代码逻辑更加清晰,因为程序员不需要担心在递归调用时如何正确释放锁。

  4. 支持高级并发模型:可重入锁为实现更复杂的并发控制策略提供了基础,如工作窃取模式、线程池中的任务调度等。

Lua脚本介绍

        在高并发的情况下,为了防止出现超卖问题,需要保证一个线程获取锁和释放锁的操作具有原子性,解决方案之一就是使用Lua脚本。

lua环境安装

Windows的安装可以参考菜鸟教程:学习站点:Lua 环境安装 | 菜鸟教程 (runoob.com)

        注:在IDEA中编写Lua脚本,需要先下载一个Lua脚本插件**Tarantool-EmmyLua**

Lua脚本是如何确保原子性的?

        Redis使用(支持)相同的Lua解释器,来运行所有的命令。Redis还保证脚本以原子方式执行:在执行脚本时,不会执行其他脚本或Redis命令。这个语义类似于MULTI(开启事务)/EXEC(触发事务,一并执行事务中的所有命令)。从所有其他客户端的角度来看,脚本的效果要么仍然不可见,要么已经完成。

        注意:虽然Redis在单个Lua脚本的执行期间会暂停其他脚本和Redis命令,以确保脚本的执行是原子的,但如果Lua脚本本身出错,那么无法完全保证原子性。也就是说Lua脚本中的Redis指令出错,会发生回滚以确保原子性,但Lua脚本本身出错就无法保障原子性。

Redissson可重入锁原理

        Redisson内部释放锁,并不是直接执行del命令将锁给删除,而是将锁以hash数据结构的形式存储在Redis中,每次获取锁,都将value的值+1,每次释放锁,都将value的值-1,只有锁的value值归0时才会真正的释放锁,从而确保锁的可重入性。

 

 1)编写获取锁的Lua脚本
---
--- Generated by EmmyLua(https://github.com/EmmyLua)
--- Created by ghp.
--- DateTime: 2023/2/14 16:11
---
-- 获取锁的key,即: KEY_PREFIX + name
local key = KEYS[1];
-- 获取当前线程的标识, 即: ID_PREFIX + Thread.currentThread().getId()
local threadId = ARGV[1];
-- 锁的有效期
local releaseTime = ARGV[2];

-- 判断缓存中是否存在锁
if (redis.call('EXISTS', key) == 0) then
    -- 不存在,获取锁
    redis.call('HSET', key, threadId, '1');
    -- 设置锁的有效期
    redis.call('EXPIRE', key, releaseTime);
    return 1; -- 返回1表示锁获取成功
end

-- 缓存中已存在锁,判断threadId是否说自己的
if (redis.call('HEXISTS', key, threadId) == 1) then
    -- 是自己的锁,获取锁然后重入次数+1
    redis.call('HINCRBY', key, threadId, '1');
    -- 设置有效期
    redis.call('EXPIRE', key, releaseTime);
    return 1; -- 返回1表示锁获取成功
end

-- 锁不是自己的,直接返回0,表示锁获取失败
return 0;
2)编写释放锁的Lua脚本
---
--- Generated by EmmyLua(https://github.com/EmmyLua)
--- Created by ghp.
--- DateTime: 2023/2/14 16:11
---
-- 获取锁的key,即: KEY_PREFIX + name
local key = KEYS[1];
-- 获取当前线程的标识, 即: ID_PREFIX + Thread.currentThread().getId()
local threadId = ARGV[1];
-- 锁的有效期
local releaseTime = ARGV[2];

-- 判断当前线程的锁是否还在缓存中
if (redis.call('HEXISTS', key, threadId) == 0) then
    -- 缓存中没找到自己的锁,说明锁已过期,则直接返回空
    return nil; -- 返回nil,表示啥也不干
end
-- 缓存中找到了自己的锁,则重入次数-1
local count = redis.call('HINCRBY', key, threadId, -1);

-- 进一步判断是否需要释放锁
if (count > 0) then
    -- 重入次数大于0,说明不能释放锁,且刷新锁的有效期
    redis.call('EXPIRE', key, releaseTime);
    return nil;
else
    -- 重入次数等于0,说明可以释放锁
    redis.call('DEL', key);
    return nil;
end
3)编写可重入锁:
public class ReentrantLock implements Lock {
    /**
     * RedisTemplate
     */
    private StringRedisTemplate stringRedisTemplate;
    /**
     * 锁的名称
     */
    private String name;
    /**
     * key前缀
     */
    private static final String KEY_PREFIX = "lock:";
    /**
     * ID前缀
     */
    private static final String ID_PREFIX = UUID.randomUUID().toString(true) + "-";
    /**
     * 锁的有效期
     */
    public long timeoutSec;

    public ReentrantLock(StringRedisTemplate stringRedisTemplate, String name) {
        this.stringRedisTemplate = stringRedisTemplate;
        this.name = name;
    }

    /**
     * 加载获取锁的Lua脚本
     */
    private static final DefaultRedisScript<Long> TRYLOCK_SCRIPT;

    static {
        TRYLOCK_SCRIPT = new DefaultRedisScript<>();
        TRYLOCK_SCRIPT.setLocation(new ClassPathResource("lua/re-trylock.lua"));
        TRYLOCK_SCRIPT.setResultType(Long.class);
    }

    /**
     * 获取锁
     *
     * @param timeoutSec 超时时间
     * @return
     */
    @Override
    public boolean tryLock(long timeoutSec) {
        this.timeoutSec = timeoutSec;
        // 执行lua脚本
        Long result = stringRedisTemplate.execute(
                TRYLOCK_SCRIPT,
                Collections.singletonList(KEY_PREFIX + name),
                ID_PREFIX + Thread.currentThread().getId(),
                Long.toString(timeoutSec)
        );
        return result != null && result.equals(1L);
    }

    /**
     * 加载释放锁的Lua脚本
     */
    private static final DefaultRedisScript<Long> UNLOCK_SCRIPT;

    static {
        UNLOCK_SCRIPT = new DefaultRedisScript<>();
        UNLOCK_SCRIPT.setLocation(new ClassPathResource("lua/re-unlock.lua"));
        UNLOCK_SCRIPT.setResultType(Long.class);
    }

    /**
     * 释放锁
     */
    @Override
    public void unlock() {
        // 执行lua脚本
        stringRedisTemplate.execute(
                UNLOCK_SCRIPT,
                Collections.singletonList(KEY_PREFIX + name),
                ID_PREFIX + Thread.currentThread().getId(),
                Long.toString(this.timeoutSec)
        );
    }
}
4)编写测试类 
@SpringBootTest
@Slf4j
public class ReentrantLockTest {

    @Resource
    private StringRedisTemplate stringRedisTemplate;

    private ReentrantLock lock;

    /**
     * 方法1获取一次锁
     */
    @Test
    void method1() {
        boolean isLock = false;
        // 创建锁对象
        lock = new ReentrantLock(stringRedisTemplate, "order:" + 1);
        try {
            isLock = lock.tryLock(1200);
            if (!isLock) {
                log.error("获取锁失败,1");
                return;
            }
            log.info("获取锁成功,1");
            method2();
        } finally {
            if (isLock) {
                log.info("释放锁,1");
                lock.unlock();
            }
        }
    }

    /**
     * 方法二再获取一次锁
     */
    void method2() {
        boolean isLock = false;
        try {
            isLock = lock.tryLock(1200);
            if (!isLock) {
                log.error("获取锁失败, 2");
                return;
            }
            log.info("获取锁成功,2");
        } finally {
            if (isLock) {
                log.info("释放锁,2");
                lock.unlock();
            }
        }
    }
}

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值