前言
业务交易号的生成方式有很多,可以使用 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
,这个业务量绝大多数情况下够用了。