Java秒殺系統實戰系列~分佈式唯一ID生成訂單編號

摘要:

本篇博文是“Java秒殺系統實戰系列文章”的第七篇,在本博文中我們將重點介紹 “在高併發,如秒殺的業務場景下如何生成全局唯一、趨勢遞增的訂單編號”,我們將介紹兩種方法,一種是傳統的採用隨機數生成的方式,另外一種是採用當前比較流行的“分佈式唯一ID生成算法-雪花算法”來實現。

內容:

在上一篇博文,我們完成了商品秒殺業務邏輯的代碼實戰,在該代碼中,我們還實現了“當用戶秒殺成功後,需要在數據庫表中爲其生成一筆秒殺成功的訂單記錄”的功能,其對應的代碼如下所示:

//通用的方法-記錄用戶秒殺成功後生成的訂單-並進行異步郵件消息的通知
private void commonRecordKillSuccessInfo(ItemKill kill, Integer userId) throws Exception{
    //TODO:記錄搶購成功後生成的秒殺訂單記錄

    ItemKillSuccess entity=new ItemKillSuccess();

    //此處爲訂單編號的生成邏輯
    String orderNo=String.valueOf(snowFlake.nextId());
    //entity.setCode(RandomUtil.generateOrderCode());   //傳統時間戳+N位隨機數
    entity.setCode(orderNo); //雪花算法

    entity.setItemId(kill.getItemId());
    entity.setKillId(kill.getId());
    entity.setUserId(userId.toString());
    entity.setStatus(SysConstant.OrderStatus.SuccessNotPayed.getCode().byteValue());
    entity.setCreateTime(DateTime.now().toDate());
    //TODO:學以致用,舉一反三 -> 仿照單例模式的雙重檢驗鎖寫法
    if (itemKillSuccessMapper.countByKillUserId(kill.getId(),userId) <= 0){
        int res=itemKillSuccessMapper.insertSelective(entity);

        //其他邏輯省略
    }
}

在該實現邏輯中,其核心要點在於“在高併發的環境下,如何高效的生成訂單編號”,那麼如何纔算是高效呢?Debug認爲應該滿足以下兩點:

(1)保證訂單編號的生成邏輯要快、穩定,減少時延

(2)要保證生成的訂單編號全局唯一、不重複、趨勢遞增、有時序性

下面,我們採用兩種方式來生成“訂單編號”,並自己寫一個多線程的程序模擬生成的訂單編號是否滿足條件。

值得一提的是,爲了能直觀的觀察多線程併發生成的訂單編號是否具有唯一性、趨勢遞增,在這裏Debug藉助了一張數據庫表 random_code 來存儲生成的訂單編號,其DDL如下所示:

CREATE TABLE `random_code` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `code` varchar(255) DEFAULT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `idx_code` (`code`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

從該數據庫表數據結構定義語句中可以看出,我們設定了 訂單編號字段code 爲唯一!所以如果高併發多線程生成的訂單編號出現重複,那麼在插入數據庫表的時候必然會出現錯誤

下面,首先開始我們的第一種方式吧:基於隨機數的方式生成訂單編號

(1)首先是建立一個Thread類,其run方法的執行邏輯爲生成訂單編號,並將生成的訂單編號插入數據庫表中,其代碼如下所示:

/**
 * 隨機數生成的方式-Thread
 * @Author:debug (SteadyJack)
 * @Date: 2019/7/11 10:30
 **/
public class CodeGenerateThread implements Runnable{

    private RandomCodeMapper randomCodeMapper;

    public CodeGenerateThread(RandomCodeMapper randomCodeMapper) {
        this.randomCodeMapper = randomCodeMapper;
    }

    @Override
    public void run() {
    //生成訂單編號並插入數據庫
        RandomCode entity=new RandomCode();
        entity.setCode(RandomUtil.generateOrderCode());
        randomCodeMapper.insertSelective(entity);
    }
}

其中,RandomUtil.generateOrderCode()的生成邏輯是藉助ThreadLocalRandom來實現的,其完整的源代碼如下所示:

/**
 * 隨機數生成util
 * @Author:debug (SteadyJack)
 * @Date: 2019/6/20 21:05
 **/
public class RandomUtil {
    private static final SimpleDateFormat dateFormatOne=new SimpleDateFormat("yyyyMMddHHmmssSS");

    private static final ThreadLocalRandom random=ThreadLocalRandom.current();
    //生成訂單編號-方式一
    public static String generateOrderCode(){
        //TODO:時間戳+N爲隨機數流水號
        return dateFormatOne.format(DateTime.now().toDate()) + generateNumber(4);
    }

    //N爲隨機數流水號
    public static String generateNumber(final int num){
        StringBuffer sb=new StringBuffer();
        for (int i=1;i<=num;i++){
            sb.append(random.nextInt(9));
        }
        return sb.toString();
    }
}

(2)緊接着是在 BaseController控制器 中開發一個請求方法,目的正是用來模擬前端高併發觸發產生多線程並生成訂單編號的邏輯,在這裏我們暫且用1000個線程進行模擬,其源代碼如下所示:

@Autowired
private RandomCodeMapper randomCodeMapper;

//測試在高併發下多線程生成訂單編號-傳統的隨機數生成方法
@RequestMapping(value = "/code/generate/thread",method = RequestMethod.GET)
public BaseResponse codeThread(){
    BaseResponse response=new BaseResponse(StatusCode.Success);
    try {
        ExecutorService executorService=Executors.newFixedThreadPool(10);
        for (int i=0;i<1000;i++){
            executorService.execute(new CodeGenerateThread(randomCodeMapper));
        }
    }catch (Exception e){
        response=new BaseResponse(StatusCode.Fail.getCode(),e.getMessage());
    }
    return response;
}

(3)完了之後,就可以將整個項目、系統運行在外置的tomcat中了,然後打開postman,發起一個Http的Get請求,請求鏈接爲:http://127.0.0.1:8092/kill/base/code/generate/thread ,仔細觀察控制檯的輸出信息,會看一些令自己躁動不安的東西:

Java秒殺系統實戰系列~分佈式唯一ID生成訂單編號

竟然會出現“重複生成了重複的訂單編號”!而且,打開數據庫表進行觀察,會發現“他孃的1000個線程生成訂單編號,竟然只有900多個記錄”,這就說明了這麼多個線程在執行生成訂單編號的邏輯期間出現了“重複的訂單編號”!如下圖所示:

Java秒殺系統實戰系列~分佈式唯一ID生成訂單編號

因此,此種基於隨機數生成唯一ID或者訂單編號的方式,我們是可以Pass掉了(當然啦,在併發量不是很高的情況下,這種方式還是闊以使用的,因爲簡單而且易於理解啊!)

鑑於此種“基於隨機數生成”的方式在高併發的場景下並不符合我們的要求,接下來,我們將介紹另外一種比較流行的、典型的方式,即“分佈式唯一ID生成算法-雪花算法”來實現。

對於“雪花算法”的介紹,各位小夥伴可以參考Github上的這一鏈接,我覺得講得還是挺清晰的:https://github.com/souyunku/SnowFlake ,詳細的Debug在這裏就不贅述了,下面截取了部分概述:

Java秒殺系統實戰系列~分佈式唯一ID生成訂單編號

SnowFlake算法在分佈式的環境下,之所以能高效率的生成唯一的ID,我覺得其中很重要的一點在於其底層的實現是通過“位運算”來實現的,簡單來講,就是直接跟機器打交道!其底層數據的存儲結構(64位)如下圖所示:

Java秒殺系統實戰系列~分佈式唯一ID生成訂單編號

下面,我們就直接基於雪花算法來生成秒殺系統中需要的訂單編號吧!

(1)同樣的道理,我們首先定義一個Thread類,其run方法的實現邏輯是藉助雪花算法生成訂單編號並將其插入到數據庫中。

/** 基於雪花算法生成全局唯一的訂單編號並插入數據庫表中
 * @Author:debug (SteadyJack)
 * @Date: 2019/7/11 10:30
 **/
public class CodeGenerateSnowThread implements Runnable{

    private static final SnowFlake SNOW_FLAKE=new SnowFlake(2,3);

    private RandomCodeMapper randomCodeMapper;

    public CodeGenerateSnowThread(RandomCodeMapper randomCodeMapper) {
        this.randomCodeMapper = randomCodeMapper;
    }

    @Override
    public void run() {
        RandomCode entity=new RandomCode();
        //採用雪花算法生成訂單編號
        entity.setCode(String.valueOf(SNOW_FLAKE.nextId()));
        randomCodeMapper.insertSelective(entity);
    }
}

其中,SNOW_FLAKE.nextId() 的方法正是採用雪花算法生成全局唯一的訂單編號的邏輯,其完整的源代碼如下所示:

/** * 雪花算法
 * @author: zhonglinsen
 * @date: 2019/5/20
 */
public class SnowFlake {
    //起始的時間戳
    private final static long START_STAMP = 1480166465631L;

    //每一部分佔用的位數
    private final static long SEQUENCE_BIT = 12; //序列號佔用的位數
    private final static long MACHINE_BIT = 5;   //機器標識佔用的位數
    private final static long DATA_CENTER_BIT = 5;//數據中心佔用的位數

    //每一部分的最大值
    private final static long MAX_DATA_CENTER_NUM = -1L ^ (-1L << DATA_CENTER_BIT);
    private final static long MAX_MACHINE_NUM = -1L ^ (-1L << MACHINE_BIT);
    private final static long MAX_SEQUENCE = -1L ^ (-1L << SEQUENCE_BIT);

    //每一部分向左的位移
    private final static long MACHINE_LEFT = SEQUENCE_BIT;
    private final static long DATA_CENTER_LEFT = SEQUENCE_BIT + MACHINE_BIT;
    private final static long TIMESTAMP_LEFT = DATA_CENTER_LEFT + DATA_CENTER_BIT;

    private long dataCenterId;  //數據中心
    private long machineId;     //機器標識
    private long sequence = 0L; //序列號
    private long lastStamp = -1L;//上一次時間戳

    public SnowFlake(long dataCenterId, long machineId) {
        if (dataCenterId > MAX_DATA_CENTER_NUM || dataCenterId < 0) {
            throw new IllegalArgumentException("dataCenterId can't be greater than MAX_DATA_CENTER_NUM or less than 0");
        }
        if (machineId > MAX_MACHINE_NUM || machineId < 0) {
            throw new IllegalArgumentException("machineId can't be greater than MAX_MACHINE_NUM or less than 0");
        }
        this.dataCenterId = dataCenterId;
        this.machineId = machineId;
    }

    //產生下一個ID
    public synchronized long nextId() {
        long currStamp = getNewStamp();
        if (currStamp < lastStamp) {
            throw new RuntimeException("Clock moved backwards.  Refusing to generate id");
        }

        if (currStamp == lastStamp) {
            //相同毫秒內,序列號自增
            sequence = (sequence + 1) & MAX_SEQUENCE;
            //同一毫秒的序列數已經達到最大
            if (sequence == 0L) {
                currStamp = getNextMill();
            }
        } else {
            //不同毫秒內,序列號置爲0
            sequence = 0L;
        }

        lastStamp = currStamp;

        return (currStamp - START_STAMP) << TIMESTAMP_LEFT //時間戳部分
                | dataCenterId << DATA_CENTER_LEFT       //數據中心部分
                | machineId << MACHINE_LEFT             //機器標識部分
                | sequence;                             //序列號部分
    }

    private long getNextMill() {
        long mill = getNewStamp();
        while (mill <= lastStamp) {
            mill = getNewStamp();
        }
        return mill;
    }

    private long getNewStamp() {
        return System.currentTimeMillis();
    }
}

(2)緊接着,我們在BaseController中開發一個請求方法,用於模擬前端觸發高併發產生多線程搶單的場景。

/**
 * 測試在高併發下多線程生成訂單編號-雪花算法
 * @return
 */
@RequestMapping(value = "/code/generate/thread/snow",method = RequestMethod.GET)
public BaseResponse codeThreadSnowFlake(){
    BaseResponse response=new BaseResponse(StatusCode.Success);
    try {
        ExecutorService executorService=Executors.newFixedThreadPool(10);
        for (int i=0;i<1000;i++){
            executorService.execute(new CodeGenerateSnowThread(randomCodeMapper));
        }
    }catch (Exception e){
        response=new BaseResponse(StatusCode.Fail.getCode(),e.getMessage());
    }
    return response;
}

(3)完了之後,我們採用Postman發起一個Http的Get請求,其請求鏈接如下所示:http://127.0.0.1:8092/kill/base/code/generate/thread/snow ,觀察控制檯的輸出信息,可以看到“一片安然的景象”,再觀察數據庫表的記錄,可以發現,1000個線程成功觸發生成了1000個對應的訂單編號,如下圖所示:

Java秒殺系統實戰系列~分佈式唯一ID生成訂單編號

除此之外,各位小夥伴還可以將線程數從1000調整爲10000、100000甚至1000000,然後觀察控制檯的輸出信息以及數據庫表的記錄等等。

Debug親測了1w跟10w的場景下是木有問題的,100w的線程數的測試就交給各位小夥伴去試試了(時間比較長,要有心理準備哦!)至此,我們就可以將雪花算法生成全局唯一的訂單編號的邏輯應用到我們的“秒殺處理邏輯”中,即其代碼(在KillService的commonRecordKillSuccessInfo方法中)如下所示:

ItemKillSuccess entity=new ItemKillSuccess();
String orderNo=String.valueOf(snowFlake.nextId());//雪花算法
entity.setCode(orderNo); 
//其他代碼省略

補充:

1、目前,這一秒殺系統的整體構建與代碼實戰已經全部完成了,完整的源代碼數據庫地址可以來這裏下載:https://gitee.com/steadyjack/SpringBoot-SecondKill 記得Fork跟Star啊!!!

2、由於相應的博客的更新可能並不會很快,故而如果有想要快速入門以及實戰整套系統的,可以考慮聯繫Debug獲取這一“Java秒殺系統”的完整視頻教程(課程是收費的!),當然,大家也可以點擊下面這個鏈接 https://gitee.com/steadyjack/SpringBoot-SecondKill 聯繫Debug或者加入相應的技術交流羣進行交流!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章