Spring Boot 多數據源(讀寫分離)入門

轉載自  芋道 Spring Boot 多數據源(讀寫分離)入門

1. 概述

在項目中,我們可能會碰到需要多數據源的場景。例如說:

  • 讀寫分離:數據庫主節點壓力比較大,需要增加從節點提供讀操作,以減少壓力。

  • 多數據源:一個複雜的單體項目,因爲沒有拆分成不同的服務,需要連接多個業務的數據源。

本質上,讀寫分離,僅僅是多數據源的一個場景,從節點是隻提供讀操作的數據源。所以只要實現了多數據源的功能,也就能夠提供讀寫分離。

2. 實現方式

目前,實現多數據源有三種方案。我們逐個小節來看。

2.1 方案一

基於 Spring AbstractRoutingDataSource 做拓展

簡單來說,通過繼承 AbstractRoutingDataSource 抽象類,實現一個管理項目中多個 DataSource 的動態 DynamicRoutingDataSource 實現類。這樣,Spring 在獲取數據源時,可以通過 DynamicRoutingDataSource 返回實際的 DataSource 。

然後,我們可以自定義一個 @DS 註解,可以添加在 Service 方法、Dao 方法上,表示其實際對應的 DataSource 。

如此,整個過程就變成,執行數據操作時,通過“配置”的 @DS 註解,使用 DynamicRoutingDataSource 獲得對應的實際的 DataSource 。之後,在通過該 DataSource 獲得 Connection 連接,最後發起數據庫操作。

可能這麼說,沒有實現過多數據源的胖友會比較懵逼,比較大概率。所以推薦胖胖看看艿艿的基友寫的 《剖析 Spring 多數據源》 文章。

不過呢,這種方式在結合 Spring 事務的時候,會存在無法切換數據源的問題。具體我們在 「3. baomidou 多數據源」 中,結合示例一起來看。

艿艿目前找了一圈開源的項目,發現比較好的是 baomidou 提供的 dynamic-datasource-spring-boot-starter 。所以我們在 「3. baomidou 多數據源」 和 「4. baomidou 讀寫分離」 中,會使用到它。

2.2 方案二

不同操作類,固定數據源

關於這個方案,解釋起來略有點晦澀。以 MyBatis 舉例子,假設有 orders 和 users 兩個數據源。 那麼我們可以創建兩個 SqlSessionTemplate ordersSqlSessionTemplate 和 usersSqlSessionTemplate ,分別使用這兩個數據源。

然後,配置不同的 Mapper 使用不同的 SqlSessionTemplate 。

如此,整個過程就變成,執行數據操作時,通過 Mapper 可以對應到其  SqlSessionTemplate ,使用 SqlSessionTemplate 獲得對應的實際的 DataSource 。之後,在通過該 DataSource 獲得 Connection 連接,最後發起數據庫操作。

咳咳咳,是不是又處於懵逼狀態了?!沒事,咱在 「5. MyBatis 多數據源」、「6. Spring Data JPA 多數據源」、「7. JdbcTemplate 多數據源」 中,結合案例一起看。「Talk is cheap. Show me the code」

不過呢,這種方式在結合 Spring 事務的時候,也會存在無法切換數據源的問題。淡定淡定。多數據源的情況下,這個基本是逃不掉的問題。

2.3 方案三

分庫分表中間件

對於分庫分表的中間件,會解析我們編寫的 SQL ,路由操作到對應的數據源。那麼,它們天然就支持多數據源。如此,我們僅需配置好每個表對應的數據源,中間件就可以透明的實現多數據源或者讀寫分離。

目前,Java 最好用的分庫分表中間件,就是 Apache ShardingSphere ,沒有之一。

那麼,這種方式在結合 Spring 事務的時候,會不會存在無法切換數據源的問題呢?答案是不會。在上述的方案一和方案二中,在 Spring 事務中,會獲得對應的 DataSource ,再獲得 Connection 進行數據庫操作。而獲得的 Connection 以及其上的事務,會通過 ThreadLocal 的方式和當前線程進行綁定。這樣,就導致我們無法切換數據源。

難道分庫分表中間件不也是需要 Connection 進行這些事情麼?答案是的,但是不同的是分庫分表中間件返回的 Connection 返回的實際是動態的 DynamicRoutingConnection ,它管理了整個請求(邏輯)過程中,使用的所有的 Connection ,而最終執行 SQL 的時候,DynamicRoutingConnection 會解析 SQL ,獲得表對應的真正的 Connection 執行 SQL 操作。

難道方案一和方案二不可以這麼做嗎?答案是,當然可以。前提是,他們要實現解析 SQL 的能力。

那麼,分庫分表中間件就是多數據源的完美方案落?從一定程度上來說,是的。但是,它需要解決多個 Connection 可能產生的多個事務的一致性問題,也就是我們常說的,分佈式事務。關於這塊,艿艿最近有段時間沒跟進 Sharding-JDBC 的版本,所以無法給出肯定的答案。不過我相信,Sharding-JDBC 最終會解決分佈式事務的難題,提供透明的多數據源的功能。

在 「8. Sharding-JDBC 多數據源」、「9. Sharding-JDBC 讀寫分離」 中,我們會演示這種方案。

3. baomidou 多數據源

示例代碼對應倉庫:lab-17-dynamic-datasource-baomidou-01 。

本小節,我們使用實現開源項目 dynamic-datasource-spring-boot-starter ,來實現多數據源的功能。我們會使用 test_orders 和 test_users 兩個數據源作爲兩個數據源,然後實現在其上的 SQL 操作。並且,會結合在 Spring 事務的不同場景下,會發生的結果以及原因。

另外,關於 dynamic-datasource-spring-boot-starter 的介紹,胖友自己看 官方文檔 。😈 它和 MyBatis-Plus 都是開發者 baomidou 提供的。

3.1 引入依賴

在 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">
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.1.3.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>lab-17-dynamic-datasource-baomidou-01</artifactId>

    <dependencies>
        <!-- 實現對數據庫連接池的自動化配置 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-jdbc</artifactId>
        </dependency>
        <dependency> <!-- 本示例,我們使用 MySQL -->
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>5.1.48</version>
        </dependency>

        <!-- 實現對 MyBatis 的自動化配置 -->
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>2.1.1</version>
        </dependency>

        <!-- 實現對 dynamic-datasource 的自動化配置 -->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>dynamic-datasource-spring-boot-starter</artifactId>
            <version>2.5.7</version>
        </dependency>
        <!-- 不造爲啥 dynamic-datasource-spring-boot-starter 會依賴這個 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-actuator</artifactId>
        </dependency>

        <!-- 方便等會寫單元測試 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>

    </dependencies>

</project>

具體每個依賴的作用,胖友自己認真看下艿艿添加的所有註釋噢。

3.2 Application

創建 Application.java 類,代碼如下:

// Application.java

@SpringBootApplication
@MapperScan(basePackages = "cn.iocoder.springboot.lab17.dynamicdatasource.mapper")
@EnableAspectJAutoProxy(exposeProxy = true) // http://www.voidcn.com/article/p-zddcuyii-bpt.html
public class Application {
}
  • 添加 @MapperScan 註解,cn.iocoder.springboot.lab17.dynamicdatasource.mapper 包路徑下,就是我們 Mapper 接口所在的包路徑。

  • 添加 @EnableAspectJAutoProxy 註解,重點是配置 exposeProxy = true ,因爲我們希望 Spring AOP 能將當前代理對象設置到 AopContext 中。具體用途,我們會在下文看到。想要提前看的胖友,可以看看 《Spring AOP 通過獲取代理對象實現事務切換》 文章。

3.3 應用配置文件

在 resources 目錄下,創建 application.yaml 配置文件。配置如下:

spring:
  datasource:
    # dynamic-datasource-spring-boot-starter 動態數據源的配置內容
    dynamic:
      primary: users # 設置默認的數據源或者數據源組,默認值即爲 master
      datasource:
        # 訂單 orders 數據源配置
        orders:
          url: jdbc:mysql://127.0.0.1:3306/test_orders?useSSL=false&useUnicode=true&characterEncoding=UTF-8
          driver-class-name: com.mysql.jdbc.Driver
          username: root
          password:
        # 用戶 users 數據源配置
        users:
          url: jdbc:mysql://127.0.0.1:3306/test_users?useSSL=false&useUnicode=true&characterEncoding=UTF-8
          driver-class-name: com.mysql.jdbc.Driver
          username: root
          password:

# mybatis 配置內容
mybatis:
  config-location: classpath:mybatis-config.xml # 配置 MyBatis 配置文件路徑
  mapper-locations: classpath:mapper/*.xml # 配置 Mapper XML 地址
  type-aliases-package: cn.iocoder.springboot.lab17.dynamicdatasource.dataobject # 配置數據庫實體包路徑
  • spring.datasource.dynamic 配置項,設置 dynamic-datasource-spring-boot-starter 動態數據源的配置內容。

    • primary 配置項,設置默認的數據源或者數據源組,默認值即爲 master 。

    • datasource 配置項,配置每個動態數據源。這裏,我們配置了 ordersusers兩個動態數據源。

  • mybatis 配置項,設置 mybatis-spring-boot-starter MyBatis 的配置內容。

3.4 MyBatis 配置文件

在 resources 目錄下,創建 mybatis-config.xml 配置文件。配置如下:

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

    <settings>
        <!-- 使用駝峯命名法轉換字段。 -->
        <setting name="mapUnderscoreToCamelCase" value="true"/>
    </settings>

    <typeAliases>
        <typeAlias alias="Integer" type="java.lang.Integer"/>
        <typeAlias alias="Long" type="java.lang.Long"/>
        <typeAlias alias="HashMap" type="java.util.HashMap"/>
        <typeAlias alias="LinkedHashMap" type="java.util.LinkedHashMap"/>
        <typeAlias alias="ArrayList" type="java.util.ArrayList"/>
        <typeAlias alias="LinkedList" type="java.util.LinkedList"/>
    </typeAliases>

</configuration>

因爲在數據庫中的表的字段,我們是使用下劃線風格,而數據庫實體的字段使用駝峯風格,所以通過 mapUnderscoreToCamelCase = true 來自動轉換。

3.5 實體類

在 cn.iocoder.springboot.lab17.dynamicdatasource.dataobject 包路徑下,創建 UserDO.java 和 OrderDO.java 類。代碼如下:

// OrderDO.java
/**
 * 訂單 DO
 */
public class OrderDO {

    /**
     * 訂單編號
     */
    private Integer id;
    /**
     * 用戶編號
     */
    private Integer userId;

    // 省略 setting/getting 方法

}

// UserDO.java
/**
 * 用戶 DO
 */
public class UserDO {

    /**
     * 用戶編號
     */
    private Integer id;
    /**
     * 賬號
     */
    private String username;

    // 省略 setting/getting 方法
}

對應的創建表的 SQL 如下:

-- 在 `test_orders` 庫中。
CREATE TABLE `orders` (
  `id` int(11) DEFAULT NULL COMMENT '訂單編號',
  `user_id` int(16) DEFAULT NULL COMMENT '用戶編號'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin COMMENT='訂單表';

-- 在 `test_users` 庫中。
CREATE TABLE `users` (
  `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '用戶編號',
  `username` varchar(64) COLLATE utf8mb4_bin DEFAULT NULL COMMENT '賬號',
  `password` varchar(32) COLLATE utf8mb4_bin DEFAULT NULL COMMENT '密碼',
  `create_time` datetime DEFAULT NULL COMMENT '創建時間',
  PRIMARY KEY (`id`),
  UNIQUE KEY `idx_username` (`username`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;

3.6 Mapper

在 cn.iocoder.springboot.lab17.dynamicdatasource.mapper 包路徑下,創建 UserDO.java 和 UserMapper.java 接口。代碼如下:

// OrderMapper.java
@Repository
@DS(DBConstants.DATASOURCE_ORDERS)
public interface OrderMapper {

    OrderDO selectById(@Param("id") Integer id);

}

// UserMapper.java
@Repository
@DS(DBConstants.DATASOURCE_USERS)
public interface UserMapper {

    UserDO selectById(@Param("id") Integer id);

}
  • DBConstants.java 類,枚舉了 DATASOURCE_ORDERS 和 DATASOURCE_USERS 兩個數據源。

  • @DS 註解,是 dynamic-datasource-spring-boot-starter 提供,可添加在 Service 或 Mapper 的類/接口上,或者方法上。在其 value 屬性種,填寫數據源的名字。

    • OrderMapper 接口上,我們添加了 @DS(DBConstants.DATASOURCE_ORDERS) 註解,訪問 orders 數據源。

    • UserMapper 接口上,我們添加了 @DS(DBConstants.DATASOURCE_USERS) 註解,訪問 users 數據源。

  • 爲了讓整個測試用例精簡,我們在 OrderMapper 和 UserMapper 中,只添加了根據編號查詢單條記錄的方法。

在 resources/mapper 路徑下,創建 OrderMapper.xml 和 UserMapper.xml 配置文件。代碼如下:

<!-- OrderMapper.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="cn.iocoder.springboot.lab17.dynamicdatasource.mapper.OrderMapper">

    <sql id="FIELDS">
        id, user_id
    </sql>

    <select id="selectById" parameterType="Integer" resultType="OrderDO">
        SELECT
            <include refid="FIELDS" />
        FROM orders
        WHERE id = #{id}
    </select>

</mapper>

<!-- UserMapper.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="cn.iocoder.springboot.lab17.dynamicdatasource.mapper.UserMapper">

    <sql id="FIELDS">
        id, username
    </sql>

    <select id="selectById" parameterType="Integer" resultType="UserDO">
        SELECT
            <include refid="FIELDS" />
        FROM users
        WHERE id = #{id}
    </select>

</mapper>

3.7 簡單測試

創建 UserMapperTest 和 OrderMapperTest 測試類,我們來測試一下簡單的 UserMapper 和 OrderMapper 的每個操作。代碼如下:

// OrderMapperTest.java
@RunWith(SpringRunner.class)
@SpringBootTest(classes = Application.class)
public class OrderMapperTest {

    @Autowired
    private OrderMapper orderMapper;

    @Test
    public void testSelectById() {
        OrderDO order = orderMapper.selectById(1);
        System.out.println(order);
    }

}

// UserMapperTest.java
@RunWith(SpringRunner.class)
@SpringBootTest(classes = Application.class)
public class UserMapperTest {

    @Autowired
    private UserMapper userMapper;

    @Test
    public void testSelectById() {
        UserDO user = userMapper.selectById(1);
        System.out.println(user);
    }

}

胖友自己跑下測試用例。如果跑通,說明配置就算成功了。

3.8 詳細測試

在本小節,我們會編寫 5 個測試用例,嘗試闡述 dynamic-datasource-spring-boot-starter 在和 Spring 事務結合碰到的情況,以便胖友更好的使用。當然,這個不僅僅是 dynamic-datasource-spring-boot-starter 獨有的,而是方案一【基於 Spring AbstractRoutingDataSource 做拓展】都存在的情況。

在 cn.iocoder.springboot.lab17.dynamicdatasource.service 包路徑下,創建 OrderService.java 類。代碼如下:

// OrderService.java

@Service
public class OrderService {

    @Autowired
    private OrderMapper orderMapper;
    @Autowired
    private UserMapper userMapper;

    private OrderService self() {
        return (OrderService) AopContext.currentProxy();
    }

    public void method01() {
        // ... 省略代碼
    }

    @Transactional
    public void method02() {
        // ... 省略代碼
    }

    public void method03() {
        // ... 省略代碼
    }

    public void method04() {
        // ... 省略代碼
    }

    @Transactional
    @DS(DBConstants.DATASOURCE_ORDERS)
    public void method05() {
        // ... 省略代碼
    }

}
  • #self() 方法,通過 AopContext 獲得自己這個代理對象。舉個例子,在 #method01() 方法中,如果直接使用 this.method02() 方法進行調用,因爲 this 代表的是 OrderService Bean 自身,而不是其 AOP 代理對象。這樣會導致,無法觸發 AOP 的邏輯,在此處,就是 Spring 事務的邏輯。因此,我們通過 AopContext 獲得自己這個代理對象。

  • 每一個 #methodXX() 方法,都代表一個測試用例,胖友可以使用 OrderServiceTest 進行測試。

下面,我們來一個一個看。

場景一:#method01()

// OrderService.java

public void method01() {
    // 查詢訂單
    OrderDO order = orderMapper.selectById(1);
    System.out.println(order);
    // 查詢用戶
    UserDO user = userMapper.selectById(1);
    System.out.println(user);
}
  • 方法未使用 @Transactional 註解,不會開啓事務。

  • 對於 OrderMapper 和 UserMapper 的查詢操作,分別使用其接口上的 @DS 註解,找到對應的數據源,執行操作。

  • 這樣一看,在未開啓事務的情況下,我們已經能夠自由的使用多數據源落。

場景二:#method02()

// OrderService.java

@Transactional
public void method02() {
    // 查詢訂單
    OrderDO order = orderMapper.selectById(1);
    System.out.println(order);
    // 查詢用戶
    UserDO user = userMapper.selectById(1);
    System.out.println(user);
}
  • 和 #method01() 方法,差異在於,方法上增加了 @Transactional 註解,聲明要使用 Spring 事務。

  • 執行方法,拋出如下異常:

    Caused by: com.mysql.jdbc.exceptions.jdbc4.MySQLSyntaxErrorException: Table 'test_users.orders' doesn't exist
    
    • 在執行 OrderMapper 查詢訂單操作時,拋出在 test_users 庫中,不存在 orders表。

  • 這是爲什麼呢?咱不是在 OrderMapper 上,聲明使用 orders 數據源了麼?結果爲什麼會使用 users 數據庫,路由到 test_users 庫上呢。

    • 這裏,就和 Spring 事務的實現機制有關係。因爲方法添加了 @Transactional 註解,Spring 事務就會生效。此時,Spring TransactionInterceptor 會通過 AOP 攔截該方法,創建事務。而創建事務,勢必就會獲得數據源。那麼,TransactionInterceptor 會使用 Spring DataSourceTransactionManager 創建事務,並將事務信息通過 ThreadLocal 綁定在當前線程。

    • 而事務信息,就包括事務對應的 Connection 連接。那也就意味着,還沒走到 OrderMapper 的查詢操作,Connection 就已經被創建出來了。並且,因爲事務信息會和當前線程綁定在一起,在 OrderMapper 在查詢操作需要獲得 Connection 時,就直接拿到當前線程綁定的 Connection ,而不是 OrderMapper 添加 @DS 註解所對應的 DataSource 所對應的 Connection 。

    • OK ,那麼我們現在可以把問題聚焦到 DataSourceTransactionManager 是怎麼獲取 DataSource 從而獲得 Connection 的了。對於每個 DataSourceTransactionManager 數據庫事務管理器,創建時都會傳入其需要管理的 DataSource 數據源。在使用 dynamic-datasource-spring-boot-starter 時,它創建了一個 DynamicRoutingDataSource ,傳入到 DataSourceTransactionManager 中。

    • 而 DynamicRoutingDataSource 負責管理我們配置的多個數據源。例如說,本示例中就管理了 ordersusers 兩個數據源,並且默認使用 users 數據源。那麼在當前場景下,DynamicRoutingDataSource 需要基於 @DS 獲得數據源名,從而獲得對應的 DataSource ,結果因爲我們在 Service 方法上,並沒有添加 @DS 註解,所以它只好返回默認數據源,也就是 users 。故此,就發生了 Table 'test_users.orders' doesn't exist 的異常。

    • 咳咳咳,這裏涉及 Spring 事務的實現機制,如果胖友不是很瞭解源碼會比較懵逼,推薦可以嘗試將 TransactionInterceptor 作爲入口,進行調試。當然,也歡迎胖友給艿艿留言。

場景三:#method03()

// OrderService.java

public void method03() {
    // 查詢訂單
    self().method031();
    // 查詢用戶
    self().method032();
}

@Transactional // 報錯,因爲此時獲取的是 primary 對應的 DataSource ,即 users 。
public void method031() {
    OrderDO order = orderMapper.selectById(1);
    System.out.println(order);
}

@Transactional
public void method032() {
    UserDO user = userMapper.selectById(1);
    System.out.println(user);
}
  • 執行方法,拋出如下異常:

    Table 'test_users.orders' doesn't exist
    
  • 按照艿艿在場景二的解釋,胖友可以思考下原因。

    😈 其實,場景三和場景二是等價的。

  • 如果此時,我們將 #self() 代碼替換成 this 之後,誒,結果就正常執行。這又是爲什麼呢?胖友在思考一波。

    😈 其實,這樣調整後,因爲 this 不是代理對象,所以 #method031() 和 #method032() 方法上的 @Transactional 直接沒有作用,Spring 事務根本沒有生效。所以,最終結果和場景一是等價的。

場景四:#method04()

// OrderService.java

public void method04() {
    // 查詢訂單
    self().method041();
    // 查詢用戶
    self().method042();
}

@Transactional
@DS(DBConstants.DATASOURCE_ORDERS)
public void method041() {
    OrderDO order = orderMapper.selectById(1);
    System.out.println(order);
}

@Transactional
@DS(DBConstants.DATASOURCE_USERS)
public void method042() {
    UserDO user = userMapper.selectById(1);
    System.out.println(user);
}
  • 和 @method03() 方法,差異在於,#method041() 和 #method042() 方法上,添加 @DS 註解,聲明對應使用的 DataSource 。

  • 執行方法,正常結束,未拋出異常。是不是覺得有點奇怪?

  • 在執行 #method041() 方法前,因爲有 @Transactional 註解,所以 Spring 事務機制觸發。DynamicRoutingDataSource 根據 @DS 註解,獲得對應的 orders 的 DataSource ,從而獲得 Connection 。所以後續 OrderMapper 執行查詢操作時,即使使用的是線程綁定的 Connection ,也可能不會報錯。😈 嘿嘿,實際上,此時 OrderMapper 上的 @DS 註解,也沒有作用。

  • 對於 #method042() ,也是同理。但是,我們上面不是提了 Connection 會綁定在當前線程麼?那麼,在 #method042() 方法中,應該使用的是 #method041() 的 orders對應的 Connection 呀。在 Spring 事務機制中,在一個事務執行完成後,會將事務信息和當前線程解綁。所以,在執行 #method042() 方法前,又可以執行一輪事務的邏輯。

  • 【重要】總的來說,對於聲明瞭 @Transactional 的 Service 方法上,也同時通過 @DS 聲明對應的數據源。

場景五:#method05()

// OrderService.java

@Transactional
@DS(DBConstants.DATASOURCE_ORDERS)
public void method05() {
    // 查詢訂單
    OrderDO order = orderMapper.selectById(1);
    System.out.println(order);
    // 查詢用戶
    self().method052();
}

@Transactional(propagation = Propagation.REQUIRES_NEW)
@DS(DBConstants.DATASOURCE_USERS)
public void method052() {
    UserDO user = userMapper.selectById(1);
    System.out.println(user);
}
  • 和 @method04() 方法,差異在於,我們直接在 #method05() 方法中,此時處於一個事務中,直接調用了 #method052() 方法。

  • 執行方法,正常結束,未拋出異常。是不是覺得有點奇怪?

  • 我們仔細看看 #method052() 方法,我們添加的 @Transactionl 註解,使用的事務傳播級別是 Propagation.REQUIRES_NEW 。此時,在執行 #method052() 方法之前,TransactionInterceptor 會將原事務掛起,暫時性的將原事務信息和當前線程解綁。

    • 所以,在執行 #method052() 方法前,又可以執行一輪事務的邏輯。

    • 之後,在執行 #method052() 方法完成後,會將原事務恢復,重新將原事務信息和當前線程綁定。

  • 編寫這個場景的目的,是想告訴胖友,如果在使用方案一【基於 Spring AbstractRoutingDataSource 做拓展】,在事務中時,如何切換數據源。當然,一旦切換數據源,可能產生多個事務,就會碰到多個事務一致性的問題,也就是分佈式事務。😈

😝 五個場景,胖友在好好理解。可以嘗試調試下源碼,更好的幫助理解。

咳咳咳,如果有解釋不到位的地方,歡迎胖友給艿艿留言。

4. baomidou 讀寫分離

示例代碼對應倉庫:lab-17-dynamic-datasource-baomidou-02 。

在絕大多數情況下,我們使用多數據源的目的,是爲了實現讀寫分離。所以,在本小節中,我們來使用 dynamic-datasource-spring-boot-starter ,實現一個讀寫分離的示例。

4.1 引入依賴

和 「3.1 引入依賴」 一致。

4.2 Application

和 「3.2 Application」 一致。

4.3 應用配置文件

在 resources 目錄下,創建 application.yaml 配置文件。配置如下:

spring:
  datasource:
    # dynamic-datasource-spring-boot-starter 動態數據源的配置內容
    dynamic:
      primary: master # 設置默認的數據源或者數據源組,默認值即爲 master
      datasource:
        # 訂單 orders 主庫的數據源配置
        master:
          url: jdbc:mysql://127.0.0.1:3306/test_orders?useSSL=false&useUnicode=true&characterEncoding=UTF-8
          driver-class-name: com.mysql.jdbc.Driver
          username: root
          password:
        # 訂單 orders 從庫數據源配置
        slave_1:
          url: jdbc:mysql://127.0.0.1:3306/test_orders_01?useSSL=false&useUnicode=true&characterEncoding=UTF-8
          driver-class-name: com.mysql.jdbc.Driver
          username: root
          password:
        # 訂單 orders 從庫數據源配置
        slave_2:
          url: jdbc:mysql://127.0.0.1:3306/test_orders_02?useSSL=false&useUnicode=true&characterEncoding=UTF-8
          driver-class-name: com.mysql.jdbc.Driver
          username: root
          password:

# mybatis 配置內容
mybatis:
  config-location: classpath:mybatis-config.xml # 配置 MyBatis 配置文件路徑
  mapper-locations: classpath:mapper/*.xml # 配置 Mapper XML 地址
  type-aliases-package: cn.iocoder.springboot.lab17.dynamicdatasource.dataobject # 配置數據庫實體包路徑
  • 相比 「3.3 應用配置」 來說,我們配置了訂單庫的多個數據源:

    • master :訂單庫的主庫。

    • slave_1 和 slave_2 :訂單庫的兩個從庫。

  • 在 dynamic-datasource-spring-boot-starter 中,多個相同角色的數據源可以形成一個數據源組。判斷標準是,數據源名以下劃線 _ 分隔後的首部即爲組名。例如說,slave_1 和  slave_2 形成了 slave 組。

    • 我們可以使用 @DS("slave_1") 或 @DS("slave_2") 註解,明確訪問數據源組的指定數據源。

    • 也可以使用 @DS("slave") 註解,此時會負載均衡,選擇分組中的某個數據源進行訪問。目前,負載均衡默認採用輪詢的方式。

  • 因爲艿艿本地並未搭建 MySQL 一主多從的環境,所以是通過創建了 test_orders_01test_orders_02 庫,手動模擬作爲 test_orders 的從庫。

4.4 MyBatis 配置文件

和 「3.4 MyBatis 配置文件」 一致。

4.5 OrderDO

只使用 「3.5 實體類」 的 OrderDO.java 類。

4.6 OrderMapper

在 cn.iocoder.springboot.lab17.dynamicdatasource.mapper 包路徑下,創建 OrderMapper.java 接口。代碼如下:

// OrderMapper.java

@Repository
public interface OrderMapper {

    @DS(DBConstants.DATASOURCE_SLAVE)
    OrderDO selectById(@Param("id") Integer id);

    @DS(DBConstants.DATASOURCE_MASTER)
    int insert(OrderDO entity);

}
  • DBConstants.java 類,枚舉了 DATASOURCE_MASTER 和 DATASOURCE_SLAVE 兩個數據源。

  • 對 #selectById(Integer id) 讀操作,我們配置了 @DS(DBConstants.DATASOURCE_SLAVE) ,訪問從庫。

  • 對 #insert(OrderDO entity) 寫操作,我們配置了 @DS(DBConstants.DATASOURCE_MASTER) ,訪問主庫。

在 resources/mapper 路徑下,創建 OrderMapper.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="cn.iocoder.springboot.lab17.dynamicdatasource.mapper.OrderMapper">

    <sql id="FIELDS">
        id, user_id
    </sql>

    <select id="selectById" parameterType="Integer" resultType="OrderDO">
        SELECT
            <include refid="FIELDS" />
        FROM orders
        WHERE id = #{id}
    </select>

    <insert id="insert" parameterType="OrderDO" useGeneratedKeys="true" keyProperty="id">
        INSERT INTO orders (
          user_id
        ) VALUES (
          #{userId}
        )
    </insert>

</mapper>

3.7 簡單測試

創建 UserMapperTest 測試類,我們來測試一下簡單的 UserMapper 的讀寫操作。代碼如下:

// UserMapperTest.java

@RunWith(SpringRunner.class)
@SpringBootTest(classes = Application.class)
public class OrderMapperTest {

    @Autowired
    private OrderMapper orderMapper;

    @Test
    public void testSelectById() {
        for (int i = 0; i < 10; i++) {
            OrderDO order = orderMapper.selectById(1);
            System.out.println(order);
        }
    }

    @Test
    public void testInsert() {
        OrderDO order = new OrderDO();
        order.setUserId(10);
        orderMapper.insert(order);
    }

}

胖友自己跑下測試用例。如果跑通,說明配置就算成功了。

另外,在 #testSelectById() 測試方法中,艿艿會了看看 slave 分組是不是真的在負載均衡。所以在數據庫中,分別插入數據如下。

主庫:[id = 1, user_id = 1]
從庫 01:[id = 1, user_id = 2]
從庫 02:[id = 1, user_id = 3]
  • 這樣,通過手動設置相同 id = 1 的記錄,對應不同的 user_id ,那麼我們就可以觀察 #testSelectById() 測試方法的輸出結果。如果是,user_id = 2 和 user_i = 3循環輸出,說明就正常了。

3.8 詳細測試

在 cn.iocoder.springboot.lab17.dynamicdatasource.service 包路徑下,創建 OrderService.java 類。代碼如下:

// OrderService.java

@Service
public class OrderService {

    @Autowired
    private OrderMapper orderMapper;

    @Transactional
    @DS(DBConstants.DATASOURCE_MASTER)
    public void add(OrderDO order) {
        // 這裏先假模假樣的讀取一下
        orderMapper.selectById(order.getId());

        // 插入訂單
        orderMapper.insert(order);
    }

    public OrderDO findById(Integer id) {
        return orderMapper.selectById(id);
    }

}
  • 對於 #add(OrderDO order) 方法,我們希望在 @Transactional 聲明的事務中,讀操作也訪問主庫,所以聲明瞭 @DS(DBConstants.DATASOURCE_MASTER) 。因此,後續的所有 OrderMapper 的操作,都訪問的是訂單庫的 MASTER 數據源。

  • 對於 #findById(Integer id) 方法,讀取指定訂單信息,使用 OrderMapper 的 #selectById(Integer id) 配置的 SLAVE 數據源即可。

創建 OrderServiceTest 測試類,測試 OrderService 的讀寫邏輯。代碼如下:

// OrderServiceTest.java

@RunWith(SpringRunner.class)
@SpringBootTest(classes = Application.class)
public class OrderServiceTest {

    @Autowired
    private OrderService orderService;

    @Test
    public void testAdd() {
        OrderDO order = new OrderDO();
        order.setUserId(20);
        orderService.add(order);
    }

    @Test
    public void testFindById() {
        OrderDO order = orderService.findById(1);
        System.out.println(order);
    }

}
  • 胖友自己跑下測試用例。如果跑通,說明配置就算成功了。

另外,如果胖友的業務場景,是純的讀寫分離,可以看看 《純讀寫分離(mybatis 環境)》 文檔。

5. MyBatis 多數據源

示例代碼對應倉庫:lab-17-dynamic-datasource-mybatis 。

本小節,我們會基於方案二【不同操作類,固定數據源】的方式,實現 MyBatis 多數據源。

整個配置過程會相對繁瑣,胖友請保持耐心。

如果胖友對 Spring Data JPA 不瞭解的話,可以看看 《芋道 Spring Boot MyBatis 入門》》 文章。

5.1 引入依賴

在 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">
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.1.3.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>lab-17-dynamic-datasource-mybatis</artifactId>

    <dependencies>
        <!-- 實現對數據庫連接池的自動化配置 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-jdbc</artifactId>
        </dependency>
        <dependency> <!-- 本示例,我們使用 MySQL -->
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>5.1.48</version>
        </dependency>

        <!-- MyBatis 相關依賴 -->
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>2.1.1</version>
        </dependency>

        <!-- 保證 Spring AOP 相關的依賴包 -->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-aspects</artifactId>
        </dependency>

        <!-- 方便等會寫單元測試 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>

    </dependencies>

</project>
  • 具體每個依賴的作用,胖友自己認真看下艿艿添加的所有註釋噢。

  • 對於 mybatis-spring-boot-starter 依賴,這裏並不使用它實現對 MyBatis 的自動化配置。這麼引入,只是單純方便,實際只要引入 mybatis 和 mybatis-spring 即可。

5.2 Application

創建 Application.java 類,代碼如下:

@SpringBootApplication
@EnableAspectJAutoProxy(exposeProxy = true) // http://www.voidcn.com/article/p-zddcuyii-bpt.html
public class Application {
}
  • 我們並沒有添加 @MapperScan 註解,爲什麼呢?答案我們在 「5.5 配置類」 上看。

5.3 應用配置文件

在 resources 目錄下,創建 application.yaml 配置文件。配置如下:

spring:
  # datasource 數據源配置內容
  datasource:
    # 訂單數據源配置
    orders:
      jdbc-url: jdbc:mysql://127.0.0.1:3306/test_orders?useSSL=false&useUnicode=true&characterEncoding=UTF-8
      driver-class-name: com.mysql.jdbc.Driver
      username: root
      password:
    # 用戶數據源配置
    users:
      jdbc-url: jdbc:mysql://127.0.0.1:3306/test_users?useSSL=false&useUnicode=true&characterEncoding=UTF-8
      driver-class-name: com.mysql.jdbc.Driver
      username: root
      password:

# mybatis 配置內容
#mybatis:
#  config-location: classpath:mybatis-config.xml # 配置 MyBatis 配置文件路徑
#  type-aliases-package: cn.iocoder.springboot.lab17.dynamicdatasource.dataobject # 配置數據庫實體包路徑
  • 在 spring.datasource 配置項中,我們設置了 orders 和 users 兩個數據源。

  • 註釋掉 mybatis 配置項,因爲我們不使用 mybatis-spring-boot-starter 自動化配置 MyBatis ,而是自己寫配置類,自定義配置 MyBatis 。

5.4 MyBatis 配置文件

和 「3.4 MyBatis 配置文件」 一致。

5.5 MyBatis 配置類

在 cn.iocoder.springboot.lab17.dynamicdatasource.config 包路徑下,我們會分別創建:

  • MyBatisOrdersConfig 配置類,配置使用 orders 數據源的 MyBatis 配置。

  • MyBatisUsersConfig 配置類,配置使用 users 數據源的 MyBatis 配置。

兩個 MyBatis 配置類代碼是一致的,只是部分配置項的值不同。所以我們僅僅來看下 MyBatisOrdersConfig 配置類,而 MyBatisUsersConfig 配置類胖友自己看看即可。代碼如下:

// MyBatisOrdersConfig.java

@Configuration
@MapperScan(basePackages = "cn.iocoder.springboot.lab17.dynamicdatasource.mapper.orders", sqlSessionTemplateRef = "ordersSqlSessionTemplate")
public class MyBatisOrdersConfig {

    /**
     * 創建 orders 數據源
     */
    @Bean(name = "ordersDataSource")
    @ConfigurationProperties(prefix = "spring.datasource.orders")
    public DataSource dataSource() {
        return DataSourceBuilder.create().build();
    }

    /**
     * 創建 MyBatis SqlSessionFactory
     */
    @Bean(name = "ordersSqlSessionFactory")
    public SqlSessionFactory sqlSessionFactory() throws Exception {
        SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
        // <2.1> 設置 orders 數據源
        bean.setDataSource(this.dataSource());
        // <2.2> 設置 entity 所在包
        bean.setTypeAliasesPackage("cn.iocoder.springboot.lab17.dynamicdatasource.dataobject");
        // <2.3> 設置 config 路徑
        bean.setConfigLocation(new PathMatchingResourcePatternResolver().getResource("classpath:mybatis-config.xml"));
        // <2.4> 設置 mapper 路徑
        bean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath:mapper/orders/*.xml"));
        return bean.getObject();
    }

    /**
     * 創建 MyBatis SqlSessionTemplate
     */
    @Bean(name = "ordersSqlSessionTemplate")
    public SqlSessionTemplate sqlSessionTemplate() throws Exception {
        return new SqlSessionTemplate(this.sqlSessionFactory());
    }

    /**
     * 創建 orders 數據源的 TransactionManager 事務管理器
     */
    @Bean(name = DBConstants.TX_MANAGER_ORDERS)
    public PlatformTransactionManager transactionManager() {
        return new DataSourceTransactionManager(this.dataSource());
    }

}
  • #dataSource() 方法,創建 orders 數據源。

  • #sqlSessionFactory() 方法,創建 MyBatis SqlSessionFactory Bean 。

    • <2.1> 處,設置 orders 數據源。

    • <2.2> 處,設置 entity 所在包,作爲類型別名。

    • <2.3> 處,設置 config 路徑,這裏我們使用 classpath:mybatis-config.xml"配置文件。

    • <2.4> 處,設置 Mapper 路徑,這裏我們使用 classpath:mapper/orders/*.xml。我們將 resource/mapper 路徑下,拆分爲 orders 路徑下的 Mapper XML 用於 orders 數據源,users 路徑下的 Mapper XML 用於 users 數據源。

    • 通過上述設置,我們就創建出使用 orders 數據源的 SqlSessionFactory Bean 對象。

  • #sqlSessionTemplate() 方法,創建 MyBatis SqlSessionTemplate Bean 。其內部的 sqlSessionFactory 使用的就是對應 orders 數據源的 SqlSessionFactory 對象。

  • 在類上,有 @MapperScan 註解:

    • 配置 basePackages 屬性,它會掃描 cn.iocoder.springboot.lab17.dynamicdatasource.mapper 包下的 orders 包下的 Mapper 接口。和 resource/mapper 路徑一樣,我們也將 mapper 包路徑,拆分爲 orders 包下的 Mapper 接口用於 orders 數據源,users 包下的 Mapper 接口用於 users 數據源。

    • 配置 sqlSessionTemplateRef 屬性,它會使用 #sqlSessionTemplate() 方法創建的 SqlSessionTemplate Bean 對象。

    • 這樣,我們就能保證 cn.iocoder.springboot.lab17.dynamicdatasource.mapper.orders 下的 Mapper 使用的是操作 orders 數據源的 SqlSessionFactory ,從而操作 orders 數據源。

  • #transactionManager() 方法,創建 orders 數據源的 Spring 事務管理器。因爲,我們項目中,一般使用 Spring 管理事務。另外,我們在 DBConstants.java 枚舉了 TX_MANAGER_ORDERS 和 TX_MANAGER_USERS 兩個事務管理器的名字。

艿艿:相比來說,這種方式會相對繁瑣。但是如果項目中大量採用,可以封裝自己的 Spring Boot Starter ,以實現自動化配置。

5.6 實體類

和 「3.5 實體類」 一致。

5.7 Mapper

和 「3.6 Mapper」 基本一致,差別在於分出了 orders 和 users 兩個。具體看如下兩個傳送門:

  • Mapper 接口

  • Mapper XML

5.8 簡單測試

創建 UserMapperTest 和 OrderMapperTest 測試類,我們來測試一下簡單的 UserMapper 和 OrderMapper 的每個操作。代碼如下:

// OrderMapperTest.java
@RunWith(SpringRunner.class)
@SpringBootTest(classes = Application.class)
public class OrderMapperTest {

    @Autowired
    private OrderMapper orderMapper;

    @Test
    public void testSelectById() {
        OrderDO order = orderMapper.selectById(1);
        System.out.println(order);
    }

}

// UserMapperTest.java
@RunWith(SpringRunner.class)
@SpringBootTest(classes = Application.class)
public class UserMapperTest {

    @Autowired
    private UserMapper userMapper;

    @Test
    public void testSelectById() {
        UserDO user = userMapper.selectById(1);
        System.out.println(user);
    }

}

胖友自己跑下測試用例。如果跑通,說明配置就算成功了。

5.9 詳細測試

在本小節,我們會編寫 4 個測試用例,嘗試方案二【不同操作類,固定數據源】存在的情況。

在 cn.iocoder.springboot.lab17.dynamicdatasource.service 包路徑下,創建 OrderService.java 類。代碼如下:

// OrderService.java

@Service
public class OrderService {

    @Autowired
    private OrderMapper orderMapper;
    @Autowired
    private UserMapper userMapper;

    private OrderService self() {
        return (OrderService) AopContext.currentProxy();
    }

    public void method01() {
        // ... 省略代碼
    }

    @Transactional // 報錯,找不到事務管理器
    public void method02() {
        // ... 省略代碼
    }

    public void method03() {
        // ... 省略代碼
    }

    @Transactional(transactionManager = DBConstants.TX_MANAGER_ORDERS)
    public void method05() {
        // 查詢訂單
        OrderDO order = orderMapper.selectById(1);
        System.out.println(order);
        // 查詢用戶
        self().method052();
    }

}
  • 每個測試場景,和 「3.8 詳細測試」 的測試場景是相對應的,按照編號。

  • 每一個 #methodXX() 方法,都代表一個測試用例,胖友可以使用 OrderServiceTest 進行測試。

下面,我們來一個一個看。

場景一:#method01()

// OrderService.java

public void method01() {
    // 查詢訂單
    OrderDO order = orderMapper.selectById(1);
    System.out.println(order);
    // 查詢用戶
    UserDO user = userMapper.selectById(1);
    System.out.println(user);
}
  • 方法未使用 @Transactional 註解,不會開啓事務。

  • 對於 OrderMapper 和 UserMapper 的查詢操作,分別使用其接口對應的 SqlSessionTemplate ,找到對應的數據源,執行操作。

  • 這樣一看,在未開啓事務的情況下,我們已經能夠自由的使用多數據源落。

場景二:#method02()

// OrderService.java

@Transactional // 報錯,找不到事務管理器
public void method02() {
    // 查詢訂單
    OrderDO order = orderMapper.selectById(1);
    System.out.println(order);
    // 查詢用戶
    UserDO user = userMapper.selectById(1);
    System.out.println(user);
}
  • 和 #method02() 方法,差異在於,方法上增加了 @Transactional 註解,聲明要使用 Spring 事務。

  • 執行方法,拋出如下異常:

    NoUniqueBeanDefinitionException: No qualifying bean of type 'org.springframework.transaction.PlatformTransactionManager' available: expected single matching bean but found 2: ordersTransactionManager,usersTransactionManager
    
    • 在 @Transactional 註解上,如果未設置使用的事務管理器,它會去選擇一個事務管理器。但是,我們這裏創建了 ordersTransactionManager 和 usersTransactionManager 兩個事務管理器,它就不知道怎麼選了。此時,它只好拋出 NoUniqueBeanDefinitionException 異常。

場景三:#method03()

// OrderService.java

public void method03() {
    // 查詢訂單
    self().method031();
    // 查詢用戶
    self().method032();
}

@Transactional(transactionManager = DBConstants.TX_MANAGER_ORDERS)
public void method031() {
    OrderDO order = orderMapper.selectById(1);
    System.out.println(order);
}

@Transactional(transactionManager = DBConstants.TX_MANAGER_USERS)
public void method032() {
    UserDO user = userMapper.selectById(1);
    System.out.println(user);
}
  • 執行方法,正常結束,未拋出異常。

  • #method031() 和 #method032() 方法上,聲明的事務管理器,和後續 Mapper 操作是同一個 DataSource 數據源,從而保證不報錯。

場景四:#method05()

// OrderService.java

@Transactional(transactionManager = DBConstants.TX_MANAGER_ORDERS)
public void method05() {
    // 查詢訂單
    OrderDO order = orderMapper.selectById(1);
    System.out.println(order);
    // 查詢用戶
    self().method052();
}

@Transactional(transactionManager = DBConstants.TX_MANAGER_USERS,
        propagation = Propagation.REQUIRES_NEW)
public void method052() {
    UserDO user = userMapper.selectById(1);
    System.out.println(user);
}
  • 執行方法,正常結束,未拋出異常。

  • 我們仔細看看 #method052() 方法,我們添加的 @Transactionl 註解,使用的事務傳播級別是 Propagation.REQUIRES_NEW 。此時,在執行 #method052() 方法之前,TransactionInterceptor 會將原事務掛起,暫時性的將原事務信息和當前線程解綁。

    • 所以,在執行 #method052() 方法前,又可以執行一輪事務的邏輯。

    • 之後,在執行 #method052() 方法完成後,會將原事務恢復,重新將原事務信息和當前線程綁定。

  • 編寫這個場景的目的,是想告訴胖友,如果在使用方案二【不同操作類,固定數據源】,在事務中時,如何切換數據源。當然,一旦切換數據源,可能產生多個事務,就會碰到多個事務一致性的問題,也就是分佈式事務。😈

😝 四個場景,胖友在好好理解。可以嘗試調試下源碼,更好的幫助理解。

咳咳咳,如果有解釋不到位的地方,歡迎胖友給艿艿留言。

5.10 讀寫分離

按照這個思路,如果想要實現 MyBatis 讀寫分離。還是類似的思路。只是將從庫作爲一個“特殊”的數據源,需要做的是:

  • 應用配置文件增加從庫的數據源。

  • 增加一套從庫的 MyBatis 配置類。

  • 增加一套從庫相關的 MyBatis Mapper 接口、Mapper XML 文件。

相比方案一【基於 Spring AbstractRoutingDataSource 做拓展】來說,更加麻煩。並且,萬一有多從呢?嘿嘿。

所以呢,實際項目在選型時,方案一會於方案二,被更普遍的採用。

6. Spring Data JPA 多數據源

示例代碼對應倉庫:lab-17-dynamic-datasource-springdatajpa 。

本小節,我們會基於方案二【不同操作類,固定數據源】的方式,實現 Spring Data JPA 多數據源。

整個配置過程會相對繁瑣,胖友請保持耐心。

艿艿:整個過程,和 「5. MyBatis 多數據源」 是類似的,所以講解會想對精簡一些。

內心 OS :就是想偷懶,嘿嘿。

如果胖友對 Spring Data JPA 不瞭解的話,可以看看 《芋道 Spring Boot JPA 入門》》 文章。

6.1 引入依賴

在 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">
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.1.3.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>lab-17-dynamic-datasource-springdatajpa</artifactId>

    <dependencies>
        <!-- 實現對數據庫連接池的自動化配置 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-jdbc</artifactId>
        </dependency>
        <dependency> <!-- 本示例,我們使用 MySQL -->
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>5.1.48</version>
        </dependency>

        <!-- JPA 相關依賴 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>

        <!-- 方便等會寫單元測試 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>

    </dependencies>

</project>
  • 具體每個依賴的作用,胖友自己認真看下艿艿添加的所有註釋噢。

  • 對於 spring-boot-starter-data-jpa 依賴,這裏並不使用它實現對 JPA 的自動化配置。這麼引入,只是單純方便,不然需要引入 spring-data-jpa 和 hibernate-core等等依賴。

6.2 Application

創建 Application.java 類,代碼如下:

@SpringBootApplication
@EnableAspectJAutoProxy(exposeProxy = true) // http://www.voidcn.com/article/p-zddcuyii-bpt.html
public class Application {
}

6.3 應用配置文件

在 resources 目錄下,創建 application.yaml 配置文件。配置如下:

spring:
  # datasource 數據源配置內容
  datasource:
    # 訂單數據源配置
    orders:
      jdbc-url: jdbc:mysql://127.0.0.1:3306/test_orders?useSSL=false&useUnicode=true&characterEncoding=UTF-8
      driver-class-name: com.mysql.jdbc.Driver
      username: root
      password:
    # 用戶數據源配置
    users:
      jdbc-url: jdbc:mysql://127.0.0.1:3306/test_users?useSSL=false&useUnicode=true&characterEncoding=UTF-8
      driver-class-name: com.mysql.jdbc.Driver
      username: root
      password:
  jpa:
    show-sql: true # 打印 SQL 。生產環境,建議關閉
    # Hibernate 配置內容,對應 HibernateProperties 類
    hibernate:
      ddl-auto: none
  • 在 spring.datasource 配置項中,我們設置了 orders 和 users 兩個數據源。

6.4 Spring Data JPA 配置類

在 cn.iocoder.springboot.lab17.dynamicdatasource.config 包路徑下,創建 HibernateConfig.java 配置類。代碼如下:

// HibernateConfig.java

@Configuration
public class HibernateConfig {

    @Autowired
    private JpaProperties jpaProperties;
    @Autowired
    private HibernateProperties hibernateProperties;

    /**
     * 獲取 Hibernate Vendor 相關配置
     */
    @Bean(name = "hibernateVendorProperties")
    public Map<String, Object> hibernateVendorProperties() {
        return hibernateProperties.determineHibernateProperties(
                jpaProperties.getProperties(), new HibernateSettings());
    }

}
  • 目的是獲得 Hibernate Vendor 相關配置。不用糾結它是什麼,知道需要獲得即可。

在 cn.iocoder.springboot.lab17.dynamicdatasource.config 包路徑下,我們會分別創建:

  • JpaOrdersConfig 配置類,配置使用 orders 數據源的 Spring Data JPA 配置。

  • JpaUsersConfig 配置類,配置使用 users 數據源的 Spring Data JPA 配置。

兩個 Spring Data JPA 配置類代碼是一致的,只是部分配置項的值不同。所以我們僅僅來看下 JpaOrdersConfig 配置類,而 JpaUsersConfig 配置類胖友自己看看即可。代碼如下:

// JpaOrdersConfig.java

@Configuration
@EnableJpaRepositories(
        entityManagerFactoryRef = DBConstants.ENTITY_MANAGER_FACTORY_ORDERS,
        transactionManagerRef = DBConstants.TX_MANAGER_ORDERS,
        basePackages = {"cn.iocoder.springboot.lab17.dynamicdatasource.repository.orders"}) // 設置 Repository 接口所在包
public class JpaOrdersConfig {

    @Resource(name = "hibernateVendorProperties")
    private Map<String, Object> hibernateVendorProperties;

    /**
     * 創建 orders 數據源
     */
    @Bean(name = "ordersDataSource")
    @ConfigurationProperties(prefix = "spring.datasource.orders")
    @Primary // 需要特殊添加,否則初始化會有問題
    public DataSource dataSource() {
        return DataSourceBuilder.create().build();
    }

    /**
     * 創建 LocalContainerEntityManagerFactoryBean
     */
    @Bean(name = DBConstants.ENTITY_MANAGER_FACTORY_ORDERS)
    public LocalContainerEntityManagerFactoryBean entityManagerFactory(EntityManagerFactoryBuilder builder) {
        return builder
                .dataSource(this.dataSource()) // 數據源
                .properties(hibernateVendorProperties) // 獲取並注入 Hibernate Vendor 相關配置
                .packages("cn.iocoder.springboot.lab17.dynamicdatasource.dataobject") // 數據庫實體 entity 所在包
                .persistenceUnit("ordersPersistenceUnit") // 設置持久單元的名字,需要唯一
                .build();
    }

    /**
     * 創建 PlatformTransactionManager
     */
    @Bean(name = DBConstants.TX_MANAGER_ORDERS)
    public PlatformTransactionManager transactionManager(EntityManagerFactoryBuilder builder) {
        return new JpaTransactionManager(entityManagerFactory(builder).getObject());
    }

}
  • #dataSource() 方法,創建 orders 數據源。

  • #entityManagerFactoryPrimary(EntityManagerFactoryBuilder builder) 方法,創建 LocalContainerEntityManagerFactoryBean Bean ,它是創建 EntityManager 實體管理器的工廠 Bean ,最終會創建對應的 EntityManager Bean 。

    • <2.1> 處,設置使用的數據源是 orders 。

    • <2.2> 處,設置 Hibernate Vendor 相關配置。

    • <2.3> 處,設置數據庫實體 Entity 所在包。

    • <2.4> 處,設置持久單元的名字,需要唯一。

  • #transactionManager(EntityManagerFactoryBuilder builder) 方法,創建使用上述 EntityManager 的 JpaTransactionManager Bean 對象。這樣,該事務管理器使用的也是 orders 數據源。

  • 最終,通過 @EnableJpaRepositories 註解,串聯在一起:

    • entityManagerFactoryRef 屬性,保證了使用 orders 數據源的 EntityManager 實體管理器的工廠 Bean 。

    • transactionManagerRef 屬性,保證了使用 orders 數據源的 PlatformTransactionManager 事務管理器 Bean 。

    • basePackages 屬性,它會掃描 cn.iocoder.springboot.lab17.dynamicdatasource.repository 包下的 orders 包下的 Repository 接口。我們將 repository 包路徑,拆分爲 orders 包下的 Repository 接口用於 orders 數據源,users 包下的 Repository 接口用於 users 數據源。

  • 另外,我們在 DBConstants.java 類中,枚舉了:

    • TX_MANAGER_ORDERS 和 TX_MANAGER_USERS 兩個事務管理器的名字,方便代碼中使用。

    • ENTITY_MANAGER_FACTORY_ORDERS 和 ENTITY_MANAGER_FACTORY_USERS 兩個實體管理器的名字。

艿艿:相比來說,這種方式會相對繁瑣。但是如果項目中大量採用,可以封裝自己的 Spring Boot Starter ,以實現自動化配置。

6.5 實體類

和 「3.5 實體類」 基本一致,差別在於增加了 JPA 相關注解。具體看如下兩個傳送門:

  • OrderDO.java

  • UserDO.java

6.6 Repository

和 「3.6 Mapper」 基本一致,差別在於使用 Spring Data Repository 接口。具體看如下兩個傳送門:

  • OrderRepository

  • UserRepository

6.7 簡單測試

和 「5.8 簡單測試」 基本一致,具體看如下兩個傳送門:

  • OrderRepositoryTest

  • UserRepositoryTest

6.8 詳細測試

和 「5.9 詳細測試」 基本一致,具體看如下兩個傳送門:

  • OrderService

  • OrderServiceTest

6.9 讀寫分離

和 「5.10 讀寫分離」 思路基本一致。

7. JdbcTemplate 多數據源

示例代碼對應倉庫:lab-17-dynamic-datasource-jdbctemplate 。

本小節,我們會基於方案二【不同操作類,固定數據源】的方式,實現 Spring JdbcTemplate 多數據源。

整個配置過程會相對繁瑣,胖友請保持耐心。

艿艿:整個過程,和 「5. MyBatis 多數據源」 是類似的,所以講解會想對精簡一些。

內心 OS :我只是想趕緊進入 Sharding-JDBC 的環節,真的不是想偷懶,哈哈哈哈。

如果胖友對 Spring JdbcTemplate 不瞭解的話,可以看看 《芋道 Spring Boot JdbcTemplate 入門》》 文章。

7.1 引入依賴

在 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">
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.1.3.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>lab-17-dynamic-datasource-jdbctemplate</artifactId>

    <dependencies>
        <!-- 實現對數據庫連接池的自動化配置 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-jdbc</artifactId>
        </dependency>
        <dependency> <!-- 本示例,我們使用 MySQL -->
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>5.1.48</version>
        </dependency>

        <!-- 保證 Spring AOP 相關的依賴包 -->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-aspects</artifactId>
        </dependency>

        <!-- 方便等會寫單元測試 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>


</project>

具體每個依賴的作用,胖友自己認真看下艿艿添加的所有註釋噢。

7.2 Application

創建 Application.java 類,代碼如下:

@SpringBootApplication
@EnableAspectJAutoProxy(exposeProxy = true) // http://www.voidcn.com/article/p-zddcuyii-bpt.html
public class Application {
}

7.3 應用配置文件

在 resources 目錄下,創建 application.yaml 配置文件。配置如下:

spring:
  # datasource 數據源配置內容
  datasource:
    # 訂單數據源配置
    orders:
      jdbc-url: jdbc:mysql://127.0.0.1:3306/test_orders?useSSL=false&useUnicode=true&characterEncoding=UTF-8
      driver-class-name: com.mysql.jdbc.Driver
      username: root
      password:
    # 用戶數據源配置
    users:
      jdbc-url: jdbc:mysql://127.0.0.1:3306/test_users?useSSL=false&useUnicode=true&characterEncoding=UTF-8
      driver-class-name: com.mysql.jdbc.Driver
      username: root
      password:
  • 在 spring.datasource 配置項中,我們設置了 orders 和 users 兩個數據源。

7.4 JdbcTemplate 配置類

在 cn.iocoder.springboot.lab17.dynamicdatasource.config 包路徑下,我們會分別創建:

  • JdbcTemplateOrdersConfig 配置類,配置使用 orders 數據源的 MyBatis 配置。

  • JdbcTemplateUsersConfig 配置類,配置使用 users 數據源的 MyBatis 配置。

兩個 JdbcTemplate 配置類代碼是一致的,只是部分配置項的值不同。所以我們僅僅來看下 JdbcTemplateOrdersConfig 配置類,而 JdbcTemplateUsersConfig 配置類胖友自己看看即可。代碼如下:

// JdbcTemplateOrdersConfig.java

@Configuration
public class JdbcTemplateOrdersConfig {

    /**
     * 創建 orders 數據源
     */
    @Bean(name = "ordersDataSource")
    @ConfigurationProperties(prefix = "spring.datasource.orders")
    public DataSource dataSource() {
        return DataSourceBuilder.create().build();
    }

    /**
     * 創建 orders JdbcTemplate
     */
    @Bean(name = DBConstants.JDBC_TEMPLATE_ORDERS)
    public JdbcTemplate jdbcTemplate() {
        return new JdbcTemplate(this.dataSource());
    }

    /**
     * 創建 orders 數據源的 TransactionManager 事務管理器
     */
    @Bean(name = DBConstants.TX_MANAGER_ORDERS)
    public PlatformTransactionManager transactionManager() {
        return new DataSourceTransactionManager(this.dataSource());
    }

}
  • #dataSource() 方法,創建 orders 數據源。

  • #jdbcTemplate() 方法,創建使用 orders 數據源的 JdbcTemplate Bean 。

  • #transactionManager() 方法,創建 orders 數據源的 Spring 事務管理器。因爲,我們項目中,一般使用 Spring 管理事務。另外,我們在 DBConstants.java 枚舉了 TX_MANAGER_ORDERS 和 TX_MANAGER_USERS 兩個事務管理器的名字。

艿艿:相比來說,這種方式會相對繁瑣。但是如果項目中大量採用,可以封裝自己的 Spring Boot Starter ,以實現自動化配置。

7.5 實體類

和 「3.5 實體類」 一致。

7.6 Dao

和 「5.8 簡單測試」 基本一致,具體看如下兩個傳送門:

  • OrderDao

  • UserDao

7.7 簡單測試

和 「5.8 簡單測試」 基本一致,具體看如下兩個傳送門:

  • OrderDaoTest

  • UserDaoTest

7.8 詳細測試

和 「5.9 詳細測試」 基本一致,具體看如下兩個傳送門:

  • OrderService

  • OrderServiceTest

7.9 讀寫分離

和 「5.10 讀寫分離」 思路基本一致。

8. Sharding-JDBC 多數據源

示例代碼對應倉庫:lab-17-dynamic-datasource-sharding-jdbc-01 。

Sharding-JDBC 是 Apache ShardingSphere 下,基於 JDBC 的分庫分表組件。對於 Java 語言來說,我們推薦選擇 Sharding-JDBC 優於 Sharding-Proxy ,主要原因是:

  • 減少一層 Proxy 的開銷,性能更優。

  • 去中心化,無需多考慮一次 Proxy 的高可用。

下面,我們來使用 Sharding-JDBC 來實現多數據源。整個的示例,我們會和 「2. baomidou 多數據源」 是一樣的功能,方便胖友做類比。

8.1 引入依賴

在 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">
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.1.3.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>lab-17-dynamic-datasource-sharding-jdbc-01</artifactId>

    <dependencies>
        <!-- 實現對數據庫連接池的自動化配置 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-jdbc</artifactId>
        </dependency>
        <dependency> <!-- 本示例,我們使用 MySQL -->
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>5.1.48</version>
        </dependency>

        <!-- 實現對 MyBatis 的自動化配置 -->
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>2.1.1</version>
        </dependency>

        <!-- 實現對 Sharding-JDBC 的自動化配置 -->
        <dependency>
            <groupId>org.apache.shardingsphere</groupId>
            <artifactId>sharding-jdbc-spring-boot-starter</artifactId>
            <version>4.0.0-RC2</version>
        </dependency>

        <!-- 保證 Spring AOP 相關的依賴包 -->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-aspects</artifactId>
        </dependency>

        <!-- 方便等會寫單元測試 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>

    </dependencies>

</project>

具體每個依賴的作用,胖友自己認真看下艿艿添加的所有註釋噢。

8.2 Application

創建 Application.java 類,代碼如下:

// Application.java

@SpringBootApplication
@MapperScan(basePackages = "cn.iocoder.springboot.lab17.dynamicdatasource.mapper")
@EnableAspectJAutoProxy(exposeProxy = true) // http://www.voidcn.com/article/p-zddcuyii-bpt.html
public class Application {
}
  • 和 「3.2 Application」 是完全一致的。

8.3 應用配置文件

在 resources 目錄下,創建 application.yaml 配置文件。配置如下:

spring:
  # ShardingSphere 配置項
  shardingsphere:
    datasource:
      # 所有數據源的名字
      names: ds-orders, ds-users
      # 訂單 orders 數據源配置
      ds-orders:
        type: com.zaxxer.hikari.HikariDataSource # 使用 Hikari 數據庫連接池
        driver-class-name: com.mysql.jdbc.Driver
        jdbc-url: jdbc:mysql://127.0.0.1:3306/test_orders?useSSL=false&useUnicode=true&characterEncoding=UTF-8
        username: root
        password:
      # 訂單 users 數據源配置
      ds-users:
        type: com.zaxxer.hikari.HikariDataSource # 使用 Hikari 數據庫連接池
        driver-class-name: com.mysql.jdbc.Driver
        jdbc-url: jdbc:mysql://127.0.0.1:3306/test_users?useSSL=false&useUnicode=true&characterEncoding=UTF-8
        username: root
        password:
    # 分片規則
    sharding:
      tables:
        # orders 表配置
        orders:
          actualDataNodes: ds-orders.orders # 映射到 ds-orders 數據源的 orders 表
        # users 表配置
        users:
          actualDataNodes: ds-users.users # 映射到 ds-users 數據源的 users 表

# mybatis 配置內容
mybatis:
  config-location: classpath:mybatis-config.xml # 配置 MyBatis 配置文件路徑
  mapper-locations: classpath:mapper/*.xml # 配置 Mapper XML 地址
  type-aliases-package: cn.iocoder.springboot.lab17.dynamicdatasource.dataobject # 配置數據庫實體包路徑
  • spring.shardingsphere.datasource 配置項下,我們配置了 ds_orders 和 ds_users 兩個數據源。

  • spring.shardingsphere.sharding 配置項下,我們配置了分片規則,將 orders 邏輯表的操作路由到 ds-orders 數據源的 orders 真實表 ,將 users 邏輯表的操作路由到 ds-users 數據源的 users 真實表 。

    艿艿:這裏涉及到了一些 ShardingSphere 的概念,後續胖友最好可以看看 官方文檔 。

  • mybatis 配置項,設置 mybatis-spring-boot-starter MyBatis 的配置內容。

8.4 MyBatis 配置文件

和 「3.4 MyBatis 配置文件」 一致。

8.5 實體類

和 「3.5 實體類」 一致。

8.6 Mapper

和 「3.6 Mapper」 一致。

8.7 簡單測試

和 「3.7 簡單測試」 一致。

8.8 詳細測試

和 「3.8 詳細測試」 代碼一致,結果略有差異

在 「3.8 詳細測試」 的場景二 #method02() 的測試,它會拋出異常。而對於本小節使用 Sharding-JDBC 的情況下,正常跑通。這是爲什麼呢?

原因實際在 「2.3 方案三」 已經解釋了:分庫分表中間件返回的 Connection 返回的實際是動態的 DynamicRoutingConnection ,它管理了整個請求(邏輯)過程中,使用的所有的 Connection ,而最終執行 SQL 的時候,DynamicRoutingConnection 會解析 SQL ,獲得表對應的真正的 Connection 執行 SQL 操作

所以,即使在和 Spring 事務結合的時候,會通過 ThreadLocal 的方式將 Connection 和當前線程進行綁定。此時這個 Connection 也是一個 動態的 DynamicRoutingConnection 連接。

9. Sharding-JDBC 讀寫分離

示例代碼對應倉庫:lab-17-dynamic-datasource-sharding-jdbc-02 。

Sharding-JDBC 已經提供了讀寫分離的支持,胖友可以看看如下兩個文檔:

  • ShardingSphere > 概念 & 功能 > 讀寫分離

  • ShardingSphere > 用戶手冊 > Sharding-JDBC > 使用手冊 > 讀寫分離

當然,也可以先不看。

下面,我們來使用 Sharding-JDBC 來實現讀寫分離。整個的示例,我們會和 「3. baomidou 讀寫分離」 是一樣的功能,方便胖友做類比。

9.1 引入依賴

和 「8.1 引入依賴」 一致。

9.2 Application

和 「8.2 Application」 一致。

9.3 應用配置文件

在 resources 目錄下,創建 application.yaml 配置文件。配置如下:

spring:
  # ShardingSphere 配置項
  shardingsphere:
    # 數據源配置
    datasource:
      # 所有數據源的名字
      names: ds-master, ds-slave-1, ds-slave-2
      # 訂單 orders 主庫的數據源配置
      ds-master:
        type: com.zaxxer.hikari.HikariDataSource # 使用 Hikari 數據庫連接池
        driver-class-name: com.mysql.jdbc.Driver
        jdbc-url: jdbc:mysql://127.0.0.1:3306/test_orders?useSSL=false&useUnicode=true&characterEncoding=UTF-8
        username: root
        password:
      # 訂單 orders 從庫數據源配置
      ds-slave-1:
        type: com.zaxxer.hikari.HikariDataSource # 使用 Hikari 數據庫連接池
        driver-class-name: com.mysql.jdbc.Driver
        jdbc-url: jdbc:mysql://127.0.0.1:3306/test_orders_01?useSSL=false&useUnicode=true&characterEncoding=UTF-8
        username: root
        password:
      # 訂單 orders 從庫數據源配置
      ds-slave-2:
        type: com.zaxxer.hikari.HikariDataSource # 使用 Hikari 數據庫連接池
        driver-class-name: com.mysql.jdbc.Driver
        jdbc-url: jdbc:mysql://127.0.0.1:3306/test_orders_02?useSSL=false&useUnicode=true&characterEncoding=UTF-8
        username: root
        password:
    # 讀寫分離配置,對應 YamlMasterSlaveRuleConfiguration 配置類
    masterslave:
      name: ms # 名字,任意,需要保證唯一
      master-data-source-name: ds-master # 主庫數據源
      slave-data-source-names: ds-slave-1, ds-slave-2 # 從庫數據源

# mybatis 配置內容
mybatis:
  config-location: classpath:mybatis-config.xml # 配置 MyBatis 配置文件路徑
  mapper-locations: classpath:mapper/*.xml # 配置 Mapper XML 地址
  type-aliases-package: cn.iocoder.springboot.lab17.dynamicdatasource.dataobject # 配置數據庫實體包路徑
  • spring.shardingsphere.datasource 配置項下,我們配置了 一個主數據源 ds-master 、兩個從數據源 ds-slave-1ds-slave-2 。

  • spring.shardingsphere.masterslave 配置項下,配置了讀寫分離。對於從庫來說,Sharding-JDBC 提供了多種負載均衡策略,默認爲輪詢。

  • mybatis 配置項,設置 mybatis-spring-boot-starter MyBatis 的配置內容。

  • 因爲艿艿本地並未搭建 MySQL 一主多從的環境,所以是通過創建了 test_orders_01test_orders_02 庫,手動模擬作爲 test_orders 的從庫。

9.4 MyBatis 配置文件

和 「3.4 MyBatis 配置文件」 一致。

9.5 OrderDO

只使用 「3.5 實體類」 的 OrderDO.java 類。

9.6 OrderMapper

和 「4.6 OrderMapper」 基本一致,差別是無需 @DS 註解,具體看如下兩個傳送門:

  • OrderMapper

  • OrderMapper.xml

9.7 簡單測試

創建 OrderMapperTest 測試類,我們來測試一下簡單的 OrderMapper 的讀寫操作。代碼如下:

// OrderMapper.java

@RunWith(SpringRunner.class)
@SpringBootTest(classes = Application.class)
public class OrderMapperTest {

    @Autowired
    private OrderMapper orderMapper;

    @Test
    public void testSelectById() { // 測試從庫的負載均衡
        for (int i = 0; i < 10; i++) {
            OrderDO order = orderMapper.selectById(1);
            System.out.println(order);
        }
    }

    @Test
    public void testSelectById02() { // 測試強制訪問主庫
        try (HintManager hintManager = HintManager.getInstance()) {
            // 設置強制訪問主庫
            hintManager.setMasterRouteOnly();
            // 執行查詢
            OrderDO order = orderMapper.selectById(1);
            System.out.println(order);
        }
    }

    @Test
    public void testInsert() { // 插入
        OrderDO order = new OrderDO();
        order.setUserId(10);
        orderMapper.insert(order);
    }

}
  • #testSelectById() 方法,測試從庫的負載均衡查詢。

  • #testSelectById02() 方法,測試強制訪問主庫。在一些業務場景下,對數據延遲敏感,所以只能強制讀取主庫。此時,可以使用 HintManager 強制訪問主庫。

    • 不過要注意,在使用完後,需要去清理下 HintManager (HintManager 是基於線程變量,透傳給 Sharding-JDBC 的內部實現),避免污染下次請求,一直強制訪問主庫。

    • Sharding-JDBC 比較貼心,HintManager 實現了 AutoCloseable 接口,可以通過 Try-with-resources 機制,自動關閉。

  • #testInsert() 方法,測試主庫的插入。

胖友自己跑下測試用例。如果跑通,說明配置就算成功了。

另外,在 #testSelectById() 測試方法中,艿艿會了看看 slave 分組是不是真的在負載均衡。所以在數據庫中,分別插入數據如下。

主庫:[id = 1, user_id = 1]
從庫 01:[id = 1, user_id = 2]
從庫 02:[id = 1, user_id = 3]
  • 這樣,通過手動設置相同 id = 1 的記錄,對應不同的 user_id ,那麼我們就可以觀察 #testSelectById() 測試方法的輸出結果。如果是,user_id = 2 和 user_i = 3循環輸出,說明就正常了。

9.8 詳細測試

在 cn.iocoder.springboot.lab17.dynamicdatasource.service 包路徑下,創建 OrderService.java 類。代碼如下:

// OrderService.java

@Service
public class OrderService {

    @Autowired
    private OrderMapper orderMapper;

    @Transactional
    public void add(OrderDO order) {
        // <1.1> 這裏先假模假樣的讀取一下。讀取從庫
        OrderDO exists = orderMapper.selectById(1);
        System.out.println(exists);

        // <1.2> 插入訂單
        orderMapper.insert(order);

        // <1.3> 這裏先假模假樣的讀取一下。讀取主庫
        exists = orderMapper.selectById(1);
        System.out.println(exists);
    }

    public OrderDO findById(Integer id) {
        return orderMapper.selectById(id);
    }

}
  • 我們創建了 OrderServiceTest 測試類,可以測試上面編寫的兩個方法。

  • 在 #add(OrderDO order) 方法中,開啓事務,插入一條訂單記錄。

    • <1.1> 處,往從庫發起一次訂單查詢。在 Sharding-JDBC 的讀寫分離策略裏,默認讀取從庫。

    • <1.2> 處,往主庫發起一次訂單寫入。寫入,肯定是操作主庫的。

    • <1.3> 處,往主庫發起一次訂單查詢。在 Sharding-JDBC 中,讀寫分離約定:同一線程且同一數據庫連接內,如有寫入操作,以後的讀操作均從主庫讀取,用於保證數據一致性。

  • 在 #findById(Integer id) 方法,往從庫發起一次訂單查詢。

666. 彩蛋

我們看完了三種多數據源的方案,實際場景下怎麼選擇呢?

首先,我們基本排除了方案二【不同操作類,固定數據源】。配置繁瑣,使用不變。艿艿也去問了一圈朋友,暫時沒有這麼做的。這種方案,更加適合不同類型的數據源,例如說一個項目中,既有 MySQL 數據源,又有 MongoDB、Elasticsarch 等其它數據源。

然後,對於大多數場景下,方案一【基於 SpringAbstractRoutingDataSource 做拓展】,基本能夠滿足。這種方案,目前是比較主流的方案,大多數項目都採用。在實現上,我們可以比較容易的自己封裝一套,當然也可以考慮使用 dynamic-datasource-spring-boot-starter 開源項目。不過呢,建議可以把它的源碼擼一下,核心代碼估計 1000 行左右,不要慌。

當然,方案一和方案二,會存在和 Spring 事務結合的時候,在事務中無法切換數據源。這是因爲 Spring 事務會將 Connection 和當前線程變量綁定定,後續會通過線程變量重用該 Connection ,導致無法切換數據源。所以,方案一和方案二,可以理解成 DataSource 級別上實現的數據源方案。

最後,方案三【分庫分表中間件】是完美解決方案,基本滿足了所有的場景。艿艿個人強烈推薦使用 Apache ShardingSphere 的 Sharding-JDBC 組件,無論胖友是有多數據源,還是分庫分表,還是讀寫分離,都能完美的匹配。並且,Apache ShardingSphere 已經提供多種分佈式事務方案,也能解決在文章的開頭,艿艿提到的分佈式事務的問題。這種類型的方案,目前很多大廠都是這樣去玩的。

  • 京東:採用 client 模式的讀寫分離和分庫分表。

  • 美團:採用 client 模式的讀寫分離和分庫分表。

  • 陌陌:採用 client 模式的讀寫分離和分庫分表。

... 繼續補充調研 ing 。

😝 因爲本文寫的相對匆忙,如果有表述不正確,或者錯誤的地方,煩請胖友指出。感謝~

推薦閱讀:

  • 《芋道 Spring Boot 分庫分表入門》 對應 lab-18 。

  • 《Spring + MyBatis 實現數據庫讀寫分離方案》

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章