溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊(cè)×
其他方式登錄
點(diǎn)擊 登錄注冊(cè) 即表示同意《億速云用戶服務(wù)條款》

分布式業(yè)務(wù)系統(tǒng)中全局ID生成策略的示例分析

發(fā)布時(shí)間:2022-01-15 11:05:32 來(lái)源:億速云 閱讀:138 作者:小新 欄目:軟件技術(shù)

這篇文章主要為大家展示了“分布式業(yè)務(wù)系統(tǒng)中全局ID生成策略的示例分析”,內(nèi)容簡(jiǎn)而易懂,條理清晰,希望能夠幫助大家解決疑惑,下面讓小編帶領(lǐng)大家一起研究并學(xué)習(xí)一下“分布式業(yè)務(wù)系統(tǒng)中全局ID生成策略的示例分析”這篇文章吧。

一、全局ID簡(jiǎn)介

在實(shí)際的開發(fā)中,幾乎所有的業(yè)務(wù)場(chǎng)景產(chǎn)生的數(shù)據(jù),都需要一個(gè)唯一ID作為核心標(biāo)識(shí),用來(lái)流程化管理。比如常見的:

  • 訂單:order-id,查訂單詳情,物流狀態(tài)等;

  • 支付:pay-id,支付狀態(tài),基于ID事務(wù)管理;

如何生成唯一標(biāo)識(shí),在普通場(chǎng)景下,一般的方法就可以解決,例如:

import java.util.UUID;
public class UuidUtil {
    public static String getUUid() {
        UUID uuid = UUID.randomUUID();
        return String.valueOf(uuid).replace("-","");
    }
}

這個(gè)方法可以解決絕大部分唯一ID需求的場(chǎng)景業(yè)務(wù),但是網(wǎng)上各種UUID重復(fù)場(chǎng)景的描述帖,說(shuō)的好像該API不好用。

絮叨一句:說(shuō)一個(gè)真實(shí)使用的業(yè)務(wù)場(chǎng)景,大概是半年近3000萬(wàn)的數(shù)據(jù)流水,用的就是UUID的API,暫時(shí)未捕捉到ID重復(fù)的問(wèn)題,僅供參考。

二、雪花算法

1、概念簡(jiǎn)介

Twitter公司開源的分布式ID生成算法策略,生成的ID遵循時(shí)間的順序。

分布式業(yè)務(wù)系統(tǒng)中全局ID生成策略的示例分析

  • 1為位標(biāo)識(shí),始終為0,不可用;

  • 41位時(shí)間截,存儲(chǔ)時(shí)間截的差值(當(dāng)前時(shí)間截-開始時(shí)間截);

  • 10位的機(jī)器標(biāo)識(shí),10位的長(zhǎng)度最多支持部署1024個(gè)節(jié)點(diǎn);

  • 12位序列,毫秒內(nèi)的計(jì)數(shù),12位的計(jì)數(shù)順序號(hào)支持每個(gè)節(jié)點(diǎn)每毫秒產(chǎn)生4096個(gè)ID序號(hào);

SnowFlake的優(yōu)點(diǎn)是,整體上按照時(shí)間自增排序,并且整個(gè)分布式系統(tǒng)內(nèi)不會(huì)產(chǎn)生ID碰撞(由數(shù)據(jù)中心ID和機(jī)器ID作區(qū)分),并且效率較高。

2、編碼實(shí)現(xiàn)

工具類中很多可以自定義的,比如起始時(shí)間,機(jī)器ID配置等。

/**
 * 雪花算法ID生成
 */
public class SnowIdWorkerUtil {
    // 開始時(shí)間截 (2020-01-02)
    private final long timeToCut = 1577894400000L;
    // 機(jī)器ID所占的位數(shù)
    private final long workerIdBits = 2L;
    // 數(shù)據(jù)標(biāo)識(shí)ID所占的位數(shù)
    private final long dataCenterIdBits = 8L;
    // 支持的最大機(jī)器ID,結(jié)果是31 (這個(gè)移位算法可以很快的計(jì)算出幾位二進(jìn)制數(shù)所能表示的最大十進(jìn)制數(shù))
    private final long maxWorkerId = -1L ^ (-1L << workerIdBits);
    // 支持的最大數(shù)據(jù)標(biāo)識(shí)ID,結(jié)果是31
    private final long maxDataCenterId = -1L ^ (-1L << dataCenterIdBits);
    // 序列在ID中占的位數(shù)
    private final long sequenceBits = 12L;
    // 機(jī)器ID向左移12位
    private final long workerIdShift = sequenceBits;
    // 數(shù)據(jù)標(biāo)識(shí)ID向左移17位(12+5)
    private final long dataCenterIdShift = sequenceBits + workerIdBits;
    // 時(shí)間截向左移22位(5+5+12)
    private final long timestampLeftShift = sequenceBits + workerIdBits + dataCenterIdBits;
    // 生成序列的掩碼
    private final long sequenceMask = -1L ^ (-1L << sequenceBits);
    // 工作機(jī)器ID(0~31)
    private long workerId;
    // 數(shù)據(jù)中心ID(0~31)
    private long dataCenterId;
    // 毫秒內(nèi)序列(0~4095)
    private long sequence = 0L;
    // 上次生成ID的時(shí)間截
    private long lastTimestamp = -1L;
    /**
     * 構(gòu)造函數(shù)
     * @param workerId 工作ID (0~31)
     * @param dataCenterId 數(shù)據(jù)中心ID (0~31)
     */
    public SnowIdWorkerUtil (long workerId, long dataCenterId) {
        if (workerId > maxWorkerId || workerId < 0) {
            throw new IllegalArgumentException("workerId 不符合條件");
        }
        if (dataCenterId > maxDataCenterId || dataCenterId < 0) {
            throw new IllegalArgumentException("dataCenterId 不符合條件");
        }
        this.workerId = workerId;
        this.dataCenterId = dataCenterId;
    }
    public synchronized String nextIdVar(){
        return String.valueOf(nextId());
    }
    /**
     * 線程安全,獲得下一個(gè)ID
     */
    private synchronized long nextId() {
        long timestamp = timeGen();
        // 如果當(dāng)前時(shí)間小于上一次ID生成的時(shí)間戳,拋出異常
        if (timestamp < lastTimestamp) {
            throw new RuntimeException(String.format(
                                        "時(shí)間順序異常,時(shí)間差(上次時(shí)間-現(xiàn)在)=%d",
                                        lastTimestamp - timestamp));
        }
        // 如果是同一時(shí)間生成的,則進(jìn)行毫秒內(nèi)序列
        if (lastTimestamp == timestamp) {
            sequence = (sequence + 1) & sequenceMask;
            //毫秒內(nèi)序列溢出
            if (sequence == 0) {
                //阻塞到下一個(gè)毫秒,獲得新的時(shí)間戳
                timestamp = tilNextMillis(lastTimestamp);
            }
        } else {
            // 時(shí)間戳改變,毫秒內(nèi)序列重置
            sequence = 0L;
        }
        // 上次生成ID的時(shí)間截
        lastTimestamp = timestamp;
        // 移位并通過(guò)或運(yùn)算拼到一起組成64位的ID
        return ((timestamp - timeToCut) << timestampLeftShift)
                | (dataCenterId << dataCenterIdShift)
                | (workerId << workerIdShift) | sequence;
    }
    /**
     * 阻塞,獲得新的時(shí)間戳
     */
    private long tilNextMillis(long lastTimestamp) {
        long timestamp = timeGen();
        while (timestamp <= lastTimestamp) {
            timestamp = timeGen();
        }
        return timestamp;
    }
    /**
     * 返回當(dāng)前時(shí)間節(jié)點(diǎn)
     */
    private long timeGen() {
        return System.currentTimeMillis();
    }
    public static void main(String[] args) {
        // 參數(shù)在實(shí)際業(yè)務(wù)下需要配置管理
        SnowIdWorkerUtil idWorker = new SnowIdWorkerUtil(1, 1);
        for (int i = 0; i < 100; i++) {
            String id = idWorker.nextIdVar();
            System.out.println(id+"  "+id.length()+"位");
        }
    }
}

三、自定義實(shí)現(xiàn)

還有一種常見的實(shí)現(xiàn)思路,基于數(shù)據(jù)庫(kù)的自增主鍵ID,不過(guò)基于這個(gè)原理,卻有各種不同的實(shí)現(xiàn)策略。

簡(jiǎn)單表結(jié)構(gòu):

CREATE TABLE `du_temp_id` (
  `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主鍵id',
  `create_time` datetime DEFAULT NULL COMMENT '創(chuàng)建時(shí)間',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='主鍵ID臨時(shí)表';

分布式業(yè)務(wù)系統(tǒng)中全局ID生成策略的示例分析

1、基于主鍵

這種模式的原理比較單調(diào),向臨時(shí)表寫入一條記錄,借助MySQL生成的唯一主鍵ID,然后拿出來(lái)稍微處理一下,作為各種業(yè)務(wù)場(chǎng)景的唯一ID使用。

@Service
public class TempIdServiceImpl implements TempIdService {
    @Resource
    private TempIdMapper tempIdMapper ;
    @Override
    public List<String> getIdList() {
        List<String> idList = new ArrayList<>() ;
        TempIdEntity tempIdEntity = new TempIdEntity ();
        tempIdEntity.setCreateTime(new Date());
        for (int i = 0 ; i < 10 ; i++){
            tempIdMapper.insert(tempIdEntity);
            idList.add(UuidUtil.getNoId(8,Long.parseLong(tempIdEntity.getId().toString()))) ;
        }
        return idList ;
    }
}

問(wèn)題點(diǎn):如果作為ID生成的臨時(shí)表所在的MySQL服務(wù)宕掉,那可能會(huì)影響整個(gè)業(yè)務(wù)流程,造成雪崩效應(yīng)。

2、高可用集群

單服務(wù)如果不能安穩(wěn)的支撐業(yè)務(wù)需求,很自然集群模式就該上場(chǎng)了。提供多臺(tái)MySQL服務(wù)[A,B,C],處理策略也不止一種:

  • 庫(kù)設(shè)置主鍵自增策略

例如A庫(kù)[1,4,7],B庫(kù)[2,5,8],C庫(kù)[3,6,9],基于不同自增規(guī)則,生成統(tǒng)一的自增唯一標(biāo)識(shí)。

  • 生成ID做分庫(kù)標(biāo)識(shí)

這種先把ID生成,然后不同的數(shù)據(jù)庫(kù)生成的ID給一個(gè)不同的標(biāo)識(shí),例如UIDA,UIDB,UIDC。

@Service
public class TempIdServiceImpl implements TempIdService {
    @Resource
    private TempIdMapper tempIdMapper ;
    @Override
    public List<String> getRouteIdList() {
        List<String> idList = new ArrayList<>() ;
        TempIdEntity tempIdEntity = new TempIdEntity ();
        tempIdEntity.setCreateTime(new Date());
        for (int i = 0 ; i < 2 ; i++){
            tempIdMapper.insertA(tempIdEntity);
            idList.add(UuidUtil.getRouteId("UID-A",10,
                       Long.parseLong(tempIdEntity.getId().toString()))) ;
            tempIdMapper.insertB(tempIdEntity);
            idList.add(UuidUtil.getRouteId("UID-B",10,
                    Long.parseLong(tempIdEntity.getId().toString()))) ;
            tempIdMapper.insertC(tempIdEntity);
            idList.add(UuidUtil.getRouteId("UID-C",10,
                    Long.parseLong(tempIdEntity.getId().toString()))) ;
        }
        return idList ;
    }
}

結(jié)果樣例:

UID-A00001,UID-B00001,UID-C00001

UID-A00002,UID-B00002,UID-C00002

3、ID樣式優(yōu)化

從數(shù)據(jù)獲取的ID基本是一個(gè)自增的整數(shù)序列,可以提供一個(gè)格式美化工具方法。

public class UuidUtil {
    private static final String ZERO = "00000000000";
    private static final String PREFIX = "UID";
    public static String getNoId(int length,Long id){
        String idVar = String.valueOf(id) ;
        if (idVar.length()>length){
            return PREFIX+idVar ;
        } else {
            int gapLen = length-idVar.length()-PREFIX.length() ;
            return PREFIX+ZERO.substring(0,gapLen)+idVar ;
        }
    }
    public static String getRouteId(String route,Integer length,Long id){
        String idVar = String.valueOf(id) ;
        if (idVar.length()>length){
            return route+idVar ;
        } else {
            int gapLen = length-idVar.length()-route.length() ;
            return route+ZERO.substring(0,gapLen)+idVar ;
        }
    }
}

基于不同的策略,把ID格式為統(tǒng)一的位數(shù)。

4、性能問(wèn)題

如果在高并發(fā)的業(yè)務(wù)場(chǎng)景下,實(shí)時(shí)基于MySQL去生成唯一ID容易產(chǎn)生性能瓶頸,當(dāng)然其他方法也可能產(chǎn)生這個(gè)問(wèn)題。可以在系統(tǒng)空閑時(shí)間批量生成一批,放入緩存中,在使用的時(shí)候直接從緩存層取出即可。

以上是“分布式業(yè)務(wù)系統(tǒng)中全局ID生成策略的示例分析”這篇文章的所有內(nèi)容,感謝各位的閱讀!相信大家都有了一定的了解,希望分享的內(nèi)容對(duì)大家有所幫助,如果還想學(xué)習(xí)更多知識(shí),歡迎關(guān)注億速云行業(yè)資訊頻道!

向AI問(wèn)一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如果涉及侵權(quán)請(qǐng)聯(lián)系站長(zhǎng)郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。

id
AI