基于Redis中SETNXEX组合命令的分布式锁的请求限流实践

本文介绍了一种使用Redis实现的分布式锁机制,通过设置键值和过期时间来控制资源访问,支持阻塞式和非阻塞式的锁获取,并讨论了其应用场景及潜在问题。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

rpc-tech-stack 系列的实践文章 [1],平日看了很多技术文章,看完之后给人一种我看完了我就会了的错觉~ 但其实什么都不会。俗话说“好记性不如烂笔头”,那我就把常用的技术点通过 demo 的方式来实现,并增强自己的记忆吧~

实践的效果

场景:请求不到锁超时等待,自旋至超过等待时间

场景:用户请求一个资源,请求成功一次之后,锁过期之前限制其再次消费,实现对资源调用的限流。

参数:

  • 锁过期时间:10s

  • 获取不到锁的等待时间:5s

  • 等待重试时间:200ms

模拟用户第一次请求

因为key 不存在~所以直接成功,没什么好说的。

在这里插入图片描述

模拟用户 5 秒内的第二次获取锁

用户再次请求资源,此时上一次的锁还未过期释放,按照设计思路,锁会进行自旋,每间隔 200ms 再次发起获取锁的请求。

  • 若请求到锁,则进行上锁。

  • 若到达等待时间还未获取锁,则获取失败。

在这里插入图片描述

获取锁的流程图

在这里插入图片描述

  1. 最开始的服务通过组合命令直接获取锁,并设置过期时间,过期时间内其他线程无法获得锁。

  2. 随后的服务通过同样的指令尝试获取锁,获取失败就自旋,超过超时等待时间没拿到就失败~

Redis command

下面命令的语义:如果 key 不存在的话设置 key 与 value 并设置过期时间为 10 秒

set "key" "value" NX EX 10 

拓展~

  • NX :仅当key不存在时,set才会生效

  • XX :仅当key存在时,set才会生效

  • EX :设置键的过期时间为 second 秒

  • PX :设置键的过期时间为 millisecond 毫秒

核心代码块

阻塞式获取锁

/**
     * 阻塞式分布式锁
     *
     * @param key
     * @param timeout 超时时间
     * @param unit    超时单位
     * @return
     */
    public boolean tryLock(String key, long timeout, TimeUnit unit) {
        log.info("[Redis Lock] 尝试获取阻塞式分布式锁 -- start time:{} -- key :{}", System.currentTimeMillis(), key);
        // 获取系统此刻的时间,用毫秒来返回
        final long startMillis = System.currentTimeMillis();
        // 转换参数中的 timeout 为毫秒并返回
        final Long millisToWait = unit.toMillis(timeout);
        // 分布式锁的值
        boolean lockValue;
        // 上锁成功立即返回,上锁失败就直到等待超时还未上锁返回
        int failed_cnt = 0;
        while (true) {
            // 拿到上锁结果 MAX_EXPIRE_TIME = 10s
            lockValue = lock(key, MAX_EXPIRE_TIME, TimeUnit.SECONDS);
            // 若上锁成功,就返回
            if (lockValue) {
                log.info("[Redis Lock] 尝试获取阻塞式分布式锁 | 成功 -- key :{}", key);
                return true;
            }
            // 超时的判断 : 当前时间 - 开始时间 - 重试等待时间 > 超时时间
            Long waitDiffTime = System.currentTimeMillis() - startMillis - retryAwait;
            // 若上锁不成功,判断是否超时,若超时就返回
            if (waitDiffTime > millisToWait) {
                // 超时返回, 上锁失败
                log.warn("[Redis Lock] 尝试获取阻塞式分布式锁{}次,耗时{}ms | 最终超时失败 -- key :{}"
                        , failed_cnt,waitDiffTime, key);
                return false;
            }
            log.info("[Redis Lock] 尝试获取阻塞式分布式锁 | 失败{}次, 尝试超时等待:{}ms 后重新发起获取 -- key :{}",
                    ++failed_cnt, retryAwait, key);
            // 阻塞当前线程, 阻塞时长是重试等待时间
            LockSupport.parkNanos(TimeUnit.MILLISECONDS.toNanos(retryAwait));
        }
    }

非阻塞式获取锁

/**
     * 无阻塞分布式锁
     *
     * @param key
     * @param expireTime 过期时间
     * @param unit 时间单位
     * @return
     */
    public boolean lock(String key, long expireTime, TimeUnit unit) {
        // key 和 value 的序列化方式
        RedisSerializer keySerializer = redisTemplate.getKeySerializer();
        RedisSerializer valueSerializer = redisTemplate.getValueSerializer();

        final String SET = "SET";
        byte[] key_cmd = keySerializer.serialize(key);
        byte[] value_cmd = valueSerializer.serialize("1");
        final byte[] NX = encode("NX");
        final byte[] EX = encode("EX");
        byte[] expireTime_cmd = encode(String.valueOf(unit.toSeconds(expireTime)));

        // 例 : set "key" "1" NX EX 3000
        // 如果 key - "key" 不存在就设置, value 设置成 "1", 若存在就设置失败
        return redisTemplate.execute((RedisCallback<Boolean>) connection ->
                connection.execute(SET, key_cmd, value_cmd, NX, EX, expireTime_cmd) != null);
    }

Mock 的获取资源并加锁

@Override
    public boolean GetResourceAndLockSomeAWhile(String resourceName) {
        if (StringUtils.isBlank(resourceName)){
            return false;
        }
        RedisDistributedLock redisDistributedLock = new RedisDistributedLock(stringRedisTemplate);
        log.info("[{}] 尝试获取资源锁 key:{},",this.getClass().getSimpleName(),resourceName);
        boolean lock_res = redisDistributedLock.tryLock(resourceName,5, TimeUnit.SECONDS);
        // todo 如果 lock_res == true 执行相关获取资源的逻辑
        log.info("[{}] 尝试获取资源锁 key:{} | 结果:{},",this.getClass().getSimpleName(),resourceName,lock_res);
        return lock_res;
    }

存在的问题 (非常重要)

这个方案仅仅适用于对事务的持续时间短,对一致性不敏感的场景。存在的问题很多,我思考后提出了以下几个缺点,可以后续进行优化。

  • 如果过期时间到了,事务还未结束怎么办?那不是提前释放锁了么,这就出现问题了

  • 怎么判断在什么时候手动释放锁,什么时候过期释放锁?

  • 在 Redis 2.6 之前,setnx 与 setex 是两个命令,若因某些原因(宕机)执行完 setnx 时 setex 未执行,则会导致永远不能释放锁,但是在 redis2.6 之后支持命令组合~还可以通过 lua 脚本保证多条命令的原子性

项目地址

Github:https://github.com/teavmac/java-rpc-tech-stack

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值