Java秒殺系統(tǒng)(七):分布式唯一ID生成訂單編號

作者: 修羅debug
版權(quán)聲明:本文為博主原創(chuàng)文章,遵循 CC 4.0 by-sa 版權(quán)協(xié)議,轉(zhuǎn)載請附上原文出處鏈接和本聲明。



摘要:本篇博文是“Java秒殺系統(tǒng)實戰(zhàn)系列文章”的第七篇,在本博文中我們將重點介紹 “在高并發(fā),如秒殺的業(yè)務(wù)場景下如何生成全局唯一、趨勢遞增的訂單編號”,我們將介紹兩種方法,一種是傳統(tǒng)的采用隨機數(shù)生成的方式,另外一種是采用當前比較流行的“分布式唯一ID生成算法-雪花算法”來實現(xiàn)。

內(nèi)容:在上一篇博文,我們完成了商品秒殺業(yè)務(wù)邏輯的代碼實戰(zhàn),在該代碼中,我們還實現(xiàn)了“當用戶秒殺成功后,需要在數(shù)據(jù)庫表中為其生成一筆秒殺成功的訂單記錄”的功能,其對應的代碼如下所示:

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

ItemKillSuccess entity=new ItemKillSuccess();

//此處為訂單編號的生成邏輯
String orderNo=String.valueOf(snowFlake.nextId());
//entity.setCode(RandomUtil.generateOrderCode()); //傳統(tǒng)時間戳+N位隨機數(shù)
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);

//其他邏輯省略
}
}

在該實現(xiàn)邏輯中,其核心要點在于“在高并發(fā)的環(huán)境下,如何高效的生成訂單編號”,那么如何才算是高效呢?Debug認為應該滿足以下兩點:

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

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

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

值得一提的是,為了能直觀的觀察多線程并發(fā)生成的訂單編號是否具有唯一性、趨勢遞增,在這里Debug借助了一張數(shù)據(jù)庫表 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;

從該數(shù)據(jù)庫表數(shù)據(jù)結(jié)構(gòu)定義語句中可以看出,我們設(shè)定了 訂單編號字段code 為唯一!所以如果高并發(fā)多線程生成的訂單編號出現(xiàn)重復,那么在插入數(shù)據(jù)庫表的時候必然會出現(xiàn)錯誤

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

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

/**
* 隨機數(shù)生成的方式-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() {
//生成訂單編號并插入數(shù)據(jù)庫
RandomCode entity=new RandomCode();
entity.setCode(RandomUtil.generateOrderCode());
randomCodeMapper.insertSelective(entity);
}
}


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

/**
* 隨機數(shù)生成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為隨機數(shù)流水號
return dateFormatOne.format(DateTime.now().toDate()) + generateNumber(4);
}

//N為隨機數(shù)流水號
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控制器 中開發(fā)一個請求方法,目的正是用來模擬前端高并發(fā)觸發(fā)產(chǎn)生多線程并生成訂單編號的邏輯,在這里我們暫且用1000個線程進行模擬,其源代碼如下所示:  

@Autowired
private RandomCodeMapper randomCodeMapper;

//測試在高并發(fā)下多線程生成訂單編號-傳統(tǒng)的隨機數(shù)生成方法
@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) 完了之后,就可以將整個項目、系統(tǒng)運行在外置的tomcat中了,然后打開postman,發(fā)起一個Http的Get請求,請求鏈接為:http://127.0.0.1:8092/kill/base/code/generate/thread ,仔細觀察控制臺的輸出信息,會看一些令自己躁動不安的東西:  



竟然會出現(xiàn)“重復生成了重復的訂單編號”!而且,打開數(shù)據(jù)庫表進行觀察,會發(fā)現(xiàn)“他娘的1000個線程生成訂單編號,竟然只有900多個記錄”,這就說明了這么多個線程在執(zhí)行生成訂單編號的邏輯期間出現(xiàn)了“重復的訂單編號”!如下圖所示:  



因此,此種基于隨機數(shù)生成唯一ID或者訂單編號的方式,我們是可以Pass掉了(當然啦,在并發(fā)量不是很高的情況下,這種方式還是闊以使用的,因為簡單而且易于理解?。。?/span>

鑒于此種“基于隨機數(shù)生成”的方式在高并發(fā)的場景下并不符合我們的要求,接下來,我們將介紹另外一種比較流行的、典型的方式,即“分布式唯一ID生成算法-雪花算法”來實現(xiàn)。

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



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


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

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

/** 基于雪花算法生成全局唯一的訂單編號并插入數(shù)據(jù)庫表中
* @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;

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

//每一部分的最大值
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; //數(shù)據(jù)中心
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;
}

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

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

lastStamp = currStamp;

return (currStamp - START_STAMP) << TIMESTAMP_LEFT //時間戳部分
| dataCenterId << DATA_CENTER_LEFT //數(shù)據(jù)中心部分
| 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中開發(fā)一個請求方法,用于模擬前端觸發(fā)高并發(fā)產(chǎn)生多線程搶單的場景。  

/**
* 測試在高并發(fā)下多線程生成訂單編號-雪花算法
* @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發(fā)起一個Http的Get請求,其請求鏈接如下所示:http://127.0.0.1:8092/kill/base/code/generate/thread/snow ,觀察控制臺的輸出信息,可以看到“一片安然的景象”,再觀察數(shù)據(jù)庫表的記錄,可以發(fā)現(xiàn),1000個線程成功觸發(fā)生成了1000個對應的訂單編號,如下圖所示: 

 


除此之外,各位小伙伴還可以將線程數(shù)從1000調(diào)整為10000、100000甚至1000000,然后觀察控制臺的輸出信息以及數(shù)據(jù)庫表的記錄等等。

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

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


補充:

1、目前,這一秒殺系統(tǒng)的整體構(gòu)建與代碼實戰(zhàn)已經(jīng)全部完成了,該秒殺系統(tǒng)對應的視頻教程的鏈接地址為:https://www.fightjava.com/web/index/course/detail/6,可以點擊鏈接進行試看以及學習,實戰(zhàn)期間有任何問題都可以留言或者與Debug聯(lián)系、交流!

2、另外,Debug也開源了該秒殺系統(tǒng)對應的完整的源代碼以及數(shù)據(jù)庫,其地址可以來這里下載:https://gitee.com/steadyjack/SpringBoot-SecondKill 記得Fork跟Star?。。?!

3、最后,不要忘記了關(guān)注一下Debug的技術(shù)微信公眾號: