這篇文章總結(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)程問題。