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