日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問(wèn) 生活随笔!

生活随笔

當(dāng)前位置: 首頁(yè) > 编程资源 > 编程问答 >内容正文

编程问答

jdbc map获取keys_跟我学shardingjdbc之分布式主键及其自定义

發(fā)布時(shí)間:2024/1/23 编程问答 31 豆豆
生活随笔 收集整理的這篇文章主要介紹了 jdbc map获取keys_跟我学shardingjdbc之分布式主键及其自定义 小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,幫大家做個(gè)參考.

博客地址:朝·聞·道?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)題。

如果覺得生活随笔網(wǎng)站內(nèi)容還不錯(cuò),歡迎將生活随笔推薦給好友。