一、背景
傳統(tǒng)的將數(shù)據(jù)集中存儲(chǔ)至單一數(shù)據(jù)節(jié)點(diǎn)的解決方案,在性能、可用性和運(yùn)維成本這三方面已經(jīng)難于滿足互聯(lián)網(wǎng)的海量數(shù)據(jù)場(chǎng)景。
從性能方面來(lái)說(shuō),由于關(guān)系型數(shù)據(jù)庫(kù)大多采用B+樹(shù)類型的索引,在數(shù)據(jù)量超過(guò)閾值的情況下,索引深度的增加也將使得磁盤(pán)訪問(wèn)的IO次數(shù)增加,進(jìn)而導(dǎo)致查詢性能的下降;同時(shí),高并發(fā)訪問(wèn)請(qǐng)求也使得集中式數(shù)據(jù)庫(kù)成為系統(tǒng)的最大瓶頸。
從可用性的方面來(lái)講,服務(wù)化的無(wú)狀態(tài)型,能夠達(dá)到較小成本的隨意擴(kuò)容,這必然導(dǎo)致系統(tǒng)的最終壓力都落在數(shù)據(jù)庫(kù)之上。而單一的數(shù)據(jù)節(jié)點(diǎn),或者簡(jiǎn)單的主從架構(gòu),已經(jīng)越來(lái)越難以承擔(dān)。數(shù)據(jù)庫(kù)的可用性,已成為整個(gè)系統(tǒng)的關(guān)鍵。
從運(yùn)維成本方面考慮,當(dāng)一個(gè)數(shù)據(jù)庫(kù)實(shí)例中的數(shù)據(jù)達(dá)到閾值以上,對(duì)于DBA的運(yùn)維壓力就會(huì)增大。數(shù)據(jù)備份和恢復(fù)的時(shí)間成本都將隨著數(shù)據(jù)量的大小而愈發(fā)不可控。一般來(lái)講,單一數(shù)據(jù)庫(kù)實(shí)例的數(shù)據(jù)的閾值在1TB之內(nèi),是比較合理的范圍。
在傳統(tǒng)的關(guān)系型數(shù)據(jù)庫(kù)無(wú)法滿足互聯(lián)網(wǎng)場(chǎng)景需要的情況下,將數(shù)據(jù)存儲(chǔ)至原生支持分布式的NoSQL的嘗試越來(lái)越多。 但NoSQL對(duì)SQL的不兼容性以及生態(tài)圈的不完善,使得它們?cè)谂c關(guān)系型數(shù)據(jù)庫(kù)的博弈中始終無(wú)法完成致命一擊,而關(guān)系型數(shù)據(jù)庫(kù)的地位卻依然不可撼動(dòng)。
數(shù)據(jù)分片指按照某個(gè)維度將存放在單一數(shù)據(jù)庫(kù)中的數(shù)據(jù)分散地存放至多個(gè)數(shù)據(jù)庫(kù)或表中以達(dá)到提升性能瓶頸以及可用性的效果。 數(shù)據(jù)分片的有效手段是對(duì)關(guān)系型數(shù)據(jù)庫(kù)進(jìn)行分庫(kù)和分表。分庫(kù)和分表均可以有效的避免由數(shù)據(jù)量超過(guò)可承受閾值而產(chǎn)生的查詢瓶頸。 除此之外,分庫(kù)還能夠用于有效的分散對(duì)數(shù)據(jù)庫(kù)單點(diǎn)的訪問(wèn)量;分表雖然無(wú)法緩解數(shù)據(jù)庫(kù)壓力,但卻能夠提供盡量將分布式事務(wù)轉(zhuǎn)化為本地事務(wù)的可能,一旦涉及到跨庫(kù)的更新操作,分布式事務(wù)往往會(huì)使問(wèn)題變得復(fù)雜。 使用多主多從的分片方式,可以有效的避免數(shù)據(jù)單點(diǎn),從而提升數(shù)據(jù)架構(gòu)的可用性。
通過(guò)分庫(kù)和分表進(jìn)行數(shù)據(jù)的拆分來(lái)使得各個(gè)表的數(shù)據(jù)量保持在閾值以下,以及對(duì)流量進(jìn)行疏導(dǎo)應(yīng)對(duì)高訪問(wèn)量,是應(yīng)對(duì)高并發(fā)和海量數(shù)據(jù)系統(tǒng)的有效手段。
二、挑戰(zhàn)
雖然數(shù)據(jù)分片解決了性能、可用性以及單點(diǎn)備份恢復(fù)等問(wèn)題,但分布式的架構(gòu)在獲得了收益的同時(shí),也引入了新的問(wèn)題。
面對(duì)如此散亂的分庫(kù)分表之后的數(shù)據(jù),應(yīng)用開(kāi)發(fā)工程師和數(shù)據(jù)庫(kù)管理員對(duì)數(shù)據(jù)庫(kù)的操作變得異常繁重就是其中的重要挑戰(zhàn)之一。他們需要知道數(shù)據(jù)需要從哪個(gè)具體的數(shù)據(jù)庫(kù)的分表中獲取。另一個(gè)挑戰(zhàn)則是,能夠正確的運(yùn)行在單節(jié)點(diǎn)數(shù)據(jù)庫(kù)中的SQL,在分片之后的數(shù)據(jù)庫(kù)中并不一定能夠正確運(yùn)行。例如,分表導(dǎo)致表名稱的修改,或者分頁(yè)、排序、聚合分組等操作的不正確處理??鐜?kù)事務(wù)也是分布式的數(shù)據(jù)庫(kù)集群要面對(duì)的棘手事情。 合理采用分表,可以在降低單表數(shù)據(jù)量的情況下,盡量使用本地事務(wù),善于使用同庫(kù)不同表可有效避免分布式事務(wù)帶來(lái)的麻煩。 在不能避免跨庫(kù)事務(wù)的場(chǎng)景,有些業(yè)務(wù)仍然需要保持事務(wù)的一致性。 而基于XA的分布式事務(wù)由于在并發(fā)度高的場(chǎng)景中性能無(wú)法滿足需要,并未被互聯(lián)網(wǎng)巨頭大規(guī)模使用,他們大多采用最終一致性的柔性事務(wù)代替強(qiáng)一致事務(wù)。
三、系統(tǒng)瓶頸
略
四、目標(biāo)
略
五、分庫(kù)分表策略
縱向切分
1、對(duì)于包含狀態(tài)的數(shù)據(jù)分為冷熱數(shù)據(jù),對(duì)于還未完成的流程做為熱數(shù)據(jù)實(shí)時(shí)更新信息,對(duì)于已完成的流程數(shù)據(jù)做為冷數(shù)據(jù)留檔,用于信息追溯;
橫向切分策略
1、 查詢切分
記錄id與db的關(guān)系,根據(jù)mapping關(guān)系分庫(kù)
優(yōu)點(diǎn):Mapping 關(guān)系可以隨時(shí)修改
缺點(diǎn):增加了額外的單點(diǎn)
2、 范圍切分
按照時(shí)間、ID或其它字段切分
優(yōu)點(diǎn):?jiǎn)伪泶笮】煽?、天然支持水平擴(kuò)展
缺點(diǎn):無(wú)法解決集中寫(xiě)入瓶頸問(wèn)題,如 按ID范圍切分時(shí),ID有序插入會(huì)優(yōu)先進(jìn)入其中一個(gè)DB,而其它DB此時(shí)未完全利用
3、 Hash切分(建議)
傳統(tǒng)hash切分
通過(guò)取模算法 key.hashcode mod databases_num,將不同的數(shù)據(jù)分布在不同的數(shù)據(jù)庫(kù)節(jié)點(diǎn)上。
優(yōu)點(diǎn):可以較為均勻的將數(shù)據(jù)切分
缺點(diǎn):擴(kuò)容時(shí) 需要重新計(jì)算切分后的值,涉及到大量的數(shù)據(jù)遷移
一致性Hash切分

建立一個(gè)2^32 hash節(jié)點(diǎn)環(huán),計(jì)算database的hash值(常用:節(jié)點(diǎn)前綴 +(ip+端口).hahcode%2^32),并映射到hash環(huán)的具體位置,將待 切分 數(shù)據(jù)取值key.hashcode mod 2^32,映射到hash 環(huán)上,并按照規(guī)則從root節(jié)點(diǎn)開(kāi)始順時(shí)針查找,if node_1 < key_hash <= node_2 ;then data_key in node_2 如果循環(huán)一遍沒(méi)有找到則data_key in node_root。
擴(kuò)容時(shí)遷移數(shù)據(jù)時(shí),如 新增節(jié)點(diǎn)在 node_1 ,node_2000 之間 node_1000,則只需要重新計(jì)算node_2000節(jié)點(diǎn)上的key_hash 重新分配即可(如果key_hash以持久化 則可以省略計(jì)算的步驟);
移除或節(jié)點(diǎn)異常下線時(shí),如node_1,node_1000,node_2000 node_1000 下線,則node_1000的數(shù)據(jù)可以直接遷移到node_2000上
優(yōu)點(diǎn):擴(kuò)容時(shí)只需要遷移小部分的數(shù)據(jù)
缺點(diǎn):如果節(jié)點(diǎn)不夠分散,會(huì)出現(xiàn)hash環(huán)的傾斜,導(dǎo)致節(jié)點(diǎn)數(shù)據(jù)不均勻
一致性Hash + 虛擬Hash節(jié)點(diǎn)

該思路基于一致性Hash 增加了虛擬節(jié)點(diǎn),這些節(jié)點(diǎn)會(huì)映射到真實(shí)的物理節(jié)點(diǎn),這樣就可以調(diào)控節(jié)點(diǎn)數(shù)據(jù)分配
優(yōu)點(diǎn):減少了Hash環(huán)傾斜帶來(lái)的影響
缺點(diǎn):增加復(fù)雜度
實(shí)現(xiàn)方案
注:實(shí)現(xiàn)分庫(kù)后,需要重新設(shè)計(jì)唯一ID
一致性Hash + 虛擬Hash節(jié)點(diǎn)
方案思路
基于sharding-jdbc 自定義分片算法
Sharding-JDBC 簡(jiǎn)介(詳情參考官網(wǎng):Sharding-JDBC :: ShardingSphere)
Sharding-JDBC是ShardingSphere的第一個(gè)產(chǎn)品,也是ShardingSphere的前身。 它定位為輕量級(jí)Java框架,在Java的JDBC 層提供的額外服務(wù)。它使用客戶端直連數(shù)據(jù)庫(kù),以jar包形式提供服務(wù),無(wú)需額外部署和依賴,可理解為增強(qiáng)版的JDBC驅(qū)動(dòng),完全兼容JDBC和各種ORM框架。
適用于任何基于JDBC的ORM框架,如:JPA, Hibernate, Mybatis, Spring JDBC Template或直接使用JDBC。
支持任何第三方的數(shù)據(jù)庫(kù)連接池,如:DBCP, C3P0, BoneCP, Druid, HikariCP等。
支持任意實(shí)現(xiàn)JDBC規(guī)范的數(shù)據(jù)庫(kù)。目前支持MySQL,Oracle,SQLServer,PostgreSQL以及任何遵循SQL92標(biāo)準(zhǔn)的數(shù)據(jù)庫(kù)。
實(shí)現(xiàn)步驟
1、集成sharding-jdbc
pom.xml
<!-- https://mvnrepository.com/artifact/org.apache.shardingsphere/sharding-jdbc-core -->
<dependency>
<groupId>org.apache.shardingsphere</groupId>
<artifactId>sharding-jdbc-spring-boot-starter</artifactId>
<version>4.1.1</version>
</dependency>
新增sharding-jdbc 配置
spring:
shardingsphere:
#數(shù)據(jù)源配置,可配置多個(gè)data_source_name
datasource:
names: pdmp-0,pdmp-1
pdmp-0:
type: com.alibaba.druid.pool.DruidDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://XXXXXX/pdmp-0?useUnicode=true&characterEncoding=UTF-8
username: XXXXXX
password: XXXXXX
pdmp-1:
type: com.alibaba.druid.pool.DruidDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://XXXXXX/pdmp-1?useUnicode=true&characterEncoding=UTF-8
username: XXXXXX
password: XXXXXX
sharding:
#默認(rèn)數(shù)據(jù)庫(kù)分片策略
default-database-strategy:
inline:
#分片算法行表達(dá)式,需符合groovy語(yǔ)法
algorithm-expression: pdmp-$->{model_version_id % 2}
sharding-column: model_version_id
#數(shù)據(jù)表分片策略
tables:
feature_base:
#由數(shù)據(jù)源名 + 表名組成,以小數(shù)點(diǎn)分隔。多個(gè)表以逗號(hào)分隔,支持inline表達(dá)式。缺省表示使用已知數(shù)據(jù)源與邏輯表名稱生成數(shù)據(jù)節(jié)點(diǎn),用于廣播表(即每個(gè)庫(kù)中都需要一個(gè)同樣的表用于關(guān)聯(lián)查詢,多為字典表)或只分庫(kù)不分表且所有庫(kù)的表結(jié)構(gòu)完全一致的情況
actual-data-nodes: pdmp-$->{0..1}.feature_base$->{0..9}
table-strategy:
standard:
#分片列名稱
sharding-column: image_uuid
#精確分片算法類名稱,用于=和IN。該類需實(shí)現(xiàn)PreciseShardingAlgorithm接口并提供無(wú)參數(shù)的構(gòu)造器
precise-algorithm-class-name: com.hanshow.afms.admin.sharding.ConsistentShardingAlgorithm
props:
sql:
show: true
自定義分片算法
// 實(shí)現(xiàn)了兩種算法 PreciseShardingAlgorithm(精準(zhǔn)分片算法)、RangeShardingAlgorithm(范圍分片算法)
public class ConsistentShardingAlgorithm
implements PreciseShardingAlgorithm<Long>, RangeShardingAlgorithm<Long> {
/**
* 精確分片
* 一致性hash算法
*/
@Override
public String doSharding(Collection<String> availableTargetNames, PreciseShardingValue<Long> shardingValue) {
//獲取已經(jīng)初始化的分表節(jié)點(diǎn)
InitTableNodesToHashLoop initTableNodesToHashLoop =
(InitTableNodesToHashLoop) SpringContextUtils.getBean(InitTableNodesToHashLoop.class);
if (CollectionUtils.isEmpty(availableTargetNames)) {
return shardingValue.getLogicTableName();
}
//這里主要為了兼容當(dāng)聯(lián)表查詢時(shí),如果兩個(gè)表非關(guān)聯(lián)表則
//當(dāng)對(duì)副表分表時(shí)shardingValue這里傳遞進(jìn)來(lái)的依然是主表的名稱,
//但availableTargetNames中確是副表名稱,所有這里要從availableTargetNames中匹配真實(shí)表
ArrayList<String> availableTargetNameList = new ArrayList<>(availableTargetNames);
String logicTableName = availableTargetNameList.get(0).replaceAll("[^(a-zA-Z_)]", "");
SortedMap<Long, String> tableHashNode =
initTableNodesToHashLoop.getTableVirtualNodes().get(logicTableName);
ConsistentHashAlgorithm consistentHashAlgorithm = new ConsistentHashAlgorithm(tableHashNode,
availableTargetNames);
return consistentHashAlgorithm.getTableNode(String.valueOf(shardingValue.getValue()));
}
/**
* 范圍查詢規(guī)則
* 可以根據(jù)實(shí)際場(chǎng)景進(jìn)行修改
* Sharding.
*
* @param availableTargetNames available data sources or tables's names
* @param shardingValue sharding value
* @return sharding results for data sources or tables's names
*/
@Override
public Collection<String> doSharding(Collection<String> availableTargetNames, RangeShardingValue<Long> shardingValue) {
return availableTargetNames;
}
}
hash環(huán)初始化
@Log4j2
public class InitTableNodesToHashLoop {
@Resource
private ShardingDataSource shardingDataSource;
@Getter
private HashMap<String, SortedMap<Long, String>> tableVirtualNodes = new HashMap<>();
@PostConstruct
public void init() {
try {
ShardingRule rule = shardingDataSource.getRuntimeContext().getRule();
Collection<TableRule> tableRules = rule.getTableRules();
ConsistentHashAlgorithm consistentHashAlgorithm = new ConsistentHashAlgorithm();
for (TableRule tableRule : tableRules) {
String logicTable = tableRule.getLogicTable();
tableVirtualNodes.put(logicTable,
consistentHashAlgorithm.initNodesToHashLoop(
tableRule.getActualDataNodes()
.stream()
.map(DataNode::getTableName)
.collect(Collectors.toList()))
);
}
} catch (Exception e) {
log.error("分表節(jié)點(diǎn)初始化失敗 {}", e);
}
}
一致散列算法文章來(lái)源:http://www.zghlxwxcb.cn/news/detail-494824.html
public class ConsistentHashAlgorithm {
//虛擬節(jié)點(diǎn),key表示虛擬節(jié)點(diǎn)的hash值,value表示虛擬節(jié)點(diǎn)的名稱
@Getter
private SortedMap<Long, String> virtualNodes = new TreeMap<>();
//當(dāng)節(jié)點(diǎn)的數(shù)目很少時(shí),容易造成數(shù)據(jù)的分布不均,所以增加虛擬節(jié)點(diǎn)來(lái)平均數(shù)據(jù)分布
//虛擬節(jié)點(diǎn)的數(shù)目;虛擬節(jié)點(diǎn)的生成主要是用來(lái)讓數(shù)據(jù)盡量均勻分布
//虛擬節(jié)點(diǎn)是真實(shí)節(jié)點(diǎn)的不同映射而已
//比如真實(shí)節(jié)點(diǎn)user1的hash值為100,那么我們?cè)黾?個(gè)虛擬節(jié)點(diǎn)user1-1、user1-2、user1-3,分別計(jì)算出來(lái)的hash值可能就為200,345,500;通過(guò)這種方式來(lái)將節(jié)點(diǎn)分布均勻
private static final int VIRTUAL_NODES = 3;
public ConsistentHashAlgorithm() {
}
public ConsistentHashAlgorithm(SortedMap<Long, String> virtualTableNodes, Collection<String> tableNodes) {
if (Objects.isNull(virtualTableNodes)) {
virtualTableNodes = initNodesToHashLoop(tableNodes);
}
this.virtualNodes = virtualTableNodes;
}
public SortedMap<Long, String> initNodesToHashLoop(Collection<String> tableNodes) {
SortedMap<Long, String> virtualTableNodes = new TreeMap<>();
// 每個(gè)物理節(jié)點(diǎn)綁定多個(gè)虛擬節(jié)點(diǎn)
for (String node : tableNodes) {
for (int i = 0; i < VIRTUAL_NODES; i++) {
String s = String.valueOf(i);
String virtualNodeName = node + "-VN" + s;
long hash = getHash(virtualNodeName);
virtualTableNodes.put(hash, virtualNodeName);
}
}
return virtualTableNodes;
}
/**
* 通過(guò)計(jì)算key的hash
* 計(jì)算映射的表節(jié)點(diǎn)
*
* @param key
* @return
*/
public String getTableNode(String key) {
String virtualNode = getVirtualTableNode(key);
//虛擬節(jié)點(diǎn)名稱截取后獲取真實(shí)節(jié)點(diǎn)
if (StringUtils.isNotBlank(virtualNode)) {
return virtualNode.substring(0, virtualNode.indexOf("-"));
}
return null;
}
/**
* 獲取虛擬節(jié)點(diǎn)
* @param key
* @return
*/
public String getVirtualTableNode(String key) {
long hash = getHash(key);
// 得到大于該Hash值的所有Map
SortedMap<Long, String> subMap = virtualNodes.tailMap(hash);
String virtualNode;
if (subMap.isEmpty()) {
//如果沒(méi)有比該key的hash值大的,則從第一個(gè)node開(kāi)始
Long i = virtualNodes.firstKey();
//返回對(duì)應(yīng)的服務(wù)器
virtualNode = virtualNodes.get(i);
} else {
//第一個(gè)Key就是順時(shí)針過(guò)去離node最近的那個(gè)結(jié)點(diǎn)
Long i = subMap.firstKey();
//返回對(duì)應(yīng)的服務(wù)器
virtualNode = subMap.get(i);
}
return virtualNode;
}
/**
* 使用FNV1_32_HASH算法計(jì)算key的Hash值
*
* @param key
* @return
*/
public long getHash(String key) {
final int p = 16777619;
int hash = (int) 2166136261L;
for (int i = 0; i < key.length(); i++)
hash = (hash ^ key.charAt(i)) * p;
hash += hash << 13;
hash ^= hash >> 7;
hash += hash << 3;
hash ^= hash >> 17;
hash += hash << 5;
// 如果算出來(lái)的值為負(fù)數(shù)則取其絕對(duì)值
if (hash < 0)
hash = Math.abs(hash);
return hash;
}
}
參考文章:https://blog.csdn.net/free_ant/article/details/111461606文章來(lái)源地址http://www.zghlxwxcb.cn/news/detail-494824.html
到了這里,關(guān)于Sharding JDBC 分庫(kù)分表(一致性Hash + 虛擬節(jié)點(diǎn))的文章就介紹完了。如果您還想了解更多內(nèi)容,請(qǐng)?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!