We had an incident online that looked like this:

There are two identical order numbers in the system, but the content of the order is not different. Moreover, the system has been throwing mistakes when querying according to the order number, and it cannot be called back normally. Moreover, the thing happened more than once, so this system upgrade must be solved.

My colleague had changed it several times before, but the effect was not good. There was always the problem of repeating the order number. Therefore, I took advantage of this problem to make a good arrangement of the code written by my colleague.

Here’s a quick look at the code:

/** * OD order number generation * Order number generation rule: OD + yyMMddHHmmssSSS + 5 digits (merchant ID3 digits + random number 2 digits) 22 digits */
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();
}


  /** Generates a random number of specified digits **/
  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);
  }      
Copy the code

As you can see, this code is actually not very good, the code part is not discussed, the main factor in the code so that the order number does not repeat is random number and millisecond, but the random number here is only two, in high concurrency environment is very prone to repeat problems.

Also, the millisecond option is not very good. In multi-threaded cpus, the millisecond can be said to be fixed for a certain period of time (extremely small), so I first use 100 concurrent tests to generate the order number.

The test code is as follows:

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("Generate order number:"+orderNos.size());
    System.out.println("Filter repeat order number:"+filterOrderNos.size());
    System.out.println(Repeat order number:+(orderNos.size()-filterOrderNos.size()));
}
Copy the code

Sure enough, the test results were as follows:

At that time I was shocked 🤯, 100 concurrent inside actually have 13 repeat!!

I hurriedly let with do not send version in advance, I took this job!

I spent about 6+ minutes discussing the business scenario with my colleagues and decided to make the following changes:

  • Remove the input of merchant ID (according to my colleague, the input of merchant ID is also to prevent repeated orders, but it has not been used)
  • Keep only three milliseconds (reduce the length and make sure there is no duplication between application switches)
  • Use a thread-safe counter for numerical increments (three-digit minimum guaranteed concurrency 800 not repeated, I gave 4 bits in the code)
  • Replace date converted to Java8 date classes for formatting (thread safety and code brevity considerations)

My final code is:

/** Order number generation (NEW) **/
private static final AtomicInteger SEQ = new AtomicInteger(1000);
private static final DateTimeFormatter DF_FMT_PREFIX = DateTimeFormatter.ofPattern("yyMMddHHmmssSS");
private static ZoneId ZONE_ID = ZoneId.of("Asia/Shanghai");
public static String generateOrderNo(a){
    LocalDateTime dataTime = LocalDateTime.now(ZONE_ID);
    if(SEQ.intValue()>9990){
        SEQ.getAndSet(1000);
    }
    return  dataTime.format(DF_FMT_PREFIX)+SEQ.getAndIncrement();
}
Copy the code

Now we need to test the main function:

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("Generate order number:"+orderNos.size());
    System.out.println("Filter repeat order number:"+filterOrderNos.size());
    System.out.println(Repeat order number:+(orderNos.size()-filterOrderNos.size()));
}

/** Test result: Generated order number: 8000 Filtered repeat order number: 8000 Repeated order number: 0 **/
Copy the code

Great, one time success, can go straight online…

However, when I look back at the above code, although the problem of duplicate order numbers is solved to the greatest extent, there is still a potential problem for our system architecture: if the current application has multiple instances (clusters), is there no possibility of duplicate?

Since this problem requires an effective solution, I wondered: How can multiple instance application order numbers be distinguished?

Here are some general directions of my thinking:

  • Use a UUID(initialize one the first time an order number is generated)
  • Use Redis to record a growth ID
  • Maintain a growth ID using database tables
  • IP address of the network where the application resides
  • Port number of the application
  • Use third-party algorithms (Snowflake algorithms, etc.)
  • Use process ids (to some extent possible)

Here I think, our application runs in docker, and the application port in each Docker container is the same, but the network IP does not exist the problem of duplication, as for the process there is also the possibility of duplication, for the way of UUID suffered losses before, far, redis or DB is also a better way. But less independent…

At the same time, another important factor is that all applications involved in order number generation are on the same host (Linux physical server), so I choose IP for the current system architecture.

Here is my code:

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 {

    /** Order number generated **/
    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(a){
        LocalDateTime dataTime = LocalDateTime.now(ZONE_ID);
        if(SEQ.intValue()>9990){
            SEQ.getAndSet(1000);
        }
        return  dataTime.format(DF_FMT_PREFIX)+ getLocalIpSuffix()+SEQ.getAndIncrement();
    }

    private volatile static String IP_SUFFIX = null;
    private static String getLocalIpSuffix (a){
        if(null! = IP_SUFFIX){return IP_SUFFIX;
        }
        try {
            synchronized (OrderGen2Test.class){
                if(null! = IP_SUFFIX){return IP_SUFFIX;
                }
                InetAddress addr = InetAddress.getLocalHost();
                / / 172.17.0.4 172.17.0.199,
                String hostAddress = addr.getHostAddress();
                if (null! = hostAddress && 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 = RandomUtils.nextInt(10.20) + "";
                returnIP_SUFFIX; }}catch (Exception e){
            System.out.println("Failed to obtain IP address :"+e.getMessage());
            IP_SUFFIX =  RandomUtils.nextInt(10.20) +"";
            returnIP_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("Sample order:"+ orderNos.get(22));
        System.out.println("Generate order number:"+orderNos.size());
        System.out.println("Filter repeat order number:"+filterOrderNos.size());
        System.out.println(Repeat order number:+(orderNos.size()-filterOrderNos.size())); }}/** Order example: 20082115575546011022 Generated order number: 8000 Filtered repeat order number: 8000 Repeated order number: 0 **/
Copy the code

Finally, the code description and some suggestions

  • The generateOrderNo() method does not need to be locked because the CAS self-rotating lock is used within AtomicInteger.
  • GetLocalIpSuffix () method does not need to add synchronization locks to non-null logic (two-way check locks, which is a secure singleton)
  • The way I implement is not the only way to solve the problem, the specific solution to the problem needs to be specific according to the current system architecture
  • Any testing is necessary, my colleague did not test himself after the first few attempts to solve this problem, not testing hurts development professionalism!