Sharding-JDBC 讀寫分離

面對日益增加的系統(tǒng)訪問量把曼,數(shù)據(jù)庫的吞吐量面臨著巨大瓶頸。 對于同一時間有大量并發(fā)讀操作和較少寫操作類型的應(yīng)用系統(tǒng)來說,將單一的數(shù)據(jù)庫拆分為主庫和從庫偏陪,主庫負(fù)責(zé)處理事務(wù)性的增刪改操作,從庫負(fù)責(zé)處理查詢操作煮嫌,能夠有效的避免由數(shù)據(jù)更新導(dǎo)致的行鎖笛谦,使得整個系統(tǒng)的查詢性能得到極大的改善。透明化讀寫分離所帶來的影響昌阿,讓使用方盡量像使用一個數(shù)據(jù)庫一樣使用主從數(shù)據(jù)庫饥脑,是讀寫分離中間件的主要功能恳邀。

Sharding-JDBC 已經(jīng)提供了讀寫分離的支持,可以看看如下兩個文檔:


本文不涉及數(shù)據(jù)庫主從同步的相關(guān)配置灶轰,只是在讀庫進(jìn)行手動插入數(shù)據(jù)來模擬谣沸。

一、 引入依賴

<?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.3.0.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>dynamic-datasource-sharding-jdbc-02</artifactId>

    <dependencies>
        <!-- 實現(xiàn)對數(shù)據(jù)庫連接池的自動化配置 -->
        <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>

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

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

        <!-- 保證 Spring AOP 相關(guān)的依賴包 -->
        <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>

二乳附、Application

package cn.iocoder.springboot.lab17.dynamicdatasource;

import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.EnableAspectJAutoProxy;

/**
 * @description 啟動類
 * @ClassName: Application
 * @author: 郭秀志 jbcode@126.com
 * @date: 2020/6/30 17:17
 * @Copyright:
 */
@SpringBootApplication
@MapperScan(basePackages = "cn.iocoder.springboot.lab17.dynamicdatasource.mapper")
@EnableAspectJAutoProxy(exposeProxy = true) // http://www.voidcn.com/article/p-zddcuyii-bpt.html
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}

三、配置文件

在 [resources] 目錄下伴澄,創(chuàng)建 [application.yaml]配置文件赋除。配置如下:

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

# mybatis 配置內(nèi)容
mybatis:
  config-location: classpath:mybatis-config.xml # 配置 MyBatis 配置文件路徑
  mapper-locations: classpath:mapper/*.xml # 配置 Mapper XML 地址
  type-aliases-package: cn.iocoder.springboot.lab17.dynamicdatasource.dataobject # 配置數(shù)據(jù)庫實體包路徑

  1. spring.shardingsphere.datasource 配置項下,我們配置了 一個主數(shù)據(jù)源 ds-master 敞嗡、兩個從數(shù)據(jù)源 ds-slave-1颁糟、ds-slave-2 。
  2. spring.shardingsphere.masterslave 配置項下喉悴,配置了讀寫分離棱貌。對于從庫來說,Sharding-JDBC 提供了多種負(fù)載均衡策略箕肃,默認(rèn)為輪詢婚脱。
  3. mybatis 配置項,設(shè)置 mybatis-spring-boot-starter MyBatis 的配置內(nèi)容突雪。

四起惕、創(chuàng)建表

因為本地并未搭建 MySQL 一主多從的環(huán)境,所以是通過創(chuàng)建了 test_orders_01咏删、test_orders_02 庫惹想,手動模擬作為 test_orders 的從庫。
對應(yīng)的創(chuàng)建表的 SQL 如下:

CREATE DATABASE `test_orders` CHARACTER SET 'utf8' COLLATE 'utf8_general_ci';
-- 在 `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='訂單表';

五嘀粱、MyBatis 配置文件

<?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>
        <!-- 使用駝峰命名法轉(zhuǎn)換字段。 -->
        <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>

六辰狡、OrderDO

package cn.iocoder.springboot.lab17.dynamicdatasource.dataobject;

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

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

    public Integer getId() {
        return id;
    }

    public OrderDO setId(Integer id) {
        this.id = id;
        return this;
    }

    public Integer getUserId() {
        return userId;
    }

    public OrderDO setUserId(Integer userId) {
        this.userId = userId;
        return this;
    }

    @Override
    public String toString() {
        return "OrderDO{" +
                "id=" + id +
                ", userId=" + userId +
                '}';
    }

}

七锋叨、OrderMapper

package cn.iocoder.springboot.lab17.dynamicdatasource.mapper;

import cn.iocoder.springboot.lab17.dynamicdatasource.dataobject.OrderDO;
import org.apache.ibatis.annotations.Param;
import org.springframework.stereotype.Repository;
/**
 * @description 
 * @ClassName: OrderMapper
 * @author: 郭秀志 jbcode@126.com
 * @date: 2020/6/30 17:42    
 * @Copyright:  
 */
@Repository
public interface OrderMapper {

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

    int insert(OrderDO entity);

}

八、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>

九宛篇、簡單測試

創(chuàng)建 OrderMapperTest 測試類娃磺,我們來測試一下簡單的 OrderMapper 的讀寫操作。代碼如下:

// OrderMapper.java

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

 @Autowired
 private OrderMapper orderMapper;

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

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

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

}
  • testSelectById() 方法叫倍,測試從庫的負(fù)載均衡查詢偷卧。
  • testSelectById02() 方法豺瘤,測試強(qiáng)制訪問主庫。在一些業(yè)務(wù)場景下听诸,對數(shù)據(jù)延遲敏感坐求,所以只能強(qiáng)制讀取主庫。此時晌梨,可以使用 HintManager 強(qiáng)制訪問主庫桥嗤。
    • 不過要注意,在使用完后仔蝌,需要去清理下 HintManager (HintManager 是基于線程變量泛领,透傳給 Sharding-JDBC 的內(nèi)部實現(xiàn)),避免污染下次請求掌逛,一直強(qiáng)制訪問主庫师逸。
    • Sharding-JDBC 比較貼心司倚,HintManager 實現(xiàn)了 AutoCloseable 接口豆混,可以通過 Try-with-resources 機(jī)制,自動關(guān)閉动知。代碼為:try (HintManager hintManager = HintManager.getInstance())
  • testInsert() 方法皿伺,測試主庫的插入。

跑下測試用例盒粮。如果跑通鸵鸥,說明配置就算成功了。

另外丹皱,在 testSelectById()測試方法中妒穴,測試 slave 分組是不是真的在負(fù)載均衡。所以在數(shù)據(jù)庫中摊崭,分別插入數(shù)據(jù)如下讼油。

主庫:[id = 1, user_id = 1]
從庫 01:[id = 1, user_id = 2]
從庫 02:[id = 1, user_id = 3]

這樣,通過手動設(shè)置相同 id = 1 的記錄呢簸,對應(yīng)不同的 user_id 矮台,那么我們就可以觀察 testSelectById()測試方法的輸出結(jié)果。如果是根时,user_id = 2 和 user_i = 3 交替循環(huán)輸出瘦赫,說明就正常了。

2個從庫負(fù)載均衡讀取

十蛤迎、 詳細(xì)測試

cn.iocoder.springboot.lab17.dynamicdatasource.service 包路徑下确虱,創(chuàng)建 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);
 }

}
  • #add(OrderDO order) 方法中唱较,開啟事務(wù),插入一條訂單記錄召川。
    • <1.1> 處南缓,往從庫發(fā)起一次訂單查詢。在 Sharding-JDBC 的讀寫分離策略里荧呐,默認(rèn)讀取從庫汉形。
    • <1.2> 處,往主庫發(fā)起一次訂單寫入倍阐。寫入概疆,肯定是操作主庫的。
    • <1.3> 處峰搪,往主庫發(fā)起一次訂單查詢岔冀。在 Sharding-JDBC 中,讀寫分離約定:同一線程且同一數(shù)據(jù)庫連接內(nèi)概耻,如有寫入操作使套,以后的讀操作均從主庫讀取,用于保證數(shù)據(jù)一致性鞠柄。
  • #findById(Integer id) 方法侦高,往從庫發(fā)起一次訂單查詢。

我們創(chuàng)建了 [OrderServiceTest]測試類厌杜,可以測試上面編寫的兩個方法奉呛。

package cn.iocoder.springboot.lab17.dynamicdatasource.service;

import cn.iocoder.springboot.lab17.dynamicdatasource.Application;
import cn.iocoder.springboot.lab17.dynamicdatasource.dataobject.OrderDO;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;

/**
 * @description 測試orderservice
 * @ClassName: OrderServiceTest
 * @author: 郭秀志 jbcode@126.com
 * @date: 2020/7/1 10:52
 * @Copyright:
 */
@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);
    }

}

運(yùn)行測試:
testAdd方法

十一、源代碼

本文源代碼可從Gitee下載夯尽。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末瞧壮,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子匙握,更是在濱河造成了極大的恐慌咆槽,老刑警劉巖,帶你破解...
    沈念sama閱讀 212,686評論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件肺孤,死亡現(xiàn)場離奇詭異罗晕,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)赠堵,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,668評論 3 385
  • 文/潘曉璐 我一進(jìn)店門小渊,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人茫叭,你說我怎么就攤上這事酬屉。” “怎么了?”我有些...
    開封第一講書人閱讀 158,160評論 0 348
  • 文/不壞的土叔 我叫張陵呐萨,是天一觀的道長杀饵。 經(jīng)常有香客問我,道長谬擦,這世上最難降的妖魔是什么切距? 我笑而不...
    開封第一講書人閱讀 56,736評論 1 284
  • 正文 為了忘掉前任,我火速辦了婚禮惨远,結(jié)果婚禮上谜悟,老公的妹妹穿的比我還像新娘。我一直安慰自己北秽,他們只是感情好葡幸,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,847評論 6 386
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著贺氓,像睡著了一般蔚叨。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上辙培,一...
    開封第一講書人閱讀 50,043評論 1 291
  • 那天蔑水,我揣著相機(jī)與錄音,去河邊找鬼虏冻。 笑死肤粱,一個胖子當(dāng)著我的面吹牛弹囚,可吹牛的內(nèi)容都是我干的厨相。 我是一名探鬼主播,決...
    沈念sama閱讀 39,129評論 3 410
  • 文/蒼蘭香墨 我猛地睜開眼鸥鹉,長吁一口氣:“原來是場噩夢啊……” “哼蛮穿!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起毁渗,我...
    開封第一講書人閱讀 37,872評論 0 268
  • 序言:老撾萬榮一對情侶失蹤践磅,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后灸异,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體府适,經(jīng)...
    沈念sama閱讀 44,318評論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,645評論 2 327
  • 正文 我和宋清朗相戀三年肺樟,在試婚紗的時候發(fā)現(xiàn)自己被綠了檐春。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,777評論 1 341
  • 序言:一個原本活蹦亂跳的男人離奇死亡么伯,死狀恐怖疟暖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤俐巴,帶...
    沈念sama閱讀 34,470評論 4 333
  • 正文 年R本政府宣布骨望,位于F島的核電站,受9級特大地震影響欣舵,放射性物質(zhì)發(fā)生泄漏擎鸠。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 40,126評論 3 317
  • 文/蒙蒙 一缘圈、第九天 我趴在偏房一處隱蔽的房頂上張望糠亩。 院中可真熱鬧,春花似錦准验、人聲如沸赎线。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,861評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽垂寥。三九已至,卻和暖如春另锋,著一層夾襖步出監(jiān)牢的瞬間滞项,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,095評論 1 267
  • 我被黑心中介騙來泰國打工夭坪, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留文判,地道東北人。 一個月前我還...
    沈念sama閱讀 46,589評論 2 362
  • 正文 我出身青樓室梅,卻偏偏與公主長得像戏仓,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子亡鼠,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,687評論 2 351