實(shí)戰(zhàn):Springboot3+ShardingSphere5.2.1生產(chǎn)級(jí)分庫分表實(shí)現(xiàn)
大家好,我是飄渺。
隨著業(yè)務(wù)的不斷發(fā)展,DailyMart每天產(chǎn)生的銷售訂單已經(jīng)達(dá)到了約100萬,并且呈持續(xù)增長(zhǎng)趨勢(shì)。按照這樣的發(fā)展速度,每年的數(shù)據(jù)量將達(dá)到約4億左右。目前,DailyMart采用的是MySQL單表進(jìn)行存儲(chǔ),但鑒于業(yè)務(wù)的快速發(fā)展,我們迫切需要對(duì)其進(jìn)行分庫分表的改造。今天,我們來探討如何實(shí)現(xiàn)分庫分表功能,以及相關(guān)的步驟和注意事項(xiàng)。
這是本系列文章的第31篇,歡迎持續(xù)關(guān)注。
對(duì)于分庫分表的相關(guān)知識(shí),我的星球分庫分表專欄有詳細(xì)的介紹說明,強(qiáng)烈推薦大家加入學(xué)習(xí)。
分庫分表的核心在于合理選擇分片鍵以及快速定位非分片鍵的數(shù)據(jù)。
分片鍵的選擇
DailyMart作為一個(gè)ToC的業(yè)務(wù)系統(tǒng),大部分業(yè)務(wù)訪問都是基于用戶ID進(jìn)行的,比如登錄用戶查看自己的購買記錄等。因此,對(duì)于訂單模塊我們決定以用戶ID作為分片鍵。
在訂單模塊中,訂單主表 CUSTOMER_ORDER 和訂單明細(xì)表 ORDER_ITEM 是最核心的兩張表,由于它們經(jīng)常會(huì)一起使用,我們也需要將訂單明細(xì)表的用戶字段 CUSTOMER_ID 作為分片鍵,以確?;谟脩艟S度的查詢?cè)趩蝹€(gè)分片上完成。下面是一個(gè)示例SQL:
SELECT * FROM CUSTOMER_ORDER ORDER
LEFT JOIN ORDER_ITEM ITEM ON ORDER.order_sn = ITEM.order_sn
WHERE ORDER.customer_id = 2846741676215238657
ORDER BY create_time DESC LIMIT 10
非分片鍵查詢
既然確定使用用戶ID作為分片鍵,大部分查詢都需要帶上CUSTOMER_ID作為查詢條件。但在實(shí)際使用中,經(jīng)常會(huì)根據(jù)訂單編號(hào)ORDER_SN進(jìn)行精確查詢,比如庫存扣減、支付后的反查等。在默認(rèn)情況下,根據(jù)訂單編號(hào)(非分片鍵)進(jìn)行查詢將需要在所有分片上進(jìn)行查詢,然后對(duì)結(jié)果進(jìn)行聚合,顯然這樣的查詢效率是很低的。
為了解決這個(gè)問題,業(yè)界一般采用基因法來解決,即將分片鍵的信息保存在想要查詢的列中,這樣通過查詢的列就能直接知道數(shù)據(jù)所在的分片信息。
基因法的原理是 對(duì)一個(gè)數(shù)取余2的n次方,那么余數(shù)就是這個(gè)數(shù)的二進(jìn)制的最后n位數(shù)。
以訂單表為例,對(duì)訂單表我們根據(jù)CUSOMER_ID將其拆成16張表,采用CUSOMER_ID % 16的方式來進(jìn)行數(shù)據(jù)庫路由,這里的CUSOMER_ID % 16,其本質(zhì)是CUSOMER_ID的最后4個(gè)bit位 log(16,2) = 4 決定這行數(shù)據(jù)落在哪個(gè)分片上,這4個(gè)bit就是分片基因。
基于這一理論,基因法有兩種具體的實(shí)現(xiàn):
基因替換法
- 在生成訂單編號(hào)ORDER_SN時(shí),先使用一種分布式ID生成算法生成前60bit
- 計(jì)算出分片基因:分庫基因是CUSTOMER_ID的最后4個(gè)bit,log(16,2) = 4,即1001
- 將分庫基因加入到ORDER_SN的最后4個(gè)bit(上圖中粉色部分)
- 拼裝成最終的64bit訂單ORDER_SN(上圖中藍(lán)色部分)
圖片
這樣保證了同一個(gè)用戶創(chuàng)建的所有訂單都落到了同一個(gè)分片上,ORDER_SN的最后4個(gè)bit都相同,通過CUSTOMER_ID %16 能夠定位到分片,通過ORDER_SN % 16也能定位到分片。
基因替換法可能會(huì)導(dǎo)致ORDER_SN重復(fù),以雪花算法為例,假設(shè)同一個(gè)用戶在一毫秒內(nèi)創(chuàng)建了 2 個(gè)訂單,這樣生產(chǎn)的序列號(hào)相差1,替換掉基因后對(duì)應(yīng)的二進(jìn)制都相同了,導(dǎo)致ORDER_SN也是重復(fù)的。但這種情況非常少見,除非是機(jī)器人刷單。當(dāng)然如果要徹底杜絕訂單編號(hào)重復(fù)問題可以使用下面介紹的基因拼接法。
基因拼接法
基因拼接法更簡(jiǎn)單,就是在構(gòu)建訂單編號(hào)時(shí)直接將用戶基因拼接在生成的ID后面,即:ORDER_SN = string(ORDER_SN + CUSTOMER_ID)
假設(shè)開始生成的訂單號(hào)是3531318506608209922,用戶ID為2846741676215238658,那最終生成的編號(hào)為35313185066082099222846741676215238658。為了減少長(zhǎng)度,我們可以只取用戶ID的最后6位進(jìn)行拼接,生成的編號(hào)為3531318506608209922238658,這樣可以支持2^6=64個(gè)分片。
那么此時(shí)如果根據(jù) ORDER_SN 進(jìn)行查詢:
SELECT * FROM CUSTOMER_ORDER
WHERE ORDER_SN = '3531318506608209922238658';
由于字段 ORDER_SN 的設(shè)計(jì)中直接包含了分片鍵信息,所以我們可以直接通過分片鍵部分直接定位到分片上。
基因拼接法的缺點(diǎn)是,對(duì)應(yīng)的鍵會(huì)變大一些,存儲(chǔ)也會(huì)相應(yīng)變大,但是卻可以大大提升后續(xù)的查詢效率,這種空間換時(shí)間的設(shè)計(jì),總體上看是非常值得的。
實(shí)際上淘寶的訂單號(hào)也是這樣構(gòu)建的,如下圖所示,訂單的最后6位都是607041,所以大概率推測(cè)出:
- 淘寶訂單表的分片鍵是用戶 ID;
- 淘寶訂單表,訂單表的主鍵包含用戶 ID,也就是分片信息。這樣通過訂單號(hào)進(jìn)行查詢,可以獲得分片信息,從而查詢 1 個(gè)分片就能得到最終的結(jié)果。
圖片
代碼實(shí)現(xiàn)
在DailyMart中選擇使用shardingsphere實(shí)現(xiàn)分庫分表功能,不過為了方便演示,我在這里只進(jìn)行分表操作。
1、首先,將原始訂單表和訂單明細(xì)表分別拆成4個(gè)表
圖片
2、在訂單模塊基礎(chǔ)設(shè)施層中引入shardingsphere,
<dependency>
<groupId>org.apache.shardingsphere</groupId>
<artifactId>shardingsphere-jdbc-core-spring-boot-starter</artifactId>
<version>5.2.1</version>
</dependency>
3、編寫復(fù)合分片算法,實(shí)現(xiàn)基于order_sn和customer_id的查詢
public class OrderGenComplexTableAlgorithm implements ComplexKeysShardingAlgorithm<Comparable<?>> {
...
@Override
public Collection<String> doSharding(Collection<String> availableTargetNames, ComplexKeysShardingValue<Comparable<?>> shardingValue) {
Map<String, Collection<Comparable<?>>> columnNameAndShardingValuesMap = shardingValue.getColumnNameAndShardingValuesMap();
Collection<String> result = new LinkedHashSet<>(availableTargetNames.size());
if(MapUtils.isNotEmpty(columnNameAndShardingValuesMap)){
// 獲取用戶ID
Collection<Comparable<?>> userIdCollection = columnNameAndShardingValuesMap.get(USER_ID_COLUMN);
//用戶分片
if(CollectionUtils.isNotEmpty(userIdCollection)){
userIdCollection.stream().findFirst().ifPresent(comparable -> {
long tableNameSuffix = (Long) comparable % shardingCount;
result.add(shardingValue.getLogicTableName() + "_" + tableNameSuffix);
});
}else {
Collection<Comparable<?>> orderSnCollection = columnNameAndShardingValuesMap.get(ORDER_ID_COLUMN);
orderSnCollection.stream().findFirst().ifPresent(comparable -> {
String orderSn = String.valueOf(comparable);
//獲取用戶基因
String substring = orderSn.substring(Math.max(0, orderSn.length() - 6));
long tableNameSuffix = Long.parseLong(substring) % shardingCount;
result.add(shardingValue.getLogicTableName() + "_" + tableNameSuffix);
});
}
}
return result;
}
...
}
在上述代碼中,當(dāng)通過用戶ID進(jìn)行查詢時(shí)直接通過分片鍵取模定位分片,如果是基于訂單查詢先獲取用戶基因,再根據(jù)用戶基因取模定位分片。
4、在application.yaml中配置分庫分表
spring:
shardingsphere:
datasource:
names: ds0
ds0:
type: com.zaxxer.hikari.HikariDataSource
driver-class-name: org.mariadb.jdbc.Driver
rules:
sharding:
sharding-algorithms:
order-gen-complex-sharding:
type: CLASS_BASED
props:
strategy: COMPLEX
algorithmClassName: com.jianzh5.dailymart.module.order.infrastructure.config.OrderGenComplexTableAlgorithm
sharding-count: 4
tables:
customer_order:
actual-data-nodes: ds0.customer_order_$->{0..3}
table-strategy:
complex:
sharding-algorithm-name: order-gen-complex-sharding
sharding-columns: order_sn,customer_id
order_item:
actual-data-nodes: ds0.order_item_$->{0..3}
table-strategy:
complex:
sharding-algorithm-name: order-gen-complex-sharding
sharding-columns: order_sn,customer_id
通過上述步驟,在訂單模塊中已經(jīng)集成了分庫分表功能,接下來編寫兩個(gè)接口對(duì)其進(jìn)行測(cè)試。
測(cè)試
在訂單模塊的接口層我們定義了兩個(gè)接口用于模擬實(shí)際的業(yè)務(wù)場(chǎng)景:1、獲取指定用戶的訂單分頁列表;2、根據(jù)訂單編號(hào)獲取訂單詳情。
接口定義如下:
@Operation(summary = "根據(jù)用戶ID分頁查詢訂單")
@GetMapping("/api/pd/order/page")
public PageResponse<OrderRespDTO> pageQuery(@Valid OrderPageQueryDTO orderPageQueryDTO) {
return orderService.findListByUserId(orderPageQueryDTO);
}
@Operation(summary = "根據(jù)訂單號(hào)查詢訂單詳情")
@GetMapping("/api/pd/order/{orderSn}")
public OrderRespDTO getOrderBySn(@PathVariable("orderSn") String orderSn) {
return orderService.getOrderBySn(orderSn);
}
通過運(yùn)行結(jié)果可知,根據(jù)用戶訂單獲取分頁列表時(shí)直接根據(jù)Customer_id取模,只需要一次查詢即可定位。
圖片
當(dāng)根據(jù)訂單號(hào)查詢訂單詳情時(shí),根據(jù)用戶基因取模,同樣也只需要一次查詢即可定位。
圖片
小結(jié)
通過以上步驟,我們完成了在DailyMart中集成分庫分表功能的實(shí)踐,大家在實(shí)施分庫分表過程中一定要結(jié)合自己的業(yè)務(wù)實(shí)際選擇合理的分片鍵,分片鍵的好壞決定了你分庫分表架構(gòu)方案的好壞。