1. 程式人生 > 程式設計 >初探分庫分表

初探分庫分表

本文來自:DanielLin07的部落格《初探分庫分表》

前言

分庫分表是企業開發資料儲存中非常常見的一項優化工作,但之前一直沒有去認真瞭解過,直到最近接觸了一個Spark表日同步千萬資料到MySQL表的工作,才對分庫分表有了一個初步的認識。 本文就是對這次分庫分表初步學習的一個記錄總結。

概述

在資料量較小的時候,資料多是以單表的形式儲存。但隨著業務量的擴大儲存資料量的增加,單表的操作效能也會大大降低,影響正常的業務工作。 這時就需要考慮使用分庫分表,一般而言,在單表資料量達到1000萬左右(公司DBA建議)時,就可以考慮使用分庫分表。

分庫分表策略

垂直切分

用簡單的話來說,垂直切分就是將一個表中涉及的多個欄位切分到不同的表甚至是庫中儲存。如下圖所示:

垂直切分

我們常用的 資料庫三大正規化 設計,其實也是一種垂直切分。 另一種常用的垂直切分,則是將熱門訪問欄位與冷門訪問欄位進行切分,從而讓資料庫可以以更少的欄位快取更多的行,進而帶來效能的提升。

水平切分

用簡單的話來說,水平切分就是將一個表中儲存的資料依照某種策略儲存到不同的表上。如下圖所示:

水平切分

Range

水平切分的第一種方式就是Range,即根據一定的範圍進行分發。 如:根據時間範圍,一個月的資料儲存一張表,或者是根據使用者ID這種自增序列,使用者ID在000000至100000範圍的存一張表,100001至200000範圍的存一張表等。 根據Range分發的好處就是資料擴容時方便。缺點就是容易產生資料熱點問題。

Hash

水平切分的第二種方式就是Hash,即通過一次雜湊運算然後取餘分表數量-1的方式確定資料要存的表的位置。 如:根據使用者姓名進行Hash分發。使用者姓名小明,計算hashcode,得到754703,預先確定分表數量為8,再取餘7,得到3,即分發到索引為3的資料表上。 根據Hash分發的好處就是資料分發均勻,不會產生資料熱點問題,但是擴容的時候非常不方便,還需要重新計算資料的雜湊值。

MyBatis + ShardingJDBC 實踐分庫分表

ShardingJDBC是ShardingSphere的子專案,在Java的JDBC層提供的額外服務。具體可見ShardingPhere官方檔案

資料庫準備

現有使用者資訊需要儲存,分別有五個欄位:uid、name、mobile、credit_id、create_time。 現在的分庫分表策略是:

  • 根據uid進行水平切分,uid最後一位為偶數的,分到sharding0db資料庫,否則分到sharding1db資料庫。
  • 在各資料庫中,uid倒數第二位為偶數的,分到t_user_0表,否則分到t_user_1表。 所以每個表儲存的欄位都是一樣的,其中一個表的資料庫 Schema 指令碼如下:
DROP TABLE IF EXISTS `t_user_0`;
CREATE TABLE `t_user_0`  (
  `uid` int(6) NOT NULL,`name` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,`mobile` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,`credit_id` varchar(16) NOT NULL,`create_time` datetime(0) NULL,PRIMARY KEY (`uid`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
複製程式碼

在sharding0db與sharding1db都建立了資料表後,結構如下圖所示:

資料庫結構

Maven依賴

本專案使用的是Spring-Boot 2.0.3.RELEASE,在專案中匯入以下Maven依賴:

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

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

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

   <dependency>
      <groupId>org.mybatis.spring.boot</groupId>
      <artifactId>mybatis-spring-boot-starter</artifactId>
      <version>1.3.2</version>
   </dependency>

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

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

   <dependency>
      <groupId>mysql</groupId>
      <artifactId>mysql-connector-java</artifactId>
   </dependency>
</dependencies>
複製程式碼

配置檔案

application.yml中進行配置:

spring:

  shardingsphere:
    datasource:
      names: sharding0db,sharding1db
      sharding0db:
        type: com.zaxxer.hikari.HikariDataSource
        jdbc-url: jdbc:mysql://localhost:3306/sharding0db?useUnicode=true&useSSL=false&useAffectedRows=true&characterEncoding=utf8
        driver-class-name: com.mysql.jdbc.Driver
        username: root
        password:
      sharding1db:
        type: com.zaxxer.hikari.HikariDataSource
        jdbc-url: jdbc:mysql://localhost:3306/sharding1db?useUnicode=true&useSSL=false&useAffectedRows=true&characterEncoding=utf8
        driver-class-name: com.mysql.jdbc.Driver
        username: root
        password:

    sharding:
      # 分庫分表策略
      default-database-strategy:
        inline:
          # 分片的列
          sharding-column: uid
          # 分片的表示式,groovy語言,這裡是對uid進行取餘,如果為結果為0則分到sharding0db,結果為1則分到sharding1db
          algorithm-expression: sharding$->{uid % 2}db
      tables:
        t_user:
          actual-data-nodes: sharding$->{0..1}db.t_user_$->{0..1}
          table-strategy:
            inline:
              sharding-column: uid
              # 分片的表示式,對uid倒數第二位取餘,如果為結果為0則分到t_user_0,結果為1則分到t_user_1
              algorithm-expression: t_user_$->{uid.intdiv(10) % 2}

# MyBatis配置
mybatis:
  # Mapper對映檔案的位置
  mapper-locations: classpath:mapper/*.xml
  # 包下所有類的別名,配置別名為了在物件對映檔案中接收引數型別和返回引數型別時省略包路徑
  type-aliases-package: com.daniellin.demosharding.entity
複製程式碼

編碼

準備UserDAO檔案:

@Mapper
@Repository
public interface UserDAO {

    /**
     * 獲取所有使用者
     *
     * @return 所有使用者
     */
    List<User> queryList();

    /**
     * 新增新使用者
     *
     * @param user 新使用者
     */
    void insert(User user);
}
複製程式碼

準備UserDAO的XML對映:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">

<mapper namespace="com.daniellin.demosharding.dao.UserDAO">

    <resultMap id="UserMapping" type="User">
        <id column="uid" property="uid" javaType="int"/>
        <result column="name" property="name" javaType="String"/>
        <result column="mobile" property="mobile" javaType="long"/>
        <result column="credit_id" property="creditId" javaType="long"/>
        <result column="create_time" property="createTime"/>
    </resultMap>

    <!-- 獲取所有使用者 -->
    <select id="queryList" resultMap="UserMapping">
        SELECT * FROM t_user
    </select>

    <!-- 新增新使用者 -->
    <insert id="insert" keyProperty="uid" parameterType="User" >
        INSERT INTO t_user(uid,name,mobile,credit_id,create_time)
        VALUES (#{uid},#{name},#{mobile},#{creditId},#{createTime})
    </insert>

</mapper>
複製程式碼

準備User實體:

@Data
public class User {

    private Integer uid;

    private String name;

    private String mobile;

    private String creditId;

    private Date createTime;
}
複製程式碼

編寫單元測試插入資料,這裡是通過隨機生成100個使用者的uid進行測試:

@RunWith(SpringRunner.class)
@SpringBootTest
public class DemoShardingApplicationTests {

    @Autowired
    private UserDAO userDAO;

    @Test
    public void testInsert() {
        System.out.println(("----- sharding insert method test ------"));
        for (int i = 0; i < 100; i++) {
            User userData = new User();
            userData.setUid(new Random().nextInt(999999));
            userData.setName(UUID.randomUUID().toString().replaceAll("-",""));
            userData.setCreditId("1234567890");
            userData.setMobile("1234567890");
            userData.setCreateTime(new Date());
            userDAO.insert(userData);
        }
    }
}
複製程式碼

執行結果

檢視資料結果,可以看到資料已成功插入到指定的資料庫表中。 最後一位為奇數,倒數第二位為偶數的,被插入到sharding1db.t_user_0:

sharding1db.t_user_0

最後一位為偶數,倒數第二位為奇數的,被插入到sharding0db.t_user_1:

sharding0db.t_user_1

參考資料

# 文章連結 作者
1 sharding:誰都能讀懂的分庫、分表、分割槽 駿馬金龍
2 一次難得的分庫分表實踐 crossoverjie
3 advanced-java doocs