美文网首页数据库redis
Redisson失效场景

Redisson失效场景

作者: AC编程 | 来源:发表于2022-05-12 16:47 被阅读0次

一、失效场景说明

环境是Redis集群,下面主要列举三种场景,其中场景一和场景二在开发过程中会经常遇到。场景三出现的机率比较小,但是能加深我们对分布式锁的理解。

二、失效场景场景一(Redisson)

在事务内部使用锁,锁在事务提交前释放

2.1 场景描述

假设有这样一个需求:创建付款单,要求不能重复创建相同业务单号的付款单。为了保证幂等,我们需要判断数据库中是否已经存在相同业务单号的付款单,并且需要加锁处理并发安全性问题。

@Transactional
public void createPaymentOrderInnerLock(PaymentOrder paymentOrder){
    RLock lock = redissonClient.getLock(paymentOrder.getBizNo());
    //采用的redisson可重入锁,提供watchdog机制,在锁释放前默认每10s重置锁失效时间为30s
    lock.lock();
    try {
        LambdaQueryWrapper<PaymentOrder> paymentOrderLambdaQueryWrapper = new LambdaQueryWrapper<>();
        paymentOrderLambdaQueryWrapper.eq(PaymentOrder::getBizNo,paymentOrder.getBizNo());
        //判断数据库中是否存在相同业务单号的付款单
        long count = this.count(paymentOrderLambdaQueryWrapper);
        //存在相同业务单号的付款单则抛异常
        if(count>0){
            throw new RuntimeException("不可重复提交付款单");
        }else{
            //无重复数据,创建付款单
            this.save(paymentOrder);
            //其他DB操作
            ...
        }
    } finally {
        //释放锁
        lock.unlock();
    }
}
2.2 问题分析

上述问题的流程图如下

1
2.3 解决方案

为了避免锁在事务提交前释放,我们应该在事务外层使用锁。

  • 方式一:在Controller层用Redisson,而不是在Service层用Redisson。
  • 方式二:在Service层用Redisson,不用声明式事务,而采用编程式事务(最小范围控制事务)。

三、失效场景场景二(非Redisson)

业务未执行完,锁超时释放

3.1 场景描述

需求:创建付款单,要求不能重复创建相同业务单号的付款单

@Override
public void createPaymentOrderRenault(List<PaymentOrder> paymentOrderList){
    if(!CollectionUtils.isEmpty(paymentOrderList)){
        for (PaymentOrder paymentOrder : paymentOrderList) {
            /**
             * 采用公司框架提供的分布式锁
             * 10---等待锁释放时间
             * 1---尝试获取锁时间间隔
             * 5---锁失效时间
             * 注意:此处设置锁失效时间为5秒,在createPaymentOrderNoLock中睡眠5秒模拟耗时操作,此时会出现业务未执行完,锁超时释放的问题
             */
            try (AutoReleaseLock lock = acquireLock(paymentOrder.getBizNo(),  10, 1, 5, TimeUnit.SECONDS)) {
                if(lock != null) {
                    paymentOrderService.createPaymentOrderNoLock(paymentOrder);
                } else {
                    log.info("未获取到锁!");
                }
            }catch (CacheParamException e) {
                log.info("获取锁失败");
            }
        }
    }
}


@Override
@Transactional
public void createPaymentOrderNoLock(PaymentOrder paymentOrder) {
    LambdaQueryWrapper<PaymentOrder> paymentOrderLambdaQueryWrapper = new LambdaQueryWrapper<>();
    paymentOrderLambdaQueryWrapper.eq(PaymentOrder::getBizNo,paymentOrder.getBizNo());
    long count = this.count(paymentOrderLambdaQueryWrapper);
    if(count>0){
        log.info("不可重复提交付款单");
        throw new RuntimeException("不可重复提交付款单");
    }else{
        this.save(paymentOrder);
        //模拟耗时操作...
        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
3.2 问题分析

出现上述问题是因为在指定的锁的失效时间内(并且没有续命机制),锁内部的业务代码没有执行完,锁超时释放了。尤其我们财务端处于业务链下游,处理的数据量一般都比较大,交互的端比较多,尤其要注意这种情况。下列情形都有可能出现代码没有执行完,锁超时释放的问题。

  • 锁的失效时间设置的太短
  • 锁的粒度太大,处理链路冗长
  • 锁内部包含很多耗时操作,比如远程调用、大数据量处理等
3.3 解决方案

首先会想到,把失效时间设置长一点,确实可以。但设置多长合适呢,设置过长有可能存在拿到锁的客户端宕掉了,此时就要等锁过期才能释放,其他节点处于阻塞状态,降低了系统吞吐。又或者预估了一个失效时间在项目初期没问题,随着数据量增多,或者其他一些不确定因素造成了超时,也会出现问题。

可以采用类似Redisson的watchdog机制给锁续命。另外,注意减小锁的粒度,把存在并发安全性问题的关键代码锁住即可,增加系统吞吐量。同时也要注意减小事务的粒度,把查询操作、甚至一些远程调用放到事务外部(注意读写分离的情况),避免出现大事务问题。

四、失效场景场景三(非Redisson)

Redis节点主从切换

4.1 场景描述

我们在使用Redis时,一般会采用主从集群 + 哨兵的模式部署,这样做的好处在于当主库异常宕机时,哨兵可以实现故障自动切换,把从库提升为主库,继续提供服务,以此保证可用性。
当【主从发生切换】时,Redis分布锁会存在安全性问题

  • 客户端A从master获取到锁

  • 在master将锁同步到slave之前,master宕掉了。

  • slave节点被晋升为master节点

  • 客户端B取得了同一个资源被客户端A已经获取到的同一个锁。

4.2 问题分析

首先要说明一点,出现这种情形的概率是很低的。针对于这种情况,Redis的作者antirez设计出了RedLock算法,然而RedLock算法依赖时钟正确性,存在争议。

Redlock 必须「强依赖」多个节点的时钟是保持同步的,一旦有节点时钟发生错误,那这个算法模型就失效了。

  • 客户端 A 获取节点 1、2、3 上的锁。由于网络问题,无法访问 4 和 5。
  • 节点 3 上的时钟向前跳跃,导致锁到期。
  • 客户端 B 获取节点 3、4、5 上的锁。由于网络问题,无法访问 1 和 2。
  • 客户端 A 和 B 现在都相信他们持有锁。
4.3 Redisson弃用RedLock

起初Redisson也提供的RedLock的实现,但在3.12.5版本后弃用了。

//redisson 3.12.5版本之前 RedLock 使用示例,基于RedissonMultiLock实现
RLock lock1 = redissonInstance1.getLock("lock1");
RLock lock2 = redissonInstance2.getLock("lock2");
RLock lock3 = redissonInstance3.getLock("lock3");

RedissonRedLock lock = new RedissonRedLock(lock1, lock2, lock3);
// 同时加锁:lock1 lock2 lock3
// 红锁在大部分节点上加锁成功就算成功。
lock.lock();
...
lock.unlock();

Redisson 的开发者认为 Redis 的红锁存在争议,但是为了保证可用性,RLock 对象执行的每个 Redis 命令执行都通过 Redis 3.0 中引入的 WAIT 命令进行同步。WAIT 命令会阻塞当前客户端,直到所有以前的写命令都成功的传输并被指定数量的副本确认。如果达到以毫秒为单位指定的超时,则即使尚未达到指定数量的副本,该命令也会返回。WAIT 命令同步复制也并不能保证强一致性,不过在主节点宕机之后,只不过会尽可能的选择最佳的副本(slaves)。

4.4 解决方案

Redis分布式锁在极端情况下,不一定是安全的。如果你对并发安全性带来的问题零容忍,为了保证正确性,我们可以做一些兜底工作,
例如:

  • 建立唯一索引
  • 监控、告警、提供补偿方案

转载自:Redis分布式锁失效场景分析

相关文章

  • Redisson失效场景

    一、失效场景说明 环境是Redis集群,下面主要列举三种场景,其中场景一和场景二在开发过程中会经常遇到。场景三出现...

  • SpringBoot整合Redisson

    Redisson的Github地址:https://github.com/redisson/redisson/wi...

  • @Transactional失效场景

    上一篇:事务的两种形式 @Transactional介绍 @Transactional注解底层使用的是动态代理来进...

  • 顾睿2

    What is Redisson? Redisson is a Redis Java library that p...

  • 最强分布式锁工具:Redisson

    一、Redisson概述 什么是Redisson? Redisson是一个在Redis的基础上实现的Java驻内存...

  • mysql索引失效场景

    WHERE字句的查询条件里有不等于号(WHERE column!=…),MYSQL将无法使用索引类似地,如果WHE...

  • @Transactional注解失效场景

    通过@Transactional进行事务控制是我们在日常开发中非常常见的使用方式,虽然用起来感觉很简单,但是其内部...

  • 索引失效的场景

    最近看一些博客文章,稳重列举了一些索引失效的场景,作了验证,发现有些结论是对的,有些事错误的。 如果索引了多列,要...

  • MySql | 索引失效场景

    一、数据库及索引准备 创建表结构 在上述表结构中有三个索引: id:为数据库主键 union_idx:为id_no...

  • 一个Redis分布式锁的工具类

    20200707: 这个更好 https://github.com/redisson/redisson 仅供个人记...

网友评论

    本文标题:Redisson失效场景

    本文链接:https://www.haomeiwen.com/subject/ksfeurtx.html