SpringBoot + MyBatis + MySQL讀寫分離實踐!

2020-12-10 酷扯兒

本文轉載自【微信公眾號:java進階架構師,ID:java_jiagoushi】經微信公眾號授權轉載,如需轉載與原文作者聯繫

1. 引言

讀寫分離要做的事情就是對於一條SQL該選擇哪個資料庫去執行,至於誰來做選擇資料庫這件事兒,無非兩個,要麼中間件幫我們做,要麼程序自己做。因此,一般來講,讀寫分離有兩種實現方式。第一種是依靠中間件(比如:MyCat),也就是說應用程式連接到中間件,中間件幫我們做SQL分離;第二種是應用程式自己去做分離。這裡我們選擇程序自己來做,主要是利用Spring提供的路由數據源,以及AOP

然而,應用程式層面去做讀寫分離最大的弱點(不足之處)在於無法動態增加資料庫節點,因為數據源配置都是寫在配置中的,新增資料庫意味著新加一個數據源,必然改配置,並重啟應用。當然,好處就是相對簡單。

2. AbstractRoutingDataSource

基於特定的查找key路由到特定的數據源。它內部維護了一組目標數據源,並且做了路由key與目標數據源之間的映射,提供基於key查找數據源的方法。

3. 實踐

3.1. maven依賴

<?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> <groupId>com.cjs.example</groupId> <artifactId>cjs-datasource-demo</artifactId> <version>0.0.1-SNAPSHOT</version> <packaging>jar</packaging> <name>cjs-datasource-demo</name> <description></description> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.0.5.RELEASE</version> <relativePath/> <!-- lookup parent from repository --> </parent> <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding> <java.version>1.8</java.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-aop</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-jdbc</artifactId> </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.apache.commons</groupId> <artifactId>commons-lang3</artifactId> <version>3.8</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> <!--<plugin> <groupId>org.mybatis.generator</groupId> <artifactId>mybatis-generator-maven-plugin</artifactId> <version>1.3.5</version> <dependencies> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>5.1.46</version> </dependency> </dependencies> <configuration> <configurationFile>${basedir}/src/main/resources/myBatisGeneratorConfig.xml</configurationFile> <overwrite>true</overwrite> </configuration> <executions> <execution> <id>Generate MyBatis Artifacts</id> <goals> <goal>generate</goal> </goals> </execution> </executions> </plugin>--> </plugins> </build></project>

3.2. 數據源配置

application.yml

spring:datasource: master: jdbc-url: jdbc:mysql://192.168.102.31:3306/test username: root password: 123456 driver-class-name: com.mysql.jdbc.Driver slave1: jdbc-url: jdbc:mysql://192.168.102.56:3306/test username: pig # 只讀帳戶 password: 123456 driver-class-name: com.mysql.jdbc.Driver slave2: jdbc-url: jdbc:mysql://192.168.102.36:3306/test username: pig # 只讀帳戶 password: 123456 driver-class-name: com.mysql.jdbc.Driver

多數據源配置

package com.cjs.example.config;import com.cjs.example.bean.MyRoutingDataSource;import com.cjs.example.enums.DBTypeEnum;import org.springframework.beans.factory.annotation.Qualifier;import org.springframework.boot.context.properties.ConfigurationProperties;import org.springframework.boot.jdbc.DataSourceBuilder;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;import javax.sql.DataSource;import java.util.HashMap;import java.util.Map;/*** 關於數據源配置,參考SpringBoot官方文檔第79章《Data Access》 * 79. Data Access * 79.1 Configure a Custom DataSource * 79.2 Configure Two DataSources */@Configurationpublic class DataSourceConfig { @Bean @ConfigurationProperties("spring.datasource.master") public DataSource masterDataSource() { return DataSourceBuilder.create().build(); } @Bean @ConfigurationProperties("spring.datasource.slave1") public DataSource slave1DataSource() { return DataSourceBuilder.create().build(); } @Bean @ConfigurationProperties("spring.datasource.slave2") public DataSource slave2DataSource() { return DataSourceBuilder.create().build(); } @Bean public DataSource myRoutingDataSource(@Qualifier("masterDataSource") DataSource masterDataSource, @Qualifier("slave1DataSource") DataSource slave1DataSource, @Qualifier("slave2DataSource") DataSource slave2DataSource) { Map<Object, Object> targetDataSources = new HashMap<>(); targetDataSources.put(DBTypeEnum.MASTER, masterDataSource); targetDataSources.put(DBTypeEnum.SLAVE1, slave1DataSource); targetDataSources.put(DBTypeEnum.SLAVE2, slave2DataSource); MyRoutingDataSource myRoutingDataSource = new MyRoutingDataSource(); myRoutingDataSource.setDefaultTargetDataSource(masterDataSource); myRoutingDataSource.setTargetDataSources(targetDataSources); return myRoutingDataSource; }}

這裡,我們配置了4個數據源,1個master,2兩個slave,1個路由數據源。前3個數據源都是為了生成第4個數據源,而且後續我們只用這最後一個路由數據源。

MyBatis配置

package com.cjs.example.config;import org.apache.ibatis.session.SqlSessionFactory;import org.mybatis.spring.SqlSessionFactoryBean;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;import org.springframework.core.io.support.PathMatchingResourcePatternResolver;import org.springframework.jdbc.datasource.DataSourceTransactionManager;import org.springframework.transaction.PlatformTransactionManager;import org.springframework.transaction.annotation.EnableTransactionManagement;import javax.annotation.Resource;import javax.sql.DataSource;@EnableTransactionManagement@Configurationpublic class MyBatisConfig {@Resource(name = "myRoutingDataSource") private DataSource myRoutingDataSource; @Bean public SqlSessionFactory sqlSessionFactory() throws Exception { SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean(); sqlSessionFactoryBean.setDataSource(myRoutingDataSource); sqlSessionFactoryBean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath:mapper/*.xml")); return sqlSessionFactoryBean.getObject(); } @Bean public PlatformTransactionManager platformTransactionManager() { return new DataSourceTransactionManager(myRoutingDataSource); }}

由於Spring容器中現在有4個數據源,所以我們需要為事務管理器和MyBatis手動指定一個明確的數據源。

3.3 設置路由key / 查找數據源

目標數據源就是那前3個這個我們是知道的,但是使用的時候是如果查找數據源的呢?

首先,我們定義一個枚舉來代表這三個數據源

package com.cjs.example.enums;public enum DBTypeEnum {MASTER, SLAVE1, SLAVE2;}

接下來,通過ThreadLocal將數據源設置到每個線程上下文中

package com.cjs.example.bean;import com.cjs.example.enums.DBTypeEnum;import java.util.concurrent.atomic.AtomicInteger;public class DBContextHolder {private static final ThreadLocal<DBTypeEnum> contextHolder = new ThreadLocal<>(); private static final AtomicInteger counter = new AtomicInteger(-1); public static void set(DBTypeEnum dbType) { contextHolder.set(dbType); } public static DBTypeEnum get() { return contextHolder.get(); } public static void master() { set(DBTypeEnum.MASTER); System.out.println("切換到master"); } public static void slave() { // 輪詢 int index = counter.getAndIncrement() % 2; if (counter.get() > 9999) { counter.set(-1); } if (index == 0) { set(DBTypeEnum.SLAVE1); System.out.println("切換到slave1"); }else { set(DBTypeEnum.SLAVE2); System.out.println("切換到slave2"); } }}

獲取路由key

package com.cjs.example.bean;import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;import org.springframework.lang.Nullable;public class MyRoutingDataSource extends AbstractRoutingDataSource {@Nullable @Override protected Object determineCurrentLookupKey() { return DBContextHolder.get(); }}

設置路由key

默認情況下,所有的查詢都走從庫,插入/修改/刪除走主庫。我們通過方法名來區分操作類型(CRUD)

package com.cjs.example.aop;import com.cjs.example.bean.DBContextHolder;import org.apache.commons.lang3.StringUtils;import org.aspectj.lang.JoinPoint;import org.aspectj.lang.annotation.Aspect;import org.aspectj.lang.annotation.Before;import org.aspectj.lang.annotation.Pointcut;import org.springframework.stereotype.Component;@Aspect@Componentpublic class DataSourceAop {@Pointcut("!@annotation(com.cjs.example.annotation.Master) " + "&& (execution(* com.cjs.example.service..*.select*(..)) " + "|| execution(* com.cjs.example.service..*.get*(..)))") public void readPointcut() { } @Pointcut("@annotation(com.cjs.example.annotation.Master) " + "|| execution(* com.cjs.example.service..*.insert*(..)) " + "|| execution(* com.cjs.example.service..*.add*(..)) " + "|| execution(* com.cjs.example.service..*.update*(..)) " + "|| execution(* com.cjs.example.service..*.edit*(..)) " + "|| execution(* com.cjs.example.service..*.delete*(..)) " + "|| execution(* com.cjs.example.service..*.remove*(..))") public void writePointcut() { } @Before("readPointcut()") public void read() { DBContextHolder.slave(); } @Before("writePointcut()") public void write() { DBContextHolder.master(); } /** * 另一種寫法:if...else... 判斷哪些需要讀從資料庫,其餘的走主資料庫 */// @Before("execution(* com.cjs.example.service.impl.*.*(..))")// public void before(JoinPoint jp) {// String methodName = jp.getSignature().getName();//// if (StringUtils.startsWithAny(methodName, "get", "select", "find")) {// DBContextHolder.slave();// }else {// DBContextHolder.master();// }// }}

有一般情況就有特殊情況,特殊情況是某些情況下我們需要強制讀主庫,針對這種情況,我們定義一個主鍵,用該註解標註的就讀主庫

package com.cjs.example.annotation;public @interface Master {}

例如,假設我們有一張表member

package com.cjs.example.service.impl;import com.cjs.example.annotation.Master;import com.cjs.example.entity.Member;import com.cjs.example.entity.MemberExample;import com.cjs.example.mapper.MemberMapper;import com.cjs.example.service.MemberService;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.stereotype.Service;import org.springframework.transaction.annotation.Transactional;import java.util.List;@Servicepublic class MemberServiceImpl implements MemberService {@Autowired private MemberMapper memberMapper; @Transactional @Override public int insert(Member member) { return memberMapper.insert(member); } @Master @Override public int save(Member member) { return memberMapper.insert(member); } @Override public List<Member> selectAll() { return memberMapper.selectByExample(new MemberExample()); } @Master @Override public String getToken(String appId) { // 有些讀操作必須讀主資料庫 // 比如,獲取微信access_token,因為高峰時期主從同步可能延遲 // 這種情況下就必須強制從主數據讀 return null; }}

4. 測試

package com.cjs.example;import com.cjs.example.entity.Member;import com.cjs.example.service.MemberService;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;@RunWith(SpringRunner.class)@SpringBootTestpublic class CjsDatasourceDemoApplicationTests {@Autowired private MemberService memberService; @Test public void testWrite() { Member member = new Member(); member.setName("zhangsan"); memberService.insert(member); } @Test public void testRead() { for (int i = 0; i < 4; i++) { memberService.selectAll(); } } @Test public void testSave() { Member member = new Member(); member.setName("wangwu"); memberService.save(member); } @Test public void testReadFromMaster() { memberService.getToken("1234"); }}

查看控制臺

5. 工程結構

6. 參考

https://www.jianshu.com/p/f2f4256a2310http://www.cnblogs.com/gl-developer/p/6170423.htmlhttps://www.cnblogs.com/huangjuncong/p/8576935.htmlhttps://blog.csdn.net/liu976180578/article/details/77684583

相關焦點

  • 01:springboot最快入門之三分鐘啟動springboot項目
    三、本章目標使用springboot完成一個簡單的web(springmvc)應用程式,通過@restController輸出"HelloWord"到界面,讓我們初步體驗springboot的快速開發、簡單的特性。目標時間:三分鐘。
  • mysql-proxy資料庫中間件架構 | 架構師之路
    一、mysql-proxy簡介mysql-proxy是mysql官方提供的mysql中間件服務
  • 史上最全的SpringBoot 中引入 MyBatisPlus 的常規流程!|乾貨
    一、前言:mybatis在持久層框架中還是比較火的,一般項目都是基於ssm。雖然mybatis可以直接在xml中通過SQL語句操作資料庫,很是靈活。但操作都要通過SQL語句進行,就必須寫大量的xml文件,很是麻煩。mybatis-plus就很好的解決了這個問題。
  • 「計算機畢設」基於springboot開發倉庫管理系統
    使用技術本套系統採用的是springboot、mybatisPlus、shiro、layui開發而成,都屬於現在比較流行的技術!看懂這個源碼絕大部分項目都能夠輕鬆應對!使用工具tomcat8.0+jdk8+idea+mysql8系統功能這是用戶登錄的界面,採用shiro基於不同角色對不同登錄用戶進行權限控制!
  • 主庫n -> 從庫s - MySQL5.7多主一從(多源複製)同步配置 - 計算機...
    讀寫分離,從庫只用於查詢,提高資料庫整體性能。部署環境註:使用docker部署mysql實例,方便快速搭建演示環境。但本文重點是講解主從配置,因此簡略描述docker環境構建mysql容器實例。資料庫:MySQL 5.7.x (相比5.5,5.6而言,5.7同步性能更好,支持多源複製,可實現多主一從,主從庫版本應保證一致)作業系統:CentOS 7.x容器:Docker 17.09.0-ce鏡像:mysql:5.7主庫300:IP=192.168.10.212; PORT=4300; server-id=300; database=test3; table=user主庫400
  • 「程式設計師分享」基於SpringBoot開發的天貓商城源碼
    實用技術天貓商城系統使用的是Spring,SpringMvc,SpringBoot和mybatis開發而成的!有前臺網頁和後臺管理兩個登錄地址!使用工具jdk8+tomcat8+mysql5.7+IntelliJ IDEA+maven系統功能
  • JeecgBoot 2.4 微服務正式版發布,基於 SpringBoot 的低代碼平臺
    前後端分離架構 SpringBoot2.x,SpringCloud,Ant Design&Vue,Mybatis-plus,Shiro,JWT 支持微服務。強大的代碼生成器讓前後端代碼一鍵生成,實現低代碼開發! JeecgBoot 引領新的低代碼開發模式(OnlineCoding-> 代碼生成-> 手工MERGE), 幫助解決Java項目70%的重複工作,讓開發更多關注業務。
  • 用SpringBoot搭建SSM項目環境,真簡便
    其中mybatis有一個插件叫通用mapper,單表查詢時使用起來非常方便。本來我是打算做個簡單的說明就好了的,結果越寫越多,就獨立成一篇文章了,也就是今天的另外一篇。當然SpringBoot並不知道我們的資料庫是什麼,所以需要配置mysql的依賴。②配置資料庫信息資料庫四大金剛的配置,不用多說。
  • 啥,聽說你用了springboot,但是開發的接口還在裸奔?
    id=5fe86eb74c636312f4b709551、應用場景簡介啥,聽說你用了springboot,但是開發的接口還在裸奔?快來試試這個PopularMVC吧,它也許是你想要找的神器!神器亮相springboot叫一鍵啟動,popularmvc為你的springboot項目插上翅膀,力求一鍵起飛!
  • DTCC:MySQl核心代碼開發經驗揭示
    測試用例依照功能分為多個suite,main suite測試文件放在mysql-test/t,結果文件放在mysQL-test/r,其他的suite在mysQL-test/suite下,如:mysql-test/suite/rpl/{t,r}.
  • springboot項目整合rabbitmq學習第一步
    springboot項目整合rabbitmq的也是很簡單的。1、前提安裝好rabbitmq。2、pom.xml添加rabbitmq依賴。這個spring-boot-starter-amqp裡面的amqp指的是高級消息隊列協議,而rabbitmq就是amqp協議的一種實現中間件。
  • 歐洲最大MySQL用戶Booking.com資料庫構架探秘!
    接下來給大家詳細介紹一下如何進行資料庫的讀寫擴展。 以上圖為例,對DB1的tableB和tableC進行表分離。● 當所有的tableB和tableC的流量分離到DB2上後,進行寫操作的分離○   DB2的cname指向到DB2的主伺服器,此時tableB,tableC的寫操作是宕機的,這個過程大概是5-10秒○   在DB2的主伺服器上去掉replication filter,寫分離完成。
  • MySQL基於MHA的FailOver過程
    >什麼是FailOver故障轉移主庫宕機,一直到業務恢復正常的處理過程如何處理FailOver1.快速監控到主庫宕機2.選擇新主節點,選擇策略mysqladmin/binlog創建必要目錄mkdir -p /data/mysql/binlog chown -R mysql.mysql /data/*修改完成後,將主庫binlog接過來(從000001開始拉取,之後的binlog
  • 常用的三種修改mysql最大連接數的方法
    默認最大連接數是100,一般流量稍微大一點的論壇或網站這個連接數是遠遠不夠的,增加默認MYSQL連接數的方法有兩個方法一:進入MYSQL安裝目錄 打開MYSQL配置文件 my.ini 或 my.cnf查找 max_connections=100   修改為 max_connections=1000 服務裡重起MYSQL即可方法二:MySQL的最大連接數默認是100客戶端登錄:mysql
  • MySQL怎麼刪除#sql開頭的臨時表
    /* 直接刪除,表名前加#mysql50*/root@testdb 01:42:57> DROP TABLE `#mysql50##sql-ib87-856498050`;註: #mysql50#前綴是MySQL 5. 1 中引入的文件名安全編碼。另外,表名因不符合命名規範,想要執行該腳本需要將表名用反引號括起來。
  • 您的包裹「 MySQL靈魂十連」 待籤收
    從節點開啟一個線程(I/O Thread)把自己扮演成 mysql 的客戶端,通過 mysql 協議,請求主節點的二進位日誌文件中的事件 。但如果寫操作都是尾插入,那還是可以支持一定程度的讀寫並發從 MyISAM 所支持的鎖中也可以看出,MyISAM 是一個支持讀讀並發,但不支持通用讀寫並發,寫寫並發的資料庫引擎,所以它更適合用於讀多寫少的應用場合,一般工程中也用的較少。
  • 面試官:Mybatis 使用了哪些設計模式?
    1、Builder模式Builder模式的定義是「將一個複雜對象的構建與它的表示分離,使得同樣的構建過程可以創建不同的表示。」設計模式之單例模式實踐,這篇文章推薦你看下。Mybatis支持動態SQL的強大功能,比如下面的這個SQL:<update id="update"parameterType="org.format.dynamicproxy.mybatis.bean.User">UPDATE users <trim