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.

Java Backend Technology
Java Backend Technology
Java Backend Technology
How to Generate Collision‑Free Order Numbers in High‑Concurrency Java Apps

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
重复订单数:13

The 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.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

Distributed SystemsJavaorder-numberUnique ID
Java Backend Technology
Written by

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!

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.