How to Generate Collision‑Free Order Numbers in High‑Concurrency Java Apps
The article examines a real‑world incident where duplicate order IDs appeared under high concurrency, analyzes the shortcomings of the original timestamp‑and‑random‑based scheme, and presents a revised Java implementation using thread‑safe counters, Java 8 date‑time APIs, and IP‑based suffixes to reliably produce unique order numbers even in clustered environments.
Incident Overview
At the end of last year an online service suffered an outage where two identical order numbers were generated, but the order contents differed. The system threw errors when querying by order number, and the problem occurred repeatedly, indicating a fundamental flaw in the order‑number generation logic.
Root Cause Analysis
The original implementation built the order number from a timestamp (down to milliseconds) and a two‑digit random number. In a high‑concurrency environment the millisecond component can remain constant across threads, and a two‑digit random value provides insufficient entropy, leading to collisions.
Original Code
/**
* OD单号生成
* 订单号生成规则:OD + yyMMddHHmmssSSS + 5位数(商户ID3位+随机数2位) 22位
*/
public static String getYYMMDDHHNumber(String merchId) {
StringBuffer orderNo = new StringBuffer(new SimpleDateFormat("yyMMddHHmmssSSS").format(new Date()));
if (StringUtils.isNotBlank(merchId)) {
if (merchId.length() > 3) {
orderNo.append(merchId.substring(0, 3));
} else {
orderNo.append(merchId);
}
}
int orderLength = orderNo.toString().length();
String randomNum = getRandomByLength(20 - orderLength);
orderNo.append(randomNum);
return orderNo.toString();
}
/** 生成指定位数的随机数 **/
public static String getRandomByLength(int size) {
if (size > 8 || size < 1) {
return "";
}
Random ne = new Random();
StringBuffer endNumStr = new StringBuffer("1");
StringBuffer staNumStr = new StringBuffer("9");
for (int i = 1; i < size; i++) {
endNumStr.append("0");
staNumStr.append("0");
}
int randomNum = ne.nextInt(Integer.valueOf(staNumStr.toString())) + Integer.valueOf(endNumStr.toString());
return String.valueOf(randomNum);
}Initial Stress Test
public static void main(String[] args) {
final String merchId = "12334";
List<String> orderNos = Collections.synchronizedList(new ArrayList<String>());
IntStream.range(0, 100).parallel().forEach(i -> {
orderNos.add(getYYMMDDHHNumber(merchId));
});
List<String> filterOrderNos = orderNos.stream().distinct().collect(Collectors.toList());
System.out.println("生成订单数:" + orderNos.size());
System.out.println("过滤重复后订单数:" + filterOrderNos.size());
System.out.println("重复订单数:" + (orderNos.size() - filterOrderNos.size()));
}Result:
生成订单数:100
过滤重复后订单数:87
重复订单数:13The test revealed 13 duplicate order numbers out of 100 concurrent requests.
Improved Design
To eliminate collisions the following changes were made:
Remove the merchant‑ID prefix.
Keep only three digits of the millisecond part.
Use a thread‑safe AtomicInteger counter that increments for each request (four digits provide enough space for 800 concurrent calls).
Replace SimpleDateFormat with Java 8 DateTimeFormatter for thread safety and cleaner code.
Revised Code (Single‑Instance)
/** 订单号生成(NEW) **/
private static final AtomicInteger SEQ = new AtomicInteger(1000);
private static final DateTimeFormatter DF_FMT_PREFIX = DateTimeFormatter.ofPattern("yyMMddHHmmssSS");
private static final ZoneId ZONE_ID = ZoneId.of("Asia/Shanghai");
public static String generateOrderNo() {
LocalDateTime dataTime = LocalDateTime.now(ZONE_ID);
if (SEQ.intValue() > 9990) {
SEQ.getAndSet(1000);
}
return dataTime.format(DF_FMT_PREFIX) + SEQ.getAndIncrement();
}Verification
public static void main(String[] args) {
List<String> orderNos = Collections.synchronizedList(new ArrayList<String>());
IntStream.range(0, 8000).parallel().forEach(i -> {
orderNos.add(generateOrderNo());
});
List<String> filterOrderNos = orderNos.stream().distinct().collect(Collectors.toList());
System.out.println("生成订单数:" + orderNos.size());
System.out.println("过滤重复后订单数:" + filterOrderNos.size());
System.out.println("重复订单数:" + (orderNos.size() - filterOrderNos.size()));
}
/**
* 测试结果:
* 生成订单数:8000
* 过滤重复后订单数:8000
* 重复订单数:0
*/The test produced zero duplicates for 8,000 concurrent calls.
Multi‑Instance Considerations
When the application runs in multiple containers or servers, the counter alone cannot guarantee global uniqueness. Several strategies were evaluated:
Initialize a UUID once per instance and embed it in the order number.
Use a Redis key that atomically increments.
Maintain a database table with an auto‑increment column.
Append the host IP address.
Append the listening port.
Adopt a third‑party algorithm such as Snowflake.
Include the process ID.
Given the deployment runs inside Docker containers on a single physical host, the author chose the IP‑suffix approach because each container receives a distinct internal IP.
Final Multi‑Instance Implementation
import org.apache.commons.lang3.RandomUtils;
import java.net.InetAddress;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
public class OrderGen2Test {
private static ZoneId ZONE_ID = ZoneId.of("Asia/Shanghai");
private static final AtomicInteger SEQ = new AtomicInteger(1000);
private static final DateTimeFormatter DF_FMT_PREFIX = DateTimeFormatter.ofPattern("yyMMddHHmmssSS");
public static String generateOrderNo() {
LocalDateTime dataTime = LocalDateTime.now(ZONE_ID);
if (SEQ.intValue() > 9990) {
SEQ.getAndSet(1000);
}
return dataTime.format(DF_FMT_PREFIX) + getLocalIpSuffix() + SEQ.getAndIncrement();
}
private static volatile String IP_SUFFIX = null;
private static String getLocalIpSuffix() {
if (IP_SUFFIX != null) {
return IP_SUFFIX;
}
try {
synchronized (OrderGen2Test.class) {
if (IP_SUFFIX != null) {
return IP_SUFFIX;
}
InetAddress addr = InetAddress.getLocalHost();
String hostAddress = addr.getHostAddress();
if (hostAddress != null && hostAddress.length() > 4) {
String ipSuffix = hostAddress.trim().split("\\.")[3];
if (ipSuffix.length() == 2) {
IP_SUFFIX = ipSuffix;
return IP_SUFFIX;
}
ipSuffix = "0" + ipSuffix;
IP_SUFFIX = ipSuffix.substring(ipSuffix.length() - 2);
return IP_SUFFIX;
}
IP_SUFFIX = String.valueOf(RandomUtils.nextInt(10, 20));
return IP_SUFFIX;
}
} catch (Exception e) {
System.out.println("获取IP失败:" + e.getMessage());
IP_SUFFIX = String.valueOf(RandomUtils.nextInt(10, 20));
return IP_SUFFIX;
}
}
public static void main(String[] args) {
List<String> orderNos = Collections.synchronizedList(new ArrayList<String>());
IntStream.range(0, 8000).parallel().forEach(i -> {
orderNos.add(generateOrderNo());
});
List<String> filterOrderNos = orderNos.stream().distinct().collect(Collectors.toList());
System.out.println("订单样例:" + orderNos.get(22));
System.out.println("生成订单数:" + orderNos.size());
System.out.println("过滤重复后订单数:" + filterOrderNos.size());
System.out.println("重复订单数:" + (orderNos.size() - filterOrderNos.size()));
}
}
/**
* 订单样例:20082115575546011022
* 生成订单数:8000
* 过滤重复后订单数:8000
* 重复订单数:0
*/Key Takeaways
The generateOrderNo() method does not need explicit locking because AtomicInteger provides atomic increments.
The IP‑suffix logic uses a double‑checked locking pattern to avoid unnecessary synchronization.
Multiple valid strategies exist; the choice depends on the specific system architecture.
Thorough testing is essential—previous attempts missed duplicate generation under load.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Java Backend Technology
Focus on Java-related technologies: SSM, Spring ecosystem, microservices, MySQL, MyCat, clustering, distributed systems, middleware, Linux, networking, multithreading. Occasionally cover DevOps tools like Jenkins, Nexus, Docker, and ELK. Also share technical insights from time to time, committed to Java full-stack development!
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.
