• Reids实战——分布式锁优化(Lua脚本)


    1 基于Redis分布式锁的问题

    先来看看之前分布式锁的实现。

    这个基于Redis的分布式锁仍然有着一个问题,那就是误删锁的问题。 

    简单的来说,就是当第一个线程,也就是线程1,拿到锁后,但由于本身业务复杂,而导致了阻塞,超过了锁设置的超时时间,锁自动释放。这个时候,线程2进来了,也拿到了锁,但是就在线程2执行业务的途中,线程1业务完成,主动释放了锁,又因为我们释放锁的逻辑是直接删除key,这就导致了线程2的锁被误删

     

    这就导致了线程安全的问题。

    解决方法:在每个线程要释放锁的时候,主动判断reids中存入的线程标识,来判断是不是自己的锁,如果不是,就不能删除。

    代码实现:

    1. package com.hmdp.utils;
    2. import cn.hutool.core.collection.CollectionUtil;
    3. import cn.hutool.core.lang.UUID;
    4. import org.springframework.core.io.ClassPathResource;
    5. import org.springframework.data.redis.core.StringRedisTemplate;
    6. import org.springframework.data.redis.core.script.DefaultRedisScript;
    7. import org.springframework.util.CollectionUtils;
    8. import java.util.Collections;
    9. import java.util.concurrent.TimeUnit;
    10. /**
    11. * @Author 华子
    12. * @Date 2022/12/3 20:53
    13. * @Version 1.0
    14. */
    15. public class SimpleRedisLock implements ILock {
    16. //Redis
    17. private StringRedisTemplate stringRedisTemplate;
    18. //业务名称,也就是锁的名称
    19. private String name;
    20. public SimpleRedisLock(StringRedisTemplate stringRedisTemplate, String name) {
    21. this.stringRedisTemplate = stringRedisTemplate;
    22. this.name = name;
    23. }
    24. //key的前缀
    25. private static final String KEY_PREFIX = "lock:";
    26. //线程标识的前缀
    27. private static final String ID_PREFIX = UUID.randomUUID().toString(true);
    28. @Override
    29. public boolean tryLock(long timeoutSec) {
    30. //获取线程id,当作set的value
    31. String threadId = Thread.currentThread().getId()+ID_PREFIX;
    32. Boolean success = stringRedisTemplate.opsForValue()
    33. .setIfAbsent(KEY_PREFIX + name, threadId, timeoutSec, TimeUnit.SECONDS);
    34. return Boolean.TRUE.equals(success);
    35. }
    36. //释放锁
    37. @Override
    38. public void unlock() {
    39. String threadId = Thread.currentThread().getId()+ID_PREFIX;
    40. String id = stringRedisTemplate.opsForValue().get(KEY_PREFIX + name);
    41. if (threadId.equals(id)){
    42. //删除key
    43. stringRedisTemplate.delete(KEY_PREFIX+name);
    44. }
    45. }
    46. }

     2 Lua脚本的使用

     上述代码仍然会在极端的情况下仍然会出现误删锁的问题。

    试想一下这种情况:

    在线程判断完线程标识时,发现是自己的id,就在准备释放锁的时候,发生了阻塞。然后就是锁超时时间到了,别的线程有获取到了锁,又出现了误删的问题。

    那么,又该如何解决这个问题呢?

    我们不妨这样想想:

    就是我们把判断id和删除id整合成一个代码,让他一次执行,不用分成两次,这样不就好了?

    这里就要使用我们Redis中的脚本:Lua 。

    Redis提供了Lua脚本功能,在一个脚本中编写多条Redis命令,确保多条命令执行时的原子性。Lua是一种编程语言,它的基本语法大家可以参考网站:Lua 教程 | 菜鸟教程

    这里重点介绍Redis提供的调用函数,语法如下:

    例如,我们要执行set name jack,则脚本是这样:

    1. -- 执行reids命令
    2. redis.call('set','name','jack')

    使用redis来运行脚本

    例如,我们要使用Redis调用脚本来执行set name jack,则脚本是这样:

    1. EVAL "return redis.call('set', 'name', 'jack')" 0

    注:后面的0是参数个数。

    如果脚本中的keyvalue不想写死,可以作为参数传递key类型参数会放入KEYS数组,其它参数会放入ARGV数组,在脚本中可以从KEYSARGV数组获取这些参数:

    1. EVAL "return redis.call('set',KEYS[1],ARGV[1])" 1 name Rose

    注:我们实际的业务就是需要这种,参数不能写死。

    接下来,就是用Lua脚本编写代码逻辑

    代码 :

    1. -- 锁的key
    2. -- local key = KEYS[1]
    3. -- 线程标识
    4. -- local threadId = ARGV[1];
    5. --获取锁中的线程标识,get key
    6. -- local id = redis.call('get',KEYS[1]);
    7. --具体脚本
    8. if(redis.call('get',KEYS[1]) == ARGV[1]) then
    9. --释放锁 del key
    10. return redis.call('del',KEYS[1])
    11. end
    12. return 0

    然后在IDEA中编写此代码,并使用Redis调用。(注:编写Lua代码需下载EmmyLua插件)

    具体实现代码:(主要看释放锁代码)

    1. package com.hmdp.utils;
    2. import cn.hutool.core.collection.CollectionUtil;
    3. import cn.hutool.core.lang.UUID;
    4. import org.springframework.core.io.ClassPathResource;
    5. import org.springframework.data.redis.core.StringRedisTemplate;
    6. import org.springframework.data.redis.core.script.DefaultRedisScript;
    7. import org.springframework.util.CollectionUtils;
    8. import java.util.Collections;
    9. import java.util.concurrent.TimeUnit;
    10. /**
    11. * @Author 华子
    12. * @Date 2022/12/3 20:53
    13. * @Version 1.0
    14. */
    15. public class SimpleRedisLock implements ILock {
    16. //Redis
    17. private StringRedisTemplate stringRedisTemplate;
    18. //业务名称,也就是锁的名称
    19. private String name;
    20. public SimpleRedisLock(StringRedisTemplate stringRedisTemplate, String name) {
    21. this.stringRedisTemplate = stringRedisTemplate;
    22. this.name = name;
    23. }
    24. //key的前缀
    25. private static final String KEY_PREFIX = "lock:";
    26. //线程标识的前缀
    27. private static final String ID_PREFIX = UUID.randomUUID().toString(true);
    28. //获取lua脚本
    29. private static final DefaultRedisScript UNLOCK_SCRIPT;
    30. static {
    31. UNLOCK_SCRIPT = new DefaultRedisScript<>();
    32. UNLOCK_SCRIPT.setLocation(new ClassPathResource("unlock.lua"));
    33. UNLOCK_SCRIPT.setResultType(Long.class);
    34. }
    35. @Override
    36. public boolean tryLock(long timeoutSec) {
    37. //获取线程id,当作set的value
    38. String threadId = Thread.currentThread().getId()+ID_PREFIX;
    39. Boolean success = stringRedisTemplate.opsForValue()
    40. .setIfAbsent(KEY_PREFIX + name, threadId, timeoutSec, TimeUnit.SECONDS);
    41. return Boolean.TRUE.equals(success);
    42. }
    43. @Override
    44. public void unlock() {
    45. //调用Lua脚本
    46. stringRedisTemplate.execute(
    47. UNLOCK_SCRIPT,
    48. Collections.singletonList(KEY_PREFIX + name),
    49. Thread.currentThread().getId()+ID_PREFIX
    50. );
    51. }
    52. }

     主要还是使用了StringRedisTemplate中的excute方法,这里传一个脚本参数,一个key参数,一个value参数

    1. 脚本参数:使用DefaultRedisScript 进行获取脚本,需要传入脚本文件的路径,调用new ClassPathResource("unlock.lua") 当做脚本文件路径

    2. key参数,就是我们的KEYS[1],是一个集合,我们把我们使用的锁的key转成一个集合当成key参数

    3. value,就是我们的线程标识。

    这样,我们就使用的Lua脚本将原来两步需要实现的释放锁,合成一行代码实现,就不会出现问题啦~ 

  • 相关阅读:
    js算法之旅:二叉搜索树实现
    安卓手机部署大模型实战
    超声波检测(AE)
    python中的async和await
    区块链技术的飞跃: 2023年的数字革命
    目标检测学习--yolo v4
    记一次简单的网络通信遇到的问题点总结
    物联网设备上云难?华为云IoT帮你一键完成模型定义,快速在线调试设备
    JS中的元素样式
    防火墙基础
  • 原文地址:https://blog.csdn.net/qq_59212867/article/details/128209569