Design and Implementation of a Non‑Sequential Business Transaction ID Generator
This article explains the design and Java Spring‑Boot implementation of a business transaction ID generator that combines business type, date, random digits and a non‑sequential long integer, discusses database schema, concurrency handling, performance testing, and potential duplication pitfalls.
There are many ways to generate business transaction numbers, such as using UUID , concatenating a business type with a SnowFlakeId, or using auto‑increment IDs, but each method has drawbacks: UUIDs reveal no business information, SnowFlake IDs are purely numeric, and auto‑increment IDs expose daily volume.
The desired transaction number format is business type + date + N‑digit random number + a non‑repeating, non‑continuous long integer , which provides both readability and uniqueness.
For example, a repayment transaction ID TQYHK20240920142987500 is composed of several parts, allowing us to identify the business type and the generation date, while the remaining digits are generated to avoid continuity.
To ensure the tail part is non‑continuous, a fixed length (e.g., 6 digits) is used with a segment range of 1000. The start value begins at 1; numbers within the range increment normally, and when the range is exhausted the current sequence value is recorded and the start value moves to the next segment (e.g., 1001). This process repeats until the Long type maximum is reached, after which the start value resets.
The database schema for storing sequence segments is defined as follows:
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;The core Java service that allocates and returns the next ID is implemented with Spring Boot, using a process lock, in‑memory maps for segment allocation, and transactional database updates:
@Component
@Slf4j
public class SequenceService {
@Autowired
private SequenceMapper sequenceMapper;
@Autowired
private SequenceService sequenceService;
/**
* 号段大小
* */
private final int allocateSize = 1000;
/**
* key - sequenceType value - 号段起始值
* */
private final Map
allocateMaps = new ConcurrentHashMap<>();//当前号段
/**
* key - sequenceType value - 号段内自增值
* */
private final Map
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;
}
}A simple test in the main application iterates 10,000 times, formats the returned long to a six‑digit string, and logs each generated sequence ID:
@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);
}
}
}The design has a hidden risk: if the last six digits of generated IDs repeat across different segments within the same day (e.g., 987500, 1987500, 2987500) and the preceding three‑digit random part also coincides, duplicate transaction numbers could occur. This scenario only becomes probable when daily volume exceeds one million; using an eight‑digit tail mitigates the risk.
In conclusion, the segment‑based approach can be combined with SnowFlake IDs for the tail, but SnowFlake alone yields continuous numbers. Performance testing showed that generating 2 million IDs in a single thread takes about 2500 ms, which is sufficient for most business loads.
Rare Earth Juejin Tech Community
Juejin, a tech community that helps developers grow.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.