Sharding-JDBC基本使用,整合Springboot實現分庫分表,讀寫分離

coffeebabe發表於2021-10-29

結合上一篇docker部署的mysql主從, 本篇主要講解SpringBoot專案結合Sharding-JDBC如何實現分庫分表、讀寫分離。

一、Sharding-JDBC介紹

1、這裡引用官網上的介紹:

  定位為輕量級Java框架,在Java的JDBC層提供的額外服務。 它使用客戶端直連資料庫,以jar包形式提供服務,無需額外部署和依賴,可理解為增強版的JDBC驅動,完全相容JDBC和各種ORM框架。

  適用於任何基於JDBC的ORM框架,如:JPA, Hibernate, Mybatis, Spring JDBC Template或直接使用JDBC。
  支援任何第三方的資料庫連線池,如:DBCP, C3P0, BoneCP, Druid, HikariCP等。
  支援任意實現JDBC規範的資料庫。目前支援MySQL,Oracle,SQLServer,PostgreSQL以及任何遵循SQL92標準的資料庫。

2、自己的理解:
  增強版的JDBC驅動,客戶端使用的時候,就像正常使用JDBC驅動一樣, 引入Sharding-JDBC依賴包,連線好資料庫,配置好分庫分表規則,讀寫分離配置,然後客戶端的sql 操作 Sharding-JDBC會自動根據配置完成 分庫分表和讀寫分離操作。


二、實現效果

1、下圖展示了我們通過Sharding-JDBC實現的分庫分表及讀寫分離效果圖

  分庫分表:結合上一篇的主從,這裡我們使用上次搭建的主從資料庫,3307的app1是主資料庫,3308的app1是對應的從資料庫。同時,我們在3307新建app2庫和user2表,這裡的app2庫需要和app1庫一樣,user2表和user1表結構一樣,主從會自動幫我們建表同步到3308,然後我們在專案中使用Sharding-JDBC 配置響應的分庫分表策略,使得插入資料的時候 根據配置欄位的分片規則將資料打入對應的庫和表。在我們這裡主要是 根據分庫的分片規則決定資料進入3307的app1庫還是app2庫,然後再根據分表的分片規則決定進入user1表還是user2表。
  讀寫分離:讀寫分離 在我們這裡主要指的是 我們專案DQL會根據Sharding-JDBC配置的master-slave-rule走的3308的資料來源,而專案的DML會根據master-slave-rule走3307的資料來源

分庫分表讀寫分離

三、Spring-Boot專案整合Sharding-JDBC實現分庫分表、讀寫分離

1、這裡建立一個maven專案,首先引入依賴,pom.xml檔案如下。

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.3.3.RELEASE</version>
    </parent>

    <groupId>com.cgg</groupId>
    <artifactId>sharding-jdbc-test</artifactId>
    <version>1.0-SNAPSHOT</version>

    <properties>
        <java.version>1.8</java.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
        </dependency>

        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>8.0.15</version>
        </dependency>

        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid</artifactId>
            <version>1.1.21</version>
        </dependency>

        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.1.1</version>
        </dependency>

        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-extension</artifactId>
            <version>3.1.1</version>
        </dependency>

        <dependency>
            <groupId>org.apache.shardingsphere</groupId>
            <artifactId>sharding-jdbc-spring-boot-starter</artifactId>
            <version>4.0.0-RC1</version>
        </dependency>

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>

    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>

注意:這裡使用的是4.0的sharding-jdbc,spring-boot的版本是2.x的,在整合過程中遇見了許多問題,後面會有錯誤的解決步驟。
2、application.yml檔案如下

spring:
  jpa:
    properties:
      hibernate:
        hbm2ddl:
          auto: create
        dialect: org.hibernate.dialect.MySQL5Dialect
        show_sql: true
  shardingsphere:
    props:
      sql:
        show: true
    datasource:
      names: master0,master0slave0,master1,master1slave0
      master0:
        type: com.alibaba.druid.pool.DruidDataSource
        driver-class-name: com.mysql.cj.jdbc.Driver
        url: jdbc:mysql://127.0.0.1:3307/app1?useUnicode=true&characterEncoding=utf8&tinyInt1isBit=false&useSSL=false&serverTimezone=GMT
        username: root
        password: 654321
      master1:
        type: com.alibaba.druid.pool.DruidDataSource
        driver-class-name: com.mysql.cj.jdbc.Driver
        url: jdbc:mysql://127.0.0.1:3307/app2?useUnicode=true&characterEncoding=utf8&tinyInt1isBit=false&useSSL=false&serverTimezone=GMT
        username: root
        password: 654321
      master0slave0:
        type: com.alibaba.druid.pool.DruidDataSource
        driver-class-name: com.mysql.cj.jdbc.Driver
        url: jdbc:mysql://127.0.0.1:3308/app1?useUnicode=true&characterEncoding=UTF-8&allowMultiQueries=true&useSSL=false&serverTimezone=GMT
        username: root
        password: 654321
      master1slave0:
        type: com.alibaba.druid.pool.DruidDataSource
        driver-class-name: com.mysql.cj.jdbc.Driver
        url: jdbc:mysql://127.0.0.1:3308/app2?useUnicode=true&characterEncoding=UTF-8&allowMultiQueries=true&useSSL=false&serverTimezone=GMT
        username: root
        password: 654321
    sharding:
      default-database-strategy:
        inline:
          sharding-column: id
          algorithm-expression: app$->{(id % 2)+1}
      tables:
        user:
          actual-data-nodes: app$->{1..2}.user$->{1..2}
          table-strategy:
            inline:
              sharding-column: id
              algorithm-expression: user$->{((""+id)[2..10].toInteger() % 2)+1}
          key-generator:
            column: id
            type: SNOWFLAKE
      master-slave-rules:
        app1:
          master-data-source-name: master0
          slave-data-source-names: master0slave0
        app2:
          master-data-source-name: master1
          slave-data-source-names: master1slave0
sharding:
  jdbc:
    config:
      masterslave:
        load-balance-algorithm-type: random

3、application.properties檔案

spring.main.allow-bean-definition-overriding=true

mybatis-plus.mapper-locations= classpath:/mapper/*.xml

mybatis-plus.configuration.log-impl= org.apache.ibatis.logging.stdout.StdOutImpl

4、分庫分表實現

4.1、先說下資料來源,結合之前mysql主從的文章,我本地127.0.0.1:3307埠是主,127.0.0.1:3308埠是從。

   在3307下建立兩個庫app1和app2,同時每個庫裡面建立兩張表user1和user2表,用來完成分庫分表。

   下面是app1庫SQL語句:

SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;

-- ----------------------------
-- Table structure for user1
-- ----------------------------
DROP TABLE IF EXISTS `user1`;
CREATE TABLE `user1`  (
  `id` bigint(11) NOT NULL COMMENT '主鍵id',
  `name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic;

-- ----------------------------
-- Table structure for user2
-- ----------------------------
DROP TABLE IF EXISTS `user2`;
CREATE TABLE `user2`  (
  `id` bigint(11) NOT NULL COMMENT '主鍵id',
  `name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = DYNAMIC;

SET FOREIGN_KEY_CHECKS = 1;

   下面是app2庫SQL語句:

SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;

-- ----------------------------
-- Table structure for user1
-- ----------------------------
DROP TABLE IF EXISTS `user1`;
CREATE TABLE `user1`  (
  `id` bigint(11) NOT NULL COMMENT '主鍵id',
  `name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = DYNAMIC;

-- ----------------------------
-- Table structure for user2
-- ----------------------------
DROP TABLE IF EXISTS `user2`;
CREATE TABLE `user2`  (
  `id` bigint(11) NOT NULL COMMENT '主鍵id',
  `name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic;

SET FOREIGN_KEY_CHECKS = 1;

4.2、這裡我們解釋一下配置的分庫分表規則實現將資料插入到app1和app2庫,user1和user2表

    sharding:
      default-database-strategy:
        inline:
          sharding-column: id #分片的欄位是id主鍵
          algorithm-expression: app$->{(id % 2)+1} #分片的演算法是 id對2求餘然後加1
      tables:
        user:
          actual-data-nodes: app$->{1..2}.user$->{1..2}  #實際的資料節點是(app1/app2).(user1/user2)
          table-strategy:
            inline:
              sharding-column: id #分表的分片欄位是主鍵id
              algorithm-expression: user$->{((""+id)[2..10].toInteger() % 2)+1} #分表的演算法是取id的2-10位對2求餘然後加1
          key-generator:
            column: id # 自動生成主鍵
            type: SNOWFLAKE # 生成主鍵的規則是雪花演算法

   上面配置的規則指的是,當有資料要插入資料庫,或者進行查詢時,sharding-jdbc通過分片配置的欄位id的值,去根據配置的演算法 進行運算,得到結果,例如上述分庫規則,拿到id值 對2求餘加1,那麼不管id怎麼變化演算法返回的值永遠是1和2,即app$->{(id % 2)+1} 對應的就是app1和app2庫,分表的規則是同樣道理。
   說明:這裡只是配置了簡單的分片規則來演示sharding-jdbc如何完成分庫分表,我們也可以使用程式碼重寫
方法來實現更復雜的分片策略。最後,這裡的$->{(id % 2)+1} 的{}中實際上是一個Groovy語法的表示式,sharding-jdbc是通過Groovy語法糖來解析分片策略的。所以想要配置更為複雜的策略,建議學一下Groovy語法。

4.3、接下來我們介紹配置的讀寫分離規則,如何實現讀寫分離

      master-slave-rules:
        app1: #分割槽 app1
          master-data-source-name: master0 #分割槽 app1的主資料來源
          slave-data-source-names: master0slave0 #分割槽 app1的從資料來源
        app2: #分割槽 app2
          master-data-source-name: master1 #分割槽 app2的主資料來源
          slave-data-source-names: master1slave0 #分割槽 app2的從資料來源

   上面讀寫分離的規則指的是,分割槽app1的主從資料來源,分割槽app2的主從資料來源。至於這裡的分割槽為什麼是app1和app2?這裡說明一下,我自己配置的時候,配置了幾次都沒有成功,一開始參照官網手冊配置,以為分割槽名稱可以自定義,於是配置的是ds0和ds1,但是專案啟動報錯了。報錯資訊是:
Cannot find data source in sharding rule, invalid actual data node is: 'app1.user1'
啟動分表規則報錯

開始以為是使用的sharding-jdbc版本問題,但是換了版本還是有問題,於是開始除錯了一下原始碼:

   
報錯除錯

報錯除錯2

    從上面的截圖中很明顯就能發現,這裡是要判斷我們配置的分割槽集合也就是ds0和ds1是否包含 實際節點的資料來源名稱,也就是資料庫名稱。所以這裡的分割槽名稱是和我們上面配置的分片策略的資料庫名稱有關係的。

4.4、驗證
    接下來我們驗證實際的效果。這裡貼一下單元測試的程式碼。

/**
 * @author cgg
 * @version 1.0.0
 * @date 2021/10/25
 */
@SpringBootTest(classes = ShardingJdbcApp.class)
@RunWith(SpringRunner.class)
public class AppTest {

    @Resource
    private IUserService userService;


    /**
     * 測試sharding-jdbc新增資料
     */
    @Test
    public void testShardingJdbcInsert() {

        userService.InsertUser();
    }

    /**
     * 測試sharding-jdbc查詢資料
     */
    @Test
    public void testShardingJdbcQuery() {

        //全部查詢
        userService.queryUserList();

        //根據指定條件查詢
        userService.queryUserById(1452619866473324545L);

    }

}

/**
 * @author cgg
 * @version 1.0.0
 * @date 2021/10/25
 */
@Service
@Slf4j
public class UserServiceImpl implements IUserService {

    @Resource
    private UserMapper userMapper;

    @Resource
    private DataSource dataSource;

    @Override
    public List<User> queryUserList() {
        List<User> userList = userMapper.queryUserList();
        userList.forEach(user -> System.out.println(user.toString()));
        return userList;
    }

    @Override
    public User queryUserById(Long id) {
        User user = userMapper.selectOne(Wrappers.<User>lambdaQuery().eq(User::getId, id));
        System.out.println(user.toString());
        return user;
    }

    @Override
    public void InsertUser() {
        for (int i = 20; i < 40; i++) {
            User user = new User();
            user.setName("XX-" + i);
            int count = userMapper.insert(user);
            System.out.println(count);
        }
    }


}

    4.4.1、首先看全部查詢的結果

全查測試結果

    4.4.2、再看下單條查詢的結果

單條查詢結果

    4.4.3、再看下新增結果(實際插入到了主資料來源的app1庫user1表,並且後續每條插入都是走的主資料來源,沒有slave的操作)

新增測試結果


四、問題及總結

   到這裡sharding-jdbc的初步基本使用已經沒問題了,除了最簡單的使用,我們還需要考慮分庫分表後事務怎麼處理,即分散式事務問題,還有讀寫分離後,資料不一致及同步延時問題,這些就需要我們從概念理論學習,然後在結合實際業務考慮方案,後續會接著這篇文章先出一篇sharding-proxy的使用。再出一篇關於分散式事務和主從資料延時的部落格。

相關文章