1024创新实验室-公告

助力抖音1000个粉丝,开播写代码🎉🎉

打开【抖音APP】-点击【左上角侧边栏】-【点击扫一扫】-【进行关注】🎉🎉

和1024创新实验室一起,热爱代码,热爱生活,永远年轻,永远前行🎉🎉


Skip to content

编号生成器

一、背景与问题

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

js
1024LAB-20231024-0001
1024LAB-20231024-0002
1024LAB-20231024-0003

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

二、架构与思想

2.1、自定义规则

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

java
yyyy 表示年
mm   表示月
dd   表示天
nnn  表示数字,几个n就表示几位

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

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

2.2、锁机制

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

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

2.3、表结构

sql
t_serial_number  编号定义表
t_serial_number_record   编号生成记录表

三、具体使用

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方法; 如下:

java
  @Autowired
  private SerialNumberService serialNumberService;
  ....
  ....
  // 生成5个 订单的 单号
  serialNumberService.generate(SerialNumberIdEnum.ORDER, 5);

四、技术实现

4.1、编号定义

表结构如下

[nnnn] 增长策略如下: SerialNumberRuleTypeEnum.java

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:

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,核心代码如下:

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,核心代码:

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,核心代码如下:

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

sql
    <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、保存生成记录

记录的数据如下:

java
@TableName("t_serial_number_record")
public class SerialNumberRecordEntity {
    /**
     * 单号id
     */
    private Integer serialNumberId;
    /**
     * 记录日期
     */
    private LocalDate recordDate;
    /**
     * 最后更新值
     */
    private Long lastNumber;
    /**
     * 上次生成时间
     */
    private LocalDateTime lastTime;

    /**
     * 每日生成的数量
     */
    private Long count;
}

联系我们

1024创新实验室-主任:卓大,混迹于各个技术圈,研究过计算机,熟悉点 java,略懂点前端。
1024创新实验室 致力于成为中原领先、国内一流的技术团队, 以AI+数字化为驱动,用技术为产业互联网提供无限可能, 业务如下:
  • 教育领域(高职院校数字化、就业创业大数据平台、继续教育平台;在线教育系统、视频直播、题库等,包含:医学、应急管理、成考、专升本等)
  • 供应链领域(网络货运平台、大宗贸易进销存ERP、物流管理TMS、B2B电商、仓储WMS、AI提效等)
  • 中医领域(诊所数字化管理、互联网医院、AI辅助诊疗、中医适宜技术、在线云问诊、空中药房等)
  • AI+软件领域(软件定制外包、开源技术、数据大屏、国产化改造、技术升级换代、人员外包、技术顾问、技术培训等)
加微信: 卓大
拉你入群,一起学习
公众号 :六边形工程师
分享:赚钱、代码、生活
请 “1024创新实验室”
烩面里加肉
咖啡配胡辣汤,提神又饱腹
抖音 : 六边形工程师
直播:赚钱、代码、中医