面试连环问之Redis分布式锁

面试连环问之Redis分布式锁

核心

  • Redis分布式锁的小总结:
  • 1、只使用Redis计数,线程不安全,有超卖的问题,解决方案:加ReenterLock或者synchronized解决
  • 2、加ReenterLock或者synchronized是虚拟机层面的,解决不了分布式部署,解决方案:Redis setnx命令
  • 3、如果占用setnx的线程异常一直不释放锁,导致后续用户无法执行逻辑,解决方案:setnx命令超时时间,并且在finally中释放setnx锁
  • 4、setnx 命令加锁和设置过期时间不是原子操作,导致执行不成功,解决方案:setnx命令和加锁时间设置为原子操作
  • 5、释放setnx锁时,错误的删除了别人的锁,解决方案:释放锁之前判断是否是本线程持有
  • 6、释放锁之前判断是否是本线程持有 的过程不是原子操作的,解决方案:使用lua脚本,保证原子性
  • 7、确保Redis锁的超时时间大于逻辑执行时间,否则仍然执行不了操作,解决方案:加守护进程
  • 8、终极解决方案:使用redisson分布式锁,注意解锁之前是否本线程所持有

主要介绍

  • 以抽奖为例,Redis锁住奖品库存,是如何一步步升级到可用,回答面试官的Redis锁连环问。

主要代码

1、新建RedisLock

@Component
@Slf4j
public class RedisLock {

    private final String script = "if redis.call(\"get\",KEYS[1]) == ARGV[1]\n" + "then\n" + "    return redis.call(\"del\",KEYS[1])\n" + "else\n" + "    return 0\n" + "end";

    @Autowired
    private RedisTemplate redisTemplate;

    public boolean tryLock(String lockKey, String lockUuid, Long expireTime, Long tryTime, TimeUnit timeUnit) {
        long endTime = System.currentTimeMillis() + timeUnit.toMillis(tryTime);
        while (System.currentTimeMillis() < endTime) {
            try {
                RedisCallback<Boolean> callback = (connection) ->
                        connection.set(lockKey.getBytes(StandardCharsets.UTF_8), lockUuid.getBytes(StandardCharsets.UTF_8),
                                Expiration.seconds(timeUnit.toSeconds(endTime)), RedisStringCommands.SetOption.SET_IF_ABSENT);
                return (boolean) redisTemplate.execute(callback);
            } catch (Exception e) {
                log.error("异常信息", e);
            }
        }
        return false;
    }

    public boolean unlock(String lockKey, String lockUuid) {
        try {
            RedisCallback<String> callback = (connection) -> connection.eval(script.getBytes(StandardCharsets.UTF_8),
                    ReturnType.BOOLEAN, 1, lockKey.getBytes(StandardCharsets.UTF_8), lockUuid.getBytes(StandardCharsets.UTF_8));
            return (boolean) redisTemplate.execute(callback);
        } catch (Exception e) {
            log.error("异常信息", e);
        }
        return false;
    }
}

2、使用Redis锁遇到的问题

@Component
@Slf4j
public class DemoRedisLock {

    @Autowired
    private RedisUtils redisUtils;

    @Autowired
    private Redisson redisson;


    @Autowired
    private RedisLock redisLock;

    public final String LUCK_REDIS_LOCK = "LUCK:REDIS:LOCK:";

    public final String LUCK_REDIS_NX_LOCK = "LUCK:REDIS:NX:LOCK:";

    /**
     * 终极解决方案1,redisson
     *
     * @param luckId
     * @return
     */
    public String getAwardF1(String luckId) throws InterruptedException {
        RLock rLock = redisson.getLock(LUCK_REDIS_NX_LOCK);
        try {
            if (rLock.tryLock(3L, 6L, TimeUnit.SECONDS)) {
                int number = (int) this.redisUtils.getString(LUCK_REDIS_LOCK + luckId);
                if (number > 0) {
                    int leftNumber = number - 1;
                    this.redisUtils.incr(LUCK_REDIS_LOCK, -1);
                    log.info("抽奖奖品还剩余:{}", leftNumber);
                    return "抽奖成功";
                }
            } else {
                throw new RuntimeException("抽奖太火爆了,请稍后再试");
            }
        } finally {
            if (rLock.isLocked() && rLock.isHeldByCurrentThread()) {
                rLock.unlock();
            }
        }
        throw new RuntimeException("抽奖已经结束");
    }

    /**
     * 终极解决方案2
     *
     * @param luckId
     * @return
     * @throws InterruptedException
     */
    public String getAwardF2(String luckId) {
        String uuid = UUID.randomUUID().toString();
        ;
        try {
            if (redisLock.tryLock(LUCK_REDIS_LOCK, uuid, 3L, 5L, TimeUnit.SECONDS)) {
                int number = (int) this.redisUtils.getString(LUCK_REDIS_LOCK + luckId);
                if (number > 0) {
                    int leftNumber = number - 1;
                    this.redisUtils.incr(LUCK_REDIS_LOCK, -1);
                    log.info("抽奖奖品还剩余:{}", leftNumber);
                    return "抽奖成功";
                }
            } else {
                throw new RuntimeException("抽奖太火爆了,请稍后再试");
            }
        } finally {
            redisLock.unlock(LUCK_REDIS_LOCK, uuid);
        }
        throw new RuntimeException("抽奖已经结束");
    }

    /**
     * 抽奖1
     * 带来的问题:
     * 1、线程不安全,有超卖的问题,
     * 如何解决:
     * 2、加ReenterLock或者synchronized解决?
     *
     * @return
     */
    public String getAward1(String luckId) {
        int number = (int) this.redisUtils.getString(LUCK_REDIS_LOCK + luckId);
        if (number > 0) {
            int leftNumber = number - 1;
            this.redisUtils.incr(LUCK_REDIS_LOCK, -1);
            log.info("抽奖奖品还剩余:{}", leftNumber);
            return "抽奖成功";
        } else {
            throw new RuntimeException("抽奖太火爆了,请稍后再试");
        }
    }

    /**
     * 抽奖2
     * 带来的问题:
     * 1、加ReenterLock或者synchronized是虚拟机层面的,解决不了分布式部署
     * 如何解决:
     * 2、使用Redis setnx(如果key存在则设置不成功,如果key不存在,才可以设置成功)解决?
     * https://redis.io/commands/set/
     *
     * @return
     */
    public String getAward2(String luckId) {
        synchronized (this) {
            int number = (int) this.redisUtils.getString(LUCK_REDIS_LOCK + luckId);
            if (number > 0) {
                int leftNumber = number - 1;
                this.redisUtils.incr(LUCK_REDIS_LOCK, -1);
                log.info("抽奖奖品还剩余:{}", leftNumber);
                return "抽奖成功";
            } else {
                throw new RuntimeException("抽奖太火爆了,请稍后再试");
            }
        }
    }

    /**
     * 抽奖3
     * 带来的问题:
     * 1、如果占用setnx的线程异常一直不释放锁,导致后续用户无法抽奖,
     * 如何解决:
     * 2、新增setnx命令超时时间,并且在finally中释放setnx锁
     *
     * @return
     */
    public String getAward3(String luckId) {
        String uuid = UUID.randomUUID().toString();
        boolean success = this.redisUtils.setStringNx(LUCK_REDIS_NX_LOCK + luckId, uuid, 3L, TimeUnit.SECONDS);
        if (!success) {
            throw new RuntimeException("抽奖已经结束");
        }
        int number = (int) this.redisUtils.getString(LUCK_REDIS_LOCK + luckId);
        if (number > 0) {
            int leftNumber = number - 1;
            this.redisUtils.incr(LUCK_REDIS_LOCK, -1);
            log.info("抽奖奖品还剩余:{}", leftNumber);
            return "抽奖成功";
        } else {
            throw new RuntimeException("抽奖太火爆了,请稍后再试");
        }
    }

    /**
     * 抽奖4
     * 带来的问题:
     * 1、setnx 命令加锁和设置过期时间不是原子操作,导致执行不成功
     * 如何解决:
     * 2、setnx命令和加锁时间设置为原子操作
     *
     * @return
     */
    public String getAward4(String luckId) {
        String uuid = UUID.randomUUID().toString();
        try {
            boolean success = this.redisUtils.setStringNx(LUCK_REDIS_NX_LOCK + luckId, uuid);
            this.redisUtils.expire(LUCK_REDIS_NX_LOCK + luckId, 3L);
            if (!success) {
                throw new RuntimeException("抽奖已经结束");
            }
            int number = (int) this.redisUtils.getString(LUCK_REDIS_LOCK + luckId);
            if (number > 0) {
                int leftNumber = number - 1;
                this.redisUtils.incr(LUCK_REDIS_LOCK, -1);
                log.info("抽奖奖品还剩余:{}", leftNumber);
                return "抽奖成功";
            } else {
                throw new RuntimeException("抽奖太火爆了,请稍后再试");
            }
        } finally {
            redisUtils.delKey(LUCK_REDIS_NX_LOCK);
        }
    }

    /**
     * 抽奖5
     * 带来的问题:
     * 1、释放setnx锁时,错误的删除了别人的锁
     * 如何解决:
     * 2、释放锁之前判断是否是本线程持有
     *
     * @return
     */
    public String getAward5(String luckId) {
        String uuid = UUID.randomUUID().toString();
        try {
            boolean success = this.redisUtils.setStringNx(LUCK_REDIS_NX_LOCK + luckId, uuid, 3L, TimeUnit.SECONDS);
            if (!success) {
                throw new RuntimeException("抽奖已经结束");
            }
            int number = (int) this.redisUtils.getString(LUCK_REDIS_LOCK + luckId);
            if (number > 0) {
                int leftNumber = number - 1;
                this.redisUtils.incr(LUCK_REDIS_LOCK, -1);
                log.info("抽奖奖品还剩余:{}", leftNumber);
                return "抽奖成功";
            } else {
                throw new RuntimeException("抽奖太火爆了,请稍后再试");
            }
        } finally {
            //删除其他线程的锁
            redisUtils.delKey(LUCK_REDIS_NX_LOCK);
        }
    }

    /**
     * 抽奖6
     * 带来的问题:
     * 1、释放锁之前判断是否是本线程持有 的过程不是原子操作的
     * 如何解决:
     * 2、使用Lua脚本,保证原子性
     *
     * @return
     */
    public String getAward6(String luckId) {
        String uuid = UUID.randomUUID().toString();
        try {
            boolean success = this.redisUtils.setStringNx(LUCK_REDIS_NX_LOCK + luckId, uuid, 3L, TimeUnit.SECONDS);
            if (!success) {
                throw new RuntimeException("抽奖已经结束");
            }
            int number = (int) this.redisUtils.getString(LUCK_REDIS_LOCK + luckId);
            if (number > 0) {
                int leftNumber = number - 1;
                this.redisUtils.incr(LUCK_REDIS_LOCK, -1);
                log.info("抽奖奖品还剩余:{}", leftNumber);
                return "抽奖成功";
            } else {
                throw new RuntimeException("抽奖太火爆了,请稍后再试");
            }
        } finally {
            Object value = this.redisUtils.getString(LUCK_REDIS_NX_LOCK);
            if (StringUtils.equals(uuid, value.toString())) {
                redisUtils.delKey(LUCK_REDIS_NX_LOCK);
            }
        }
    }

    /**
     * 抽奖7
     * 带来的问题:
     * 1、确保Redis锁的超时时间大于抽奖执行时间(执行减库存逻辑超过过期时间,这个时候还没有减库存,但是setnx锁已经释放,其他线程已经进入,仍然可以减库存)
     * 如何解决:
     * 2、守护线程
     *
     * @return
     */
    public String getAward7(String luckId) {
        String uuid = UUID.randomUUID().toString();
        try {
            boolean success = this.redisUtils.setStringNx(LUCK_REDIS_NX_LOCK + luckId, uuid, 3L, TimeUnit.SECONDS);
            if (!success) {
                throw new RuntimeException("抽奖已经结束");
            }
            int number = (int) this.redisUtils.getString(LUCK_REDIS_LOCK + luckId);
            if (number > 0) {
                int leftNumber = number - 1;
                //.... 这里会出现执行其他逻辑(假如是调用第三方系统,时间超过了Redis setnx的过期时间,这个时候还没有减库存,但是setnx锁已经释放,其他线程已经进入)
                this.redisUtils.incr(LUCK_REDIS_LOCK, -1);
                log.info("抽奖奖品还剩余:{}", leftNumber);
                return "抽奖成功";
            } else {
                throw new RuntimeException("抽奖太火爆了,请稍后再试");
            }
        } finally {
            String script = "if redis.call(\"get\",KEYS[1]) == ARGV[1]\n" + "then\n" + "    return redis.call(\"del\",KEYS[1])\n" + "else\n" + "    return 0\n" + "end";
            Jedis jedis = this.redisUtils.getJedis();
            String value = (String) redisUtils.getString(LUCK_REDIS_NX_LOCK);
            Object eval = jedis.eval(script, Collections.singletonList(LUCK_REDIS_NX_LOCK), Collections.singletonList(value));
            if (StringUtils.equals(eval.toString(), "1")) {
                redisUtils.delKey(LUCK_REDIS_NX_LOCK);
            }
        }
    }

}
  • 3
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值