基于Redis实现的分布式唯一编号生成工具类

首先,直接上代码:

import 复制代码
import org.springframework.stereotype.Component;
import org.springframework.stereotype.Service;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import javax.annotation.PostConstruct;
import javax.annotation.Resource;
import java.util.Date;
import java.util.concurrent.TimeUnit;
import com.baomidou.mybatisplus.core.toolkit.DateUtils;
import com.xxxxx.blade.redis.BladeRedis; // 根据实际包路径调整
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import com.xxxxx.common.exception.ServiceException; // 根据实际包路径调整

/**
 * 通用编号生成工具类
 * 功能:生成格式为【业务编码+日期+3位自增序号】的唯一编号(例如:JJ20250826001)
 * 特性:基于Redis实现分布式自增,通过Redisson分布式锁保证并发安全,序号按日期重置
 */
@Slf4j // Lombok注解,自动注入日志对象log
@Component // Spring组件注解,将该类注册为Bean,交由Spring容器管理
public class CommonNumber {

    /**
     * Redis操作客户端(静态变量)
     * 用于执行自增、过期时间设置等Redis命令
     */
    private static BladeRedis BLADE_REDIS;

    /**
     * Redisson客户端(静态变量)
     * 用于获取分布式锁,保证多实例环境下的并发安全
     */
    private static RedissonClient REDISSSON_CLIENT;

    /**
     * Redisson客户端(实例变量)
     * 由Spring容器注入,通过@Resource注解按名称匹配
     */
    @Resource
    private RedissonClient RedissonClient;

    /**
     * Redis操作客户端(实例变量)
     * 由Spring容器注入,Blade框架封装的Redis客户端
     */
    @Resource
    private BladeRedis bladeRedis;

    /**
     * 初始化方法(PostConstruct注解)
     * 作用:在Spring Bean初始化完成后,将实例变量赋值给静态变量
     * 原因:工具方法为static,无法直接注入Spring Bean,通过该方式间接获取容器中的Bean实例
     */
    @PostConstruct
    public void init() {
        BLADE_REDIS = bladeRedis;
        REDISSSON_CLIENT = RedissonClient;
    }

    /**
     * 生成通用唯一编号
     * 格式:业务编码(code) + 年月日(yyyyMMdd) + 3位自增序号(不足补0)
     * 示例:code=JJ → JJ20250826001、JJ20250826002...
     *
     * @param code 业务编码(区分不同业务场景的编号前缀)
     * @return 格式化后的唯一编号
     * @throws ServiceException 当获取分布式锁失败或Redis操作异常时抛出
     */
    public static String getCommonNumber(String code) {
        // 1. 定义分布式锁key:按业务编码区分,避免不同业务锁竞争
        RLock lock = REDISSSON_CLIENT.getLock("common-number-lock:" + code);

        try {
            // 2. 尝试获取分布式锁:最多等待3秒,持有锁5秒(防止死锁)
            // tryLock返回false表示获取锁失败(并发过高)
            if (!lock.tryLock(3, 5, TimeUnit.SECONDS)) {
                throw new ServiceException("系统繁忙,请稍后重试");
            }

            // 3. 格式化当前日期为yyyyMMdd格式(用于序号按日期重置)
            String currentTime = DateUtils.format(new Date(), "yyyyMMdd");

            // 4. 定义Redis自增key:业务编码+日期,确保每天的序号从1开始
            String companyNumberKey = "common_number_key:" + code + currentTime;

            // 5. Redis自增操作:原子性递增,保证序号唯一(初始值为1,每次+1)
            Long incr = BLADE_REDIS.incr(companyNumberKey);

            // 6. 拼接最终编号:业务编码 + 日期 + 3位补0序号(例如:1→001,10→010,100→100)
            String companyNumber = code + currentTime + String.format("%03d", incr);

            // 7. 设置Rediskey过期时间:48小时(确保过期数据自动清理,节省Redis空间)
            BLADE_REDIS.expire(companyNumberKey, 60 * 60 * 48L);

            // 8. 返回生成的编号
            return companyNumber;

        } catch (InterruptedException e) {
            // 捕获线程中断异常(获取锁过程中线程被中断)
            log.error("获取编号时线程被中断,code:{}", code, e);
            throw new ServiceException("获取编号失败");
        } catch (ServiceException e) {
            // 抛出获取锁失败的自定义异常(无需额外日志,已在抛出时明确)
            throw e;
        } catch (Exception e) {
            // 捕获其他异常(Redis操作失败等)
            log.error("获取编号失败,code:{}", code, e);
            throw new ServiceException("获取编号失败");
        } finally {
            // 9. 释放分布式锁:必须在finally中执行,确保锁一定会释放
            // 先判断当前线程是否持有锁,避免释放其他线程的锁
            if (lock.isHeldByCurrentThread()) {
                lock.unlock();
            }
        }
    }
}

```java

这是一个 基于Redis实现的分布式唯一编号生成工具类 ,核心功能是生成格式为 业务编码+日期+3位自增序号(如 JJ20250826001)的全局唯一编号,适用于分布式系统中需要有序、不重复编号的场景(如订单号、单据号等)。以下是详细解析:

一、类结构与依赖说明

1. 核心注解
  • @Slf4j:Lombok注解,自动生成日志对象 log,用于打印异常日志。
  • @Component:Spring注解,将该类注册为Spring容器中的Bean,支持依赖注入。
2. 依赖组件
  • BladeRedis:bladex框架封装的Redis操作工具(类似Spring Data Redis),用于执行incr(自增)、expire(设置过期时间)等Redis命令。
  • RedissonClient:Redisson框架的客户端,用于操作Redis分布式锁(解决分布式环境下的并发冲突)。
  • @Resource:Spring依赖注入注解,用于注入RedissonClientBladeRedis实例。
  • @PostConstruct:Spring生命周期注解,在Bean初始化完成后执行init方法,将注入的实例赋值给静态变量(因为getCommonNumber是静态方法,无法直接使用非静态成员变量)。

二、核心逻辑:编号生成流程

1. 方法定义
java 复制代码
public static String getCommonNumber(String code)
  • 入参 code:业务编码(如JJ代表某种单据类型),用于区分不同业务场景的编号。
  • 出参:格式为 code + 日期(yyyyMMdd) + 3位自增序号 的唯一编号。
2. 关键步骤(带并发安全保障)
(1)分布式锁获取
java 复制代码
RLock lock = REDISSSON_CLIENT.getLock("common-number-lock:"+code);
if (!lock.tryLock(3, 5, TimeUnit.SECONDS)) {
    throw new ServiceException("系统繁忙,请稍后重试");
}
  • 锁key设计common-number-lock: + 业务编码code,确保不同业务的锁相互隔离,避免锁竞争加剧。
  • 锁参数
    • 最多等待3秒(waitTime=3):线程获取锁时,最多等待3秒,超过则认为获取失败。
    • 锁持有时间5秒(leaseTime=5):即使线程未主动释放锁,5秒后Redis也会自动释放,避免死锁。
  • 作用:解决分布式环境下的并发冲突,确保同一业务编码的自增序号不会重复。
(2)Redis自增生成序号
java 复制代码
// 1. 生成当天日期(格式:yyyyMMdd)
String currentTime = DateUtil.format(DateUtil.date(), "yyyyMMdd");
// 2. 构建Redis自增key(业务编码+日期,确保每天的序号独立重置)
String companyNumberKey = "common_number_key:" + code + currentTime;
// 3. Redis自增(原子操作,确保序号唯一)
Long incr = BLADE_REDIS.incr(companyNumberKey);
// 4. 序号补零(3位,不足3位前面补0,如1→001,10→010)
String seq = String.format("%03d", incr);
  • Redis key设计common_number_key: + 业务编码code + 日期currentTime,确保:
    • 不同业务编码的序号相互独立;
    • 同一业务编码每天的序号从1开始重置(因为日期变化后key会变化)。
  • 原子性保障 :Redis的incr命令是原子操作,即使多个线程同时调用,也能保证自增结果唯一,无需额外加锁(但此处仍加分布式锁是为了避免极端情况下的并发问题,或配合序号补零等逻辑)。
(3)设置Redis key过期时间
java 复制代码
BLADE_REDIS.expire(companyNumberKey, 60 * 60 * 48L); // 48小时过期
  • 自增key的过期时间设置为48小时,原因:
    • 确保当天的序号能正常自增(当天内key未过期);
    • 避免Redis中存储大量过期的自增key,节省存储空间;
    • 48小时覆盖跨天场景(如凌晨23:59生成的key,到次日仍可正常使用,避免提前过期)。
(4)组装最终编号
java 复制代码
String companyNumber = code + currentTime + seq;
  • 例如:业务编码JJ + 日期20250826 + 序号001 → 最终编号JJ20250826001
3. 异常处理与锁释放
java 复制代码
try {
    // 编号生成逻辑...
} catch (Exception e) {
    log.error("获取编号失败", e); // 打印异常堆栈,便于排查
    throw new ServiceException("获取编号失败"); // 抛出自定义业务异常,上层处理
} finally {
    // 确保锁一定释放(避免死锁)
    if (lock.isHeldByCurrentThread()) {
        lock.unlock();
    }
}
  • finally块释放锁:无论生成编号成功与否,都要释放分布式锁,避免锁资源泄露。
  • 锁持有判断lock.isHeldByCurrentThread() 确保当前线程确实持有锁时才释放,避免释放其他线程的锁(如线程等待锁超时后,未持有锁却执行解锁操作)。

三、核心设计亮点

1. 分布式并发安全
  • 双重保障:Redisincr原子操作 + Redisson分布式锁,确保序号唯一,无并发冲突。
  • 锁自动过期:避免因线程异常导致的死锁问题。
2. 序号合理性
  • 按天重置:每天的序号从1开始,编号可读性强(通过编号可直接看出日期)。
  • 固定长度:3位序号补零,确保编号长度一致(便于存储和展示)。
3. 资源优化
  • Redis key过期:48小时自动清理,避免Redis存储冗余数据。
  • 锁粒度细:按业务编码分锁,减少锁竞争,提高并发效率。

四、潜在问题与优化建议

1. 潜在问题
  • 锁竞争风险:如果同一业务编码的并发请求极高,分布式锁可能成为性能瓶颈(线程需等待3秒)。
  • Redis依赖风险:Redis服务不可用时,编号生成会失败(无降级方案)。
  • 序号溢出 :3位序号最大支持999,如果单日同一业务编码的编号超过999,会生成code+日期+1000(如JJ202508261000),破坏3位固定长度格式。
2. 优化建议
  • 优化锁策略
    • 去掉分布式锁(Redisincr已保证原子性),仅在需要序号补零、特殊逻辑时加锁,提高并发效率。
    • 调整锁等待时间和持有时间(根据业务并发量动态调整)。
  • 降级方案
    • 当Redis不可用时,可临时使用本地缓存(如AtomicLong)+ 机器标识生成编号,避免服务不可用。
  • 序号扩容
    • 将3位序号改为4位(%04d),支持单日9999个编号,满足更高并发场景。
  • 防止重复生成
    • 可将生成的编号存入Redis或数据库,做最终去重校验(极端情况下Redis自增失败时兜底)。
  • 静态成员变量优化
    • 目前通过@PostConstruct给静态变量赋值,依赖Spring初始化顺序,可改为使用@Autowired + 非静态方法(去掉static),更符合Spring依赖注入规范(需将工具类注入使用,而非直接调用静态方法)。

五、使用场景

适用于分布式系统中需要生成 有序、唯一、可读 编号的场景,例如:

  • 订单编号、支付单号、物流单号;
  • 单据编号(如入库单、出库单);
  • 业务流水号等。

总结

该工具类基于Redis的原子自增和Redisson的分布式锁,实现了分布式环境下的唯一编号生成,设计简洁、实用性强,同时也存在一些可优化的细节(如锁策略、降级方案),可根据实际业务场景调整。

相关推荐
while(1){yan}2 小时前
MYSQL索引的底层数据结构
数据结构·数据库·mysql
by__csdn2 小时前
Spring Boot 全面解析
java·数据库·spring boot·后端·spring
西岭千秋雪_2 小时前
Kafka客户端参数(一)
java·分布式·后端·kafka·linq
q***49452 小时前
分布式监控Skywalking安装及使用教程(保姆级教程)
分布式·skywalking
合作小小程序员小小店2 小时前
web网页开发,在线%人力资源管理%系统,基于Idea,html,css,jQuery,java,jsp,ssh,mysql。
java·前端·css·数据库·mysql·html·intellij-idea
ASKED_20192 小时前
常用 Linux 命令大全(文件、网络、时间、进程、数据库、工具全覆盖)
linux·网络·数据库
Felix_XXXXL2 小时前
28.<Spring博客系统⑤(部署的整个过程
java·后端
Ace_31750887762 小时前
拼多多商品详情接口深度解析:从加密参数破解到数据全量获取
前端·数据库·github
yuejich2 小时前
命名规范snake_case
服务器·前端·数据库