jdbc map获取keys_跟我学shardingjdbc之分布式主键及其自定义
博客地址:朝·聞·道?www.wuwenliang.net本文是 “跟我學(xué)Sharding-JDBC” 系列的第三篇,我將帶領(lǐng)讀者一起了解下Sharding-JDBC的分布式主鍵,并實(shí)現(xiàn)業(yè)務(wù)性更強(qiáng)的自定義主鍵。
首先了解下,什么是分布式主鍵。
傳統(tǒng)的關(guān)系型數(shù)據(jù)庫(kù),如MySQL中,數(shù)據(jù)庫(kù)本身自帶自增主鍵生成機(jī)制,但在分布式環(huán)境下,由于分庫(kù)分表導(dǎo)致數(shù)據(jù)水平拆分后無(wú)法使用單表自增主鍵,因此我們需要一種全局唯一id生成策略作為分布式主鍵。
當(dāng)前業(yè)界已經(jīng)有不少成熟的方案能夠解決分布式主鍵的生成問(wèn)題,如:UUID、SnoWflake算法(Twitter)、Leaf算法(美團(tuán)點(diǎn)評(píng))等。
UUIDUUID是Universally Unique Identifier的縮寫,它是在一定的范圍內(nèi)(從特定的名字空間到全球)唯一的機(jī)器生成的標(biāo)識(shí)符。
UUID具有如下特點(diǎn): 1. 經(jīng)由一定的算法機(jī)器生成,算法定義了網(wǎng)卡MAC地址、時(shí)間戳、名字空間(Namespace)、隨機(jī)或偽隨機(jī)數(shù)、時(shí)序等元素,以及從這些元素生成UUID的算法。UUID的復(fù)雜特性在保證了其唯一性的同時(shí),意味著只能由計(jì)算機(jī)生成。 2. 非人工指定,非人工識(shí)別。UUID的復(fù)雜性決定了“一般人“不能直接從一個(gè)UUID知道哪個(gè)對(duì)象和它關(guān)聯(lián)。 3. 在特定的范圍內(nèi)重復(fù)的可能性極小。
UUID能夠保證最少在3000+年內(nèi)不會(huì)重復(fù)。因此它的唯一性是很可靠的。但也有不足之處,就是可讀性差,不能直接用來(lái)做分片鍵并進(jìn)行取模分庫(kù)表的操作,需要進(jìn)行額外的開發(fā),如:轉(zhuǎn)換UUID為unicode/ASCII碼,對(duì)數(shù)字進(jìn)行疊加后取模。
SnoWflake
雪花算法(SnoWflake)是Twitter公布的分布式主鍵生成算法,也是ShardingSphere默認(rèn)提供的配置分布式主鍵生成策略方式。在ShardingSphere的類路徑為:io.shardingsphere.core.keygen.DefaultKeyGenerator
SnoWflake能夠保證不同進(jìn)程主鍵的不重復(fù)性,以及相同進(jìn)程內(nèi)主鍵的有序性。
在同一個(gè)進(jìn)程中,SnoWflake首先是通過(guò)時(shí)間位保證不重復(fù),如果時(shí)間相同則是通過(guò)序列位保證。 同時(shí)由于時(shí)間位是單調(diào)遞增的,且各個(gè)服務(wù)器如果大體做了時(shí)間同步,那么生成的主鍵在分布式環(huán)境可以認(rèn)為是總體有序的,這就保證了對(duì)索引字段的插入的高效性。例如MySQL的Innodb存儲(chǔ)引擎的主鍵。
雪花算法生成的主鍵的二進(jìn)制表示形式包含4部分,從高位到低位分別為:1bit符號(hào)位、41bit時(shí)間戳位、10bit工作進(jìn)程位以及12bit序列號(hào)位。
雪花算法能夠保證全局唯一,同時(shí)也存在一些問(wèn)題,如時(shí)鐘回?fù)芸赡軐?dǎo)致產(chǎn)生重復(fù)序列。為了解決這個(gè)問(wèn)題,ShardingSphere默認(rèn)分布式主鍵生成器提供了一個(gè)最大容忍的時(shí)鐘回?fù)芎撩霐?shù)。
如果時(shí)鐘回?fù)艿臅r(shí)間超過(guò)最大容忍的毫秒數(shù)閾值,則程序報(bào)錯(cuò);如果在可容忍的范圍內(nèi),默認(rèn)分布式主鍵生成器會(huì)等待時(shí)鐘同步到最后一次主鍵生成的時(shí)間后再繼續(xù)工作。 最大容忍的時(shí)鐘回?fù)芎撩霐?shù)的默認(rèn)值為0,可通過(guò)調(diào)用靜態(tài)方法DefaultKeyGenerator.setMaxTolerateTimeDifferenceMilliseconds()設(shè)置。
其他方案這里再簡(jiǎn)單介紹下其他的分布式主鍵生成的方案。
Leaf算法
Redis計(jì)數(shù)器
我們還可以通過(guò)第三方的組件的特性二次開發(fā)自己的分布式id生成器。如:使用Redis的 INCR key自增計(jì)數(shù)器,它是 Redis 的原子性自增操作最直觀的模式,其原理相當(dāng)簡(jiǎn)單:每當(dāng)某個(gè)操作發(fā)生時(shí),向 Redis 發(fā)送一個(gè) INCR 命令。
比如在一個(gè) web 應(yīng)用中,想知道用戶在一年中每天的點(diǎn)擊量,那么只要將用戶ID及相關(guān)的日期信息作為鍵,并在每次用戶點(diǎn)擊頁(yè)面時(shí),執(zhí)行一次自增操作即可。
它有著多種擴(kuò)展模式,如: 1. 通過(guò)組合使用 INCR 和 EXPIRE達(dá)到只在規(guī)定的生存時(shí)間內(nèi)進(jìn)行計(jì)數(shù)(counting)的目的 2. 客戶端通過(guò)使用 GETSET 命令原子性地獲取計(jì)數(shù)器當(dāng)前值并將計(jì)數(shù)器清零,更多信息請(qǐng)參考 GETSET 命令。 3. 通過(guò)用其他自增/自減操作,比如 DECR 和 INCRBY ,用戶可以在完成業(yè)務(wù)操作之后增加或減少計(jì)數(shù)器的值,如在游戲中的記分器就是一個(gè)典型的場(chǎng)景。
它的優(yōu)點(diǎn)在于: 1. 不依賴數(shù)據(jù)庫(kù)且性能優(yōu)于數(shù)據(jù)庫(kù)。 2. ID天然有序,對(duì)分頁(yè)或者需要排序的場(chǎng)景很友好。
但是它還存在如下的缺點(diǎn): 1. 如果系統(tǒng)中沒有Redis需要引入Redis增加了系統(tǒng)復(fù)雜度。 2. 需要額外的編碼和配置工作。
但總體來(lái)講,這是個(gè)不錯(cuò)的方案,分布式環(huán)境下,我們通過(guò)集群Redis能夠保證生成器高可用運(yùn)行,集群之間通過(guò)復(fù)制能夠保證序列生成不會(huì)有單點(diǎn)故障。
Zookeeper
通過(guò)利用zookeeper的持久順序節(jié)點(diǎn)特性,多個(gè)客戶端同時(shí)創(chuàng)建同一節(jié)點(diǎn),zk可以保證有序的創(chuàng)建,創(chuàng)建成功并返回的path類似于/root/generateid0000000001這樣的節(jié)點(diǎn),能夠看到是順序有規(guī)律的。利用這個(gè)特性,我們能夠?qū)崿F(xiàn)基于zk的分布式id生成器。
不過(guò)一般我們很少會(huì)使用zookeeper來(lái)生成唯一ID。主要是由于需要依賴zookeeper,并且是多步調(diào)用API,如果在競(jìng)爭(zhēng)較大的情況下,需要考慮使用分布式鎖。因此,在高并發(fā)的分布式環(huán)境下,性能不甚理想。
MySQL自增id
這種方式很好理解,就是建立一張序列表,執(zhí)行插入操作,并獲取記錄的id值。
它的優(yōu)點(diǎn)如下: 1. 容易理解,開發(fā)量不多,且性能可以接受。 2. 通過(guò)自增主鍵生成的ID天然排序,對(duì)分頁(yè)或者需要排序的結(jié)果很有幫助。
同時(shí)它存在如下的缺點(diǎn): 1. 不同數(shù)據(jù)庫(kù)語(yǔ)法的和實(shí)現(xiàn)不同,如果需要切換數(shù)據(jù)庫(kù)或多數(shù)據(jù)庫(kù)版本支持的時(shí)候需要在每個(gè)庫(kù)中單獨(dú)處理。 2. 在單數(shù)據(jù)庫(kù)或讀寫分離或一主多從的情況下,只有一個(gè)主庫(kù)可以生成。有單點(diǎn)故障風(fēng)險(xiǎn)。 3. id的生成與數(shù)據(jù)庫(kù)的性能強(qiáng)關(guān)聯(lián)。 4. 如果存在數(shù)據(jù)的遷移,則id序列表也需要同步遷移。 5. 分表分庫(kù)場(chǎng)景下會(huì)有麻煩。
當(dāng)然這些問(wèn)題都有針對(duì)的解決方案: 1. 對(duì)于不同的數(shù)據(jù)庫(kù),只需要將id的生成作為單獨(dú)的服務(wù)開發(fā),不同的業(yè)務(wù)通過(guò)接口調(diào)用id生成,屏蔽后方的實(shí)現(xiàn)細(xì)節(jié) 2. 針對(duì)主庫(kù)單點(diǎn),可以改造為多Master架構(gòu) 3. 如果條件允許,使用高性能磁盤及主機(jī)部署數(shù)據(jù)庫(kù) 4. 通過(guò)雙寫操作的方式進(jìn)行數(shù)據(jù)遷移 5. 分庫(kù)分表場(chǎng)景下,只需要在每個(gè)數(shù)據(jù)分片上設(shè)置對(duì)應(yīng)表的序列生成表即可,序列表與業(yè)務(wù)表使用相同的分片規(guī)則,這樣就能保證序列與業(yè)務(wù)是一一對(duì)應(yīng)的,在每個(gè)片上,都是唯一且自增的。
我的選擇
通過(guò)了解各種分布式主鍵生成策略,我最終選擇了Redis的計(jì)數(shù)器作為自定義分布式主鍵的核心技術(shù)方案。
原因如下: 1. 業(yè)務(wù)id如果直接使用UUID、snowflake等可讀性較差,需要有業(yè)務(wù)屬性,最好能直觀的看到分片屬性 2. 業(yè)務(wù)中本身就引入了Redis集群,不需要額外的依賴 3. Redis方案開發(fā)簡(jiǎn)單且可靠性強(qiáng)
基于Redis的分布式主鍵的自定義開發(fā)到此,我們對(duì)主流的分布式主鍵的生成策略進(jìn)行了分析后選定了使用Redis的計(jì)數(shù)器進(jìn)行開發(fā),接下來(lái)就講解下如何實(shí)現(xiàn)業(yè)務(wù)友好的自定義分布式主鍵。
id格式解析
首先解析一下最終生成的ID的格式,舉個(gè)例子,如:生成訂單號(hào)如下:
OD00000101201903251029141503200002
從左往右依次為:
業(yè)務(wù)編碼(2位) + 庫(kù)下標(biāo)(2位)+ 表下標(biāo)(4位)
+ 序列版本號(hào)(默認(rèn)為01,2位)+ 時(shí)間戳(yyMMddHHmmssSSS,精確到毫秒,15位)
+ 機(jī)器id(2位)
+ 序列號(hào)(5位)
共32位。
這個(gè)格式的id對(duì)于業(yè)務(wù)而言,可讀性更好,能夠直觀的看到是哪個(gè)業(yè)務(wù)的id,分布在哪個(gè)片上,是哪個(gè)時(shí)間生成的,比純數(shù)字的更加直觀。
開發(fā)過(guò)程-01-定義分布式主鍵格式
首先,我們定義分布式主鍵的格式,這里通過(guò)枚舉實(shí)現(xiàn)。
新建名為 DbAndTableEnum 的庫(kù)表規(guī)則枚舉類,根據(jù)上述id的格式,分別定義屬性如下
public enum DbAndTableEnum {
/**
* 用戶信息表 UD+db+table+01+yyMMddHHmmssSSS+機(jī)器id+序列號(hào)id
* 例如:UD000000011902261230103345300002 共 2+6+2+15+2+5=32位
*/
T_USER("t_user", "user_id", "01", "01", "UD", 2, 2, 4, 4, 16, "用戶數(shù)據(jù)表枚舉"),
T_NEW_ORDER("t_new_order", "order_id", "01", "01", "OD", 2,2, 4, 4, 8, "訂單數(shù)據(jù)表枚舉");
/**分片表名*/
private String tableName;
/**分片鍵*/
private String shardingKey;
/**系統(tǒng)標(biāo)識(shí)*/
private String bizType;
/**主鍵規(guī)則版本*/
private String idVersion;
/**表名字母前綴*/
private String charsPrefix;
/**分片鍵值中純數(shù)字起始下標(biāo)索引,第一位是0,第二位是1,依次類推*/
private int numberStartIndex;
/**數(shù)據(jù)庫(kù)索引位開始下標(biāo)索引*/
private int dbIndexBegin;
/**表索引位開始下標(biāo)索引*/
private int tbIndexBegin;
/**分布所在庫(kù)數(shù)量*/
private int dbCount;
/**分布所在表數(shù)量-所有庫(kù)中表數(shù)量總計(jì)*/
private int tbCount;
/**描述*/
private String desc;
...省略getter setter 構(gòu)造方法...
這里我根據(jù)屬性,定義了我的demo中需要使用的兩個(gè)枚舉,分別為用戶表、訂單表的主鍵枚舉。以用戶表舉例:
T_USER("t_user", // 用戶邏輯表名
"user_id", // 用戶表分片鍵
"01", // 系統(tǒng)標(biāo)識(shí)默認(rèn)為01
"01", // 主鍵規(guī)則默認(rèn)為01
"UD", // 用戶表前綴
2, // 分片鍵值中純數(shù)字起始下標(biāo),默認(rèn)為2
2, // 數(shù)據(jù)庫(kù)索引位開始下標(biāo)索引,同上,默認(rèn)第二位
4, // 分片數(shù)量,eg:分4庫(kù)
4, // 每個(gè)分片中分表數(shù)量,每個(gè)片上4表
16, // 所有分片的分表總數(shù)
"用戶數(shù)據(jù)表枚舉"), // 描述
在不同的業(yè)務(wù)中,可以根據(jù)對(duì)應(yīng)的業(yè)務(wù)定義對(duì)應(yīng)的id枚舉,原則是:開發(fā)階段一定能夠知道當(dāng)前id是為哪個(gè)業(yè)務(wù)準(zhǔn)備的,也能夠事先預(yù)估好數(shù)據(jù)的容量。
開發(fā)過(guò)程-02-定義序列生成器接口并實(shí)現(xiàn)定義一個(gè)抽象序列接口,方便擴(kuò)展
public interface SequenceGenerator {
/**
* @param targetEnum
* @param dbIndex
* @param tbIndex
* @return
*/
String getNextVal(DbAndTableEnum targetEnum, int dbIndex, int tbIndex);
}
由于我們使用了Redis作為序列生成器,因此只需要編寫SequenceGenerator的實(shí)現(xiàn)類,利用Redis的計(jì)數(shù)器實(shí)現(xiàn)序列生成操作getNextVal()即可。
@Component(value = "redisSequenceGenerator")
public class RedisSequenceGenerator implements SequenceGenerator {
/**序列生成器key前綴*/
public static String LOGIC_TABLE_NAME = "sequence:redis:";
/**序列長(zhǎng)度=5,不足5位的用0填充*/
public static int SEQUENCE_LENGTH = 5;
/**序列最大值=90000*/
public static int sequence_max = 90000;
@Autowired
StringRedisTemplate stringRedisTemplate;
/**
* redis序列獲取實(shí)現(xiàn)方法
* @param targetEnum
* @param dbIndex
* @param tbIndex
* @return
*/
@Override
public String getNextVal(DbAndTableEnum targetEnum, int dbIndex, int tbIndex) {
//拼接key前綴
String redisKeySuffix = new StringBuilder(targetEnum.getTableName())
.append("_")
.append("dbIndex")
.append(StringUtil.fillZero(String.valueOf(dbIndex), ShardingConstant.DB_SUFFIX_LENGTH))
.append("_tbIndex")
.append(StringUtil.fillZero(String.valueOf(tbIndex), ShardingConstant.TABLE_SUFFIX_LENGTH))
.append("_")
.append(targetEnum.getShardingKey()).toString();
String increKey = new StringBuilder(LOGIC_TABLE_NAME).append(redisKeySuffix).toString();
long sequenceId = stringRedisTemplate.opsForValue().increment(increKey);
//達(dá)到指定值重置序列號(hào),預(yù)留后10000個(gè)id以便并發(fā)時(shí)緩沖
if (sequenceId == sequence_max) {
stringRedisTemplate.delete(increKey);
}
// 返回序列值,位數(shù)不夠前補(bǔ)零
return StringUtil.fillZero(String.valueOf(sequenceId), SEQUENCE_LENGTH);
}
}
由于用到了StringRedisTemplate作為Redis操作工具,因此需要引入Redis并配置對(duì)應(yīng)的參數(shù),具體方法此處不贅述,請(qǐng)移步我的另一篇文章 《springboot整合redis小結(jié)》。
分析一下代碼邏輯,首先拼接了序列在redis中的key,將當(dāng)前記錄所在的庫(kù)、表下標(biāo)以及當(dāng)前的表名和分片鍵名稱拼接在一起,在最前面拼接好當(dāng)前key的功能,最終生成的key如下:
sequence:redis:t_new_order_dbIndex00_tbIndex0001_order_id
這個(gè)key表示:redis生成的sequence序列,序列所屬表為t_new_order,分片鍵為order_id,序列所屬庫(kù)下標(biāo)為00庫(kù),所屬表下標(biāo)為0001表。
開發(fā)過(guò)程-03-實(shí)現(xiàn)自定義的KeyGen自定義主鍵生成器
上面的操作中,我們實(shí)現(xiàn)了核心的自增序列生成器,下面的內(nèi)容中我們著手開發(fā)對(duì)業(yè)務(wù)暴露的生成器KeyGenerator的核心邏輯。
新建一個(gè)類,KeyGenerator.java標(biāo)記為spring的一個(gè)Component。由于我們的業(yè)務(wù)基本上使用了Spring Boot框架,因此我開發(fā)的時(shí)候均通過(guò)Spring Bean的方式進(jìn)行類定義。如果你要在非Spring框架中使用,需要自行完成Redis的連接等操作。
由于此處的邏輯較多,我只放核心的業(yè)務(wù),完整的代碼煩請(qǐng)移步github的項(xiàng)目頁(yè),本節(jié)的代碼已經(jīng)上傳,sql腳本也同步更新了。項(xiàng)目地址:snowalker-shardingjdbc-demo
/**
* 根據(jù)路由id生成內(nèi)部系統(tǒng)主鍵id,
* 路由id可以是內(nèi)部其他系統(tǒng)主鍵id,也可以是外部第三方用戶id
* @param targetEnum 待生成主鍵的目標(biāo)表規(guī)則配置
* @param relatedRouteId 路由id或外部第三方用戶id
* @return
*/
public String generateKey(DbAndTableEnum targetEnum, String relatedRouteId) {
if (StringUtils.isBlank(relatedRouteId)) {
throw new IllegalArgumentException("路由id參數(shù)為空");
}
StringBuilder key = new StringBuilder();
/** 1.id業(yè)務(wù)前綴*/
String idPrefix = targetEnum.getCharsPrefix();
/** 2.id數(shù)據(jù)庫(kù)索引位*/
String dbIndex = getDbIndexAndTbIndexMap(targetEnum, relatedRouteId).get("dbIndex");
/** 3.id表索引位*/
String tbIndex = getDbIndexAndTbIndexMap(targetEnum, relatedRouteId).get("tbIndex");
/** 4.id規(guī)則版本位*/
String idVersion = targetEnum.getIdVersion();
/** 5.id時(shí)間戳位*/
String timeString = DateUtil.formatDate(new Date());
/** 6.id分布式機(jī)器位 2位*/
String distributedIndex = getDistributedId(2);
/** 7.隨機(jī)數(shù)位*/
String sequenceId = sequenceGenerator.getNextVal(targetEnum, Integer.parseInt(dbIndex), Integer.parseInt(tbIndex));
/** 庫(kù)表索引靠前*/
return key.append(idPrefix)
.append(dbIndex)
.append(tbIndex)
.append(idVersion)
.append(timeString)
.append(distributedIndex)
.append(sequenceId).toString();
}
該方法為外部業(yè)務(wù)調(diào)用的生成主鍵的核心API,方法聲明為:
generateKey(DbAndTableEnum targetEnum, String relatedRouteId)
第一個(gè)參數(shù)為需要生成id的目標(biāo)表的數(shù)據(jù)源/數(shù)據(jù)表枚舉,第二個(gè)參數(shù)為相對(duì)路由id。這里解釋一下相對(duì)路由id的含義。
在實(shí)際開發(fā)中,我們需要將外部的id轉(zhuǎn)換為內(nèi)部的id使用,這樣既可以保證數(shù)據(jù)的分布均勻,又有利于數(shù)據(jù)安全。如:根據(jù)支付寶uid生成系統(tǒng)內(nèi)部的用戶id。對(duì)外交互使用支付寶uid,內(nèi)部統(tǒng)一使用內(nèi)部的用戶id。
繼續(xù)我們的邏輯,當(dāng)我們有了內(nèi)部的用戶id之后,通過(guò)內(nèi)部用戶id生成業(yè)務(wù)表id,如:賬戶id、訂單id等。由于賬戶id、用戶id使用同一個(gè)相對(duì)路由id(內(nèi)部用戶id),賬戶信息與訂單信息使用了相同的路由規(guī)則,因此它們會(huì)位于同一個(gè)數(shù)據(jù)分片上,這樣就能在業(yè)務(wù)上保證同一個(gè)用戶的業(yè)務(wù)信息都在同一個(gè)數(shù)據(jù)分片上,單庫(kù)事務(wù)得以繼續(xù)使用,同庫(kù)內(nèi)的join操作也能夠支持。由于所有的數(shù)據(jù)都在一個(gè)數(shù)據(jù)分片上,因此少了跨片join及跨片的歸并操作,查詢效率大幅度提升。
代碼邏輯很清晰,就是按位填充對(duì)應(yīng)的參數(shù),其中時(shí)間戳使用SimpleDateFormat的format方法獲取,這里使用ThreadLocal包裝SimpleDateFormat保證線程安全。
我們著重看下如何獲取庫(kù)表索引及分布式機(jī)器位,
獲取庫(kù)表索引
通過(guò)方法 getDbIndexAndTbIndexMap 獲取數(shù)據(jù)庫(kù)的庫(kù)表下標(biāo),代碼如下:
/**
* 根據(jù)已知路由id取出庫(kù)表索引,外部id和內(nèi)部id均 進(jìn)行ASCII轉(zhuǎn)換后再對(duì)庫(kù)表數(shù)量取模
* @param targetEnum 待生成主鍵的目標(biāo)表規(guī)則配置
* @param relatedRouteId 路由id
* @return
*/
private Map getDbIndexAndTbIndexMap(DbAndTableEnum targetEnum,String relatedRouteId) {
Map map = new HashMap<>();
/** 獲取庫(kù)索引*/
String preDbIndex = String.valueOf(
getDbIndexByMod(
relatedRouteId,
targetEnum.getDbCount(),
targetEnum.getTbCount()));
String dbIndex = StringUtil.fillZero(preDbIndex, ShardingConstant.DB_SUFFIX_LENGTH);
/** 獲取表索引*/
String preTbIndex = String
.valueOf(StringUtil.getTbIndexByMod(relatedRouteId,targetEnum.getDbCount(),targetEnum.getTbCount()));
String tbIndex = StringUtil
.fillZero(preTbIndex,ShardingConstant.TABLE_SUFFIX_LENGTH);
map.put("dbIndex", dbIndex);
map.put("tbIndex", tbIndex);
return map;
}
public static long getDbIndexByMod(Object obj,int dbCount,int tbCount) {
long tbRange = getModValue(obj, tbCount);
BigDecimal bc = new BigDecimal(tbRange);
BigDecimal[] results = bc.divideAndRemainder(new BigDecimal(dbCount));
return (long)results[0].intValue();
}
/**
* 先對(duì)指定對(duì)象取ASCII碼后取模運(yùn)算
* @param obj
* @param num
* @return
*/
public static long getModValue(Object obj,long num) {
String str = getAscII(obj == null?"":obj.toString());
BigDecimal bc = new BigDecimal(str);
BigDecimal[] results = bc.divideAndRemainder(new BigDecimal(num));
return (long)results[1].intValue();
}
首先轉(zhuǎn)換外部id為ASCII碼,通過(guò)該ASCII碼對(duì)庫(kù)取商,對(duì)表取余,得到庫(kù)表下標(biāo),并拼接到主鍵中,如圖:
此方案是針對(duì)ShardingJDBC的分片模式的,在ShardingJDBC中,每個(gè)分片中的數(shù)據(jù)庫(kù)表的結(jié)構(gòu)是相同的,如:
db_00--
|--t_order_0000
|--t_order_0001
db_01--
|--t_order_0000
|--t_order_0001
db_02--
|--t_order_0000
|--t_order_0001
db_03--
|--t_order_0000
|--t_order_0001
獲取分布式機(jī)器id
接著看下如何獲取分布式機(jī)器id。
/**
* 生成id分布式機(jī)器位
* @return 分布式機(jī)器id
* length與hostCount位數(shù)相同
*/
private String getDistributedId(int length, int hostCount) {
return StringUtil
.fillZero(String.valueOf(getIdFromHostName() % hostCount), length);
}
/**
* 適配分布式環(huán)境,根據(jù)主機(jī)名生成id
* 分布式環(huán)境下,如:Kubernates云環(huán)境下,集群內(nèi)docker容器名是唯一的
* 通過(guò) @See org.apache.commons.lang3.SystemUtils.getHostName()獲取主機(jī)名
* @return
*/
private Long getIdFromHostName(){
//unicode code point
int[] ints = StringUtils.toCodePoints(SystemUtils.getHostName());
int sums = 0;
for (int i: ints) {
sums += i;
}
return (long)(sums);
}
這里我們通過(guò)StringUtils.toCodePoints(SystemUtils.getHostName());獲取到當(dāng)前主機(jī)名的unicode值,并將每個(gè)字符的unicode值相加,這里只要保證我們服務(wù)器的名稱是唯一的,則codePoint值就是唯一的。例如:使用K8S進(jìn)行部署的環(huán)境下,生成的docker容器的名稱是集群內(nèi)唯一的,保證了getIdFromHostName()返回值的唯一性。
我們用主機(jī)名生成的codePoint值對(duì)全局主機(jī)數(shù)量進(jìn)行取模操作,即可獲取當(dāng)前id位于哪臺(tái)機(jī)器上。
又由于在整個(gè)序列中添加了精確到毫秒的時(shí)間戳以及使用了Redis的計(jì)數(shù)器,能夠大幅度的支撐高并發(fā)環(huán)境下的主鍵生成策略。只要不存在時(shí)鐘回?fù)?#xff0c;系統(tǒng)穩(wěn)定的情況下,不存在主鍵碰撞的情況。
加餐:關(guān)于codePoint
我們之所以將主機(jī)名稱轉(zhuǎn)為CodePoint并疊加各個(gè)字符的CodePoint值,原因在于Unicode中每個(gè)字符的codePoint值是不同的,因此我們可以確定不同的主機(jī)名的CodePoint值也是不同的,因此可以根據(jù)該CodePoint的值去做機(jī)器節(jié)點(diǎn)的取模計(jì)算。
首先了解下什么是CodePoint,CodePoint(中文叫代碼點(diǎn)). wiki上關(guān)于CodePoint的解釋
CodePoint不同于pointCode, 前者是字符編碼的術(shù)語(yǔ)。后者更類似IP地址,用于標(biāo)志網(wǎng)絡(luò)結(jié)點(diǎn)地址,wiki上關(guān)于PointCode的解釋。
ASCII字符集由于使用7bit表示字符,因此有128個(gè)CodePoint.
Extended ASCII字符集(擴(kuò)展ASCII字符集)使用了8bit表示字符,因此有256個(gè)CodePoint.
而最新版Unicode6.2則擁有0x0~0x10FFFF個(gè)CodePoint. 總數(shù)可以達(dá)到1,114,112個(gè),而目前全球只使用了110,182個(gè)來(lái)表示全世界所有語(yǔ)言的字符。這里可以看到Unicode的強(qiáng)大之處了,它真正做到了統(tǒng)一編碼。
我們可以認(rèn)為CodePoint就是不同字符集用來(lái)表示字符的所有整數(shù)的范圍,且起點(diǎn)都是0.
舉例
這里以一個(gè)實(shí)例進(jìn)行講解,準(zhǔn)備這樣一個(gè)字符串:snowalker朝聞道夕死可矣
解析這個(gè)字符串每個(gè)字符的codePoint并疊加,代碼如下:
String snowalker = "snowalker朝聞道夕死可矣";
int [] snowalkerCodePoints = StringUtils.toCodePoints(snowalker);
long sum = 0;
for (int i = 0; i < snowalkerCodePoints.length; i++) {
sum += snowalkerCodePoints[i];
System.out.println("i=" + i + "--snowalkerCodePoints[" + i + "]=" + snowalkerCodePoints[i]);
}
System.out.println("sum=" + sum);
long sum2 = 0;
for (int i = 0; i < snowalkerCodePoints.length; i++) {
sum2 += snowalkerCodePoints[i];
System.out.println("原生方式--i=" + i + "--snowalkerCodePoints[" + i + "]=" + snowalker.codePointAt(i));
}
System.out.println("sum2=" + sum2);
兩種方式,分別為org.apache.commons.lang3.StringUtils.toCodePoints(String string) 以及 java.lang.String.codePointAt(int index)。
org.apache.commons.lang3.StringUtils.toCodePoints(String string)解析字符串后返回一個(gè)codePoint數(shù)組,遍歷數(shù)組并疊加。
java.lang.String.codePointAt(int index)從字符串的起始下標(biāo)開始到結(jié)束下標(biāo)為止,遍歷字符串的每個(gè)元素的codePoint并疊加。
運(yùn)行程序,控制臺(tái)打印如下:
i=0--snowalkerCodePoints[0]=115
i=1--snowalkerCodePoints[1]=110
i=2--snowalkerCodePoints[2]=111
i=3--snowalkerCodePoints[3]=119
i=4--snowalkerCodePoints[4]=97
i=5--snowalkerCodePoints[5]=108
i=6--snowalkerCodePoints[6]=107
i=7--snowalkerCodePoints[7]=101
i=8--snowalkerCodePoints[8]=114
i=9--snowalkerCodePoints[9]=26397
i=10--snowalkerCodePoints[10]=38395
i=11--snowalkerCodePoints[11]=36947
i=12--snowalkerCodePoints[12]=22805
i=13--snowalkerCodePoints[13]=27515
i=14--snowalkerCodePoints[14]=21487
i=15--snowalkerCodePoints[15]=30691
sum=205219
原生方式--i=0--snowalkerCodePoints[0]=115
原生方式--i=1--snowalkerCodePoints[1]=110
原生方式--i=2--snowalkerCodePoints[2]=111
原生方式--i=3--snowalkerCodePoints[3]=119
原生方式--i=4--snowalkerCodePoints[4]=97
原生方式--i=5--snowalkerCodePoints[5]=108
原生方式--i=6--snowalkerCodePoints[6]=107
原生方式--i=7--snowalkerCodePoints[7]=101
原生方式--i=8--snowalkerCodePoints[8]=114
原生方式--i=9--snowalkerCodePoints[9]=26397
原生方式--i=10--snowalkerCodePoints[10]=38395
原生方式--i=11--snowalkerCodePoints[11]=36947
原生方式--i=12--snowalkerCodePoints[12]=22805
原生方式--i=13--snowalkerCodePoints[13]=27515
原生方式--i=14--snowalkerCodePoints[14]=21487
原生方式--i=15--snowalkerCodePoints[15]=30691
sum2=205219
可以看到,兩種方式獲取到的unicode的codePoint是相同的,通過(guò)這些方式我們就可以完成很多需求,如:本文中我們就是通過(guò)這種方式去解析主機(jī)名并轉(zhuǎn)換為集群節(jié)點(diǎn)id。也可以通過(guò)這個(gè)方法,進(jìn)行分片算法的開發(fā),思路為:遍歷主鍵的所有元素,疊加元素的codePoint并對(duì)庫(kù)表取模,進(jìn)行數(shù)據(jù)的分片。
總結(jié)到這里,我們就完成了自定義分布式主鍵的自定義操作,詳細(xì)的代碼請(qǐng)?jiān)L問(wèn):
項(xiàng)目地址:snowalker-shardingjdbc-demo
在本文中,我們分析了多種分布式主鍵的生成策略及其優(yōu)缺點(diǎn),最終選擇了Redis作為序列的生成器。并基于Redis序列生成器開發(fā)了可讀性更好的主鍵生成工具,在接下來(lái)的文章中,我將使用該主鍵生成器,配合Sharding-JDBC的自定義分庫(kù)分表策略,將Sharding-JDBC的使用更加推向?qū)崙?zhàn)化。希望本文的思路能夠?qū)ψx者開發(fā)自己的主鍵生成組件有所啟發(fā)。
總結(jié)
以上是生活随笔為你收集整理的jdbc map获取keys_跟我学shardingjdbc之分布式主键及其自定义的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: aspose 换行写_aspose.wo
- 下一篇: 字节跳动测试开发4轮面试_字节跳动201