欢迎光临
我们一直在努力

续约项目 Redis 与 Redisson 分布式锁使用总结

本文基于续约项目源码整理,重点分析项目中 Redis、Redisson RLock 以及业务幂等锁的落地方式。

1. 项目中 Redis 的总体使用方式

pluto-renewal 是一个续期/续保业务项目,Redis 主要承担三类职责:

  • 分布式锁:通过 Redisson RLock 控制并发任务、MQ 消息、批量导入、规则执行等关键流程。
  • 轻量级幂等状态锁:通过 StringRedisTemplate#setIfAbsent 标记业务消息处理状态,避免重复消费。
  • 缓存与临时数据容器:web 模块启用 Spring Cache;部分定时/节假日外呼数据使用 Redisson RList 暂存。
  • 需要先说明一点:项目里没有发现 RedissonRedLock、RedissonMultiLock 或多 Redis 主节点 RedLock 算法的实现。本文提到的“红锁”,指项目中实际使用的 Redisson 分布式锁 RLock,不是严格意义上的 RedLock 算法。

    2. 依赖情况

    项目在parent父工程中管理了 Redisson 版本:

    <dependency>
    <groupId>org.redisson</groupId>
    <artifactId>redisson</artifactId>
    <version>3.14.0</version>
    </dependency>

    相关模块依赖情况:

    模块作用
    pluto-renewal-common 引入 Redisson,并提供 LockUtil 工具类
    pluto-renewal-core 引入 spring-data-redis,大量业务服务使用 RedissonClient / StringRedisTemplate
    pluto-renewal-server 提供服务端 Redis 配置、MQ 消费、异步处理
    pluto-renewal-web 提供 web 端 Redis 配置、Spring Cache 配置

    3. Redis 与 Redisson 配置

    项目的 Redis 配置分散在 server 与 web 两个模块中,核心思想一致:使用 Redis Sentinel 哨兵模式连接 Redis,同时初始化 StringRedisTemplate 和 RedissonClient。

    3.1 server 模块配置

    位置:

    pluto-renewal-server/src/main/java/com/huize/pluto/renewal/server/config/RedisConfig.java

    核心配置包括:

    @Value("${redis.password}")
    private String password;
    @Value("${redis.database}")
    private int database;
    @Value("${redis.sentinel.master}")
    private String master;
    @Value("${redis.sentinel.nodes}")
    private String nodes;

    Redisson 使用哨兵模式的相关配置:

    // 1. 创建Redisson核心配置对象
    Config config = new Config();
    // 2. 指定使用Redis哨兵集群模式连接
    config.useSentinelServers()
    // 开启TCP长连接保活机制,避免空闲连接被防火墙/中间链路断开
    .setKeepAlive(true)
    // 添加哨兵节点地址数组,格式示例:127.0.0.1:26379、192.168.1.100:26380
    .addSentinelAddress(node.toArray(new String[0]))
    // 订阅频道专用连接池:最小空闲连接数,维持基础长连接,减少频繁创建销毁开销
    .setSubscriptionConnectionMinimumIdleSize(10)
    // 订阅频道专用连接池:最大连接总数,发布订阅场景并发过高时扩容上限
    .setSubscriptionConnectionPoolSize(200)
    // 指定哨兵集群中主库的名称(sentinel.conf中sentinel monitor配置的master名称)
    .setMasterName(master)
    // 选择Redis逻辑库,默认0库,取值范围0~15
    .setDatabase(database)
    // 命令执行超时时间,单位毫秒;读写、订阅命令超过该时间未响应则抛出超时异常
    .setTimeout(3000)
    // Redis服务密码,无密码可传null/空字符串
    .setPassword(password);

    // 根据配置初始化并返回Redisson客户端实例(全局建议单例复用,勿频繁创建)
    return Redisson.create(config);

    StringRedisTemplate 则基于 JedisConnectionFactory:

    // 1. 构建Redis哨兵配置
    // master:哨兵配置里的主节点名称(sentinel monitor 后的名字)
    // nodes.split(","):逗号分隔的哨兵地址集合,如 127.0.0.1:26379,127.0.0.1:26380
    RedisSentinelConfiguration config =
    new RedisSentinelConfiguration(master, new HashSet<>(Arrays.asList(nodes.split(","))));

    // 2. 根据哨兵配置创建Jedis连接工厂(SpringDataRedis底层用Jedis实现连接管理)
    JedisConnectionFactory factory = new JedisConnectionFactory(config);
    // 设置Redis访问密码
    factory.setPassword(password);
    // 指定使用几号逻辑库(0~15)
    factory.setDatabase(database);

    3.2 web 模块配置

    位置:

    pluto-renewal-web/src/main/java/com/huize/pluto/renewal/web/conf/RedisConfig.java

    web 模块除了 StringRedisTemplate、RedissonClient 外,还额外配置了:

    @EnableCaching

    并定义了 JSON 序列化的 RedisTemplate<String, Object> 与 RedisCacheManager:

    redisCacheManager.setDefaultExpiration(60);
    expires.put("queryDicItemByTypeCodeByReturnSet", 3600L);
    expires.put("transferProductType", 3600L);
    redisCacheManager.setUsePrefix(true);
    redisCacheManager.setTransactionAware(true);
    redisCacheManager.setCachePrefix(cacheName ->
    ("pluto-renewal-web:cache:" + cacheName + "-").getBytes(StandardCharsets.UTF_8));

    这说明 web 层缓存默认 TTL 为 60 秒,部分字典类缓存 TTL 为 1 小时,并统一加上 pluto-renewal-web:cache: 前缀,便于隔离和排查。

    4. 公共锁工具类 LockUtil

    位置:

    pluto-renewal-common/src/main/java/com/huize/pluto/renewal/common/util/LockUtil.java

    LockUtil 是项目对 Redisson RLock 的一个轻量封装:

    public RLock getLock(String key) {
    return redisson.getLock(key);
    }

    public RLock lock(String key, long waitTimeout) {
    RLock lock = getLock(key);
    try {
    boolean b = lock.tryLock(waitTimeout, TimeUnit.MILLISECONDS);
    if (b) {
    return lock;
    } else {
    return null;
    }
    } catch (InterruptedException e) {
    log.error("加锁被中断 key->{}", key, e);
    return null;
    }
    }

    这里使用的是:

    lock.tryLock(waitTimeout, TimeUnit.MILLISECONDS)

    该重载只传了等待时间,没有显式传 leaseTime。因此锁成功后,Redisson 会启用 watchdog 自动续期机制,避免业务执行时间超过默认锁过期时间后锁被提前释放。

    公共锁 key 常量定义在:

    pluto-renewal-common/src/main/java/com/huize/pluto/renewal/common/constant/LockKeyConstant.java

    典型 key:

    public static final String RENEWAL_STATUS_CHANGE_LOCK = "renewalStatusChange:";
    public static final String ASSIGN_CUSTOMER_KEY_PREFIX = "assign.customer.key:";

    5. Redis 幂等锁:IS 批改消息处理

    位置:

    pluto-renewal-server/src/main/java/com/huize/pluto/renewal/server/renewal2/endorsement/IsEndorsementRenewalDecisionService.java

    该类没有使用 Redisson RLock,而是直接使用 StringRedisTemplate 做业务幂等锁。

    核心 key:

    private static final String REDIS_KEY_PREFIX = "pluto_renewal:is_endorsement:";
    private static final String PROCESSING = "PROCESSING";

    业务处理时根据批改号拼接幂等 key:

    String idempotencyKey = "IS_ENDORSEMENT_336:" + parsedMessage.endorsementMessage.getEndorsementNum();
    String redisKey = REDIS_KEY_PREFIX + idempotencyKey;

    加锁逻辑:

    // 尝试抢占分布式锁:SET key value NX,key不存在才设置成功,返回true;已存在返回false
    if (!Boolean.TRUE.equals(stringRedisTemplate.opsForValue().setIfAbsent(redisKey, PROCESSING))) {
    // 抢占锁失败,查询当前key存储的值
    String redisValue = stringRedisTemplate.opsForValue().get(redisKey);
    // 判断:key有值 并且 当前值不是以PROCESSING开头(代表旧锁异常残留、脏数据)
    if (StrUtil.isNotBlank(redisValue) && !redisValue.startsWith(PROCESSING)) {
    // 删除脏数据旧key
    stringRedisTemplate.delete(redisKey);
    // 删除后重新尝试抢锁
    if (!Boolean.TRUE.equals(stringRedisTemplate.opsForValue().setIfAbsent(redisKey, PROCESSING))) {
    // 二次抢锁依旧失败,返回可重试错误标识
    return RETRYABLE_ERROR;
    }
    } else {
    // 两种场景进这里:
    // 1. key值为空(并发下刚被其他线程删锁)
    // 2. key值正常是PROCESSING开头,代表其他线程正在执行业务持有锁
    // 不处理,直接返回可重试错误
    return RETRYABLE_ERROR;
    }
    }
    // 走到此处代表初次setIfAbsent抢锁成功,执行业务逻辑

    处理开始后设置过期时间:

    stringRedisTemplate.expire(redisKey, 30L, TimeUnit.MINUTES);

    终态或异常时删除 key:

    if (decision.getStatus() == NOT_RENEWAL_DATA
    || decision.getStatus() == RENEWAL_ACCEPTED
    || decision.getStatus() == TERMINAL_REJECT) {
    stringRedisTemplate.delete(redisKey);
    }

    这个场景的特点是:

    点说明
    锁类型 Redis SETNX 风格幂等锁
    key 粒度 批改号
    value PROCESSING
    TTL 30 分钟
    失败策略 返回可重试状态
    适用场景 MQ 消息重复投递、业务处理幂等

    这里的设计不是为了保护一段临界区的线程互斥,而是为了防止同一业务事件被重复处理。

    6. Redisson RLock 的主要使用场景

    6.1 新增续期信息防重复

    位置:

    pluto-renewal-core/src/main/java/com/huize/pluto/renewal/core/subject/service/impl/RenewalInfoServiceImpl.java

    配置项:

    @Value("${renewalInfo.lockKeyPrefix:pluto_renewal:addRenewalInfo:}")
    String addRenewalInfoLockKeyPrefix;

    @Value("${renewalInfo.lock.time:600}")
    private Integer leaseTime;

    锁 key 粒度为:

    String lockKey = addRenewalInfoLockKeyPrefix + insureNum + ":" + policyCompanyNum;

    加锁:

    lock = redissonClient.getLock(lockKey);
    locked = lock.tryLock(0, leaseTime, TimeUnit.SECONDS);

    业务上先查重,再插入:

    List<TRenewalInfo> renewalInfoList =
    renewalInfoMapper.selectRenewalByInsureNum(insureNum, policyCompanyNum);
    if (!CollectionUtils.isEmpty(renewalInfoList)) {
    throw new RuntimeException("重复添加");
    }
    effect = renewalInfoMapper.insert(info);

    此外,该类还用 StringRedisTemplate 维护一个短期 check key:

    redisTemplate.opsForValue().set(checkKey, UUID.randomUUID().toString(),
    addRenewalInfoCheckKeyExpire, TimeUnit.SECONDS);

    这是“双保险”设计:Redisson 锁控制并发,Redis check key 辅助防重复。

    6.2 MQ 保单状态变更串行化

    位置:

    pluto-renewal-server/src/main/java/com/huize/pluto/renewal/server/rabbit/handler/AsyncMessageHandler.java
    pluto-renewal-server/src/main/java/com/huize/pluto/renewal/server/rabbit/handler/PosMessageHandler.java

    两个 MQ handler 都使用:

    LockUtil lockUtil = LockUtil.create(redissonClient);
    RLock rLock = lockUtil.lock(
    LockKeyConstant.RENEWAL_STATUS_CHANGE_LOCK + topicMsg.getInsureNum(),
    10000
    );

    业务含义是:同一投保单号的续期状态变更、退保状态变更需要串行处理,避免 MQ 并发消费导致状态覆盖或顺序错乱。

    对应 key:

    renewalStatusChange:{insureNum}

    6.3 批量导入记录状态抢占

    位置:

    pluto-renewal-core/src/main/java/com/huize/pluto/renewal/core/renewal2/batchimport/InsureInfoImportService.java

    导入任务创建后异步执行。执行前先抢占导入记录:

    private static final String IMPORT_RECORD_LOCK_PREFIX = "renewal2:batch-import:record:";

    加锁:

    RLock lock = redissonClient.getLock(IMPORT_RECORD_LOCK_PREFIX + recordId);
    locked = lock.tryLock(5, TimeUnit.SECONDS);

    锁内执行状态 CAS:

    boolean updated = batchImportRecordService.updateStatusIfCurrent(
    recordId,
    STATUS_PENDING_IMPORT,
    STATUS_PROCESSING
    );

    这个设计很经典:Redis 锁防并发进入,数据库状态更新防重复执行。即使锁层出现边界情况,DB 状态仍然是最终保护线。

    6.4 批量导入逐行更新续期信息

    位置:

    pluto-renewal-core/src/main/java/com/huize/pluto/renewal/core/renewal2/batchimport/InsureInfoImportRowUpdateService.java

    锁前缀:

    private static final String LOCK_PREFIX = "renewal2:insure-info-import:";

    该类可能一次更新多条续期记录,因此会构建多个 lock key。为了避免死锁,项目先对 key 去重并排序:

    if (lockKeys.stream().allMatch(this::isNumericKey)) {
    lockKeys.sort(Comparator.comparingLong(Long::parseLong));
    } else {
    lockKeys.sort(String::compareTo);
    }

    然后按顺序加锁:

    RLock lock = redissonClient.getLock(lockKey);
    if (!tryLock(lock)) {
    throw new IllegalStateException("当前投保单正在导入处理中,请稍后重试");
    }
    acquiredLocks.add(lock);

    释放时倒序释放:

    for (int i = locks.size() 1; i >= 0; i) {
    RLock lock = locks.get(i);
    if (lock.isHeldByCurrentThread()) {
    lock.unlock();
    }
    }

    这是项目里分布式锁使用比较规范的一处:多锁排序获取 + 反向释放 + 判断当前线程持有锁。

    6.5 续期/保单维度导入更新

    位置:

    pluto-renewal-core/src/main/java/com/huize/pluto/renewal/core/renewal2/batchimport/RenewalInfoImportUpdateService.java

    锁前缀:

    private static final String LOCK_PREFIX = "renewal2:renewal-info-import:";

    锁粒度为投保单号:

    RLock lock = redissonClient.getLock(LOCK_PREFIX + bill.getInsureNum());
    if (!lock.tryLock(10, 60, TimeUnit.SECONDS)) {
    throw new IllegalStateException("当前投保单正在导入处理中,请稍后重试");
    }

    加锁后在事务中更新保单、险种、续期信息,并处理后续期次级联更新。

    6.6 续期单分配规则执行

    位置:

    pluto-renewal-core/src/main/java/com/huize/pluto/renewal/core/subject/service/impl/RenewalBillAllocServiceImpl.java

    锁 key:

    public static final String EXEC_ALLOC_RULE_CONFIG_LOCK_KEY =
    "pluto_renewal_exec_alloc_rule_config:";

    实际 key 包含规则 ID 和分配状态:

    String key = EXEC_ALLOC_RULE_CONFIG_LOCK_KEY
    + allocRuleConfigId + ":" + allocStatusEnum.getValue();

    加锁方式:

    locked = lock.tryLock(0, leaseTime, TimeUnit.SECONDS);

    该锁用于避免同一分配规则、同一分配状态的任务被多个线程/节点同时执行。

    6.7 触达提醒规则执行与补偿检查

    位置:

    pluto-renewal-core/src/main/java/com/huize/pluto/renewal/core/base/service/NoticeRuleService.java

    该类有两个典型锁:

    private static final String NOTICE_RULE_EXEC_LOCK_KEY =
    "pluto_renewal_exec_notice_rule:";

    private static final String NOTICE_SEND_RECORD_CHECK_LOCK_KEY =
    "pluto_renewal_notice_send_record_check:";

    规则执行锁:

    String lockKey = NOTICE_RULE_EXEC_LOCK_KEY + rule.getId();
    locked = lock.tryLock(0, noticeRuleLeaseTime, TimeUnit.SECONDS);

    待发送记录检查锁:

    String lockKey = NOTICE_SEND_RECORD_CHECK_LOCK_KEY + sendDateText;
    locked = lock.tryLock(10, noticeRuleLeaseTime, TimeUnit.SECONDS);

    这里的粒度分别是“规则 ID”和“发送日期”,用于防止定时任务重复扫描、重复生成或重复修复触达记录。

    6.8 客户服务人员分配锁

    位置:

    pluto-renewal-core/src/main/java/com/huize/pluto/renewal/core/base/service/CustomerServerRelationService.java

    锁 key 前缀:

    public static final String ASSIGN_CUSTOMER_KEY_PREFIX = "assign.customer.key:";

    根据客户维度构造 key:

    private String buildAssignRedisKey(String customerKey) {
    return LockKeyConstant.ASSIGN_CUSTOMER_KEY_PREFIX + customerKey;
    }

    加锁后再执行查询或保存客户服务关系:

    locked = lock.tryLock(waitTime, leaseTime, timeUnit);
    if (locked) {
    return supplier.get();
    }

    这个锁保护的是“客户 – 服务人员绑定关系”的唯一性,避免新客户分配时并发插入多条关系。

    7. Redisson 作为分布式数据结构使用

    项目不只用 Redisson 做锁,也用到了 RList。

    位置:

    pluto-renewal-server/src/main/java/com/huize/pluto/renewal/server/smartcall/v2/annual/SmartCallHandler.java
    pluto-renewal-server/src/main/java/com/huize/pluto/renewal/server/smartcall/v2/SmartCallHandler.java

    春节期间不外呼时,将待外呼数据暂存到 Redis List:

    RList<AnnualSmartCallResultDto> list =
    redisson.getList("infoList" + "_" + smartCallStrategy.getKey());
    list.addAll(resultDtos);
    list.expire(5, TimeUnit.DAYS);

    节后恢复外呼时再取出并删除:

    resultDtos.addAll(list);
    list.delete();

    这个场景把 Redis 当作短期任务缓冲区使用,TTL 为 5 天。

    8. 项目中锁 key 设计汇总

    场景key 前缀粒度主要类
    新增续期信息 pluto_renewal:addRenewalInfo: 投保单号 + 保单号 RenewalInfoServiceImpl
    新增续期 check key pluto_renewal:addRenewalInfo:check: 投保单号 + 保单号 RenewalInfoServiceImpl
    IS 批改幂等 pluto_renewal:is_endorsement: 批改号 IsEndorsementRenewalDecisionService
    保单状态变更 renewalStatusChange: 投保单号 AsyncMessageHandler / PosMessageHandler
    导入记录抢占 renewal2:batch-import:record: 导入记录 ID InsureInfoImportService
    投保单信息导入 renewal2:insure-info-import: 投保单号 InsureInfoImportRowUpdateService
    续期信息导入 renewal2:renewal-info-import: 投保单号 RenewalInfoImportUpdateService
    分配规则执行 pluto_renewal_exec_alloc_rule_config: 规则 ID + 状态 RenewalBillAllocServiceImpl
    触达规则执行 pluto_renewal_exec_notice_rule: 规则 ID NoticeRuleService
    触达记录检查 pluto_renewal_notice_send_record_check: 发送日期 NoticeRuleService
    客户分配 assign.customer.key: 客户唯一标识 CustomerServerRelationService

    9. 总结

    续约项目中的 Redis 使用比较典型:配置上采用 Redis Sentinel 保证可用性;业务上通过 Redisson RLock 控制跨节点并发;对于 MQ 消息幂等,则使用 StringRedisTemplate#setIfAbsent 实现更轻量的状态锁;web 层还额外接入了 Spring Cache。

    从业务角度看,分布式锁主要保护这些高风险场景:

    • 同一投保单的状态变更串行处理
    • 续期信息重复创建
    • 批量导入任务重复执行
    • 导入逐行更新并发冲突
    • 定时规则重复执行
    • 客户服务关系重复绑定

    整体来说,项目的锁 key 粒度设计比较清晰,多数地方遵循了 try-finally 释放锁的基本原则。后续如果继续优化,可以重点关注三点:统一锁封装、获取锁失败后的判空处理、以及 SETNX 幂等锁的原子 TTL 设置。

    赞(0)
    未经允许不得转载:171主机测评 » 续约项目 Redis 与 Redisson 分布式锁使用总结
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址