【Java】三种方案实现 Redis 分布式锁

news/2024/7/7 22:40:34

序言

setnx、Redisson、RedLock 都可以实现分布式锁,从易到难得排序为:setnx < Redisson < RedLock。一般情况下,直接使用 Redisson 就可以啦,有很多逻辑框架的作者都已经考虑到了。

方案一:setnx

1.1、简单实现

下面的锁实现可以用在测试或者简单场景,但是它存在以下问题,使其不适合用在正式环境。

  1. 锁可能被误删: 在解锁操作中,如果一个线程的锁已经因为超时而被自动释放,然后又被其他线程获取到,这时原线程再来解锁就会误删其他线程的锁。
  2. **临界区代码不安全:**线程 A 还没有执行完临界区代码,锁就过期释放掉了。线程 B 此时又能获取到锁,进入临界区代码,导致了临界区代码非串行执行,带来了线程不安全的问题。
public class RedisLock {

    @Autowired
    private StringRedisTemplate redisTemplate;

    /**
     * 加锁
     */
    private boolean tryLock(String key) {
        Boolean flag = redisTemplate.opsForValue().setIfAbsent(key, "1", 10, TimeUnit.SECONDS);
        return BooleanUtil.isTrue(flag);
    }

    /**
     * 解锁
     */
    private void unlock(String key) {
        redisTemplate.delete(key);
    }
}

1.2、使用 lua 脚本加锁、解锁

lua 脚本是原子的,不管写多少 lua 脚本代码,redis 都是通过一条命令去执行的。

下述代码使用了 lua 脚本进行加锁/解锁,保证了加锁和解锁的时候都是原子性的,是一种相对较好的 Redis 分布式锁的实现方式。

它支持获得锁的线程才能释放锁,如果线程 1 因为锁过期而丢掉了锁,然后线程 2 拿到了锁。此时线程 1 的业务代码执行完以后,也无法释放掉线程 2 的锁,解决了误删除的问题。

public class RedisLock {

    private final StringRedisTemplate redisTemplate;

    public RedisDistributedLock(StringRedisTemplate redisTemplate) {
        this.redisTemplate = redisTemplate;
    }

    public boolean tryLock(String lockKey, String lockValue, long expireTimeInSeconds) {
        try {
            //加锁成功返回 true,加锁失败返回 fasle。效果等同于 redisTemplate.opsForValue().setIfAbsent
            String luaScript = "if redis.call('set', KEYS[1], ARGV[1], 'NX', 'EX', ARGV[2]) then return 1 else return 0 end";
            RedisScript<Long> redisScript = new DefaultRedisScript<>(luaScript, Long.class);
            Long result = redisTemplate.execute(redisScript, Collections.singletonList(lockKey), lockValue, String.valueOf(expireTimeInSeconds));

            return result != null && result == 1;
        } catch (Exception e) {
            // Handle exceptions
            return false;
        }
    }

    public void unlock(String lockKey, String lockValue) {
        try {
            //拿到锁的线程才可以释放锁,lockValue 可以设置为 uuid。
            String luaScript = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
            RedisScript<Long> redisScript = new DefaultRedisScript<>(luaScript, Long.class);
            redisTemplate.execute(redisScript, Collections.singletonList(lockKey), lockValue);
        } catch (Exception e) {
            // Handle exceptions
        }
    }
}

方案二:Redisson

Redisson 是一个基于 Java 的客服端,通过 Redisson 我们可以快速安全的实现分布式锁。Redisson 框架具有可重入锁的支持、分布式锁的实现、锁的自动续期、红锁支持等多种特点,给我们开发过程中带来了极大的便利。

@Component
public class RedisLock {

    @Resource
    private RedissonClient redissonClient;

    /**
     * lock(), 拿不到lock就不罢休,不然线程就一直block
     */
    public RLock lock(String lockKey) {
        RLock lock = redissonClient.getLock(lockKey);
        lock.lock();
        return lock;
    }

    /**
     * leaseTime为加锁时间,单位为秒
     */
    public RLock lock(String lockKey, long leaseTime) {
        RLock lock = redissonClient.getLock(lockKey);
        lock.lock(leaseTime, TimeUnit.SECONDS);
        return null;
    }

    /**
     * timeout为加锁时间,时间单位由unit确定
     */
    public RLock lock(String lockKey, TimeUnit unit, long timeout) {
        RLock lock = redissonClient.getLock(lockKey);
        lock.lock(timeout, unit);
        return lock;
    }

    /**
     * @param lockKey   锁 key
     * @param unit      单位
     * @param waitTime  等待时间
     * @param leaseTime 锁有效时间
     * @return 加锁成功? true:成功 false: 失败
     */
    public boolean tryLock(String lockKey, TimeUnit unit, long waitTime, long leaseTime) {

        RLock lock = redissonClient.getLock(lockKey);
        try {
            return lock.tryLock(waitTime, leaseTime, unit);
        } catch (InterruptedException e) {
            return false;
        }
    }

    /**
     * unlock
     */
    public void unlock(String lockKey) {
        RLock lock = redissonClient.getLock(lockKey);
        if (lock.isLocked() && lock.isHeldByCurrentThread()) {
            lock.unlock();
        }
    }

    /**
     * unlock
     * @param lock 锁
     */
    public void unlock(RLock lock) {
        lock.unlock();
    }
}

方案三:RedLock

RedLock 又叫做红锁,是 Redis 官方提出的一种分布式锁的算法,红锁的提出是为了解决集群部署中 Redis 锁相关的问题。

比如当线程 A 请求锁成功了,这时候从节点还没有复制锁。此时主节点挂掉了,从节点成为了主节点。线程 B 请求加锁,在原来的从节点(现在是主节点)上加锁成功。这时候就会出现线程安全问题。

下图是红锁的简易思路。红锁认为 (N / 2) + 1 个节点加锁成功后,那么就认为获取到了锁,通过这种算法减少线程安全问题。简单流程为:

  1. 顺序向五个节点请求加锁
  2. 根据一定的超时时间判断是否跳过该节点
  3. (N / 2) + 1 个节点加锁成功并且小于锁的有效期
  4. 认定加锁成功

image-20231104162822749

@Service
public class MyService {

    private final RedissonClient redissonClient;

    @Autowired
    public MyService(RedissonClient redissonClient) {
        this.redissonClient = redissonClient;
    }

    public void doSomething() {
        RLock lock1 = redissonClient.getLock("lock1");
        RLock lock2 = redissonClient.getLock("lock2");
        RLock lock3 = redissonClient.getLock("lock3");

        RedissonRedLock redLock = new RedissonRedLock(lock1, lock2, lock3);
        redLock.lock();
        try {
            // 业务逻辑
        } finally {
            redLock.unlock();
        }
    }
}

总结

  • 自己玩或者测试的时候使用方案一的简单实现。
  • 单机版 Redis 使用方案二。
  • Redis 集群使用方案三。

最后

我是 xiucai,一位后端开发工程师。

如果你对我感兴趣,请移步我的个人博客,进一步了解。

  • 文中如有错误,欢迎在评论区指正,如果这篇文章帮到了你,欢迎点赞和关注😊
  • 本文首发于个人博客,未经许可禁止转载💌

http://lihuaxi.xjx100.cn/news/1752999.html

相关文章

[自定义 Vue 组件] 小尾巴顶部导航栏(2.0) TailTopNav

文章归档&#xff1a;https://www.yuque.com/u27599042/coding_star/oglrqteg8fzvvzn0 [自定义 Vue 组件] 响应式顶部导航栏(1.0) TopNav&#xff1a;https://www.yuque.com/u27599042/coding_star/hzltsltxgavwx8u2 组件效果示例 组件所依赖的子组件 [自定义 Vue 组件] 小尾巴…

降级python

起因&#xff1a; python版本过高不能下载一个包&#xff0c;需要降级 首先使用 python --version 查看python版本 然后conda install python3.10 python3.10会下载到这个目录下&#xff08;这个千万别找错&#xff09; 然后更换路径 alias python/home/zky/.conda/envs/c…

01 向量基本概念

向量基本概念 向量是什么物理专业学生视角计算机专业学生视角数学家视角 不同视角之间的关系 这是关于3Blue1Brown "线性代数的本质"的学习笔记。 向量是什么 物理专业学生视角 向量是空间中的箭头。向量的长度和方向确定一个向量。只要长度和方向相同&#xff0c…

社群团购,1个人1个月如何卖掉1家实体店1年的销量?

社群团购&#xff0c;1个人1个月如何卖掉1家实体店1年的销量&#xff1f; 有一些老板问我他的产品是否适合做社群团购。 我看了下他的朋友圈&#xff0c;问了产品价格后说&#xff1a;应该还不错&#xff0c;但要做2件事&#xff0c;1个是在百度做下铺垫&#xff0c;1个是把笔…

asp.net docker-compose添加volume配置

打开docker-compose.override.yml下面添加 volumes:killsb-one-sqldata:external: false 服务下面添加volume配置 volumes:- "./dapr/config/social-client.json:/app/OidcSettings.json" 添加volume配置成功

【马蹄集】—— 百度之星 2023

百度之星 2023 目录 BD202301 公园⭐BD202302 蛋糕划分⭐⭐⭐BD202303 第五维度⭐⭐ BD202301 公园⭐ 难度&#xff1a;钻石    时间限制&#xff1a;1秒    占用内存&#xff1a;64M 题目描述 今天是六一节&#xff0c;小度去公园玩&#xff0c;公园一共 N N N 个景点&am…

怎么测量直线模组的精度?

直线模组是一款比较多变的一种产品&#xff0c;适合多种行业&#xff0c;同时它也适合比较多的环境&#xff0c;是一种比较全面的直线运动的装置。直线模组是非常精密的自动化机械设备&#xff0c;在实践中&#xff0c;应该要严格把握具体精度&#xff0c;并且在必要的时候&…

07.Diffusion Model概述

文章目录 Diffusion Model原理Reverse ProcessDenoise模块Forward Process(Diffusion Process) 文字生成图片by Diffusion Model文字生成图像的常见套路Text EncoderFrchet Inception DistanceContrastive Language-Image Pre-Training(CLIP) DecoderGeneration Model 部分截图…