业务交易号生成方式 —— 号段

前言

业务交易号的生成方式有很多,可以使用 UUID,也可以使用业务类型 bizType 拼接雪花算法产生的 SnowFlakeId,还可以用自增编号。但是这些方式似乎都不太合适

  • UUID 是纯字母,从这个交易号中完全看不出任何业务信息,还怪怪的
  • 雪花算法,生成的号码是纯数字,看起来也不直观,拼接一些业务类型等信息还可以
  • 自增编号,能看出业务信息,但是太直观了,也能让别人看出一天有多少业务量,很不合适

我们需要的业务交易号最好是 业务类型 + 日期 + N位随机数 + 一个不重复的不连续递增的长整型数字

源码分享

完整项目代码已分享到 Github syc-sequence

思路

例如我司的还款交易号 TQYHK20240920142987500 由以下几部分组成

这样,我们能知道这个交易号是哪个业务类型的,哪天产生的,但是看不出其他相关信息。现在我们要考虑的是交易日期后面的号码怎么保证不重复,怎么保证不连续递增。

不连续递增比较好处理,中间几位随机数就解决了,保证尾号不重复,同时还要注意性能,还需要让尾号不能太长,否则手机上位置有限可能会影响 UI 展示。

我们可以考虑固定尾号的长度比如为 6 位,然后给定一个范围比如 1000,起始值 为 1 ,在 1000 以内,产生的号码递增,号码不够 6 位长度的左边补 0 ,超过 1000,记录当前序列值,再更新起始值为 当前序列值1001,然后从这个值作为起始,继续自增,一直循环下去,直到起始的号段值超过 Long 类型的最大值,然后起始值再置为一个初始值 ,重新开始。流程图如下:

如果这段没看懂,没关系,看下面的代码就明白了。下面我们开始用代码实现

建表

sql 复制代码
CREATE TABLE `sequence` (
  `sequence_type` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '序列号类型 = 区分业务类型',
  `crt_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  `start_value` bigint NOT NULL DEFAULT '1' COMMENT '起始值',
  `curr_value` bigint NOT NULL DEFAULT '1' COMMENT '序列号当前值',
  `upt_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
  PRIMARY KEY (`sequence_type`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;

代码实现

java 复制代码
@Component
@Slf4j
public class SequenceService {
    @Autowired
    private SequenceMapper sequenceMapper;
    @Autowired
    private SequenceService sequenceService;
    /**
     * 号段大小
     * */
    private final int allocateSize = 1000;
    /**
     * key - sequenceType value - 号段起始值
     * */
    private final Map<String, Long> allocateMaps = new ConcurrentHashMap<>();//当前号段
    /**
     * key - sequenceType value - 号段内自增值
     * */
    private final Map<String, AtomicLong> incrementMaps = new ConcurrentHashMap<>(); //业务号段当前值
    /**
     * 进程锁
     * */
    private final ReentrantLock lock = new ReentrantLock();

    public long next(String sequenceType) {
        //用进程锁,这样每个服务实例就会用新的号段,避免出现连续递增的情况
        lock.lock();
        try {
            if (allocateMaps.containsKey(sequenceType) && incrementMaps.get(sequenceType).incrementAndGet() < allocateSize) {
                return allocateMaps.get(sequenceType) + incrementMaps.get(sequenceType).longValue();
            }
            return sequenceService.nextValues(sequenceType,1);
        } finally {
            lock.unlock();
        }
    }

    /**
     * @param count 递增间隔
     * */
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public long nextValues(String sequenceType,int count) {
        Sequence sequence = sequenceMapper.getForUpdate(sequenceType);
        if (sequence == null) {
            sequence = new Sequence();
            sequence.setCrtTime(LocalDateTime.now());
            sequence.setSequenceType(sequenceType);
            sequence.setStartValue(1);
            sequence.setCurrValue(1);
            try {
                sequenceMapper.insert(sequence);
            } catch (Exception e) {
                // Duplicated conflict
                sequence = sequenceMapper.getForUpdate(sequenceType);
                if (sequence == null) {
                    throw new RuntimeException("Unable init sequence, sequenceType=[" + sequenceType + "].");
                }
            }
        }
        long seqValue = sequence.getCurrValue();
        long value = seqValue;
        while (value >= 0 && Long.MAX_VALUE - value < count) {
            // 序列值循环: 当value + count 大于 0Long.MAX_VALUE时,从startValue重新开始累加
            count -= (int) (Long.MAX_VALUE - value + 1);
            value = sequence.getStartValue();
        }
        sequence.setCurrValue(value + count + allocateSize); // nextValue
        sequenceMapper.updateById(sequence);
        // currValue 大于 allocateMaps 一个号段值
        allocateMaps.put(sequenceType, value + count);
        incrementMaps.put(sequenceType, new AtomicLong(0));
        return seqValue;
    }
}

在主类中测试

java 复制代码
@SpringBootApplication
@Slf4j
public class SycSequenceApplication {

    public static void main(String[] args) {
        ConfigurableApplicationContext context = SpringApplication.run(SycSequenceApplication.class, args);
        SequenceService sequenceService = context.getBean(SequenceService.class);
        for (int i = 0; i < 10000; i++) {
            long l = sequenceService.next(SequenceTypes.TEST_SEQUENCE_ID);
            String seqId = StringUtil.subOrLefPad(String.valueOf(l), 6);//补 0
            log.info("Sequence Id:{}", seqId);
        }
    }

}

隐患

从上面的设计逻辑,如果仔细分析的话我们会发现,由于我们截取了号段后六位,假如一天之内生成的多个号段里面的交易号分别是 987500、1987500、2987500。 那么后六位的号码就是相同的,此时正好前面三位随机数如果也是相同的,那么就会导致交易号重复

但是这种情况只有当我们一天之内生成的交易号超过 100w 才有可能出现这个问题,所以,根据业务量实际情况使用即可,如果这个量级都不够,那么就截取后八位即可。这样一天之内生成的交易号超过一亿才可能会出现重复。

结语

结尾的号段也可以用雪花算法生成,截取雪花算法的后六位或者后八位,但是这样一来没有号段的概念,并且雪花算法产生的序列是连续递增的。

虽然上面号段的实现逻辑里面访问了数据库,也用了进程锁,但是我测试了一下性能,单线程的情况下产生 200w 个序列号只需要 2500ms,这个业务量绝大多数情况下够用了。

如果这篇文章对你有帮助,记得点赞加关注!你的支持就是我继续创作的动力!

相关推荐
JINGWHALE1几秒前
设计模式 结构型 桥接模式(Bridge Pattern)与 常见技术框架应用 解析
前端·人工智能·后端·设计模式·性能优化·系统架构·桥接模式
深鱼~5 分钟前
【多线程初阶篇 ²】创建线程的方式
java·开发语言·jvm·深度学习·神经网络·opencv
相隔一个图书馆的距离8 分钟前
netty系列(五)IdleStateHandler和IdleStateHandlerEventState
java·netty·idlehandler
莫问alicia26 分钟前
苍穹外卖 项目记录 day03
java·开发语言·spring boot·maven
DevOpsDojo26 分钟前
Haskell语言的学习路线
开发语言·后端·golang
123yhy传奇29 分钟前
【学习总结|DAY028】后端Web实战(部门管理)
java·学习·mysql·log4j·maven·mybatis·web
m0_7482544744 分钟前
将 vue3 项目打包后部署在 springboot 项目运行
java·spring boot·后端
Cikiss1 小时前
SpringMVC解析
java·服务器·后端·mvc
旧物有情1 小时前
蓝桥杯历届真题--#R格式(C++,Java) 高精度运算
java·c++·蓝桥杯
ByteBlossom6661 小时前
R语言的语法糖
开发语言·后端·golang