新聞中心
在業(yè)務(wù)開發(fā)中經(jīng)常會遇到各種單號生成, 例如快遞單號、服務(wù)單號、訂單號等等。 這些單號生成往往是業(yè)務(wù)邏輯處理的第一步, 單號生成出問題,必然導(dǎo)致業(yè)務(wù)走不下去;另外有多少業(yè)務(wù)量就會至少有多少的單號生成需求。所以單號生成必須高可用,必須高性能。 另外業(yè)務(wù)不同需要的單號規(guī)則可能也不相同, 所以單號服務(wù)還必須具備足夠的擴展性。

一、單號定義
在進入正題之前我們先給單號下個定義, 看幾個常見的單號形式。
單號是一個數(shù)字和字符組成的序列, 它要滿足兩個條件: 一個是唯一, 保證唯一才可以作為業(yè)務(wù)標(biāo)識; 另一個是符合業(yè)務(wù)需要的規(guī)則。 例如下面三個單號:
- 2017030400001 這個單號由兩個部分序列號日期20170304+定長5位補0數(shù)字00001。
- 010-6541-00001 此單號分三部分, 中間用減號連接, 第一部分為區(qū)號, 第二部分為作業(yè)單位號碼, 第三部分為作業(yè)單位產(chǎn)生作業(yè)的序號。
- QJ000001 則是由字符QJ開頭后面跟隨數(shù)字序列的單號。
二、單號數(shù)字序列部分的生成
上述單號定義中的數(shù)字部分通常是一個自增的數(shù)字序列。 我們可以通過數(shù)據(jù)庫的自增列、 數(shù)據(jù)庫的列+1方式、 redis或者memcached的INCR指令來生成這種數(shù)字的序列。 這四種方式都可以生成序列, 但各自有各自的好處。
1. 數(shù)據(jù)庫自增列的方式
是通過數(shù)據(jù)庫的內(nèi)部機制生成的, 在普通PC上每秒約可以生成4000個數(shù)字序列, 它的好處是每一個數(shù)字序列都會保留一條記錄, 記錄生成使用時間, 缺點是吞吐量一般, 會占用一定的數(shù)據(jù)庫資源, 如下是一種推薦的表結(jié)構(gòu):
- CREATE TABLE `xx_code_sequence` (
- `id` bigint(20) NOT NULL AUTO_INCREMENT,
- `generate_time` timestamp NOT NULL
- default CURRENT_TIMESTAMP,
- PRIMARY KEY (`id`)
- ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULTCHARSET=utf8;
此表有兩列, id列為bigint類型的自增長字段,作為數(shù)字序列的值, generate_time時間戳字段可以記錄每一個單號的生成時間。生成數(shù)字序列的方式用sql說明如下:
- begin trans;
- insert into `xx_code_sequence`(generate_time)values(current_timestamp);
- select last_insert_id();
- commit;
說明:
- 表名格式xx_code_sequence,sto_code_0分為三部分sto為ownerKey, code固定不變,0表示表的序號,可以有多個下標(biāo)不同表來支持更高的并發(fā),共有幾個表需要在開始確認了,確認的依據(jù)是需要滿足的并發(fā)請求。表的個數(shù)必須是2的n次方,例如1, 2, 4, 8,16;
- `id` 即序列的部分值,是通過mysql的自增特性生成的,最終的序列值是id和表序號共同組成的,假定有4個表,序號分別為0,1,2,3;那么序列值為 id<< 2 | table_index; 即id向左移位2位(移位幾位取決于表的個數(shù)),然后和表序號求或;
- `generate_time` 為id生成時間,無其他含義。
不同序號的表可以建在不同的數(shù)據(jù)庫上,當(dāng)某個序號的表不可用時要報警,并切換到其他表上生成數(shù)字序列。
2. 數(shù)據(jù)庫的列+1方式
通過對數(shù)據(jù)庫的某列做+1操作, 來得到唯一的數(shù)字序列, 是通過數(shù)據(jù)庫的行鎖來保障唯一的, 因為涉及到行鎖, 所以這種方式生成序列的單行吞吐量不會太大, 適合需要生成多種(每一種放到一行)不同數(shù)字生成需求。 如下是一種推薦的表結(jié)構(gòu):
- create table `xx_rowbased_sequence` (
- `owner_key` varchar(32) NOT NULL,
- `current_value` bigint NOT NULL,
- PRIMARY KEY (`owner_key`)
- );
表中的ownerKey列為單號種類標(biāo)識, current_value為+1操作列。生成序列的方式用sql說明如下
- begin trans;
- UPDATE `xx_rowbased_sequence`SET current_valuecurrent_value=current_value+1 WHERE owner_key=’order-no’;
- SELECT current_value FROM `xx_rowbased_sequence` WHERE owner_key=’order-no’;
- commit;
需要注意使用此方式生成數(shù)字序列事務(wù)隔離級別需要是RR。
3. 使用redis/memcached的INCR指令方式
redis/memcached本身可以保證生成數(shù)字的唯一性,和高性能。 單一redis服務(wù)器每秒可以生成約6w左右的數(shù)字序列。 但需要注意redis必須配置主從和存儲, 以避免在極端情況下redis節(jié)點down機, 導(dǎo)致丟失序列或序列重復(fù)。
三、高可用實現(xiàn)
上面介紹了4種生成數(shù)字序列的方式, 但要保證高可用, 單靠一種序列生成方式還是不夠的, 我們還需要一種高可用的實現(xiàn)。
高可用數(shù)字序列生成器內(nèi)部是2的n次方個底層數(shù)字序列生成器, 每個底層序列生成器對應(yīng)一個下標(biāo)值, 下標(biāo)值的范圍為[0, 2n-1]。 在生成序列時, 輪詢底層生成器, 如果正常, 則將生成結(jié)果向左移n位, 并與當(dāng)前底層序列生成器下標(biāo)取或得到最終序列值。 如果底層序列生成器發(fā)生異常, 則將其標(biāo)記為不可用, 并輪詢下一個底層序列生成器, 直到成功。
高可用實現(xiàn)類com.jd.coo.sa.sequence.ha.BitwiseLoadBalanceSequenceGen,其內(nèi)部有x個底層SequenceGen實現(xiàn),此類會輪詢的調(diào)用底層SequenceGen來生成序列,如果某個底層序列生成出錯,會從可用列表中移除掉,被移除掉的底層SequenceGen在過xx時間(默認為5分鐘)后,可以重新加入到可用列表中。如果內(nèi)部序列生成單個序列時間超時,并在最近n時間內(nèi)連續(xù)超時x次,會被移動到異常列表,在異常列表中時間超過xx時間后,也會被重新放入可用列表中。
如果一個底層序列被標(biāo)記為不可用, 過配置時間后會將其恢復(fù)到可用列表中, 自動恢復(fù)機制可以避免底層序列生成器已恢復(fù)可用, 而程序卻一直不使用此底層序列生成器的情況。
高可用實現(xiàn)的內(nèi)部結(jié)構(gòu)圖, 如下圖所示:
其核心方法如下所示:
- public long gen(String ownerKey){
- long sequence=0;
- int currentPartitionIndex=-1;
- SequenceGen innerGen=null;
- do{
- long startTime=System.currentTimeMillis();
- boolean hasError=false;
- try{
- currentPartitionIndex=getCurrentPartitionIndex(ownerKey);
- LOGGER.trace("current partition index {}",currentPartitionIndex);
- innerGen=innerSequences.get(currentPartitionIndex);
- if(innerGen==SkipSequence.INSTANCE){
- LOGGER.warn("current partition index {} is skipped",currentPartitionIndex);
- if(availablePartitionIndices.contains(currentPartitionIndex)){
- LOGGER.warn("current partition index {} is skipped, remove it",currentPartitionIndex);
- availablePartitionIndices.remove(Integer.valueOf(currentPartitionIndex));
- }
- continue;
- }
- HighAvailablePartitionHolder.setPartition(currentPartitionIndex);
- sequence=innerGen.gen(ownerKey);
- onGenNewId(ownerKey,currentPartitionIndex,sequence);
- LOGGER.trace("genNewId {} with inner {}",sequence,currentPartitionIndex);
- break;
- }catch(SequenceOutOfRangeException ex){
- LOGGER.error("gen error SequenceOutOfRangeException index {} total available {}",
- currentPartitionIndex,
- availablePartitionIndices.size());
- hasError=true;
- LOGGER.error("set {} to SKIP",currentPartitionIndex);
- this.innerSequences.set(currentPartitionIndex,SkipSequence.INSTANCE);
- onError(ownerKey,currentPartitionIndex,innerGen,ex);
- LOGGER.error("after onError total available {}/{}",currentPartitionIndex,
- availablePartitionIndices.size());
- }catch(Exception ex){
- LOGGER.error("gen error index {} total available {}",currentPartitionIndex,
- availablePartitionIndices.size());
- LOGGER.error("gen error ",ex);
- hasError=true;
- onError(ownerKey,currentPartitionIndex,innerGen,ex);
- LOGGER.error("after onError total available {}/{}",currentPartitionIndex,
- availablePartitionIndices.size());
- }finally{
- long usedTime=System.currentTimeMillis()-startTime;
- boolean isTimeout=usedTime>timeoutThresholdInMilliseconds;
- if(!hasError&&isTimeout){
- onTimeout(currentPartitionIndex,innerGen,usedTime);
- }
- LOGGER.trace("gen usedTime {}",usedTime);
- }
- }while(true);
- return sequence;
- }
使用時配置bean使用即可, 如下spring bean xml配置:
四、高性能實現(xiàn)
單號生成只是業(yè)務(wù)操作的第一個步驟, 業(yè)務(wù)操作往往是復(fù)雜耗時的, 我們必須保證單號生成的性能, 使其幾乎不會影響業(yè)務(wù)時間。
上述介紹的四種序列生成方式都是跨網(wǎng)絡(luò)通過中間件獲得的序列號,要進一步優(yōu)化其性能,我們需要將序列放在離CPU更近的地方――內(nèi)存中。我們使用如下兩種方式將數(shù)字序列放到CPU更近的地方:
- 將內(nèi)部序列值向左移位n位, 然后序列的最右n位在內(nèi)存生成,一次生成2的n次方個數(shù)字序列, 然后放在內(nèi)存隊列中;
- 異步提前生成:實時計算序列號方法被調(diào)用的速度, 然后在異步線程(池)中生成最近x ms需要的序列,放入內(nèi)存隊列中備用
這兩種方式并不一定都需要, 置放入內(nèi)存隊列中的數(shù)字序列越多,重啟時丟失的也會越多。
其內(nèi)部結(jié)構(gòu)圖示如下:
高性能序列使用的bean配置如下:
通過設(shè)定memoryBitLength,指定序列的最右的memoryBitLength位在內(nèi)存中生成以提高生成的效率。 需要注意memoryBitLength值越大則在內(nèi)存中的序列條數(shù)越多, 性能越高, 如果發(fā)生重啟時丟失的序列也會越多, 要根據(jù)情況來設(shè)置。 支持異步生成序列值, 異步生成的速度會根據(jù)序列值消費速度自適應(yīng)。
五、關(guān)于可擴展性
單號規(guī)則多種多樣, 不能每增加一種規(guī)則就增加一個需求, 我們需要相對靈活的擴展性。 上述介紹了多種單號數(shù)字序列的生成方式, 和數(shù)字序列生成的高可用和高性能實現(xiàn), 他們都實現(xiàn)了同一個接口:
- /**
- * 根據(jù)序列業(yè)務(wù)類型生成新序列的接口
- *
- * 生成序列是大致遞增的
- *
- * Created by zhaoyukai on 2016/8/8.
- */
- public interface SequenceGen {
- /**
- * 生成序列
- * @param ownerKey 序列業(yè)務(wù)key
- * @return 新序列值
- */
- long gen(String ownerKey);
- }
有了這個統(tǒng)一的數(shù)字序列生成接口, 我們可以擴展多種不同的數(shù)字序列生成方式。 或者實現(xiàn)不同的高可用、高性能機制。
另外在本文的開頭我們介紹了多種不同的單號生成規(guī)則, 要靈活滿足這些不同的規(guī)則, 我們使用表達式來表達單號的組合規(guī)則, 通過將表達式解析成不同的Expression來實現(xiàn)不同單號部分的生成。 下面我們看一個單號表達式的示例, 如下是一個spring bean配置:
SmartSNGen類負責(zé)根據(jù)表達式生成不同規(guī)則的單號,其構(gòu)造函數(shù)第一個參數(shù)值:
- @{ownerKey, value=SN}-@{bean, ref=sequence}-
@{com.jd.coo.sa.sn.expression.CheckSumExpression} 即為表達式, 該表達式分為五個部分:
- @{ownerKey, value=SN} 在表達式生成的上下文中寫入key為ownerKey值為SN的參數(shù)
- “-“ 表示靜態(tài)表達式“-”
- @{bean, ref=sequence} 指定引用id為sequence的spring bean來生成表達式的一部分
- “-“表示靜態(tài)表達式”-“
- @{com.jd.coo.sa.sn.expression.CheckSumExpression} 表示要創(chuàng)建指定類com.jd.coo.sa.sn.expression.CheckSumExpression的實例來生成表達式的一部分
該bean的interpreter屬性指定了表達式的解釋器,該解釋器會將表達式值轉(zhuǎn)換為實現(xiàn)了Expression接口的對象,通過該對象可以計算出單號的值。
表達式解釋器查找表達式中的“@{”和“}”對,將其內(nèi)部的表達式解析為動態(tài)表達式,將其他部分的表達式解析為靜態(tài)表達式。動態(tài)表達式分為三種類型:
- spring配置文件中的bean引用表達式
- 指定上下文參數(shù)的表達式
- 指定自定義類型的表達式
第3種表達式留出任意擴展自定義表達式的擴展點。
Expression接口定義如下:
- import com.jd.coo.sa.sn.GenContext;
- /**
- * SmartSNGen表達式接口
- *
- * Created by zhaoyukai on 2016/10/18.
- */
- public interface Expression {
- /**
- * 計算表達式的值
- * @param context 表達式計算上下文, 表達式可以根據(jù)需要將計算中間值存儲到上下文中, 以便在表達式之間共享數(shù)據(jù)
- * @return 表達式計算值
- */
- Object eval(GenContext context);
- /**
- * 計算優(yōu)先級, 優(yōu)先級越高越先執(zhí)行, 如果表達式需要依賴其他表達式的值, 則要在依賴表達式計算之后執(zhí)行
- * @return 執(zhí)行順序
- */
- ExecuteOrder executeOrder();
- /**
- * 該表達式的最大字符串長度值
- *
- * @return 最大長度值
- */
- int maxLength();
- }
通過實現(xiàn)此接口即可實現(xiàn)任何自定義的單號生成邏輯。如下是自定義的單號校驗位生成表達式示例:
- public class CheckSumExpressionimplements Expression {
- public Object eval(GenContext context) {
- Long newId = (Long) context.get("sequence");
- if (newId == null) {
- throw newRuntimeException("sequence can not be null when calculate checksum");
- }
- return newId * 9 % 31 % 10;
- }
- public ExecuteOrder executeOrder() {
- return ExecuteOrder.AfterNormal;
- }
- public int maxLength() {
- return 1;
- }
- }
總結(jié)
本文提到了多種單號數(shù)字序列生成方式,還介紹了高可用、高性能以及擴展性的實現(xiàn)方式。
- 要根據(jù)場景, 并發(fā)量, 單號類型數(shù)量選擇數(shù)字序列生成方式;
- 不要裸奔, 要使用高可用+高性能序列生成器, 保證單號生成方式的可用性和性能;
- 底層序列要從物理上做隔離, 否則出現(xiàn)硬件故障高可用機制也會時效;
- 使用了多個底層序列生成方式時生成的序列是大致自增, 不能保證完全自增, 這是設(shè)計使然, 如果要保證完全自增, 則會出現(xiàn)單點, 在完全自增和單點的選擇上, 我們選擇了大致自增+非單點;
- 高性能序列生成的性能可以通過調(diào)節(jié)其memoryBitLength屬性來提高, 但要根據(jù)業(yè)務(wù)實際情況來做選擇,memoryBitLength屬性值越高在內(nèi)存生成的序列數(shù)越多,性能越高, 但在進程停止時丟失的序列也會越多。
作者:趙玉開,十年以上互聯(lián)網(wǎng)研發(fā)經(jīng)驗,2013年加入京東,在運營研發(fā)部任架構(gòu)師,期間先后主持了物流系統(tǒng)自動化運維平臺、青龍數(shù)據(jù)監(jiān)控系統(tǒng)和物流開放平臺的研發(fā)工作,具有豐富的物流系統(tǒng)業(yè)務(wù)和架構(gòu)經(jīng)驗。在此之前在和訊網(wǎng)負責(zé)股票基金行情系統(tǒng)的研發(fā)工作,具備高并發(fā)、高可用互聯(lián)網(wǎng)應(yīng)用研發(fā)經(jīng)驗。
【本文來自專欄作者張開濤的微信公眾號(開濤的博客),公眾號id: kaitao-1234567】
戳這里,看該作者更多好文
網(wǎng)站題目:高可用高性能可擴展的單號生成方案
網(wǎng)址分享:http://m.5511xx.com/article/copdgdh.html


咨詢
建站咨詢
