首先,直接上代码:
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依赖注入注解,用于注入RedissonClient和BladeRedis实例。@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也会自动释放,避免死锁。
- 最多等待3秒(
- 作用:解决分布式环境下的并发冲突,确保同一业务编码的自增序号不会重复。
(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. 分布式并发安全
- 双重保障:Redis
incr原子操作 + Redisson分布式锁,确保序号唯一,无并发冲突。 - 锁自动过期:避免因线程异常导致的死锁问题。
2. 序号合理性
- 按天重置:每天的序号从1开始,编号可读性强(通过编号可直接看出日期)。
- 固定长度:3位序号补零,确保编号长度一致(便于存储和展示)。
3. 资源优化
- Redis key过期:48小时自动清理,避免Redis存储冗余数据。
- 锁粒度细:按业务编码分锁,减少锁竞争,提高并发效率。
四、潜在问题与优化建议
1. 潜在问题
- 锁竞争风险:如果同一业务编码的并发请求极高,分布式锁可能成为性能瓶颈(线程需等待3秒)。
- Redis依赖风险:Redis服务不可用时,编号生成会失败(无降级方案)。
- 序号溢出 :3位序号最大支持999,如果单日同一业务编码的编号超过999,会生成
code+日期+1000(如JJ202508261000),破坏3位固定长度格式。
2. 优化建议
- 优化锁策略 :
- 去掉分布式锁(Redis
incr已保证原子性),仅在需要序号补零、特殊逻辑时加锁,提高并发效率。 - 调整锁等待时间和持有时间(根据业务并发量动态调整)。
- 去掉分布式锁(Redis
- 降级方案 :
- 当Redis不可用时,可临时使用本地缓存(如
AtomicLong)+ 机器标识生成编号,避免服务不可用。
- 当Redis不可用时,可临时使用本地缓存(如
- 序号扩容 :
- 将3位序号改为4位(
%04d),支持单日9999个编号,满足更高并发场景。
- 将3位序号改为4位(
- 防止重复生成 :
- 可将生成的编号存入Redis或数据库,做最终去重校验(极端情况下Redis自增失败时兜底)。
- 静态成员变量优化 :
- 目前通过
@PostConstruct给静态变量赋值,依赖Spring初始化顺序,可改为使用@Autowired+ 非静态方法(去掉static),更符合Spring依赖注入规范(需将工具类注入使用,而非直接调用静态方法)。
- 目前通过
五、使用场景
适用于分布式系统中需要生成 有序、唯一、可读 编号的场景,例如:
- 订单编号、支付单号、物流单号;
- 单据编号(如入库单、出库单);
- 业务流水号等。
总结
该工具类基于Redis的原子自增和Redisson的分布式锁,实现了分布式环境下的唯一编号生成,设计简洁、实用性强,同时也存在一些可优化的细节(如锁策略、降级方案),可根据实际业务场景调整。