分布式主鍵生成算法

這篇文章總結(jié)了分布式主鍵或者唯一鍵的生成算法颁督,文章最后有我們基于snowflow算法的思考和實(shí)踐。

分布式主鍵的生成方式分為中心化和去中心化兩大類赚哗。

中心化生成算法

中心化生成算法經(jīng)典的方案主要有基于SEQUENCE區(qū)間方案她紫、各數(shù)據(jù)庫按特定步長自增和基于redis生成自增序列三種

SEQUENCE區(qū)間方案

淘寶分布式數(shù)據(jù)層TDDL就是采用SEQUENCE方案實(shí)現(xiàn)了分庫分表、Master/Salve屿储、動(dòng)態(tài)數(shù)據(jù)源配置等功能贿讹。大致原理是:所有應(yīng)用服務(wù)器去同一個(gè)庫獲取可使用的sequence(樂觀鎖保證一致性),得到(sequence够掠,sequence+步長]個(gè)可被這個(gè)數(shù)據(jù)源使用的id民褂,當(dāng)應(yīng)用服務(wù)器插入“步長”個(gè)數(shù)據(jù)后,再次去爭取新的sequence區(qū)間疯潭。
優(yōu)勢(shì):生成一個(gè) 全局唯一連續(xù) 數(shù)字類型主鍵赊堪,延用單庫單表時(shí)的主鍵id。
劣勢(shì):無法保證 全局遞增 袁勺。需要開發(fā)各種數(shù)據(jù)庫類型id生成器雹食。擴(kuò)容歷史數(shù)據(jù)不好遷移

操作步驟如下:
第一步:創(chuàng)建一張sequence對(duì)應(yīng)的表。記錄每一個(gè)表的當(dāng)前最大sequence期丰,幾張邏輯表需要聲明幾個(gè)sequence群叶;
第二步:配置sequenceDao,定義步長等信息

<bean id="sequenceDao" class="com.taobao.tddl.client.sequence.impl.DefaultSequenceDao">  
        <!-- 數(shù)據(jù)源 -->  
        <property name="dataSource"  ref="dataSource" />  
        <!-- 步長-->  
        <property name="step" value="1000" />  
        <!-- 重試次數(shù)-->  
        <property name="retryTimes" value="1" />  
        <!-- sequence 表名-->  
        <property name="tableName" value="gt_sequence" />  
        <!-- sequence 名稱-->  
        <property name="nameColumnName" value="BIZ_NAME" />  
        <!-- sequence 當(dāng)前值-->  
        <property name="valueColumnName" value="CURRENT_VALUE" />  
        <!-- sequence 更新時(shí)間-->  
        <property name="gmtModifiedColumnName" value="gmt_modified" />  
    </bean>  

DefaultSequenceDao獲取區(qū)間源碼如下:

public SequenceRange nextRange(String name) throws SequenceException {
        if (name == null) {
            throw new IllegalArgumentException("序列名稱不能為空");
        }

        long oldValue;
        long newValue;

        Connection conn = null;
        PreparedStatement stmt = null;
        ResultSet rs = null;

        for (int i = 0; i < retryTimes + 1; ++i) {
            try {
                conn = dataSource.getConnection();
                stmt = conn.prepareStatement(getSelectSql());
                stmt.setString(1, name);
                rs = stmt.executeQuery();
                rs.next();
                oldValue = rs.getLong(1);

                if (oldValue < 0) {
                    StringBuilder message = new StringBuilder();
                    message.append("Sequence value cannot be less than zero, value = ").append(oldValue);
                    message.append(", please check table ").append(getTableName());

                    throw new SequenceException(message.toString());
                }

                if (oldValue > Long.MAX_VALUE - DELTA) {
                    StringBuilder message = new StringBuilder();
                    message.append("Sequence value overflow, value = ").append(oldValue);
                    message.append(", please check table ").append(getTableName());

                    throw new SequenceException(message.toString());
                }

                newValue = oldValue + getStep();
            } catch (SQLException e) {
                throw new SequenceException(e);
            } finally {
                closeResultSet(rs);
                rs = null;
                closeStatement(stmt);
                stmt = null;
                closeConnection(conn);
                conn = null;
            }

            try {
                conn = dataSource.getConnection();
                stmt = conn.prepareStatement(getUpdateSql());
                stmt.setLong(1, newValue);
                stmt.setTimestamp(2, new Timestamp(System.currentTimeMillis()));
                stmt.setString(3, name);
                stmt.setLong(4, oldValue);
                int affectedRows = stmt.executeUpdate();
                if (affectedRows == 0) {
                    // retry
                    continue;
                }

                return new SequenceRange(oldValue + 1, newValue);
            } catch (SQLException e) {
                throw new SequenceException(e);
            } finally {
                closeStatement(stmt);
                stmt = null;
                closeConnection(conn);
                conn = null;
            }
        }

        throw new SequenceException("Retried too many times, retryTimes = " + retryTimes);
    }

第三步:配置sequence生成器钝荡,用于獲取可使用的sequence區(qū)間街立,使用完后再去sequence庫獲取。

<bean id="businessSequence"  class="com.taobao.tddl.client.sequence.impl.DefaultSequence">  
    <property name="sequenceDao" ref="sequenceDao"/>  
    <property name="name" value="business_sequence" />  
</bean>  

其中DefaultSequence源碼如下:

public class DefaultSequence implements Sequence {
    private final Lock lock = new ReentrantLock();

    private SequenceDao sequenceDao;

    /**
     * 序列名稱
     */
    private String name;

    private volatile SequenceRange currentRange;

    public long nextValue() throws SequenceException {
        if (currentRange == null) {
            lock.lock();
            try {
                if (currentRange == null) {
                    currentRange = sequenceDao.nextRange(name);
                }
            } finally {
                lock.unlock();
            }
        }

        long value = currentRange.getAndIncrement();
        if (value == -1) {
            lock.lock();
            try {
                for (;;) {
                    if (currentRange.isOver()) {
                        currentRange = sequenceDao.nextRange(name);
                    }

                    value = currentRange.getAndIncrement();
                    if (value == -1) {
                        continue;
                    }

                    break;
                }
            } finally {
                lock.unlock();
            }
        }

        if (value < 0) {
            throw new SequenceException("Sequence value overflow, value = " + value);
        }

        return value;
    }

    public SequenceDao getSequenceDao() {
        return sequenceDao;
    }

    public void setSequenceDao(SequenceDao sequenceDao) {
        this.sequenceDao = sequenceDao;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}

第四步:調(diào)用

public class IbatisSmDAO extends SqlMapClientDaoSupport implements SmDAO {

  /**smSequence*/
  private DefaultSequence   businessSequence;
   
    public int insert(SmDO sm) throws DataAccessException {
        if (sm == null) {
            throw new IllegalArgumentException("Can't insert a null data object into db.");
        }
        
        try {
            sm.setId((int)businessSequence.nextValue());
        } catch (SequenceException e) {
            throw new RuntimeException("Can't get primary key.");
        }
        
        getSqlMapClientTemplate().insert("MS-SM-INSERT", sm);

        return sm.getId();
    }
}

優(yōu)勢(shì):生成一個(gè)全局唯一連續(xù)數(shù)字類型主鍵埠通,延用單庫單表時(shí)的主鍵id赎离。
劣勢(shì):無法保證全局遞增。需要開發(fā)各種數(shù)據(jù)庫類型id生成器端辱。

各數(shù)據(jù)庫按特定步長自增

可以繼續(xù)采用數(shù)據(jù)庫生成自增主鍵的方式梁剔,為每個(gè)不同的分庫設(shè)置不同的初始值虽画,并按步長設(shè)置為分片的個(gè)數(shù)即可,這種方式對(duì)分片個(gè)數(shù)有依賴荣病,一旦再次水平擴(kuò)展码撰,原有的分布式主鍵不易遷移。為了預(yù)防后續(xù)庫表擴(kuò)容个盆,這邊可以采用提前約定最大支持的庫表數(shù)量脖岛,后續(xù)擴(kuò)容為2的指數(shù)倍擴(kuò)容。
比如:我們規(guī)定最大支持1024張分表颊亮,數(shù)據(jù)庫增長的步長為1024(即使現(xiàn)在的表數(shù)量只有64)柴梆。
優(yōu)勢(shì):生成一個(gè)全局唯一的數(shù)字類型主鍵,延用單庫單表時(shí)的主鍵id终惑。當(dāng)分表數(shù)沒有達(dá)到約定的1024張分表绍在,全局不連續(xù)。
劣勢(shì):無法保證全局遞增雹有,也不保證單機(jī)連續(xù)揣苏。需要開發(fā)各種數(shù)據(jù)庫類型id生成器。需要依賴一個(gè)中心庫表sequence件舵。

基于redis的方案

另一種中心化生成分布式主鍵的方式是采用Redis在內(nèi)存中生成自增序列,通過redis的原子自增操作(incr接口)生成一個(gè)自增的序列脯厨。
優(yōu)勢(shì):生成一個(gè) 全局連續(xù)遞增 的數(shù)字類型主鍵铅祸。
劣勢(shì):此種方式新增加了一個(gè)外部組件的依賴,一旦Redis不可用合武,則整個(gè)數(shù)據(jù)庫將無法在插入临梗,可用性會(huì)大大下降,另外Redis的單點(diǎn)問題也需要解決稼跳,部署復(fù)雜度較高盟庞。

去中心化生成算法

去中心化方式無需額外部署,以jar包方式被加載汤善,可擴(kuò)展性也很好什猖,因此更推薦使用。目前主流的去中心化生成算法有:UUID及其變種红淡、Mongo的ObjectId不狮、snowflake算法及其變種

UUID及其變種

UUID 是 通用唯一識(shí)別碼(Universally Unique Identifier)的縮寫,是一種軟件建構(gòu)的標(biāo)準(zhǔn)在旱,亦為開放軟件基金會(huì)組織在分布式計(jì)算環(huán)境領(lǐng)域的一部分摇零。其目的,是讓分布式系統(tǒng)中的所有元素桶蝎,都能有唯一的辨識(shí)信息驻仅,而不需要通過中央控制端來做辨識(shí)信息的指定谅畅。UUID有很多變種實(shí)現(xiàn),目前最廣泛應(yīng)用的UUID噪服,是微軟公司的全局唯一標(biāo)識(shí)符(GUID)毡泻。
UUID是一個(gè)由4個(gè)連字號(hào)(-)將32個(gè)字節(jié)長的字符串分隔后生成的字符串,總共36個(gè)字節(jié)長芯咧。算法的核心思想是結(jié)合機(jī)器的網(wǎng)卡牙捉、當(dāng)?shù)貢r(shí)間、一個(gè)隨即數(shù)來生成GUID敬飒。從理論上講邪铲,如果一臺(tái)機(jī)器每秒產(chǎn)生10000000個(gè)GUID,則可以保證(概率意義上)3240年不重復(fù)无拗。
優(yōu)勢(shì)全局唯一带到,各種語言都有UUID現(xiàn)成實(shí)現(xiàn),Mysql也有UUID實(shí)現(xiàn)英染。
劣勢(shì):36個(gè)字符組成揽惹,按照目前Mysql最常用的編碼Utf-8,每一個(gè)字符對(duì)應(yīng)的索引成本是3字節(jié)四康,也就是一個(gè)UUID需要108個(gè)字節(jié)的索引存儲(chǔ)成本搪搏,是最大數(shù)字類型(8字節(jié))的13.5倍的存儲(chǔ)成本。

mongodb的ObjectId

objectid有12個(gè)字節(jié)闪金,包含時(shí)間信息(4字節(jié)疯溺、秒為單位)、機(jī)器標(biāo)識(shí)(3字節(jié))哎垦、進(jìn)程id(2字節(jié))囱嫩、計(jì)數(shù)器(3字節(jié),初始值隨機(jī))漏设。其中墨闲,時(shí)間位精度(秒或者毫秒)與序列位數(shù),二者決定了單位時(shí)間內(nèi)郑口,對(duì)于同一個(gè)進(jìn)程最多可產(chǎn)生多少唯一的ObjectId鸳碧,在MongoDB中,那每秒就是2^24(16777216)潘酗。但是機(jī)器標(biāo)識(shí)與進(jìn)程id一定要保證是不重復(fù)的杆兵,否則極大概率上會(huì)產(chǎn)生重復(fù)的ObjectId。由于ObjectId生成12個(gè)字節(jié)的16進(jìn)制表示仔夺,無法用現(xiàn)有基礎(chǔ)類型存儲(chǔ)琐脏,只能轉(zhuǎn)化為字符串存儲(chǔ),對(duì)應(yīng)24個(gè)字符。objectid的組成結(jié)構(gòu)如下

4字節(jié) 3字節(jié) 2字節(jié) 3字節(jié)
time machine pid 自增

ObjectId生成算法的核心代碼如下:

public class ObjectId implements Comparable<ObjectId> , java.io.Serializable {
final int _time;
    final int _machine;
    final int _inc;
boolean _new;

public ObjectId(){
        _time = (int) (System.currentTimeMillis() / 1000);
        _machine = _genmachine;
        _inc = _nextInc.getAndIncrement();
        _new = true;
}
……
}

優(yōu)勢(shì)全局唯一 日裙。
劣勢(shì):非數(shù)字類型吹艇,24個(gè)字符,按照目前Mysql最常用的編碼Utf-8昂拂,每一個(gè)字符對(duì)應(yīng)的索引成本是3字節(jié)受神,也就是一個(gè)ObjectId需要72個(gè)字節(jié)的索引存儲(chǔ)成本,是最大數(shù)字類型(8字節(jié))的9倍的存儲(chǔ)成本格侯。

snowflake算法

Snowflake算法產(chǎn)生是為了滿足Twitter每秒上萬條消息的請(qǐng)求鼻听,每條消息都必須分配一條唯一的id,這些id還需要一些大致的順序(方便客戶端排序)联四,并且在分布式系統(tǒng)中不同機(jī)器產(chǎn)生的id必須不同撑碴。Snowflake算法把時(shí)間戳,工作機(jī)器id朝墩,序列號(hào)組合在一起醉拓。生產(chǎn)Id的結(jié)構(gòu)如下:

63 62-22 21-12 11-0
1位:2 41位:支持69.7年(單位ms) 10位:1024 12位:4096

默認(rèn)情況下41bit的時(shí)間戳可以支持該算法使用到2082年,10bit的工作機(jī)器id可以支持1023臺(tái)機(jī)器收苏,序列號(hào)支持1毫秒產(chǎn)生4095個(gè)自增序列id亿卤。

工作機(jī)器id可以使用IP+Path來區(qū)分工作進(jìn)程。如果工作機(jī)器比較少鹿霸,可以使用配置文件來設(shè)置這個(gè)id是一個(gè)不錯(cuò)的選擇排吴,如果機(jī)器過多配置文件的維護(hù)是一個(gè)災(zāi)難性的事情。
實(shí)施現(xiàn)狀:工作機(jī)器id有10位懦鼠,根據(jù)我們公司目前已經(jīng)未來5-10的業(yè)務(wù)量傍念,同一個(gè)服務(wù)機(jī)器數(shù)超過1024臺(tái)基本上不太可能。工作機(jī)器id推薦使用下面的結(jié)構(gòu)來避免可能的重復(fù)葛闷。

9-8 7-0
用戶可指定(默認(rèn)為0) 機(jī)器ip的后8位

考慮到我們公司的業(yè)務(wù)級(jí)別,同一個(gè)機(jī)房ip的后8位基本上不可能重復(fù)双藕。后2位讓用戶指定是由于存在以下場景:
1)一個(gè)虛擬機(jī)下面可能存在兩個(gè)進(jìn)程號(hào)不同的同樣服務(wù)(我們不建議淑趾,后續(xù)也希望通過運(yùn)維來避免類似的部署)。如果存在這種情況忧陪,可以在JVM啟動(dòng)參數(shù)中添加HostId參數(shù)扣泊,為這個(gè)這臺(tái)機(jī)器的服務(wù)指定一個(gè)不同于其他服務(wù)的HostId。
2)存在前后臺(tái)服務(wù)部署在同一臺(tái)機(jī)器上嘶摊,都操作同一個(gè)庫(建議后臺(tái)服務(wù)通過調(diào)用前臺(tái)的服務(wù)來操作庫表延蟹,保證庫表的單一操作)。如果存在這種情況叶堆,可以通過為前后臺(tái)服務(wù)指定不同的服務(wù)編號(hào)serviceNo(只支持0阱飘,1,2,3)沥匈。
3)不同機(jī)房可能存在相同后8位ip尾號(hào)蔗喂,比如興議機(jī)房為10.10.100.123 濱安機(jī)房為10.20.100.123。如果存在這種情況高帖,可以通過 a)在其中一臺(tái)機(jī)器的環(huán)境變量中重新指定一下HostId缰儿;b)不同環(huán)境配置不同的服務(wù)編號(hào)serviceNo;c)服務(wù)啟動(dòng)JVM參數(shù)中為這個(gè)這臺(tái)機(jī)器的服務(wù)指定一個(gè)不同于其他服務(wù)的HostId

變種snowflake算法
結(jié)合公司現(xiàn)狀散址,我們?cè)趕nowflake算法的基礎(chǔ)上進(jìn)行了部分改造乖阵,得到變種snowflake算法。我們推薦使用的分布式主鍵生成算法是變種的snowflake算法预麸。這個(gè)算法更加充分利用了ID的位表達(dá)瞪浸,比原生的snowflake算法多出1位使用。產(chǎn)生的ID結(jié)構(gòu)如下:

63-62 61-52 51-20 19-0
2位:4 10位:1024 32位:136年(單位為s) 19位:1048560
保留位 機(jī)器碼 時(shí)間戳 自增碼

時(shí)間戳生成: 32位時(shí)間戳代表秒的話师崎,可以表示136年默终,比如我們?nèi)?016年11月11日0點(diǎn)0分0秒作為基準(zhǔn),32位時(shí)間表示當(dāng)前時(shí)間轉(zhuǎn)換秒數(shù)-基準(zhǔn)時(shí)間轉(zhuǎn)換秒數(shù)
自增碼:服務(wù)數(shù)據(jù)源 原子自增的long類型變量犁罩,最大支持每秒1048560條記錄齐蔽,當(dāng)一秒產(chǎn)生超過1048560個(gè)序號(hào)時(shí),再次請(qǐng)求生成序號(hào)時(shí)床估,會(huì)阻塞等待下一秒到達(dá)才生成新的序號(hào)含滴。為了避免自增碼都是從0開始計(jì)數(shù)導(dǎo)致數(shù)據(jù)傾斜,自增碼的起始值被設(shè)定成一個(gè)隨機(jī)數(shù)丐巫。
機(jī)器碼:可以參考上面描述的方案

我們實(shí)現(xiàn)變種snowflake算法的核心代碼如下

    @PostConstruct
    public void init() {
        this.initHostId();
        this.initTime();
        this.initIncNo();
    }

    /**
     * 初始化{@link #hostId} {@link #shiftedHostId}
     */
    protected void initHostId() {
        if(serviceNo > 3 || serviceNo <0){
            LOG.error("serviceNo只支持0谈况、1、2递胧、3");
            throw new ShardingJdbcException("serviceNo只支持0碑韵、1、2缎脾、3");
        }
        
        if (hostId != 0) {
            this.shiftedHostId = this.hostId << this.hostTimeRule.getHostOffset();
            LOG.info("屬性注入HostId祝闻。HostId:{}", hostId);
            LOG.info("初始化Id生成器。HostId:{},ShiftHostId:{}", hostId, shiftedHostId);
            return;
        }
        // 從JVM參數(shù)中遗菠,獲取系統(tǒng)Id
        String host = System.getProperty(HOST_ID);
        if (host != null) {
            this.hostId = Integer.valueOf(host);
            LOG.info("從JVM參數(shù)中獲取HostId联喘。HostId:{}", hostId);
        } else {
            // 從環(huán)境中獲取系統(tǒng)ID
            host = System.getenv(HOST_ID);
            if (host != null) {
                this.hostId = Integer.valueOf(host);
                LOG.info("從系統(tǒng)環(huán)境中獲取HostId。HostId:{}", hostId);
            } else if (useSystemIpAsHostId) {
                //從網(wǎng)卡讀取IP地址辙纬,轉(zhuǎn)換成HostId豁遭。取后8bit位,hostId為service*256+ip后8位
                String ip = IpUtil.getLocalIPv4();
                this.hostId = serviceNo*256 + (int) (IpUtil.convertIPv4(ip) & 255);
                LOG.info("從網(wǎng)卡中獲取HostId贺拣。serviceNo:{},IP:{}, HostId:{}", serviceNo,ip, hostId);
            } else {
                LOG.error("沒有設(shè)置HostId蓖谢,也沒有開啟useSystemIpAsHostId");
                throw new ShardingJdbcException("必須設(shè)置HostId捂蕴,或者開啟useSystemIpAsHostId為true");
            }
        }
        this.shiftedHostId = this.hostId << this.hostTimeRule.getHostOffset();
        LOG.info("初始化Id生成器。HostId:{},ShiftHostId:{}", hostId, shiftedHostId);
    }

    /**
     * 初始化{@link #baseTime }{@link #timeBaseLine } {@link #shiftedTime }
     */
    protected void initTime() {
        if (timeBaseLine > System.currentTimeMillis()) {
            LOG.error("時(shí)間戳底線時(shí)間timeBaseLine不能大于當(dāng)前毫秒級(jí)時(shí)間戳蜈抓。當(dāng)前時(shí)間:{},timeBaseTime:{}", System.currentTimeMillis(),
                timeBaseLine);
            throw new ShardingJdbcException("時(shí)間戳底線時(shí)間timeBaseLine不能大于當(dāng)前毫秒級(jí)時(shí)間戳启绰。");
        }
        LOG.info("時(shí)間底線TimeBaseLine:{},時(shí)間跨度TimeGap:{}",  timeBaseLine, timeGap);
        long baseTime = System.currentTimeMillis();
        currentShiftedTime = (((baseTime - timeBaseLine) / timeGap)%(1L << hostTimeRule.getTimeLength()))<< hostTimeRule.getTimeOffset();
    }

    /**
     * 初始化{@link #incNo}
     */
    protected void initIncNo() {
        if (!randomIncNo) {
            LOG.debug("不需要隨機(jī)IncNo沟使。");
            return;
        }
        //使用隨機(jī)初始化IncNo
        startIncNo = (int) ((1L << hostTimeRule.getIncNoLength()) * Math.random());
        //這里如果事先設(shè)置IncNo屬性委可,就不要開啟隨機(jī)IncNo。
        if (incNo == 0) {
            incNo = startIncNo;
            LOG.info("設(shè)置隨機(jī)IncNo成功腊嗡。IncNo:{}", randomIncNo);
        } else {
            LOG.info("設(shè)置隨機(jī)IncNo失敗着倾。請(qǐng)確認(rèn)初始化IncNo為0,即不要設(shè)置IncNo屬性燕少,或者關(guān)閉randomIncNo屬性卡者!IncNo:{}", incNo);
            throw new ShardingJdbcException("設(shè)置隨機(jī)IncNo失敗。請(qǐng)確認(rèn)初始化IncNo為0客们,即不要設(shè)置IncNo屬性崇决,或者關(guān)閉randomIncNo屬性!");
        }
        maxIncNo = startIncNo + (1 << hostTimeRule.getIncNoLength());
    }

    public synchronized long getAndAdd(){
        return getAndAdd(1);
    }
    
    public synchronized long getAndAdd(int size){
        long currentIncNo = 0;
        //當(dāng)一秒內(nèi)請(qǐng)求超過 1 << hostTimeRule.getIncNoLength() 時(shí)要等待下一秒
        while(true){
            getShiftTime();
            currentIncNo = incNo;
            if(currentShiftedTime == shiftedTime){
                incNo = incNo+size;
                if(currentIncNo >= maxIncNo){
                    LOG.info("當(dāng)前時(shí)間分片請(qǐng)求分布式主鍵id的自增值:{}達(dá)到算法瓶頸底挫,需要等下一個(gè)時(shí)間分片才能創(chuàng)建.起始偏移:{},每一個(gè)時(shí)間分片最大支持生成個(gè)數(shù):{}",new Object[]{currentIncNo,startIncNo,(1 << hostTimeRule.getTimeLength())});
                    continue;
                }
            }else{
                currentShiftedTime = shiftedTime;
                incNo = startIncNo; //從頭開始計(jì)數(shù)
            }
            break;
        }
        
       return ((currentIncNo)%(1L << hostTimeRule.getIncNoLength())) << hostTimeRule.getIncNoOffset();
    }
    
    private void getShiftTime(){
        long baseTime = System.currentTimeMillis();
        shiftedTime = (((baseTime - timeBaseLine) / timeGap)%(1L << hostTimeRule.getTimeLength()))<< hostTimeRule.getTimeOffset();
    }
    @Override
    public Number generateId() {
        return shiftedHostId + shiftedTime + getAndAdd();
    }

snowflake算法的優(yōu)勢(shì)和劣勢(shì)如下:
優(yōu)勢(shì):在服務(wù)器規(guī)模不是很大(不超過1024條件) 全局唯一 恒傻,單機(jī)遞增 ,是數(shù)字類型建邓,存儲(chǔ)索引成本低盈厘。
劣勢(shì):機(jī)器規(guī)模大于1024無法支持,需要運(yùn)維配合解決單機(jī)部署多個(gè)同服務(wù)進(jìn)程問題官边。

帶偏移的snowflake算法
什么是帶偏移的snowflake算法沸手?指的是某個(gè)變量的后多少位和另一個(gè)字段的后多少位有相同的二進(jìn)制,從而這兩個(gè)變量具有相同的偏移注簿。也就是一個(gè)變量的生成依賴另一個(gè)字段契吉,兩者具有相同的偏移量。我們也可以用槽位來理解诡渴,就是具有相同位偏移栅隐,從而保證取模運(yùn)算之后這兩個(gè)變量會(huì)被分到同一個(gè)槽中。
舉個(gè)栗子玩徊,當(dāng)訂單數(shù)量非常大時(shí),需要對(duì)訂單表做分庫分表谨究,查詢維度分為患者維度(對(duì)應(yīng)買家維度)和醫(yī)生維度(對(duì)應(yīng)賣家維度)恩袱。患者就診表以及醫(yī)生接單表的數(shù)量和訂單表的數(shù)量是一樣的胶哲。同理也需要對(duì)患者就診表以及醫(yī)生接單表進(jìn)行分庫分表畔塔。這樣就存在3個(gè)分庫分表。
通過偏移綁定,讓訂單的生成id的后多少位(比如后10位)和用戶id的后多少位(比如后10位)具有相同的偏移澈吨。也就是訂單生成部分依賴患者id把敢。這樣通過訂單id或者患者id進(jìn)行取模運(yùn)算(mod 1024)都能定位到同一個(gè)分庫分表(槽),這樣患者就診表和訂單表就是同一個(gè)表谅辣,從而將3個(gè)分庫分表減少為2個(gè)分庫分表修赞。以最大支撐分庫分表數(shù)量為1024,這樣我們后10位用于偏移桑阶。帶偏移的snowflake算法產(chǎn)生的ID結(jié)構(gòu)如下:

64-63 62-53 52-24 23-10 9-0
1位:符號(hào)位 10位:1024 29位:17年 14位:16384 10位:1024個(gè)slot
保留位 機(jī)器碼 時(shí)間戳 自增碼 偏移位(槽位)

這樣生成的id能夠支撐17年柏副;最大支持1024臺(tái)應(yīng)用機(jī)器同時(shí)生產(chǎn)數(shù)據(jù);最大支持同一個(gè)用戶每秒產(chǎn)生16384條記錄蚣录。

核心代碼如下

    @Override
    public Long generate(Long item) {
        return shiftedHostId + shiftedTime + getAndAdd() + ((item
            & hostTimeRule.getBiasedMask()) << hostTimeRule.getBiasedOffset());
    }

    @Override
    public Long batchGenerateMinId(Long item, int size) {
        return shiftedHostId + shiftedTime + getAndAdd(size) + (((item
            & hostTimeRule.getBiasedMask())) << hostTimeRule.getBiasedOffset());
    }

帶偏移的snowflake算法的優(yōu)勢(shì)和劣勢(shì)如下:
優(yōu)勢(shì):在服務(wù)器規(guī)模不是很大(不超過1024條件) 全局唯一割择,是數(shù)字類型,存儲(chǔ)索引成本低萎河。通過偏移綁定荔泳,能減少一個(gè)分庫分表。
劣勢(shì):不保證單機(jī)遞增虐杯,機(jī)器規(guī)模大于1024無法支持玛歌,需要運(yùn)維配合解決單機(jī)部署多個(gè)同服務(wù)進(jìn)程問題。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末厦幅,一起剝皮案震驚了整個(gè)濱河市沾鳄,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌确憨,老刑警劉巖译荞,帶你破解...
    沈念sama閱讀 218,858評(píng)論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異休弃,居然都是意外死亡吞歼,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,372評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門塔猾,熙熙樓的掌柜王于貴愁眉苦臉地迎上來篙骡,“玉大人,你說我怎么就攤上這事丈甸∨此祝” “怎么了?”我有些...
    開封第一講書人閱讀 165,282評(píng)論 0 356
  • 文/不壞的土叔 我叫張陵睦擂,是天一觀的道長得湘。 經(jīng)常有香客問我,道長顿仇,這世上最難降的妖魔是什么淘正? 我笑而不...
    開封第一講書人閱讀 58,842評(píng)論 1 295
  • 正文 為了忘掉前任摆马,我火速辦了婚禮,結(jié)果婚禮上鸿吆,老公的妹妹穿的比我還像新娘囤采。我一直安慰自己,他們只是感情好惩淳,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,857評(píng)論 6 392
  • 文/花漫 我一把揭開白布蕉毯。 她就那樣靜靜地躺著,像睡著了一般黎泣。 火紅的嫁衣襯著肌膚如雪恕刘。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,679評(píng)論 1 305
  • 那天抒倚,我揣著相機(jī)與錄音褐着,去河邊找鬼。 笑死托呕,一個(gè)胖子當(dāng)著我的面吹牛含蓉,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播项郊,決...
    沈念sama閱讀 40,406評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼馅扣,長吁一口氣:“原來是場噩夢(mèng)啊……” “哼!你這毒婦竟也來了着降?” 一聲冷哼從身側(cè)響起差油,我...
    開封第一講書人閱讀 39,311評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎任洞,沒想到半個(gè)月后蓄喇,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,767評(píng)論 1 315
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡交掏,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,945評(píng)論 3 336
  • 正文 我和宋清朗相戀三年妆偏,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片盅弛。...
    茶點(diǎn)故事閱讀 40,090評(píng)論 1 350
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡钱骂,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出挪鹏,到底是詐尸還是另有隱情见秽,我是刑警寧澤,帶...
    沈念sama閱讀 35,785評(píng)論 5 346
  • 正文 年R本政府宣布讨盒,位于F島的核電站解取,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏催植。R本人自食惡果不足惜肮蛹,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,420評(píng)論 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望创南。 院中可真熱鬧伦忠,春花似錦、人聲如沸稿辙。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,988評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽邻储。三九已至赋咽,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間吨娜,已是汗流浹背脓匿。 一陣腳步聲響...
    開封第一講書人閱讀 33,101評(píng)論 1 271
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留宦赠,地道東北人陪毡。 一個(gè)月前我還...
    沈念sama閱讀 48,298評(píng)論 3 372
  • 正文 我出身青樓,卻偏偏與公主長得像勾扭,于是被迫代替她去往敵國和親毡琉。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,033評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容

  • 一妙色,題記 所有的業(yè)務(wù)系統(tǒng)桅滋,都有生成ID的需求,如訂單id身辨,商品id丐谋,文章ID等。這個(gè)ID會(huì)是數(shù)據(jù)庫中的唯一主鍵栅表,在...
    架構(gòu)師小秘圈閱讀 4,042評(píng)論 1 18
  • 一笋鄙,題記 所有的業(yè)務(wù)系統(tǒng),都有生成ID的需求怪瓶,如訂單id萧落,商品id,文章ID等洗贰。這個(gè)ID會(huì)是數(shù)據(jù)庫中的唯一主鍵找岖,在...
    eonhu閱讀 9,383評(píng)論 0 8
  • 關(guān)于Mongodb的全面總結(jié) MongoDB的內(nèi)部構(gòu)造《MongoDB The Definitive Guide》...
    中v中閱讀 31,938評(píng)論 2 89
  • 主鍵生成策略 系統(tǒng)唯一ID是我們?cè)谠O(shè)計(jì)一個(gè)系統(tǒng)的時(shí)候常常會(huì)遇見的問題,下面介紹一些常見的ID生成策略敛滋。 Seque...
    DonneyYoung閱讀 18,877評(píng)論 1 28
  • 序 本文主要來聊聊分布式id的生成方案许布。 目標(biāo) 業(yè)務(wù)系統(tǒng)需要什么樣的ID生成器中提出了幾點(diǎn)目標(biāo): 唯一性 時(shí)間相關(guān)...
    go4it閱讀 629評(píng)論 0 3