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


    序言

    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
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    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
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33

    方案二: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();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68

    方案三: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();
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

    总结

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

    最后

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

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

    • 文中如有错误,欢迎在评论区指正,如果这篇文章帮到了你,欢迎点赞和关注😊
    • 本文首发于个人博客,未经许可禁止转载💌
  • 相关阅读:
    1385:团伙(group)
    【QT】QFileInfo文件信息读取
    测试用例的设计方法(全):正交实验设计方法|功能图分析方法|场景设计方发
    如何确保亚马逊、速卖通等平台测评补单的环境稳定性和安全性?
    SLAM从入门到精通(从仿真到实践)
    关系型数据库的设计思想,20张图给你看的明明白白
    使用 yum 安装 mysql 目录结构
    解密prompt系列5. APE+SELF=自动化指令集构建代码实现
    Flutter 中的照片管理器(photo_manager):简介与使用指南
    万字总结:分布式系统的38个知识点
  • 原文地址:https://blog.csdn.net/qq_40258748/article/details/134220228