面對日益增加的系統(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ù)庫實體包路徑
- spring.shardingsphere.datasource 配置項下,我們配置了 一個主數(shù)據(jù)源 ds-master 敞嗡、兩個從數(shù)據(jù)源 ds-slave-1颁糟、ds-slave-2 。
- spring.shardingsphere.masterslave 配置項下喉悴,配置了讀寫分離棱貌。對于從庫來說,Sharding-JDBC 提供了多種負(fù)載均衡策略箕肃,默認(rèn)為輪詢婚脱。
- 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)輸出瘦赫,說明就正常了。
十蛤迎、 詳細(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)行測試:十一、源代碼
本文源代碼可從Gitee下載夯尽。