分布式主鍵生成算法

這篇文章總結(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ù)后,再次去爭(zhēng)取新的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位讓用戶指定是由于存在以下場(chǎng)景:
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)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容