最近研究了下redis分布式锁,分享下演变过程,怎么一步步实现抗高并发的分布式锁

**

首先看下单线程的代码,未加分布式锁的情况如下:

**

@Override
    public String secKill() {
        String goodsId = "666666";
        String goodsCountStr = redisTemplate.opsForValue().get(goodsId);
        int goodsCount = Integer.parseInt(goodsCountStr) - 1;
        if(goodsCount >= 0){
            redisTemplate.opsForValue().set(goodsId, goodsCount + "");
            System.out.println("秒杀成功,剩余" + goodsCount + "件");
        }else{
            System.out.println("秒杀失败,已被抢光");
        }
        return goodsCount >= 0 ? "恭喜你,抢到商品" : "抱歉,您没抢到";
    }

这段代码,单线程情况下,无任何问题,一旦涉及到多线程,就涉及到先后顺序,得先查询redis获取库存,然后减库存,将剩余库存存入redis,这段操作需要原子执行才行

毫无疑问synchronized 和 ReentrantLock在集群环境下是不可行的,这里不做赘述

演变一:使用setnx执行抢锁过程

代码变成了这样

String goodsId = "666666";
        String lockKey = "lockKey";
        Boolean lock = redisTemplate.opsForValue().setIfAbsent(lockKey, "xxx");
        if(lock){
            String goodsCountStr = redisTemplate.opsForValue().get(goodsId);
            int goodsCount = Integer.parseInt(Objects.requireNonNull(goodsCountStr)) - 1;
            if(goodsCount >= 0){
                redisTemplate.opsForValue().set(goodsId, goodsCount + "");
                System.out.println("秒杀成功,剩余" + goodsCount + "件");
            }else{
                System.out.println("秒杀失败,已被抢光");
            }
            redisTemplate.delete(lockKey);
            return goodsCount >= 0 ? "恭喜你,抢到商品" : "抱歉,您没抢到";
        }else{
            return secKill();
        }

增加了获取锁和删除锁的过程,如果获取到了锁,执行秒杀操作,获取不到锁,继续执行获取锁的操作;
但是这样写出现了个问题,如果来不及执行删除锁的代码,抛异常或者服务宕机了,那么其他线程永远获取不到锁,变成了死锁了,于是下面开始改进:

演变二

将setnx加上过期时间
Boolean lock = redisTemplate.opsForValue().setIfAbsent(lockKey, “xxx”,30, TimeUnit.SECONDS);
这样就算服务宕机了,也能释放锁,不至于造成死锁。
但是又引发出来了新的问题,有2个线程,线程一需要10S执行完成,线程二需要35秒执行完成,线程二30秒代码没执行完,这时候线程一执行完成删除了锁,这时候会有新的线程获取到锁,线程二执行完成后,又删除了别人的锁,于是开始下面的演变

演变三:

@Override
    public String secKill() {
        String goodsId = "555";
        String lockKey = "lockKey";
        String value = UUID.randomUUID().toString();
        Boolean lock = redisTemplate.opsForValue().setIfAbsent(lockKey, value,30, TimeUnit.SECONDS);
        if(lock){
            try{
                String goodsCountStr = redisTemplate.opsForValue().get(goodsId);
                int goodsCount = Integer.parseInt(Objects.requireNonNull(goodsCountStr)) - 1;
                if(goodsCount >= 0){
                    redisTemplate.opsForValue().set(goodsId, goodsCount + "");
                    System.out.println("秒杀成功,剩余" + goodsCount + "件");
                }else{
                    System.out.println("秒杀失败,已被抢光");
                }
                return goodsCount >= 0 ? "恭喜你,抢到商品" : "抱歉,您没抢到";
            }finally {
                String myLockKey = redisTemplate.opsForValue().get(lockKey);
                if (value.equals(myLockKey)){
                    redisTemplate.delete(lockKey);
                }
            }
        }else{
            return secKill();
        }
        

这样还有个问题,执行时间还是个问题,代码执行时间不一致,无法保证所有请求在30秒或者指定时间内执行完成,还是存在删除别人锁的情况,目的就是在代码没有执行完成时不要删除锁,代码执行完成才删除锁,改进方法就是锁续期,专门开启一个线程,每隔10秒,(1/3的时间)检查锁是否还存在,如果存在,就将时间续期为30秒

演变四


Timeout task = commandExecutor.getConnectionManager().newTimeout(new TimerTask() {
            @Override
            public void run(Timeout timeout) throws Exception {
                
                RFuture<Boolean> future = renewExpirationAsync(threadId);
                
                future.addListener(new FutureListener<Boolean>() {
                    @Override
                    public void operationComplete(Future<Boolean> future) throws Exception {
                        expirationRenewalMap.remove(getEntryName());
                        if (!future.isSuccess()) {
                            log.error("Can't update lock " + getName() + " expiration", future.cause());
                            return;
                        }
                        
                        if (future.getNow()) {
                            // reschedule itself
                            scheduleExpirationRenewal(threadId);
                        }
                    }
                });
            }

        }, internalLockLeaseTime / 3, TimeUnit.MILLISECONDS);

        if (expirationRenewalMap.putIfAbsent(getEntryName(), new ExpirationEntry(threadId, task)) != null) {
            task.cancel();
        }

先查询是否是自己的锁,然后再删除锁也要是原子执行

至此,redis分布式锁算比较完整的了,redis官方给我们最好了这一套,可以直接使用redisson

Logo

鸿蒙生态一站式服务平台。

更多推荐