Redis分布式锁(踩坑)

之前接手个项目,记录一下踩坑经历,共勉。(急的同学可以直接拿底部代码)

先看一下前辈的原始代码:方便理解,我这里简化了下逻辑:

public String redisLock() {
        String lockKey = "ke";
        String clintId = UUID.randomUUID().toString();
        try {
            // 加锁,返回true, 加锁成功, false, 加锁失败, redis原生setNx的操作
            Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(lockKey, "lock", 10, TimeUnit.SECONDS);
            if (!flag) {
                return "锁占用中";
            }
            Integer stock = Integer.valueOf(stringRedisTemplate.opsForValue().get("stock"));
            // 若程序执行时间超过lock的过期时间,那么就会造成锁自己删除,业务代码就不再是原子执行了
            // try {
            // Thread.sleep(11000);
            // } catch (InterruptedException e) {
            // e.printStackTrace();
            // }
            // 减库存
            if (stock > 0) {
                stock = stock - 1;
                stringRedisTemplate.opsForValue().set("stock", stock.toString());
                log.info("库存扣减成功,剩余库存:{}", stock);
            } else {
                log.info("库存不足!!!");
            }
        } catch (Exception e) {
            log.info("处理异常", e);
        } finally {
            // 释放锁
            stringRedisTemplate.delete(lockKey);
        }
        return "end";
    }

代码很简单,却隐藏了两个问题

1、锁是固定,也就是往redis存的值是死的,这会存在持有线程的锁被其他线程误删,导致新线程重新获取锁成功。

2、在网络极端条件下,如果业务代码执行的时间超过了10秒,也就是锁超时时间,那么锁会被redis清掉,导致新线程重新获取锁成功(这边可以强制让当前线程睡一段时间,模拟极端条件的情况)。

下面是我改动之后的代码:

    // 过期时间
    public static final Integer EXPIRETIME = 10;

    // 守护线程,类似与看门狗,续命过期的redis的键
    @Autowired
    private DaemonThread daemonThread;

    private static Thread thread;

    @RequestMapping("lock")
    public String redisLock() {
        String lockKey = "ke";
        String clintId = UUID.randomUUID().toString();
        try {
            Boolean flag =
                stringRedisTemplate.opsForValue().setIfAbsent(lockKey, clintId, EXPIRETIME, TimeUnit.SECONDS);
            // 开启守护线程去判断锁是否被持有,持有就增加过期时间,一般判断周期可以设置为过期时间的一半
            if (thread != null) {
                boolean isAlive = thread.isAlive();
                if (!isAlive) {
                    thread = new Thread(daemonThread);
                    thread.setDaemon(true);
                    thread.start();
                }
            } else {
                thread = new Thread(daemonThread);
                thread.setDaemon(true);
                thread.start();
            }
            if (!flag) {
                return "锁占用中";
            }
            Integer stock = Integer.valueOf(stringRedisTemplate.opsForValue().get("stock"));
            // 若程序执行时间超过lock的过期时间,那么就会造成锁自己删除,业务代码就不再是原子执行了
            // try {
            // Thread.sleep(11000);
            // } catch (InterruptedException e) {
            // e.printStackTrace();
            // }
            if (stock > 0) {
                stock = stock - 1;
                stringRedisTemplate.opsForValue().set("stock", stock.toString());
                log.info("库存扣减成功,剩余库存:{}", stock);
            } else {
                log.info("库存不足!!!");
            }
        } catch (Exception e) {
            log.info("处理异常", e);
        } finally {
            // 避免锁被持有锁的线程误删
            if (clintId.equalsIgnoreCase(stringRedisTemplate.opsForValue().get(lockKey))) {
                stringRedisTemplate.delete(lockKey);
            }
        }
        return "end";
    }

解决的思路是这样的:

1、设置每个线程获取的锁都是唯一的(至于唯一算法,大家可以自行发挥,这里只做演示),就是这边的clientId。删除锁的时候判断删除的锁,就是之前设置的锁。

2、开启一个守护线程去判断锁的超时时间,如果锁存活并且超时时间不多了,就重新设置锁的超时时间,保证锁不被超时过期了。

总结,这段逻辑其实有现成的开源实现,Redisson,编码简单暴力,其实原理就是第二部分,有兴趣的同学可以翻看一下源码:

    @Autowired
    private RedissonClient redissonClient;

    @RequestMapping("lock")
    public String redisLock() {
        String lockKey = "ke";
        String clintId = UUID.randomUUID().toString();
        RLock lock = redissonClient.getLock(lockKey);
        try {
            lock.tryLock(10, TimeUnit.SECONDS);
            Integer stock = Integer.valueOf(stringRedisTemplate.opsForValue().get("stock"));
            if (stock > 0) {
                stock = stock - 1;
                stringRedisTemplate.opsForValue().set("stock", stock.toString());
                log.info("库存扣减成功,剩余库存:{}", stock);
            } else {
                log.info("库存不足!!!");
            }
        } catch (Exception e) {
            log.info("处理异常", e);
        } finally {
            lock.unlock();
        }
        return "end";
    }

 

你可能感兴趣的:(Redis分布式锁(踩坑))