SpringBoot+MyBatis+AOP實(shí)現(xiàn)讀寫分離的示例代碼
前言:高并發(fā)這個(gè)階段,肯定是需要做MySQL讀寫分離的。實(shí)際上大部分的互聯(lián)網(wǎng)網(wǎng)站或者App,其實(shí)都是讀多寫少。所以針對(duì)這個(gè)情況,就是寫一個(gè)主庫(kù),但是主庫(kù)掛多個(gè)從庫(kù),然后從多個(gè)從庫(kù)來(lái)讀,那不就可以支撐更高的讀并發(fā)壓力了嗎?
一、 MySQL 讀寫分離
1.1、如何實(shí)現(xiàn) MySQL 的讀寫分離?
其實(shí)很簡(jiǎn)單,就是基于主從復(fù)制架構(gòu)。簡(jiǎn)單來(lái)說(shuō),就搞一個(gè)主庫(kù),掛多個(gè)從庫(kù),然后我們就單單只是寫主庫(kù),然后主庫(kù)會(huì)自動(dòng)把數(shù)據(jù)給同步到從去,多個(gè)從庫(kù)用于讀。
讀寫分離就是對(duì)于一條SQL該選擇哪一個(gè)數(shù)據(jù)庫(kù)去執(zhí)行,至于誰(shuí)來(lái)做選擇數(shù)據(jù)庫(kù)這件事,有兩個(gè),要么使用中間件幫我們做,要么程序自己做。一般來(lái)說(shuō),讀寫分離有兩種實(shí)現(xiàn)方式。第一種是依靠中間件MyCat或Sharding-JDBC,也就是說(shuō)應(yīng)用程序連接到中間件,中間件幫我們做SQL分離,去選擇指定的數(shù)據(jù)源;第二種是應(yīng)用程序自己去做分離。這里我用程序自己來(lái)做,主要是利用Spring提供的路由數(shù)據(jù)源,以及AOP。

1.2、MySQL 主從復(fù)制原理?
主庫(kù)將變更寫入 binlog 日志,然后從庫(kù)連接到主庫(kù)之后,從庫(kù)有一個(gè) IO 線程,將主庫(kù)的 binlog 日志拷貝到自己本地,寫入一個(gè) relay 中繼日志中。接著從庫(kù)中有一個(gè) SQL 線程會(huì)從中繼日志讀取 binlog,然后執(zhí)行 binlog 日志中的內(nèi)容,也就是在自己本地再次執(zhí)行一遍 SQL,這樣就可以保證自己跟主庫(kù)的數(shù)據(jù)是一樣的。
mysql-master-slave
這里有一個(gè)非常重要的一點(diǎn),就是從庫(kù)同步主庫(kù)數(shù)據(jù)的過(guò)程是串行化的,也就是說(shuō)主庫(kù)上并行的操作,在從庫(kù)上會(huì)串行執(zhí)行。所以這就是一個(gè)非常重要的點(diǎn)了,由于從庫(kù)從主庫(kù)拷貝日志以及串行執(zhí)行 SQL 的特點(diǎn),在高并發(fā)場(chǎng)景下,從庫(kù)的數(shù)據(jù)一定會(huì)比主庫(kù)慢一些,是有延時(shí)的。所以經(jīng)常出現(xiàn),剛寫入主庫(kù)的數(shù)據(jù)可能是讀不到的,要過(guò)幾十毫秒,甚至幾百毫秒才能讀取到。
而且這里還有另外一個(gè)問題,就是如果主庫(kù)突然宕機(jī),然后恰好數(shù)據(jù)還沒同步到從庫(kù),那么有些數(shù)據(jù)可能在從庫(kù)上是沒有的,有些數(shù)據(jù)可能就丟失了。
所以 MySQL 實(shí)際上在這一塊有兩個(gè)機(jī)制,一個(gè)是半同步復(fù)制,用來(lái)解決主庫(kù)數(shù)據(jù)丟失問題;一個(gè)是并行復(fù)制,用來(lái)解決主從同步延時(shí)問題。
這個(gè)所謂半同步復(fù)制,也叫 semi-sync 復(fù)制,指的就是主庫(kù)寫入 binlog 日志之后,就會(huì)將強(qiáng)制此時(shí)立即將數(shù)據(jù)同步到從庫(kù),從庫(kù)將日志寫入自己本地的 relay log 之后,接著會(huì)返回一個(gè) ack 給主庫(kù),主庫(kù)接收到至少一個(gè)從庫(kù)的 ack 之后才會(huì)認(rèn)為寫操作完成了。
所謂并行復(fù)制,指的是從庫(kù)開啟多個(gè)線程,并行讀取 relay log 中不同庫(kù)的日志,然后并行重放不同庫(kù)的日志,這是庫(kù)級(jí)別的并行。
1.3、MySQL 主從同步延時(shí)問題(精華)
線上會(huì)發(fā)現(xiàn),每天總有那么一些數(shù)據(jù),我們期望更新一些重要的數(shù)據(jù)狀態(tài),但在高峰期時(shí)候卻沒更新。用戶跟客服反饋,而客服就會(huì)反饋給我們。
(1) 主從同步延遲的原因
一個(gè)服務(wù)器開放N個(gè)鏈接給客戶端來(lái)連接的,這樣有會(huì)有大并發(fā)的更新操作, 但是從服務(wù)器的里面讀取binlog的線程僅有一個(gè),當(dāng)某個(gè)SQL在從服務(wù)器上執(zhí)行的時(shí)間稍長(zhǎng)或者由于某個(gè)SQL要進(jìn)行鎖表就會(huì)導(dǎo)致,主服務(wù)器的SQL大量積壓,未被同步到從服務(wù)器里。這就導(dǎo)致了主從不一致, 也就是主從延遲。
(2) 主從同步延遲的解決辦法
一般來(lái)說(shuō),如果主從延遲較為嚴(yán)重,有以下解決方案:
- 分庫(kù):將一個(gè)主庫(kù)拆分為多個(gè)主庫(kù),每個(gè)主庫(kù)的寫并發(fā)就減少了幾倍,此時(shí)主從延遲可以忽略不計(jì)。
- 需要走主庫(kù)的強(qiáng)制走主庫(kù)查詢:如果確實(shí)是存在必須先插入,立馬要求就查詢到,然后立馬就要反過(guò)來(lái)執(zhí)行一些操作,對(duì)這個(gè)查詢?cè)O(shè)置直連主庫(kù)。
- 業(yè)務(wù)層面妥協(xié),重寫代碼:寫代碼的同學(xué)要慎重,插入數(shù)據(jù)時(shí)立馬查詢可能查不到。是否操作完之后馬上要馬上進(jìn)行讀???
二、SpringBoot+AOP+MyBatis實(shí)現(xiàn)MySQL讀寫分離
代碼環(huán)境是 SpringBoot+MyBatis+AOP。想要讀寫分離就需要配置多個(gè)數(shù)據(jù)源,在進(jìn)行寫操作是選擇寫的數(shù)據(jù)源(主庫(kù)),讀操作時(shí)選擇讀的數(shù)據(jù)源(從庫(kù))。
2.1、AbstractRoutingDataSource
SpringBoot提供了AbstractRoutingDataSource類根據(jù)用戶定義的規(guī)則選擇當(dāng)前的數(shù)據(jù)源,這樣我們可以在執(zhí)行查詢之前,設(shè)置使用的數(shù)據(jù)源。實(shí)現(xiàn)可動(dòng)態(tài)路由的數(shù)據(jù)源,在每次數(shù)據(jù)庫(kù)查詢操作前執(zhí)行。它的抽象方法 determineCurrentLookupKey() 決定使用哪個(gè)數(shù)據(jù)源

想要讀寫分離就需要配置多個(gè)數(shù)據(jù)源,在進(jìn)行寫操作是選擇寫的數(shù)據(jù)源,讀操作時(shí)選擇讀的數(shù)據(jù)源。其中有兩個(gè)關(guān)鍵點(diǎn):
- 如何切換數(shù)據(jù)源
- 如何根據(jù)不同的方法選擇正確的數(shù)據(jù)源
2.2、如何切換數(shù)據(jù)源
通常用 springboot 時(shí)都是使用它的默認(rèn)配置,只需要在配置文件中定義好連接屬性就行了,但是現(xiàn)在我們需要自己來(lái)配置了,spring 是支持多數(shù)據(jù)源的,多個(gè) datasource 放在一個(gè) HashMapTargetDataSource中,通過(guò)dertermineCurrentLookupKey獲取 key 來(lái)覺定要使用哪個(gè)數(shù)據(jù)源。因此我們的目標(biāo)就很明確了,建立多個(gè) datasource 放到 TargetDataSource 中,同時(shí)重寫 dertermineCurrentLookupKey 方法來(lái)決定使用哪個(gè) key。
2.3、如何選擇數(shù)據(jù)源
事務(wù)一般是注解在 Service 層的,因此在開始這個(gè) service 方法調(diào)用時(shí)要確定數(shù)據(jù)源,有什么通用方法能夠在開始執(zhí)行一個(gè)方法前做操作呢?相信你已經(jīng)想到了那就是**切面 **。怎么切有兩種辦法:
- 注解式,定義一個(gè)只讀注解,被該數(shù)據(jù)標(biāo)注的方法使用讀庫(kù)
- 方法名,根據(jù)方法名寫切點(diǎn),比如 getXXX 用讀庫(kù),setXXX 用寫庫(kù)
三 、代碼實(shí)現(xiàn)
3.0、工程目錄結(jié)構(gòu)

3.1、引入Maven依賴
<dependencies>
<!--SpringBoot集成Aop起步依賴-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<!--SpringBoot集成WEB起步依賴-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--mybatis集成SpringBoot起步依賴-->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.1.3</version>
</dependency>
<!--MySQL驅(qū)動(dòng)-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<!--SpringBoot單元測(cè)試依賴-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
</dependencies>
3.2、編寫配置文件,配置主從數(shù)據(jù)源
spring:
datasource:
#主數(shù)據(jù)源
master:
name: test
jdbc-url: jdbc:mysql://xxxxxx:3306/test?allowMultiQueries=true&useSSL=false&useUnicode=true&characterEncoding=utf-8
username: root
password: xxxxxx
driver-class-name: com.mysql.cj.jdbc.Driver
hikari:
maximum-pool-size: 20
max-lifetime: 30000
idle-timeout: 30000
data-source-properties:
prepStmtCacheSize: 250
prepStmtCacheSqlLimit: 2048
cachePrepStmts: true
useServerPrepStmts: true
#從數(shù)據(jù)源
slave:
name: test
jdbc-url: jdbc:mysql://xxxxxx:3306/test?allowMultiQueries=true&useSSL=false&useUnicode=true&characterEncoding=utf-8
username: root
password: xxxxxx
driver-class-name: com.mysql.cj.jdbc.Driver
hikari:
maximum-pool-size: 20
max-lifetime: 30000
idle-timeout: 30000
data-source-properties:
prepStmtCacheSize: 250
prepStmtCacheSqlLimit: 2048
cachePrepStmts: true
useServerPrepStmts: true
#MyBatis:
# mapper-locations: classpath:mapper/*.xml
# type-aliases-package: com.hs.demo.entity
3.3、Enum類,定義主庫(kù)從庫(kù)
定義一個(gè)枚舉類來(lái)代表這三個(gè)數(shù)據(jù)源
package com.hs.demo.config;
/**
* Enum類,定義主庫(kù)從庫(kù)兩個(gè)數(shù)據(jù)源
*/
public enum DBTypeEnum {
MASTER, SLAVE;
}
3.4、ThreadLocal定義數(shù)據(jù)源切換
通過(guò)ThreadLocal將數(shù)據(jù)源綁定到每個(gè)線程上下文中,ThreadLocal 用來(lái)保存每個(gè)線程的是使用讀庫(kù)還是寫庫(kù)。操作結(jié)束后清除該數(shù)據(jù),避免內(nèi)存泄漏。
package com.hs.demo.config;
/**
*ThreadLocal定義數(shù)據(jù)源切換,通過(guò)ThreadLocal將數(shù)據(jù)源綁定到每個(gè)線程上下文中
*/
public class DBContextHolder {
/**
* ThreadLocal 不是 Thread,是一個(gè)線程內(nèi)部的數(shù)據(jù)存儲(chǔ)類,通過(guò)它可以在指定的線程中存儲(chǔ)數(shù)據(jù),對(duì)數(shù)據(jù)存儲(chǔ)后,只有在線程中才可以獲取到存儲(chǔ)的數(shù)據(jù),對(duì)于其他線程來(lái)說(shuō)是無(wú)法獲取到數(shù)據(jù)。
* 大致意思就是ThreadLocal提供了線程內(nèi)存儲(chǔ)變量的能力,這些變量不同之處在于每一個(gè)線程讀取的變量是對(duì)應(yīng)的互相獨(dú)立的,通過(guò)get和set方法就可以得到當(dāng)前線程對(duì)應(yīng)的值。
*/
private static final ThreadLocal<DBTypeEnum> contextHolder = new ThreadLocal<>();
public static void set(DBTypeEnum dbTypeEnum){
contextHolder.set(dbTypeEnum);
}
public static DBTypeEnum get() {
return contextHolder.get();
}
public static void master() {
set(DBTypeEnum.MASTER);
System.out.println("--------以下操作為master(寫操作)--------");
}
public static void slave() {
set(DBTypeEnum.SLAVE);
System.out.println("--------以下操作為slave(讀操作)--------");
}
public static void clear() {
contextHolder.remove();
}
}
3.5、重寫路由選擇類
重寫 determineCurrentLookupKey 方法,獲取當(dāng)前線程上綁定的路由key。Spring 在開始進(jìn)行數(shù)據(jù)庫(kù)操作時(shí)會(huì)通過(guò)這個(gè)方法來(lái)決定使用哪個(gè)數(shù)據(jù)庫(kù)源,因此我們?cè)谶@里調(diào)用上面 DbContextHolder 類的getDbType()方法獲取當(dāng)前操作類別。
- AbstractRoutingDataSource的getConnection() 方法根據(jù)查找 lookup key 鍵對(duì)不同目標(biāo)數(shù)據(jù)源的調(diào)用,通常是通過(guò)(但不一定)某些線程綁定的事物上下文來(lái)實(shí)現(xiàn)。
- AbstractRoutingDataSource的多數(shù)據(jù)源動(dòng)態(tài)切換的核心邏輯是:在程序運(yùn)行時(shí),把數(shù)據(jù)源數(shù)據(jù)源通過(guò) AbstractRoutingDataSource 動(dòng)態(tài)織入到程序中,靈活的進(jìn)行數(shù)據(jù)源切換。
- 基于AbstractRoutingDataSource的多數(shù)據(jù)源動(dòng)態(tài)切換,可以實(shí)現(xiàn)讀寫分離,這么做缺點(diǎn)也很明顯,無(wú)法動(dòng)態(tài)的增加數(shù)據(jù)源。
package com.hs.demo.config;
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
import org.springframework.lang.Nullable;
/**
* 重寫路由選擇類:獲取當(dāng)前線程上綁定的路由key
*/
public class MyRoutingDataSource extends AbstractRoutingDataSource {
/**
* determineCurrentLookupKey()方法決定使用哪個(gè)數(shù)據(jù)源、
* 根據(jù)Key獲取數(shù)據(jù)源的信息,上層抽象函數(shù)的鉤子
*/
@Nullable
@Override
protected Object determineCurrentLookupKey() {
return DBContextHolder.get();
}
}
3.6、配置多數(shù)據(jù)源
這里配置了3個(gè)數(shù)據(jù)源,1個(gè)master,1個(gè)slave,1個(gè)路由數(shù)據(jù)源。前2個(gè)數(shù)據(jù)源都是為了生成第3個(gè)數(shù)據(jù)源,而且后續(xù)我們只用這最后一個(gè)路由數(shù)據(jù)源。
package com.hs.demo.config;
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;
/**
* 增加了 DataSourceConfig 這個(gè)配置文件之后,需要添加Hikari連接池,單數(shù)據(jù)源自動(dòng)裝載時(shí)不會(huì)出這
* 樣的問題
*
* @Configuration 注解,表明這就是一個(gè)配置類,指示一個(gè)類聲明一個(gè)或者多個(gè)@Bean 聲明的方法并且由Spring容器統(tǒng)一管理,以便在運(yùn)行時(shí)為這些bean生成bean的定義和服務(wù)請(qǐng)求的類。
*/
@Configuration
public class DataSourceConfig {
/**
* 注入主庫(kù)數(shù)據(jù)源
*/
@Bean
@ConfigurationProperties(prefix = "spring.datasource.master")
public DataSource masterDataSource() {
return DataSourceBuilder.create().build();
//DataSourceProperties properties放在方法參數(shù)里
// return DataSourceBuilder.create(properties.getClassLoader())
// .type(HikariDataSource.class)
// .driverClassName(properties.getDriverClassName())
// .url(properties.getUrl())
// .username(properties.getUsername())
// .password(properties.getPassword())
// .build();
}
/**
* 注入從庫(kù)數(shù)據(jù)源
*/
@Bean
@ConfigurationProperties(prefix = "spring.datasource.slave")
public DataSource slaveDataSource() {
return DataSourceBuilder.create().build();
}
/**
* 配置選擇數(shù)據(jù)源
* @param masterDataSource
* @param slaveDataSource
* @return DataSource
*/
@Bean
public DataSource myRoutingDataSource(@Qualifier("masterDataSource") DataSource masterDataSource, @Qualifier("slaveDataSource") DataSource slaveDataSource)
{
Map<Object, Object> targetDataSource = new HashMap<>();
targetDataSource.put(DBTypeEnum.MASTER, masterDataSource);
targetDataSource.put(DBTypeEnum.SLAVE, slaveDataSource);
MyRoutingDataSource myRoutingDataSource = new MyRoutingDataSource();
//找不到用默認(rèn)數(shù)據(jù)源
myRoutingDataSource.setDefaultTargetDataSource(masterDataSource);
//可選擇目標(biāo)數(shù)據(jù)源
myRoutingDataSource.setTargetDataSources(targetDataSource);
return myRoutingDataSource;
}
}
3.7、配置Mybatis指定數(shù)據(jù)源
修改SqlSessionFactory 和事務(wù)管理器
package com.hs.demo.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.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.annotation.EnableTransactionManagement;
import javax.annotation.Resource;
import javax.sql.DataSource;
/**
* 配置Mybatis指定數(shù)據(jù)源:SqlSessionFactory和事務(wù)管理器
*/
@Configuration
@EnableTransactionManagement
public class MyBatisConfig {
/**
* 注入自己重寫的數(shù)據(jù)源
*/
@Resource(name = "myRoutingDataSource")
private DataSource myRoutingDataSource;
/**
* 配置SqlSessionFactory
* @return SqlSessionFactory
* @throws Exception
*/
@Bean
public SqlSessionFactory sqlSessionFactory() throws Exception
{
SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
sqlSessionFactoryBean.setDataSource(myRoutingDataSource);
//ResourcePatternResolver(資源查找器)定義了getResources來(lái)查找資源
//PathMatchingResourcePatternResolver提供了以classpath開頭的通配符方式查詢,否則會(huì)調(diào)用ResourceLoader的getResource方法來(lái)查找
// ResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
// sqlSessionFactoryBean.setMapperLocations(resolver.getResources(mapperLocation));
return sqlSessionFactoryBean.getObject();
}
/**
* 事務(wù)管理器,不寫則事務(wù)不生效:事務(wù)需要知道當(dāng)前使用的是哪個(gè)數(shù)據(jù)源才能進(jìn)行事務(wù)處理
*/
@Bean
public PlatformTransactionManager platformTransactionManager() {
return new DataSourceTransactionManager(myRoutingDataSource);
}
// /**
// * 當(dāng)自定義數(shù)據(jù)源,用戶必須覆蓋SqlSessionTemplate,開啟BATCH處理模式
// *
// * @param sqlSessionFactory
// * @return
// */
// @Bean
// public SqlSessionTemplate sqlSessionTemplate(@Qualifier("sqlSessionFactory") SqlSessionFactory sqlSessionFactory) {
// return new SqlSessionTemplate(sqlSessionFactory, ExecutorType.BATCH);
// }
}
3.8、AOP切面實(shí)現(xiàn)數(shù)據(jù)源切換
通過(guò)Aop的前置通知來(lái)設(shè)置要使用的路由key(數(shù)據(jù)源)
package com.hs.demo.config;
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.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;
import java.lang.reflect.Method;
/**
* 默認(rèn)情況下,所有的查詢都走從庫(kù),插入/修改/刪除走主庫(kù)。我們通過(guò)方法名來(lái)區(qū)分操作類型(CRUD)
*
* 切面不能建立在DAO層,事務(wù)是在service開啟的,到dao層再切換數(shù)據(jù)源,那事務(wù)就廢了
*
*/
@Aspect
@Component
public class DataSourceAop {
/**
* 第一個(gè)”*“符號(hào) 表示返回值的類型任意;
* com.sample.service.impl AOP所切的服務(wù)的包名,即,我們的業(yè)務(wù)部分
* 包名后面的”..“ 表示當(dāng)前包及子包
* 第二個(gè)”*“ 表示類名,*即所有類。此處可以自定義,下文有舉例
* .*(..) 表示任何方法名,括號(hào)表示參數(shù),兩個(gè)點(diǎn)表示任何參數(shù)類型
*/
@Pointcut("!@annotation(com.hs.demo.config.Master) " +
"&& (execution(* com.hs.demo.service.*.select*(..)) " +
"|| execution(* com.hs.demo.service..*.find*(..)))")
public void readPointcut() {
}
@Pointcut("@annotation(com.hs.demo.config.Master) " +
"|| execution(* com.hs.demo.service..*.save*(..)) " +
"|| execution(* com.hs.demo.service..*.add*(..)) " +
"|| execution(* com.hs.demo.service..*.insert*(..)) " +
"|| execution(* com.hs.demo.service..*.update*(..)) " +
"|| execution(* com.hs.demo.service..*.edit*(..)) " +
"|| execution(* com.hs.demo..*.delete*(..)) " +
"|| execution(* com.hs.demo..*.remove*(..))")
public void writePointcut() {
}
@Before("readPointcut()")
public void read(JoinPoint jp) {
//獲取當(dāng)前的方法信息
MethodSignature methodSignature = (MethodSignature) jp.getSignature();
Method method = methodSignature.getMethod();
//判斷方法上是否存在注解@Master
boolean present = method.isAnnotationPresent(Master.class);
if (!present)
{
//如果不存在,默認(rèn)走從庫(kù)讀
DBContextHolder.slave();
}
else
{
//如果存在,走主庫(kù)讀
DBContextHolder.master();
}
}
@Before("writePointcut()")
public void write() {
DBContextHolder.master();
}
/**
* 另一種寫法:if...else... 判斷哪些需要讀從數(shù)據(jù)庫(kù),其余的走主數(shù)據(jù)庫(kù)
*/
// @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();
// }
// }
}
3.9、如果有強(qiáng)制走主庫(kù)的操作,可以定義注解
package com.hs.demo.config;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 有時(shí)候主從延遲,需要強(qiáng)制讀主庫(kù)的注解
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Master
{
//設(shè)置數(shù)據(jù)源類型
//String value();
}
3.10、自行定義CRUD讀寫操作
(1)UserEntity
package com.hs.demo.entity;
import lombok.Data;
/**
* @author heshi
* @date 2021/10/20 15:14
*/
@Data
public class UserEntity {
private Integer user_id;
private String account;
private String nickname;
private String password;
private String headimage_url;
private String introduce;
}
(2)UserMapper
package com.hs.demo.mapper;
import com.hs.demo.entity.UserEntity;
import org.apache.ibatis.annotations.*;
import java.util.List;
/**
* Spring通過(guò)@Mapper注解實(shí)現(xiàn)動(dòng)態(tài)代理,mybatis會(huì)自動(dòng)創(chuàng)建Dao接口的實(shí)現(xiàn)類代理對(duì)象注入IOC容器進(jìn)行管理,這樣就不用編寫Dao層的實(shí)現(xiàn)類
*
*/
@Mapper
public interface UserMapper {
@Select("SELECT * FROM user") //使用@Select、@Insert等注解方式來(lái)實(shí)現(xiàn)對(duì)應(yīng)的持久化操作,使得我們可以不配置XML格式的Mapper文件
List<UserEntity> findAll();
@Insert("insert into user(account,nickname,password) values(#{account}, #{nickname}, #{password})")
int insert(UserEntity user);
@Update("UPDATE user SET account=#{account},nickname=#{nickname} WHERE id =#{id}")
void update(UserEntity user);
@Delete("DELETE FROM user WHERE id =#{id}")
void delete(Long id);
}
(3)UserService(重要)
package com.hs.demo.service;
import com.hs.demo.entity.UserEntity;
import com.hs.demo.mapper.UserMapper;
import com.hs.demo.mysql.Master;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;
/**
* @author heshi
* @date 2021/10/21 10:36
*/
@Service
public class UserService {
@Autowired
UserMapper userMapper;
// @Master
public List<UserEntity> findAll()
{
List<UserEntity> userEntities = userMapper.findAll();
return userEntities;
}
public int insertUser(UserEntity user)
{
int i = userMapper.insert(user);
return i;
}
// void update(UserEntity user);
//
// void delete(Long id);
}
(4) UserController
package com.hs.demo.controller;
import com.hs.demo.entity.UserEntity;
import com.hs.demo.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
@RestController
public class UserController
{
@Autowired
UserService userService;
@RequestMapping("/listUser")
public List<UserEntity> listUser()
{
List<UserEntity> users = userService.findAll();
return users;
}
@RequestMapping("/insertUser")
public void insertUser()
{
UserEntity userEntity = new UserEntity();
userEntity.setAccount("22222");
userEntity.setNickname("hshshs");
userEntity.setPassword("123");
userService.insertUser(userEntity);
}
}
運(yùn)行結(jié)果如下圖所示
總結(jié):通過(guò)AOP來(lái)確定所使用數(shù)據(jù)源類型,然后通過(guò)路由來(lái)進(jìn)行數(shù)據(jù)源選擇。
參考鏈接:
springboot實(shí)現(xiàn)讀寫分離(基于Mybatis,mysql)
springboot實(shí)現(xiàn)mysql的讀寫分離
到此這篇關(guān)于SpringBoot+MyBatis+AOP實(shí)現(xiàn)讀寫分離的示例代碼的文章就介紹到這了,更多相關(guān)SpringBoot+MyBatis+AOP讀寫分離內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Java中ShardingSphere 數(shù)據(jù)分片的實(shí)現(xiàn)
其實(shí)很多人對(duì)分庫(kù)分表多少都有點(diǎn)恐懼,我們今天用ShardingSphere 給大家演示數(shù)據(jù)分片,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2021-09-09
Java調(diào)用Oracle存儲(chǔ)過(guò)程詳解
這篇文章主要介紹了Java調(diào)用Oracle存儲(chǔ)過(guò)程詳解的相關(guān)資料,需要的朋友可以參考下2017-02-02
詳解如何實(shí)現(xiàn)OpenAPI開發(fā)動(dòng)態(tài)處理接口的返回?cái)?shù)據(jù)
這篇文章主要為大家介紹了OpenAPI開發(fā)動(dòng)態(tài)處理接口的返回?cái)?shù)據(jù)如何實(shí)現(xiàn)詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-04-04
spring?controller層引用service報(bào)空指針異常nullpointExceptio問題
這篇文章主要介紹了spring?controller層引用service報(bào)空指針異常nullpointExceptio問題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-02-02

