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

摘要:

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

內容:

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

//通用的方法-記錄用戶秒殺成功後生成的訂單-並進行異步郵件消息的通知
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認爲應該知足如下兩點:github

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

(2)要保證生成的訂單編號全局惟1、不重複、趨勢遞增、有時序性數據庫

下面,咱們採用兩種方式來生成「訂單編號」,並本身寫一個多線程的程序模擬生成的訂單編號是否知足條件。tomcat

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

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 ,仔細觀察控制檯的輸出信息,會看一些令本身躁動不安的東西:

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

所以,此種基於隨機數生成惟一ID或者訂單編號的方式,咱們是能夠Pass掉了(固然啦,在併發量不是很高的狀況下,這種方式仍是闊以使用的,由於簡單並且易於理解啊!)

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

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

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

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

(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個對應的訂單編號,以下圖所示:



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

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

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

補充:

一、目前,這一秒殺系統的總體構建與代碼實戰已經所有完成了,完整的源代碼數據庫地址能夠來這裏下載:gitee.com/steadyjack/… 記得Fork跟Star啊!!!

相關文章
相關標籤/搜索