编号生成器
一、背景与问题
经常有这样的业务,生成“编号”,这个编号有一定的规则,比如合同编号: 以1024LAB-HT开头,然后跟年月日,最后从1开始增长,如下:
1024LAB-20231024-0001
1024LAB-20231024-0002
1024LAB-20231024-0003
而且有些是以月为单位,以年为单位等等
二、架构与思想
2.1、自定义规则
根据需求,肯定要实现一个可以自定义规则的方式,方便后期修改,同时又遵循时间格式,如下:
使用 [yyyy][mm][dd][nnnnn] 格式开配置生成器,其中:
yyyy 表示年
mm 表示月
dd 表示天
nnn 表示数字,几个n就表示几位
如上需求: HT[yyyy][mm][dd][nnnn] ,对于[nnnn]部分,系统提供四种增长周期:
NONE 一直增长
YEAR 以年为单位,跨年会重新从0开始
MONTH 以月为单位,跨月会重新从0开始
DAY 以日为单位,跨日会重新从0开始
2.2、锁机制
由于生成的这些编号全局必须高度唯一,那么必须就要用到锁,那么锁大概有三种:
- 基于内存锁实现 (不支持分布式和集群)
- 基于redis锁实现
- 基于Mysql 锁for update 实现
2.3、表结构
t_serial_number 编号定义表
t_serial_number_record 编号生成记录表
三、具体使用
3.1、定义规则
- 在
t_serial_number
表中定义一条数据,其中format
和ruleType
按照 指定的格式去填写; initNumber
为初始值,默认从1开始stepRandomRange
为每次[nnnn]的随机增长值,比如填写5,那么每次增加值为 [1 - 5]的一个随机数
3.2、添加枚举类
在SerialNumberIdEnum.java
类中 添加刚才第一步的 serialNumberId
;
3.3、选择锁方式
默认选择的是 第一种 内存锁的方式,可以见到SerialNumberInternService.java
类文件中有 @Service
注解。
如果选择redis或者mysql模式,请将 内存锁实现类的@Service
注解去掉,然后在SerialNumberMysqlService.java
或者SerialNumberRedisService.java
中添加 @Service
注解;
至于三种锁如何选择? 看你的项目了;
3.4、调用
注入 SerialNumberService
类,然后调用generate
方法; 如下:
@Autowired
private SerialNumberService serialNumberService;
....
....
// 生成5个 订单的 单号
serialNumberService.generate(SerialNumberIdEnum.ORDER, 5);
四、技术实现
4.1、编号定义
表结构如下
[nnnn] 增长策略如下: SerialNumberRuleTypeEnum.java
@AllArgsConstructor
@Getter
public enum SerialNumberRuleTypeEnum implements BaseEnum {
NONE(StringConst.EMPTY, "", "没有周期"),
YEAR("[yyyy]", "\\[yyyy\\]", "年"),
MONTH("[mm]", "\\[mm\\]", "年月"),
DAY("[dd]", "\\[dd\\]", "年月日");
private final String value;
private final String regex;
private final String desc;
}
替换规则代码如下,具体代码SerialNumberBaseService.java
:
/**
* 替换特殊rule,即替换[yyyy][mm][dd][nnn]等规则
*/
protected List<String> formatNumberList(SerialNumberGenerateResultBO generteResult, SerialNumberInfoBO serialNumberInfo) {
//第一步:替换年、月、日
LocalDate lastTime = generteResult.getLastTime().toLocalDate();
String year = String.valueOf(lastTime.getYear());
String month = lastTime.getMonthValue() > 9 ? String.valueOf(lastTime.getMonthValue()) : "0" + lastTime.getMonthValue();
String day = lastTime.getDayOfMonth() > 9 ? String.valueOf(lastTime.getDayOfMonth()) : "0" + lastTime.getDayOfMonth();
// 把年月日替换
String format = serialNumberInfo.getFormat();
if (serialNumberInfo.getHaveYearFlag()) {
format = format.replaceAll(SerialNumberRuleTypeEnum.YEAR.getRegex(), year);
}
if (serialNumberInfo.getHaveMonthFlag()) {
format = format.replaceAll(SerialNumberRuleTypeEnum.MONTH.getRegex(), month);
}
if (serialNumberInfo.getHaveDayFlag()) {
format = format.replaceAll(SerialNumberRuleTypeEnum.DAY.getRegex(), day);
}
//第二步:替换数字
List<String> numberList = Lists.newArrayListWithCapacity(generteResult.getNumberList().size());
for (Long number : generteResult.getNumberList()) {
StringBuilder numberStringBuilder = new StringBuilder();
int currentNumberCount = String.valueOf(number).length();
//数量不够,前面补0
if (serialNumberInfo.getNumberCount() > currentNumberCount) {
int remain = serialNumberInfo.getNumberCount() - currentNumberCount;
for (int i = 0; i < remain; i++) {
numberStringBuilder.append(0);
}
}
numberStringBuilder.append(number);
//最终替换
String finalNumber = format.replaceAll(serialNumberInfo.getNumberFormat(), numberStringBuilder.toString());
numberList.add(finalNumber);
}
return numberList;
}
4.2、三种锁机制
第一种使用内存及锁机制,guava的 Interners.newStrongInterner()
,具体代码在SerialNumberInternService.java
,核心代码如下:
@Override
public List<String> generateSerialNumberList(SerialNumberInfoBO serialNumberInfo, int count) {
SerialNumberGenerateResultBO serialNumberGenerateResult = null;
synchronized (POOL.intern(serialNumberInfo.getSerialNumberId())) {
// 获取上次的生成结果
SerialNumberLastGenerateBO lastGenerateBO = serialNumberLastGenerateMap.get(serialNumberInfo.getSerialNumberId());
// 生成
serialNumberGenerateResult = super.loopNumberList(lastGenerateBO, serialNumberInfo, count);
// 将生成信息保存的内存和数据库
lastGenerateBO.setLastNumber(serialNumberGenerateResult.getLastNumber());
lastGenerateBO.setLastTime(serialNumberGenerateResult.getLastTime());
serialNumberDao.updateLastNumberAndTime(serialNumberInfo.getSerialNumberId(),
serialNumberGenerateResult.getLastNumber(),
serialNumberGenerateResult.getLastTime());
// 把生成过程保存到数据库里
super.saveRecord(serialNumberGenerateResult);
}
return formatNumberList(serialNumberGenerateResult, serialNumberInfo);
}
第二种,使用redis锁机制,代码在SerialNumberRedisService.java
,核心代码:
@Override
public List<String> generateSerialNumberList(SerialNumberInfoBO serialNumberInfo, int count) {
SerialNumberGenerateResultBO serialNumberGenerateResult = null;
String lockKey = RedisKeyConst.Support.SERIAL_NUMBER + serialNumberInfo.getSerialNumberId();
try {
boolean lock = false;
for (int i = 0; i < MAX_GET_LOCK_COUNT; i++) {
try {
lock = redisService.getLock(lockKey, 60 * 1000L);
if (lock) {
break;
}
Thread.sleep(SLEEP_MILLISECONDS);
} catch (Throwable e) {
log.error(e.getMessage(), e);
}
}
if (!lock) {
throw new BusinessException("SerialNumber 尝试5次,未能生成单号");
}
// 获取上次的生成结果
SerialNumberLastGenerateBO lastGenerateBO = (SerialNumberLastGenerateBO) redisService.mget(
RedisKeyConst.Support.SERIAL_NUMBER_LAST_INFO,
String.valueOf(serialNumberInfo.getSerialNumberId()));
// 生成
serialNumberGenerateResult = super.loopNumberList(lastGenerateBO, serialNumberInfo, count);
// 将生成信息保存的内存和数据库
lastGenerateBO.setLastNumber(serialNumberGenerateResult.getLastNumber());
lastGenerateBO.setLastTime(serialNumberGenerateResult.getLastTime());
serialNumberDao.updateLastNumberAndTime(serialNumberInfo.getSerialNumberId(),
serialNumberGenerateResult.getLastNumber(),
serialNumberGenerateResult.getLastTime());
// 把生成过程保存到数据库里
super.saveRecord(serialNumberGenerateResult);
} catch (Throwable e) {
log.error(e.getMessage(), e);
throw e;
} finally {
redisService.unLock(lockKey);
}
return formatNumberList(serialNumberGenerateResult, serialNumberInfo);
}
第三种,使用mysql innnodb的 for update
机制,代码在SerialNumberMysqlService.java
,核心代码如下:
@Override
@Transactional(rollbackFor = Throwable.class)
public List<String> generateSerialNumberList(SerialNumberInfoBO serialNumberInfo, int count) {
// // 获取上次的生成结果
SerialNumberEntity serialNumberEntity = serialNumberDao.selectForUpdate(serialNumberInfo.getSerialNumberId());
if (serialNumberEntity == null) {
throw new BusinessException("cannot found SerialNumberId 数据库不存在:" + serialNumberInfo.getSerialNumberId());
}
SerialNumberLastGenerateBO lastGenerateBO = SerialNumberLastGenerateBO
.builder()
.lastNumber(serialNumberEntity.getLastNumber())
.lastTime(serialNumberEntity.getLastTime())
.serialNumberId(serialNumberEntity.getSerialNumberId())
.build();
// 生成
SerialNumberGenerateResultBO serialNumberGenerateResult = super.loopNumberList(lastGenerateBO, serialNumberInfo, count);
// 将生成信息保存的内存和数据库
lastGenerateBO.setLastNumber(serialNumberGenerateResult.getLastNumber());
lastGenerateBO.setLastTime(serialNumberGenerateResult.getLastTime());
serialNumberDao.updateLastNumberAndTime(serialNumberInfo.getSerialNumberId(),
serialNumberGenerateResult.getLastNumber(),
serialNumberGenerateResult.getLastTime());
// 把生成过程保存到数据库里
super.saveRecord(serialNumberGenerateResult);
return formatNumberList(serialNumberGenerateResult, serialNumberInfo);
}
SerialNumberMapper.xml
<select id="selectForUpdate" resultType="net.lab1024.sa.common.module.support.serialnumber.domain.SerialNumberEntity">
select * from t_serial_number where serial_number_id = #{serialNumberId} for update
</select>
4.3、保存生成记录
记录的数据如下:
@TableName("t_serial_number_record")
public class SerialNumberRecordEntity {
/**
* 单号id
*/
private Integer serialNumberId;
/**
* 记录日期
*/
private LocalDate recordDate;
/**
* 最后更新值
*/
private Long lastNumber;
/**
* 上次生成时间
*/
private LocalDateTime lastTime;
/**
* 每日生成的数量
*/
private Long count;
}
联系我们
- 教育领域(高职院校数字化、就业创业大数据平台、继续教育平台;在线教育系统、视频直播、题库等,包含:医学、应急管理、成考、专升本等)
- 供应链领域(网络货运平台、大宗贸易进销存ERP、物流管理TMS、B2B电商、仓储WMS、AI提效等)
- 中医领域(诊所数字化管理、互联网医院、AI辅助诊疗、中医适宜技术、在线云问诊、空中药房等)
- AI+软件领域(软件定制外包、开源技术、数据大屏、国产化改造、技术升级换代、人员外包、技术顾问、技术培训等)
加微信: 卓大 拉你入群,一起学习 | 公众号 :六边形工程师 分享:赚钱、代码、生活 | 请 “1024创新实验室” 烩面里加肉 咖啡配胡辣汤,提神又饱腹 | 抖音 : 六边形工程师 直播:赚钱、代码、中医 |