一、分布式锁基本原理和不同实现方式对比

多个JVM共享同一个锁监视器;多进程可见,并且互斥;

redisson分布式锁释放失败排查 redis分布式锁释放锁_redisson分布式锁释放失败排查


分布式锁:满足分布式系统或集群模式下多进程可见并且互斥的锁;


redisson分布式锁释放失败排查 redis分布式锁释放锁_可重入_02

不同的分布式锁实现方案:

redisson分布式锁释放失败排查 redis分布式锁释放锁_redisson分布式锁释放失败排查_03

二、Redis锁的实现思路

获取锁:通过setNx命令
释放锁:DEL key 命令-----手动释放,或者超时释放,给锁添加一个超时时间;(超时时间的设置需要考量,不能太长,避免类似死锁现象的发生)

redisson分布式锁释放失败排查 redis分布式锁释放锁_redisson分布式锁释放失败排查_04

redisson分布式锁释放失败排查 redis分布式锁释放锁_redis_05

/**
     * 通过redis解决分布式环境或者集群模式下的一人多单问题
     * @param userId
     * @param voucherId
     * @return {@link Result}
     */
    @Transactional
    public Result createVoucherOrder(Long userId, Long voucherId) {
        // 5、扣减库存
        // 5.1 实现一人一单功能 查询当前用户id和优惠券id是否存在
        // todo 在高并发的情况下,100个线程同时查,查询到的都是0,所以依然存在同一个用户抢了多个优惠券的情况;
        // 由于是插入操作,无法比较,所以最好加悲观锁
        // 要保证锁的是一个对象,userId.toString()每次会是一个全新的对象,
        // .intern()返回字符串的规范表示,去常量池寻找是否有这个值,确保用户id一样,值就一样
        // todo 锁名
        String name = "order:";
        // 拿到锁对象
        SimpleRedisLock simpleRedisLock = new SimpleRedisLock(name + userId,stringRedisTemplate);
        boolean tryLock = simpleRedisLock.tryLock(1200);
        if (!tryLock){
            return Result.fail("不能重复领取优惠券");
        }
        try {
            int count = this
                        .query()
                        .eq("user_id", userId)
                        .eq("voucher_id", voucherId).count();
            if (count > 0) {
                return Result.fail("你已下过单");
            }
            boolean sucess = iSeckillVoucherService.update()
                    // CAS 法 Compare and Switch**:比较修改。在版本号的基础上,
                    // 既然用version字段前后可以比较得出这条数据是否发生变化,那同样,
                    // 直接用stock库存本身来比较,stock前后是否发生了变化;
                    .setSql("stock = stock -1") // set stock = stock -1
                    .eq("voucher_id", voucherId).gt("stock", 0)
                    // 乐观锁的缺点:**   成功率低,由于多个线程同时对优惠券进行操作,如果有一个线程拿到了锁,
                    // 其他线程可能就会直接取消抢购,没有不断的重试,造成优惠券大量富余,库存大量富余,最后库存没有卖完。
                    // 所以这里这样判断stock > 0即可
                    .update();
            if (!sucess) {
                return Result.fail("库存不足");
            }
            // 6、将数据存入优惠券订单表
            VoucherOrder voucherOrder = new VoucherOrder();
            voucherOrder.setId(redisIdWorker.generateOnlyId("order"));
            voucherOrder.setUserId(userId);
            voucherOrder.setVoucherId(voucherId);
            save(voucherOrder);
            return Result.ok(voucherOrder.getId());
        } finally {
            // 最后一定要释放锁
            simpleRedisLock.releaseLock(name + userId);
        }

    }

三、redis分布式锁误删问题

redisson分布式锁释放失败排查 redis分布式锁释放锁_java_06

极端情况:
1、业务阻塞导致锁提前释放了;
2、其他线程一上来,业务没执行完,线程1这时候执行完了,把线程2的锁给删掉;
3、所以要在释放锁的时候要判断,锁的标志是否一致;别的线程不能删;
4、解决方案:

redisson分布式锁释放失败排查 redis分布式锁释放锁_redis_07

redisson分布式锁释放失败排查 redis分布式锁释放锁_redis_08

四、分布式锁的原子性问题

1、极端情况:就算加了线程判断标志,当要释放锁的时候,其他类似垃圾回收,jvm本身导致线程阻塞;轮到我释放了,但没有释放,触发了锁的超时释放,也会导致其他线程乘虚而入;
2、判断锁标志和释放锁标志是两个动作;需要让他们一起执行

redisson分布式锁释放失败排查 redis分布式锁释放锁_java_09

解决方案:通过lua脚本执行多个redis命令;

redisson分布式锁释放失败排查 redis分布式锁释放锁_java_10


redisson分布式锁释放失败排查 redis分布式锁释放锁_redisson分布式锁释放失败排查_11

– 锁的key
local key = KEYS[1]
– 当前线程标识
local threadId = ARGV[1]
– 获取锁的线程标志
local id = redis.call(‘get’,KEYS[1])
– 比较线程标志与锁中的是否一致
if(redis.call(‘get’,KEYS[1]) == ARGV[1]) then
– 释放锁 del key
return redis.call(‘del’,key)
end
return 0

/**
     * 使用lua脚本释放锁(获取锁标志和删除锁是同步的)
     * UNLOCK_LUA
     * Collections.singletonList(RedisConstants.LOCK + name) 锁的key集合
     * LOCK_THREAD + Thread.currentThread().getId() 线程标识
     * @param 
     */
    @Override
    public void releaseLock() {
       // lua脚本
        stringRedisTemplate.execute(UNLOCK_LUA,
                Collections.singletonList(RedisConstants.LOCK + name),
                LOCK_THREAD + Thread.currentThread().getId());
    }

    /**
     * 获取线程与删除锁没有同步执行,极端情况下,比如因为JVM本身的垃圾回收,导致线程超时释放锁,其他线程会乘虚而入
     *
     * @param lock
     */
    /*
    @Override
    public void releaseLock() {
        // 获取线程标志
        String threadId = LOCK_THREAD + Thread.currentThread().getId();
        // 当前线程
        String id = stringRedisTemplate.opsForValue().get(RedisConstants.LOCK + name);
        // 判断是不是我的锁
        if (Objects.equals(threadId, id)) {
            stringRedisTemplate.delete(RedisConstants.LOCK + name);
        }
    }*/

五、Redisson分布式锁的功能介绍

基于setnx实习的分布式有下面的问题
可重入是指同一个线程可以多次获取同一把锁;比如方法A调B,A要获取锁,b也要获取同一个锁,如果是不可重入的,B就无法获取锁;要等A释放才行,造成死锁的情况;
1、不可重入:同一个线程无法多次获取同一把锁;
2、不可重试:获取锁只尝试一次就返回false,没有重试机制;
3、超时释放
4、主从一致性问题:如果redis提供了主从集群,主从同步(读写分离)存在延迟,当主宕机时,如果从没有同步主中的锁数据,会让其他线程乘虚而入;

解决方法:Redisson分布式锁

redisson分布式锁释放失败排查 redis分布式锁释放锁_分布式锁_12

六、Redisson可重入锁的原理

redisson分布式锁释放失败排查 redis分布式锁释放锁_redis_13

类似于ReetrantLock:利用Hash结构,记录线程标识,和获取锁的次数,引入了一个计数器
方法A里面调方法B,A、B都要同一把锁,A一拿到锁,计数器+1 ,B拿到锁,计数器也+1,B执行完逻辑,计数器-1;A当业务执行完成之后,计数器-1,最后判断计数器的数是否为0,为0 ,说明所有业务执行完成,最后释放锁;
由于代码逻辑复杂,为了保证原子性,所以最后用lua脚本编写,

redisson分布式锁释放失败排查 redis分布式锁释放锁_可重入_14

七、Reddison的锁可重试和看门狗机制WatchDoG

redisson分布式锁释放失败排查 redis分布式锁释放锁_java_15


redisson分布式锁释放失败排查 redis分布式锁释放锁_分布式锁_16

Reddison分布式锁总结:
1、可重入:基于Hash结构,hash里field存储线程标识threaId,value存储重入次数,每一次获取锁的时候,先判断锁是否存在,不存在直接获取锁,如果存在,不代表获取锁失败了,再去判断线程的标识是不是当前线程threaId,是当前线程,可以再次获取,重入次数+1,释放锁的时候重入次数-1,直到重入次数为0,所有业务结束,再真正释放锁;实现锁的可重入,类似jdk的ReetrantLock;

2、可重试:利用信号量和消息订阅Pubsub机制,如果第一次获取锁失败,不是立即失败,而是等待释放锁的消息,获取锁成功的线程释放锁的时候会发送消息,从而被捕获到;当线程得到消息时,就可以重新获取锁,如此反复;超过了等待时间,就不会重试了;由于使用了等待、唤醒这样的方案,cpu的性能也不会过多的消耗;

3、锁超时释放:基于看门狗机制,获取锁成功之后开启一个定时 任务,每隔一段时间重置超时时间;

八、Reddison如何解决主从一致性问题?

利用MultiLock ----联锁
1、redis主从一致性发生的原因:Redis主节点处理写操作,从节点处理读操作,主从节点需要进行数据的同步,但是因为主从不在一个机器,同步会有延时,如果主节点突然故障了,同步没有完成,redis就会从从节点选出一个新的主节点,但由于主节点的锁没有及时同步,所以新的主节点没有锁,此时其他线程来获取锁也能成功,引发线程安全问题;

利用MultiLock ----联锁
2、必须依次向redis多个节点都获取锁,全部获取了才算成功

redisson分布式锁释放失败排查 redis分布式锁释放锁_redisson分布式锁释放失败排查_17

redisson分布式锁释放失败排查 redis分布式锁释放锁_redisson分布式锁释放失败排查_18