编号生成器

9/7/2021

# 一、背景与问题

经常有这样的业务,生成“编号”,这个编号有一定的规则,比如合同编号: 以1024LAB-HT开头,然后跟年月日,最后从1开始增长,如下:

1024LAB-20221024-0001
1024LAB-20221024-0002
1024LAB-20221024-0003
1
2
3

而且有些是以月为单位,以年为单位等等

# 二、架构与思想

# 2.1、自定义规则

根据需求,肯定要实现一个可以自定义规则的方式,方便后期修改,同时又遵循时间格式,如下:
使用 [yyyy][mm][dd][nnnnn] 格式开配置生成器,其中:

yyyy 表示年
mm   表示月
dd   表示天
nnn  表示数字,几个n就表示几位   
1
2
3
4

如上需求: HT[yyyy][mm][dd][nnnn] ,对于[nnnn]部分,系统提供四种增长周期:

NONE     一直增长
YEAR     以年为单位,跨年会重新从0开始
MONTH    以月为单位,跨月会重新从0开始
DAY      以日为单位,跨日会重新从0开始
1
2
3
4

# 2.2、锁机制

由于生成的这些编号全局必须高度唯一,那么必须就要用到锁,那么锁大概有三种:

    1. 基于内存锁实现 (不支持分布式和集群)
    1. 基于redis锁实现
    1. 基于Mysql 锁for update 实现

# 2.3、表结构

t_serial_number  编号定义表
t_serial_number_record   编号生成记录表
1
2

# 三、具体使用

# 3.1、定义规则

  • t_serial_number 表中定义一条数据,其中formatruleType 按照 指定的格式去填写;
  • 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);

1
2
3
4
5
6
7

# 四、技术实现

# 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;
}
1
2
3
4
5
6
7
8
9
10
11
12

替换规则代码如下,具体代码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;
    }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42

# 4.2、三种锁机制

第一种使用内存及锁机制,guava的 Interners.newWeakInterner(),具体代码在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);
    }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

第二种,使用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);
    }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43

第三种,使用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);
    }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

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>
1
2
3

# 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;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

# 联系我们

1024创新实验室-主任:卓大 (opens new window),混迹于各个技术圈,研究过计算机,熟悉点 java,略懂点前端。
1024创新实验室(河南·洛阳) (opens new window) 致力于成为中原领先、国内一流的技术团队,以技术创新为驱动,合作各类项目。

加 主任 “卓大” 微信
拉你入群,一起学习
关注 “小镇程序员”
分享代码与生活、技术与赚钱
请 “1024创新实验室” 喝咖啡
支持我们的开源与分享

告白气球 (钢琴版)
JESSE T