ShardingSphere5.2.1生產級分庫分表實現
大家好,我是飄渺。
隨著業務的不斷發展,DailyMart每天產生的銷售訂單已經達到了約100萬,并且呈持續增長趨勢。按照這樣的發展速度,每年的數據量將達到約4億左右。目前,DailyMart采用的是MySQL單表進行存儲,但鑒于業務的快速發展,我們迫切需要對其進行分庫分表的改造。今天,我們來探討如何實現分庫分表功能,以及相關的步驟和注意事項。
這是本系列文章的第31篇,歡迎持續關注。
對于分庫分表的相關知識,我的星球分庫分表專欄有詳細的介紹說明,強烈推薦大家加入學習。
分庫分表的核心在于合理選擇分片鍵以及快速定位非分片鍵的數據。
分片鍵的選擇
DailyMart作為一個ToC的業務系統,大部分業務訪問都是基于用戶ID進行的,比如登錄用戶查看自己的購買記錄等。因此,對于訂單模塊我們決定以用戶ID作為分片鍵。
在訂單模塊中,訂單主表 CUSTOMER_ORDER 和訂單明細表 ORDER_ITEM 是最核心的兩張表,由于它們經常會一起使用,我們也需要將訂單明細表的用戶字段 CUSTOMER_ID 作為分片鍵,以確保基于用戶維度的查詢在單個分片上完成。下面是一個示例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作為查詢條件。但在實際使用中,經常會根據訂單編號ORDER_SN進行精確查詢,比如庫存扣減、支付后的反查等。在默認情況下,根據訂單編號(非分片鍵)進行查詢將需要在所有分片上進行查詢,然后對結果進行聚合,顯然這樣的查詢效率是很低的。
為了解決這個問題,業界一般采用基因法來解決,即將分片鍵的信息保存在想要查詢的列中,這樣通過查詢的列就能直接知道數據所在的分片信息。
基因法的原理是 對一個數取余2的n次方,那么余數就是這個數的二進制的最后n位數。
以訂單表為例,對訂單表我們根據CUSOMER_ID將其拆成16張表,采用CUSOMER_ID % 16的方式來進行數據庫路由,這里的CUSOMER_ID % 16,其本質是CUSOMER_ID的最后4個bit位 log(16,2) = 4 決定這行數據落在哪個分片上,這4個bit就是分片基因。
基于這一理論,基因法有兩種具體的實現:
基因替換法
- 在生成訂單編號ORDER_SN時,先使用一種分布式ID生成算法生成前60bit
- 計算出分片基因:分庫基因是CUSTOMER_ID的最后4個bit,log(16,2) = 4,即1001
- 將分庫基因加入到ORDER_SN的最后4個bit(上圖中粉色部分)
- 拼裝成最終的64bit訂單ORDER_SN(上圖中藍色部分)
這樣保證了同一個用戶創建的所有訂單都落到了同一個分片上,ORDER_SN的最后4個bit都相同,通過CUSTOMER_ID %16 能夠定位到分片,通過ORDER_SN % 16也能定位到分片。
基因替換法可能會導致ORDER_SN重復,以雪花算法為例,假設同一個用戶在一毫秒內創建了 2 個訂單,這樣生產的序列號相差1,替換掉基因后對應的二進制都相同了,導致ORDER_SN也是重復的。但這種情況非常少見,除非是機器人刷單。當然如果要徹底杜絕訂單編號重復問題可以使用下面介紹的基因拼接法。
基因拼接法
基因拼接法更簡單,就是在構建訂單編號時直接將用戶基因拼接在生成的ID后面,即:ORDER_SN = string(ORDER_SN + CUSTOMER_ID)
假設開始生成的訂單號是3531318506608209922,用戶ID為2846741676215238658,那最終生成的編號為35313185066082099222846741676215238658。為了減少長度,我們可以只取用戶ID的最后6位進行拼接,生成的編號為3531318506608209922238658,這樣可以支持2^6=64個分片。
那么此時如果根據 ORDER_SN 進行查詢:
SELECT * FROM CUSTOMER_ORDER
WHERE ORDER_SN = '3531318506608209922238658';
由于字段 ORDER_SN 的設計中直接包含了分片鍵信息,所以我們可以直接通過分片鍵部分直接定位到分片上。
基因拼接法的缺點是,對應的鍵會變大一些,存儲也會相應變大,但是卻可以大大提升后續的查詢效率,這種空間換時間的設計,總體上看是非常值得的。
實際上淘寶的訂單號也是這樣構建的,如下圖所示,訂單的最后6位都是607041,所以大概率推測出:
- 淘寶訂單表的分片鍵是用戶 ID;
- 淘寶訂單表,訂單表的主鍵包含用戶 ID,也就是分片信息。這樣通過訂單號進行查詢,可以獲得分片信息,從而查詢 1 個分片就能得到最終的結果。
代碼實現
在DailyMart中選擇使用shardingsphere實現分庫分表功能,不過為了方便演示,我在這里只進行分表操作。
1、首先,將原始訂單表和訂單明細表分別拆成4個表
2、在訂單模塊基礎設施層中引入shardingsphere,
<dependency>
<groupId>org.apache.shardingsphere</groupId>
<artifactId>shardingsphere-jdbc-core-spring-boot-starter</artifactId>
<version>5.2.1</version>
</dependency>
3、編寫復合分片算法,實現基于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;
}
...
}
在上述代碼中,當通過用戶ID進行查詢時直接通過分片鍵取模定位分片,如果是基于訂單查詢先獲取用戶基因,再根據用戶基因取模定位分片。
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
通過上述步驟,在訂單模塊中已經集成了分庫分表功能,接下來編寫兩個接口對其進行測試。
測試
在訂單模塊的接口層我們定義了兩個接口用于模擬實際的業務場景:1、獲取指定用戶的訂單分頁列表;2、根據訂單編號獲取訂單詳情。
接口定義如下:
@Operation(summary = "根據用戶ID分頁查詢訂單")
@GetMapping("/api/pd/order/page")
public PageResponse<OrderRespDTO> pageQuery(@Valid OrderPageQueryDTO orderPageQueryDTO) {
return orderService.findListByUserId(orderPageQueryDTO);
}
@Operation(summary = "根據訂單號查詢訂單詳情")
@GetMapping("/api/pd/order/{orderSn}")
public OrderRespDTO getOrderBySn(@PathVariable("orderSn") String orderSn) {
return orderService.getOrderBySn(orderSn);
}
通過運行結果可知,根據用戶訂單獲取分頁列表時直接根據Customer_id取模,只需要一次查詢即可定位。
當根據訂單號查詢訂單詳情時,根據用戶基因取模,同樣也只需要一次查詢即可定位。
圖片
小結
通過以上步驟,我們完成了在DailyMart中集成分庫分表功能的實踐,大家在實施分庫分表過程中一定要結合自己的業務實際選擇合理的分片鍵,分片鍵的好壞決定了你分庫分表架構方案的好壞。