Sharding-JDBC 分庫分表

一. 初識Sharding-JDBC

1. Sharding-JDBC是什么?

Sharding-JDBC提供標準化的數(shù)據(jù)分片、分布式事務和數(shù)據(jù)庫治理功能,定位為輕量級Java框架,在Java的JDBC層提供的額外服務。 它使用客戶端直連數(shù)據(jù)庫,以jar包形式提供服務,無需額外部署和依賴,可理解為增強版的JDBC驅(qū)動,完全兼容JDBC和各種ORM框架。
適用于任何基于Java的ORM框架,如:JPA, Hibernate, Mybatis, Spring JDBC Template或直接使用JDBC。
基于任何第三方的數(shù)據(jù)庫連接池,如:DBCP, C3P0, BoneCP, Druid, HikariCP等。
支持任意實現(xiàn)JDBC規(guī)范的數(shù)據(jù)庫。目前支持MySQL,Oracle,SQLServer和PostgreSQL。

2. 為什么要分片?

傳統(tǒng)的將數(shù)據(jù)集中存儲至單一數(shù)據(jù)節(jié)點的解決方案,在性能、可用性和運維成本這三方面已經(jīng)難于滿足互聯(lián)網(wǎng)的海量數(shù)據(jù)場景。
從性能方面來說,由于關系型數(shù)據(jù)庫大多采用B+樹類型的索引,在數(shù)據(jù)量超過閾值的情況下,索引深度的增加也將使得磁盤訪問的IO次數(shù)增加,進而導致查詢性能的下降;同時,高并發(fā)訪問請求也使得集中式數(shù)據(jù)庫成為系統(tǒng)的最大瓶頸。
從可用性的方面來講,服務化的無狀態(tài)型,能夠達到較小成本的隨意擴容,這必然導致系統(tǒng)的最終壓力都落在數(shù)據(jù)庫之上。而單一的數(shù)據(jù)節(jié)點,或者簡單的主從架構,已經(jīng)越來越難以承擔。數(shù)據(jù)庫的可用性,已成為整個系統(tǒng)的關鍵。
從運維成本方面考慮,當一個數(shù)據(jù)庫實例中的數(shù)據(jù)達到閾值以上,對于DBA的運維壓力就會增大。數(shù)據(jù)備份和恢復的時間成本都將隨著數(shù)據(jù)量的大小而愈發(fā)不可控。一般來講,單一數(shù)據(jù)庫實例的數(shù)據(jù)的閾值在1TB之內(nèi),是比較合理的范圍。
在傳統(tǒng)的關系型數(shù)據(jù)庫無法滿足互聯(lián)網(wǎng)場景需要的情況下,將數(shù)據(jù)存儲至原生支持分布式的NoSQL的嘗試越來越多。 但NoSQL對SQL的不兼容性以及生態(tài)圈的不完善,使得它們在與關系型數(shù)據(jù)庫的博弈中始終無法完成致命一擊,而關系型數(shù)據(jù)庫的地位卻依然不可撼動。
數(shù)據(jù)分片指按照某個維度將存放在單一數(shù)據(jù)庫中的數(shù)據(jù)分散地存放至多個數(shù)據(jù)庫或表中以達到提升性能瓶頸以及可用性的效果。 數(shù)據(jù)分片的有效手段是對關系型數(shù)據(jù)庫進行分庫和分表。分庫和分表均可以有效的避免由數(shù)據(jù)量超過可承受閾值而產(chǎn)生的查詢瓶頸。 除此之外,分庫還能夠用于有效的分散對數(shù)據(jù)庫單點的訪問量;分表雖然無法緩解數(shù)據(jù)庫壓力,但卻能夠提供盡量將分布式事務轉(zhuǎn)化為本地事務的可能,一旦涉及到跨庫的更新操作,分布式事務往往會使問題變得復雜。 使用多主多從的分片方式,可以有效的避免數(shù)據(jù)單點,從而提升數(shù)據(jù)架構的可用性。
通過分庫和分表進行數(shù)據(jù)的拆分來使得各個表的數(shù)據(jù)量保持在閾值以下,以及對流量進行疏導應對高訪問量,是應對高并發(fā)和海量數(shù)據(jù)系統(tǒng)的有效手段。
3. 分片的方式

數(shù)據(jù)分片的拆分方式又分為垂直分片和水平分片。

垂直拆分是把不同的表拆到不同的數(shù)據(jù)庫中,而水平拆分是把同一個表拆到不同的數(shù)據(jù)庫中(或者是把一張表數(shù)據(jù)拆分成n多個小表)。相對于垂直拆分,水平拆分不是將表的數(shù)據(jù)做分類,而是按照某個字段的某種規(guī)則來分散到多個庫中,每個表中包含一部分數(shù)據(jù)。簡單來說,我們可以將數(shù)據(jù)的水平切分理解為是按照數(shù)據(jù)行的切分,就是將表中的某些行切分到一個數(shù)據(jù)庫,而另外某些行又切分到其他的數(shù)據(jù)庫中,主要有分表,分庫兩種模式 該方式提高了系統(tǒng)的穩(wěn)定性跟負載能力,但是跨庫join性能較差。

4. Sharding-JDBC的核心/工原理

Sharding-JDBC數(shù)據(jù)分片主要流程是由SQL解析 執(zhí)行器優(yōu)化 **→ **SQL路由 SQL改寫 SQL執(zhí)行 結果歸并的流程組成。

SQL解析
分為詞法解析和語法解析。 先通過詞法解析器將SQL拆分為一個個不可再分的單詞。再使用語法解析器對SQL進行理解,并最終提煉出解析上下文。 解析上下文包括表、選擇項、排序項、分組項、聚合函數(shù)、分頁信息、查詢條件以及可能需要修改的占位符的標記。

SQL解析分為兩步, 第一步為 詞法解析, 詞法解析的意思是就是將SQL進行拆分。
例:
select * from t_user where id = 1
詞法解析:
[select] [*] [from] [t_user] [where] [id=1]
第二步語法解析,語法解析器將SQL轉(zhuǎn)換為抽象語法樹。

image
image.gif

?

執(zhí)行器優(yōu)化
合并和優(yōu)化分片條件,如OR等。

SQL路由
根據(jù)解析上下文匹配用戶配置的分片策略,并生成路由路徑。目前支持分片路由和廣播路由。

舉例說明,如果按照order_id的奇數(shù)和偶數(shù)進行數(shù)據(jù)分片,一個單表查詢的SQL如下:
SELECT * FROM t_order WHERE order_id IN (1, 2);
那么路由的結果應為:
SELECT * FROM t_order_0 WHERE order_id IN (1, 2);
SELECT * FROM t_order_1 WHERE order_id IN (1, 2);

SQL改寫
將SQL改寫為在真實數(shù)據(jù)庫中可以正確執(zhí)行的語句,SQL改寫分為正確性改寫和優(yōu)化改寫。

 從一個最簡單的例子開始,若邏輯SQL為:
 SELECT order_id FROM t_order WHERE order_id=1;
 假設該SQL配置分片鍵order_id,并且order_id=1的情況,將路由至分片表1。那么改寫之后的SQL應該為:
 SELECT order_id FROM t_order_1 WHERE order_id=1;

SQL執(zhí)行
通過多線程執(zhí)行器異步執(zhí)行。
結果歸并
將多個執(zhí)行結果集歸并以便于通過統(tǒng)一的JDBC接口輸出。結果歸并包括流式歸并、內(nèi)存歸并和使用裝飾者模式的追加歸并這幾種方式。

二. SpringBoot整合Sharding-JDBC

1. 創(chuàng)建兩個數(shù)據(jù)庫order1,order2,分別創(chuàng)建t_address表如下:

DROP TABLE IF EXISTS `t_address`;
CREATE TABLE `t_address` (
  `id` bigint(20) NOT NULL,
  `code` varchar(64) DEFAULT NULL COMMENT '編碼',
  `name` varchar(64) DEFAULT NULL COMMENT '名稱',
  `pid` varchar(64) NOT NULL DEFAULT '0' COMMENT '父id',
  `type` int(11) DEFAULT NULL COMMENT '1國家2省3市4縣區(qū)',
  `lit` int(11) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

2. 開始整合SpringBoot,這種方式比較簡單只要加入sharding-jdbc-spring-boot-starter依賴,在application.yml中配置數(shù)據(jù)源,分片策略即可使用,這種方式簡單,方便。

pom.xml

<dependency>
    <groupId>io.shardingsphere</groupId>
    <artifactId>sharding-jdbc-spring-boot-starter</artifactId>
    <version>3.0.0</version>
</dependency>
<dependency>
    <groupId>io.shardingsphere</groupId>
    <artifactId>sharding-jdbc-spring-namespace</artifactId>
    <version>3.0.0</version>
</dependency>
image.gif

** application.yml**

mybatis:
  configuration:
    mapUnderscoreToCamelCase: true
sharding:
  jdbc:
    datasource:
      names: ds0,ds1
      ds0:
        type: com.alibaba.druid.pool.DruidDataSource
        driver-class-name: com.mysql.jdbc.Driver
        url: jdbc:mysql://localhost:3306/order1
        username: root
        password: 123456
      ds1:
        type: com.alibaba.druid.pool.DruidDataSource
        driver-class-name: com.mysql.jdbc.Driver
        url: jdbc:mysql://localhost:3306/order2
        username: root
        password: 123456
    config:
      sharding:
        props:
          sql.show: true
        tables:
          t_user: #t_user表【即分庫,又分表】
            key-generator-column-name: id # 主鍵
            actual-data-nodes: ds${0..1}.t_user${0..1} #數(shù)據(jù)節(jié)點
            database-strategy: #分庫策略
              inline:
                sharding-column: city_id
                algorithm-expression: ds${city_id % 2}
            table-strategy: #分表策略
              inline:
                shardingColumn: sex
                algorithm-expression: t_user${sex % 2}
          t_address: #t_address表【只分庫】
            key-generator-column-name: id
            actual-data-nodes: ds${0..1}.t_address
            database-strategy:
              inline:
                shardingColumn: lit
                algorithm-expression: ds${lit % 2}

image.gif

編寫Dao

@Mapper
public interface IndexDao {

    @InsertProvider(type= AddressProvider.class,method="insertAddress")
    @Options(useGeneratedKeys=true)
    int insertAddress(AddressDo addressDo);

    @Select("select * from t_address order by lit")
    List<AddressDo> listAddress();

}
image.gif

** 編寫controller**

@RestController
public class IndexController {

    @Autowired
    private IndexDao indexDao;

    @PostMapping("/addAddress")
    public ResultBO addAddress(AddressDo addressDo){
        int row = indexDao.insertAddress(addressDo);
        return ResultTool.success(row);
    }

    @GetMapping("/listAddress")
    public ResultBO listAddress(@RequestParam(required=false,defaultValue="1")Integer pageNum,
                                @RequestParam(required=false,defaultValue="5")Integer pageSize){
        PageHelper.startPage(pageNum,pageSize);
        List<AddressDo> list =  indexDao.listAddress();
        if(list.isEmpty()){
            ResultTool.success("查詢內(nèi)容為空");
        }
        PageInfo<AddressDo> info = new PageInfo<>(list);
        return ResultTool.success(info);
    }
}
image.gif

此時,啟動項目,用postman訪問插入接口:

image
image.gif

?
image
image.gif

?

image
image.gif

?
image
image.gif

?
image
image.gif

?

插入四條數(shù)據(jù),可以看到兩個庫,兩個表中的數(shù)據(jù)如下:

image
image.gif

?
image
image.gif

?

可以看到,根據(jù)lit字段進行分片(取模算法),因為我們指定的為2:algorithm-expression: ds${lit % 2},所以奇數(shù)和偶數(shù)會存到不同的庫不同的表中;并且需要注意的是,由于我們指定了key-generator-column-name: id,即自動生成主鍵,采用雪花算法Twitter-Snowflake?!静煌膸臁?/p>

下面我們用postman請求查詢接口,訪問:localhost:8080/listAddress,查詢結果為:

{
    "code": 0,
    "msg": "成功",
    "data": {
        "pageNum": 1,
        "pageSize": 2,
        "size": 2,
        "startRow": 1,
        "endRow": 2,
        "total": 4,
        "pages": 2,
        "list": [
            {
                "id": 363696781952811008,
                "code": "1001",
                "name": "濟南",
                "pid": "0",
                "type": 3,
                "lit": 1
            },
            {
                "id": 363696816295772160,
                "code": "1002",
                "name": "青島",
                "pid": "0",
                "type": 3,
                "lit": 2
            }
        ],
        "prePage": 0,
        "nextPage": 2,
        "isFirstPage": true,
        "isLastPage": false,
        "hasPreviousPage": false,
        "hasNextPage": true,
        "navigatePages": 8,
        "navigatepageNums": [
            1,
            2
        ],
        "navigateFirstPage": 1,
        "navigateLastPage": 2,
        "firstPage": 1,
        "lastPage": 2
    }
}
image.gif

可以看到我們的order by 生效了,并且分頁也生效了。

【測試沒有配過分片策略的表】

image
image.gif

?

如上圖,只有order2數(shù)據(jù)庫里有訂單表address_order_table,此時我們想查詢的話直接執(zhí)行如下SQL語句即可:

@Select("select * from order2.address_order_table")
List<OrderDo> listOrder();
image.gif

【測試關聯(lián)查詢】

此時我們想關聯(lián)查詢order2庫的address_order_table表和order1,order2庫的t_address表,下圖是address_order_table數(shù)據(jù)結構

image
image.gif

?

如果想關聯(lián)兩個表,直接執(zhí)行如下SQL語句即可:

@Select("select a.id,a.address_number,a.order_remark from order2.address_order_table a inner join t_address b " +
        "on a.address_number = b.id where b.id=#{id}")
OrderDo getOrder(@Param("id")Long id);
image.gif
image
image.gif

?

【同一個庫進行分表】

上面是在不同的庫進行分表,如果要想實現(xiàn)在同一個庫下進行分表,則application.yml可以這樣配置:

mybatis:
  configuration:
    mapUnderscoreToCamelCase: true
sharding:
  jdbc:
    datasource:
      ####ds1
      names: shardingjdbc
      shardingjdbc:
        type: com.alibaba.druid.pool.DruidDataSource
        driver-class-name: com.mysql.jdbc.Driver
        url: jdbc:mysql://127.0.0.1:3306/shardingjdbc
        username: root
        password: 123456
####  分片配置(這些表都是需要提前在數(shù)據(jù)庫創(chuàng)建好)【一般都是在單個庫進行分表】
    config:
      sharding:
        tables:
          # t_order表分片策略
          t_order:
            table-strategy:
              inline:
                # 根據(jù)userid 進行分片
                sharding-column: user_id
                # ds_1.t_order_0 ds_1.t_order_1
                algorithm-expression: shardingjdbc.t_order_$->{user_id % 2}
              ###分表的總數(shù) 0到1   t_order_0 t_order_1
            actual-data-nodes: shardingjdbc.t_order_$->{0..1}
#         t_member:
#           ...
        props:
          sql: 
          ### 開啟分片日志
            show: true

image.gif

解讀:在數(shù)據(jù)庫建立兩張表:t_order_0和t_order_1,會根據(jù)user_id進行分片,取模2,即生成訂單的時候,會根據(jù)userId進行判斷,如果是用戶id是計數(shù),則存到t_order_1表,如果用戶id是偶數(shù),則存到t_order_0表中。

最后編輯于
?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

友情鏈接更多精彩內(nèi)容