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è)問(wèn)題,就是如果主庫(kù)突然宕機(jī),然后恰好數(shù)據(jù)還沒(méi)同步到從庫(kù),那么有些數(shù)據(jù)可能在從庫(kù)上是沒(méi)有的,有些數(shù)據(jù)可能就丟失了。
所以 MySQL 實(shí)際上在這一塊有兩個(gè)機(jī)制,一個(gè)是半同步復(fù)制,用來(lái)解決主庫(kù)數(shù)據(jù)丟失問(wèn)題;一個(gè)是并行復(fù)制,用來(lái)解決主從同步延時(shí)問(wèn)題。
這個(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ù)開(kāi)啟多個(gè)線程,并行讀取 relay log 中不同庫(kù)的日志,然后并行重放不同庫(kù)的日志,這是庫(kù)級(jí)別的并行。
1.3、MySQL 主從同步延時(shí)問(wèn)題(精華)
線上會(huì)發(fā)現(xiàn),每天總有那么一些數(shù)據(jù),我們期望更新一些重要的數(shù)據(jù)狀態(tài),但在高峰期時(shí)候卻沒(méi)更新。用戶跟客服反饋,而客服就會(huì)反饋給我們。
(1) 主從同步延遲的原因
一個(gè)服務(wù)器開(kāi)放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)覺(jué)定要使用哪個(gè)數(shù)據(jù)源。因此我們的目標(biāo)就很明確了,建立多個(gè) datasource 放到 TargetDataSource 中,同時(shí)重寫 dertermineCurrentLookupKey 方法來(lái)決定使用哪個(gè) key。
2.3、如何選擇數(shù)據(jù)源
事務(wù)一般是注解在 Service 層的,因此在開(kāi)始這個(gè) service 方法調(diào)用時(shí)要確定數(shù)據(jù)源,有什么通用方法能夠在開(kāi)始執(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 在開(kāi)始進(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ì)出這 * 樣的問(wèn)題 * * @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開(kāi)頭的通配符方式查詢,否則會(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,開(kāi)啟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開(kāi)啟的,到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-09Java調(diào)用Oracle存儲(chǔ)過(guò)程詳解
這篇文章主要介紹了Java調(diào)用Oracle存儲(chǔ)過(guò)程詳解的相關(guān)資料,需要的朋友可以參考下2017-02-02Activiti開(kāi)發(fā)環(huán)境的配置
本篇文章主要內(nèi)容介紹了Activiti開(kāi)發(fā)環(huán)境的配置,小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2018-04-04SpringBoot之QueryDsl嵌套子查詢問(wèn)題
這篇文章主要介紹了SpringBoot之QueryDsl嵌套子查詢問(wèn)題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-03-03詳解如何實(shí)現(xiàn)OpenAPI開(kāi)發(fā)動(dòng)態(tài)處理接口的返回?cái)?shù)據(jù)
這篇文章主要為大家介紹了OpenAPI開(kāi)發(fā)動(dòng)態(tài)處理接口的返回?cái)?shù)據(jù)如何實(shí)現(xiàn)詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-04-04spring?controller層引用service報(bào)空指針異常nullpointExceptio問(wèn)題
這篇文章主要介紹了spring?controller層引用service報(bào)空指針異常nullpointExceptio問(wèn)題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-02-02