生成一个全局唯一的单号

150 阅读2分钟

你做过的系统里一定有这个场景,生成一个业务单号,这个业务编号要求全局唯一,由前缀+日期+5位数字递增组成

方案一:缓存一天时间,不需要查询数据库

@Getter
@AllArgsConstructor
public enum BusinessTypeEnum {

    TRANSPORT_ENTER("yyyyMMdd", "TYJ", "hmy-tms:transport-enter-no:"),

    TRANSPORT_EXIT("yyyyMMdd", "TYT", "hmy-tms:transport-exit-no:"),

    RECEIVE_ADJUST("yyyyMMdd", "YSTZ", "hmy-finance:receive-adjust-no:");

    /**
     * 日期格式
     */
    private final String pattern;

    /**
     * 前缀
     */
    private final String prefix;

    /**
     * 缓存key
     */
    private final String cacheKey;

}

package com.hmy.generator.service;

import cn.hutool.core.date.DatePattern;
import cn.hutool.core.date.DateUtil;
import com.hmy.generator.common.enums.BusinessTypeEnum;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.support.atomic.RedisAtomicLong;
import org.springframework.stereotype.Service;

import java.util.Date;
import java.util.Objects;

/**
 * 编码生成器
 */
@Service
public class CommonNoGenerator {
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;

    /**
     * 获取单号
     *
     * @param enums 业务枚举
     * @return 业务单号
     */
    public String getNextNo(BusinessTypeEnum enums) {
        String currentDate = DateUtil.format(new Date(), DatePattern.createFormatter(enums.getPattern()));
        RedisAtomicLong aLong = new RedisAtomicLong(enums.getCacheKey() + currentDate,
                Objects.requireNonNull(redisTemplate.getConnectionFactory()));
        long andIncrement = aLong.getAndIncrement();
        //当天过期
        aLong.expireAt(DateUtil.endOfDay(new Date()));
        return enums.getPrefix() + currentDate + String.format("%04d", andIncrement);
    }
}

方案二:缓存一段时间,需要查询数据库

package com.hmy.generator.service;


import com.hmy.generator.common.constants.Constant;
import com.hmy.generator.common.enums.DocumentTypeEnum;
import com.hmy.generator.common.utils.GeneratorRedisUtil;
import com.hmy.generator.dal.mapper.GeneratorMapper;
import com.hmy.generator.dal.po.GeneratorPo;
import com.mysql.cj.util.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;

import javax.annotation.Resource;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;

@Service
public class BusinessNoGenerator {
    private static final Logger log = LoggerFactory.getLogger(BusinessNoGenerator.class);

    @Resource
    private GeneratorMapper generatorMapper;

    @Resource
    private GeneratorRedisUtil redisUtil;

    public String generatorNo(DocumentTypeEnum enums) {
        String businessNo = null;
        // 获取锁
        if (redisUtil.acquireLock(Constant.BUSINESS_NO_LOCK_PREFIX + enums.getTableName(), enums.getFieldName(),
                Constant.LOCK_TIME_LENGTH)) {
            businessNo = redisUtil.get(Constant.BUSINESS_NO_KEY_PREFIX + enums.getTableName());
            // 缓存为空
            if (StringUtils.isNullOrEmpty(businessNo)) {
                businessNo = generatorMapper.queryBusinessNo(buildGeneratorPo(enums));
                // 数据库为空
                if (StringUtils.isNullOrEmpty(businessNo)) {
                    businessNo = enums.getPrefix() + getLocalDate(enums.getRule()) + Constant.DEFAULT_SUFFIX;
                } else {
                    businessNo = getNewBusinessNo(businessNo, enums);
                }
            } else {
                businessNo = getNewBusinessNo(businessNo, enums);
            }
            redisUtil.set(Constant.BUSINESS_NO_KEY_PREFIX + enums.getTableName(), businessNo,
                    Constant.CACHE_TIME_LENGTH);
            redisUtil.del(Constant.BUSINESS_NO_LOCK_PREFIX + enums.getTableName());
        } else {
            // 休眠之后,再次尝试获取锁
            try {
                Thread.sleep(Constant.SLEEP_TIME);
            } catch (InterruptedException e) {
                log.error("generatorNo exception", e);
                throw new RuntimeException(e);
            }
            for (int i = 1; i < Constant.RETRY_TIME; i++) {
                businessNo = generatorNo(enums);
                if (!StringUtils.isNullOrEmpty(businessNo)) {
                    break;
                }
            }
            if (StringUtils.isNullOrEmpty(businessNo)) {
                throw new RuntimeException("获取锁失败,稍后再重试");
            }
        }
        return businessNo;
    }


    /**
     * 在原有的业务编码基础上生成新的
     *
     * @param businessNo 业务编码
     * @param enums      业务枚举
     */

    private String getNewBusinessNo(String businessNo, DocumentTypeEnum enums) {
        String newBusinessNo;
        String dateStr = businessNo.substring(enums.getPrefix().length(),
                enums.getRule().length() + enums.getPrefix().length());
        // 判断是不是当天
        if (isCurrentDay(dateStr, enums.getRule())) {
            businessNo = businessNo.substring(enums.getPrefix().length() + enums.getRule().length());
            int no = Integer.parseInt(businessNo) + 1;
            // 补全前面缺失的0
            String num = String.format("%04d", no);
            newBusinessNo = enums.getPrefix() + getLocalDate(enums.getRule()) + num;
            redisUtil.set(Constant.BUSINESS_NO_KEY_PREFIX + enums.getTableName(), newBusinessNo,
                    Constant.CACHE_TIME_LENGTH);
            return newBusinessNo;
        } else {
            newBusinessNo = enums.getPrefix() + getLocalDate(enums.getRule()) + Constant.DEFAULT_SUFFIX;
        }
        return newBusinessNo;
    }


/**
     * 构造GeneratorPo对象
     *
     * @param enums 枚举
     * @return generatorPo
     */
    private GeneratorPo buildGeneratorPo(DocumentTypeEnum enums) {
        GeneratorPo generatorPo = new GeneratorPo();
        generatorPo.setTableName(enums.getTableName());
        generatorPo.setFieldName(enums.getFieldName());
        generatorPo.setRule(enums.getRule());
        generatorPo.setPrefix(enums.getPrefix());
        return generatorPo;
    }


    /**
     * @param pattern 日期格式
     * @return 日期为字符串
     */

    private String getLocalDate(String pattern) {
        LocalDate currentDate = LocalDate.now();

        // 设置日期格式
        DateTimeFormatter formatter = DateTimeFormatter.ofPattern(pattern);

        // 格式化日期为字符串
        return currentDate.format(formatter);
    }


    /**
     * 判断字符串是否是当天
     *
     * @param date    字符串日期
     * @param pattern 日期格式
     * @return true/false
     */

    private boolean isCurrentDay(String date, String pattern) {
        LocalDate currentDate = LocalDate.now();

        DateTimeFormatter formatter = DateTimeFormatter.ofPattern(pattern);

        currentDate.format(formatter);

        return currentDate.format(formatter).equals(date);
    }

}

显然方案一比方案二简单很多

那么设计一个id生成器或者单号生成器我们要考虑哪些东西呢?我罗列了以下点

1、全局唯一

2、三高:高性能、高可靠、高并发这都依赖于redis实现安全性能和可靠问题

3、可读性要好

4、基因编辑,可以考虑在单号里加入业务信息

5、长度不要太长、方便记忆

6、灵活、方便扩展