文章目录

  • ⛅前言
  • 一、使用Redis 分布式锁 存在的问题
    • ⛄Redis 分布式锁误删问题
    • ⚡分布式锁的原子性问题
  • 二、什么是Lua?
  • 三、使用Redis 调用Lua脚本
  • 四、Java 调用Lua脚本实现分布式锁
  • 五、测试
  • ⛵小结

⛅前言

在 微服务 Spring Boot 整合Redis分布式锁 实现优惠卷秒杀 一人一单 中, 依旧会出现问题,这个问题是由于在高并发下,假设某个线程的锁等待时间过长,导致这个锁自动释放,那么此时其它线程进来就会重新获取锁,在该线程执行过程中,突然之前阻塞的锁反应了过来,转手删了这把锁,那么此时就造成了误删问题。 下面我们继续来解决该问题

解决方案:在每个线程释放锁的时候,判断一下是不是自己的,如果是,才走删除逻辑,如果不是,则不走删除逻辑。

一、使用Redis 分布式锁 存在的问题

⛄Redis 分布式锁误删问题

修改之前的代码,来实现删除锁时判断 锁是否属于自己

  • 一致:删除锁
  • 不一致:不删除

核心 : 在存入锁时,放入自己线程的标识,在删除时,判断是不是自己存入的,如果是,那就可以删除,如果不是,不删除。

加锁代码

private static final String ID_PREFIX = UUID.randomUUID().toString(true) + "-";public boolean tryLock(long timeoutSecond) {    String threadId = ID_PREFIX + Thread.currentThread().getId();    // 尝试获取锁    Boolean success = stringRedisTemplate.opsForValue().setIfAbsent(KEY_PREFIX + name, threadId, timeoutSecond, TimeUnit.SECONDS);    return BooleanUtil.isTrue(success);}

释放锁代码

public void unLock() {    //获取线程标识    String threadId = ID_PREFIX + Thread.currentThread().getId();    //获取redis中的标识    String id = stringRedisTemplate.opsForValue().get(KEY_PREFIX + name);    //判断是否一致    if (threadId.equals(id)) {        // 释放锁        stringRedisTemplate.delete(KEY_PREFIX + name);    }}

测试

重启两个 服务,利用ApiFox 分别调用两个不同的服务,看是否出现删除错误的情况

启动ApiFox 自行进行测试即可,打上断点观察是否有误删情况

测试完毕后,正常,无误删情况发生

⚡分布式锁的原子性问题

当线程1 持有锁之后,执行业务逻辑完毕时,此时,线程1要删除锁,而且已经走入了判断条件中,但正准备删除时,此时锁过期了,那么此时另外一个线程2进来时,执行自己的逻辑,线程1这时反应了过来,继续执行自己的逻辑,由于删除线程2锁的判断条件未起作用,所以导致了误删,造成了锁的非原子性,我们要解决此问题,就要将 判断和删除锁保持原子性

以上问题,我们可以使用Lua来解决。

二、什么是Lua?

Lua 是一个小巧的脚本语言。它是巴西里约热内卢天主教大学(Pontifical Catholic University of Rio de Janeiro)里的一个由Roberto Ierusalimschy、Waldemar Celes 和 Luiz Henrique de Figueiredo三人所组成的研究小组于1993年开发的。 其设计目的是为了通过灵活嵌入应用程序中从而为应用程序提供灵活的扩展和定制功能Lua由标准C编写而成,几乎在所有操作系统和平台上都可以编译,运行。Lua并没有提供强大的库,这是由它的定位决定的。所以Lua不适合作为开发独立应用程序的语言。Lua 有一个同时进行的JIT项目,提供在特定平台上的即时编译功能。

简单的来说,Lua可以实现我们命令的一致性,原子性。

三、使用Redis 调用Lua脚本

Redis 调用Lua 函数

Redis 提供了调用Lua 的函数,语法如下:

redis.call('命令名称', 'key', '其它参数')

例如,我们要执行命令存入年龄

redis.call('set', 'age', '12')

先执行添加,再执行获取

# 执行存入数据redis.call('set', 'name', 'xiaowang')# 获取数据并存入变量local name = redis.call('get', 'name')# 返回变量return name;

查看 Redis调用Lua命令

调用脚本

动态传值调用使用Redis 调用Lua 脚本

编写释放锁 Lua 脚本

业务流程

  • 获取锁中的线程标识
  • 判断是否与当前线程一致
  • 如果一致释放锁
  • 不一致,不释放

Lua 脚本展示

-- 比较线程中的标识是否与锁中的标识一致if (redis.call('get', KEYS[1]) == ARGV[1]) then    -- 释放锁    return redis.call('del', KEYS[1])endreturn 0

四、Java 调用Lua脚本实现分布式锁

Idea 新建Lua 脚本

File – Settings – Plugins

搜索 EmmyLua 插件

记住,下载完毕后,重启IDEA,激活插件

在 resources文件夹下编写 unlock.lua 脚本

-- 比较线程中的标识是否与锁中的标识一致if (redis.call('get', KEYS[1]) == ARGV[1]) then    -- 释放锁    return redis.call('del', KEYS[1])endreturn 0

在 Simple Lock 实现类中写入一些代码,调用Lua脚本实现释放锁操作

private static final DefaultRedisScript<Long> UNLOCK_SCRIPT;static {    UNLOCK_SCRIPT = new DefaultRedisScript<>();    UNLOCK_SCRIPT.setLocation(new ClassPathResource("unlock.lua"));    UNLOCK_SCRIPT.setResultType(Long.class);}public void unLock() {    // 调用Lua 脚本    stringRedisTemplate.execute(        UNLOCK_SCRIPT,        Collections.singletonList(KEY_PREFIX + name),        ID_PREFIX + Thread.currentThread().getId());}

五、测试

启动微服务集群

启动ApiFox测试工具,登录用户获取用户id,并请求秒杀业务

获取验证码

验证码

拿到验证码并登录

测试秒杀业务,打下断点自行测试

进入第一个断点,获取锁,回到redis删除该锁,模拟延迟释放。

进入第二个断点,获取锁,再次进入第一个断点,执行业务,可见,并未删除锁,由于我们采用了Lua脚本来实现分布式锁

第二个继续执行,删除锁,业务成功执行!

测试无误,采用Lua脚本优化分布式锁 成功~

⛵小结

以上就是【Bug 终结者】对 微服务 Spring Boot 整合Redis分布式锁 Lua脚本 实现优惠卷秒杀 一人一单 的简单介绍,在分布式系统下,高并发的场景下,会出现此类库存超卖问题,本篇文章介绍了采用Lua脚本实现分布式锁来解决,但是依旧不是最优解答, 下章节,我们将继续进行优化,持续关注!

如果这篇【文章】有帮助到你,希望可以给【Bug 终结者】点个赞,创作不易,如果有对【后端技术】、【前端领域】感兴趣的小可爱,也欢迎关注❤️❤️❤️ 【Bug 终结者】❤️❤️❤️,我将会给你带来巨大的【收获与惊喜】!