Spring實現(xiàn)數(shù)據(jù)庫讀寫分離詳解
1、背景
大多數(shù)系統(tǒng)都是讀多寫少,為了降低數(shù)據(jù)庫的壓力,可以對主庫創(chuàng)建多個從庫,從庫自動從主庫同步數(shù)據(jù),程序中將寫的操作發(fā)送到主庫,將讀的操作發(fā)送到從庫去執(zhí)行。
今天的主要目標(biāo):通過 spring 實現(xiàn)讀寫分離。
讀寫分離需實現(xiàn)下面 2 個功能:
1、讀的方法,由調(diào)用者來控制具體是讀從庫還是主庫
2、有事務(wù)的方法,內(nèi)部的所有讀寫操作都走主庫
2、思考 3 個問題
讀的方法,由調(diào)用者來控制具體是讀從庫還是主庫,如何實現(xiàn)?
可以給所有讀的方法添加一個參數(shù),來控制讀從庫還是主庫。
數(shù)據(jù)源如何路由?
spring-jdbc 包中提供了一個抽象類:AbstractRoutingDataSource,實現(xiàn)了 javax.sql.DataSource 接口,我們用這個類來作為數(shù)據(jù)源類,重點是這個類可以用來做數(shù)據(jù)源的路由,可以在其內(nèi)部配置多個真實的數(shù)據(jù)源,最終用哪個數(shù)據(jù)源,由開發(fā)者來決定。
AbstractRoutingDataSource 中有個 map,用來存儲多個目標(biāo)數(shù)據(jù)源
private Map<Object, DataSource> resolvedDataSources;
比如主從庫可以這么存儲
resolvedDataSources.put("master",主庫數(shù)據(jù)源); resolvedDataSources.put("salave",從庫數(shù)據(jù)源);
AbstractRoutingDataSource 中還有抽象方法determineCurrentLookupKey,將這個方法的返回值作為 key 到上面的 resolvedDataSources 中查找對應(yīng)的數(shù)據(jù)源,作為當(dāng)前操作 db 的數(shù)據(jù)源
protected abstract Object determineCurrentLookupKey();
讀寫分離在哪控制?
讀寫分離屬于一個通用的功能,可以通過 spring 的 aop 來實現(xiàn),添加一個攔截器,攔截目標(biāo)方法的之前,在目標(biāo)方法執(zhí)行之前,獲取一下當(dāng)前需要走哪個庫,將這個標(biāo)志存儲在 ThreadLocal 中,將這個標(biāo)志作為 AbstractRoutingDataSource.determineCurrentLookupKey()方法的返回值,攔截器中在目標(biāo)方法執(zhí)行完畢之后,將這個標(biāo)志從 ThreadLocal 中清除。
3、代碼實現(xiàn)
DsType
表示數(shù)據(jù)源類型,有 2 個值,用來區(qū)分是主庫還是從庫。
package com.javacode2018.readwritesplit.base; public enum DsType { MASTER, SLAVE; }
DsTypeHolder
內(nèi)部有個 ThreadLocal,用來記錄當(dāng)前走主庫還是從庫,將這個標(biāo)志放在 dsTypeThreadLocal 中
package com.javacode2018.readwritesplit.base; public class DsTypeHolder { private static ThreadLocal<DsType> dsTypeThreadLocal = new ThreadLocal<>(); public static void master() { dsTypeThreadLocal.set(DsType.MASTER); } public static void slave() { dsTypeThreadLocal.set(DsType.SLAVE); } public static DsType getDsType() { return dsTypeThreadLocal.get(); } public static void clearDsType() { dsTypeThreadLocal.remove(); } }
IService 接口
這個接口起到標(biāo)志的作用,當(dāng)某個類需要啟用讀寫分離的時候,需要實現(xiàn)這個接口,實現(xiàn)這個接口的類都會被讀寫分離攔截器攔截。
package com.javacode2018.readwritesplit.base; //需要實現(xiàn)讀寫分離的service需要實現(xiàn)該接口 public interface IService { }
ReadWriteDataSource
讀寫分離數(shù)據(jù)源,繼承 ReadWriteDataSource,注意其內(nèi)部的 determineCurrentLookupKey 方法,從上面的 ThreadLocal 中獲取當(dāng)前需要走主庫還是從庫的標(biāo)志。
package com.javacode2018.readwritesplit.base; import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource; import org.springframework.lang.Nullable; public class ReadWriteDataSource extends AbstractRoutingDataSource { @Nullable @Override protected Object determineCurrentLookupKey() { return DsTypeHolder.getDsType(); } }
ReadWriteInterceptor
讀寫分離攔截器,需放在事務(wù)攔截器前面執(zhí)行,通過@1 代碼我們將此攔截器的順序設(shè)置為 Integer.MAX_VALUE - 2,稍后我們將事務(wù)攔截器的順序設(shè)置為 Integer.MAX_VALUE - 1,事務(wù)攔截器的執(zhí)行順序是從小到達(dá)的,所以,ReadWriteInterceptor 會在事務(wù)攔截器 org.springframework.transaction.interceptor.TransactionInterceptor 之前執(zhí)行。
由于業(yè)務(wù)方法中存在相互調(diào)用的情況,比如 service1.m1 中調(diào)用 service2.m2,而 service2.m2 中調(diào)用了 service2.m3,我們只需要在 m1 方法執(zhí)行之前,獲取具體要用哪個數(shù)據(jù)源就可以了,所以下面代碼中會在第一次進(jìn)入這個攔截器的時候,記錄一下走主庫還是從庫。
下面方法中會獲取當(dāng)前目標(biāo)方法的最后一個參數(shù),最后一個參數(shù)可以是 DsType 類型的,開發(fā)者可以通過這個參數(shù)來控制具體走主庫還是從庫。
package com.javacode2018.readwritesplit.base; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.Around; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Pointcut; import org.springframework.core.annotation.Order; import org.springframework.stereotype.Component; import java.util.Objects; @Aspect @Order(Integer.MAX_VALUE - 2) //@1 @Component public class ReadWriteInterceptor { @Pointcut("target(IService)") public void pointcut() { } //獲取當(dāng)前目標(biāo)方法的最后一個參數(shù) private Object getLastArgs(final ProceedingJoinPoint pjp) { Object[] args = pjp.getArgs(); if (Objects.nonNull(args) && args.length > 0) { return args[args.length - 1]; } else { return null; } } @Around("pointcut()") public Object around(final ProceedingJoinPoint pjp) throws Throwable { //判斷是否是第一次進(jìn)來,用于處理事務(wù)嵌套 boolean isFirst = false; try { if (DsTypeHolder.getDsType() == null) { isFirst = true; } if (isFirst) { Object lastArgs = getLastArgs(pjp); if (DsType.SLAVE.equals(lastArgs)) { DsTypeHolder.slave(); } else { DsTypeHolder.master(); } } return pjp.proceed(); } finally { //退出的時候,清理 if (isFirst) { DsTypeHolder.clearDsType(); } } } }
ReadWriteConfiguration
spring 配置類,作用
1、@3:用來將 com.javacode2018.readwritesplit.base 包中的一些類注冊到 spring 容器中,比如上面的攔截器 ReadWriteInterceptor
2、@1:開啟 spring aop 的功能
3、@2:開啟 spring 自動管理事務(wù)的功能,@EnableTransactionManagement 的 order 用來指定事務(wù)攔截器 org.springframework.transaction.interceptor.TransactionInterceptor 順序,在這里我們將 order 設(shè)置為 Integer.MAX_VALUE - 1,而上面 ReadWriteInterceptor 的 order 是 Integer.MAX_VALUE - 2,所以 ReadWriteInterceptor 會在事務(wù)攔截器之前執(zhí)行。
package com.javacode2018.readwritesplit.base; import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.EnableAspectJAutoProxy; import org.springframework.transaction.annotation.EnableTransactionManagement; @Configuration @EnableAspectJAutoProxy //@1 @EnableTransactionManagement(proxyTargetClass = true, order = Integer.MAX_VALUE - 1) //@2 @ComponentScan(basePackageClasses = IService.class) //@3 public class ReadWriteConfiguration { }
@EnableReadWrite
這個注解用倆開啟讀寫分離的功能,@1 通過@Import 將 ReadWriteConfiguration 導(dǎo)入到 spring 容器了,這樣就會自動啟用讀寫分離的功能。業(yè)務(wù)中需要使用讀寫分離,只需要在 spring 配置類中加上@EnableReadWrite 注解就可以了。
package com.javacode2018.readwritesplit.base; import org.springframework.context.annotation.Import; import java.lang.annotation.*; @Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) @Documented @Import(ReadWriteConfiguration.class) //@1 public @interface EnableReadWrite { }
4、案例
讀寫分離的關(guān)鍵代碼寫完了,下面我們來上案例驗證一下效果。
執(zhí)行 sql 腳本
下面準(zhǔn)備 2 個數(shù)據(jù)庫:javacode2018_master(主庫)、javacode2018_slave(從庫)
2 個庫中都創(chuàng)建一個 t_user 表,分別插入了一條數(shù)據(jù),稍后用這個數(shù)據(jù)來驗證走的是主庫還是從庫。
DROP DATABASE IF EXISTS javacode2018_master; CREATE DATABASE IF NOT EXISTS javacode2018_master; USE javacode2018_master; DROP TABLE IF EXISTS t_user; CREATE TABLE t_user ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(256) NOT NULL DEFAULT '' COMMENT '姓名' ); INSERT INTO t_user (name) VALUE ('master庫'); DROP DATABASE IF EXISTS javacode2018_slave; CREATE DATABASE IF NOT EXISTS javacode2018_slave; USE javacode2018_slave; DROP TABLE IF EXISTS t_user; CREATE TABLE t_user ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(256) NOT NULL DEFAULT '' COMMENT '姓名' ); INSERT INTO t_user (name) VALUE ('slave庫');
spring 配置類
@1:啟用讀寫分離
masterDs()方法:定義主庫數(shù)據(jù)源
slaveDs()方法:定義從庫數(shù)據(jù)源
dataSource():定義讀寫分離路由數(shù)據(jù)源
后面還有 2 個方法用來定義 JdbcTemplate 和事務(wù)管理器,方法中都通過@Qualifier(“dataSource”)限定了注入的 bean 名稱為 dataSource:即注入了上面 dataSource()返回的讀寫分離路由數(shù)據(jù)源。
package com.javacode2018.readwritesplit.demo1; import com.javacode2018.readwritesplit.base.DsType; import com.javacode2018.readwritesplit.base.EnableReadWrite; import com.javacode2018.readwritesplit.base.ReadWriteDataSource; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.Configuration; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.jdbc.datasource.DataSourceTransactionManager; import org.springframework.transaction.PlatformTransactionManager; import javax.sql.DataSource; import java.util.HashMap; import java.util.Map; @EnableReadWrite //@1 @Configuration @ComponentScan public class MainConfig { //主庫數(shù)據(jù)源 @Bean public DataSource masterDs() { org.apache.tomcat.jdbc.pool.DataSource dataSource = new org.apache.tomcat.jdbc.pool.DataSource(); dataSource.setDriverClassName("com.mysql.jdbc.Driver"); dataSource.setUrl("jdbc:mysql://localhost:3306/javacode2018_master?characterEncoding=UTF-8"); dataSource.setUsername("root"); dataSource.setPassword("root123"); dataSource.setInitialSize(5); return dataSource; } //從庫數(shù)據(jù)源 @Bean public DataSource slaveDs() { org.apache.tomcat.jdbc.pool.DataSource dataSource = new org.apache.tomcat.jdbc.pool.DataSource(); dataSource.setDriverClassName("com.mysql.jdbc.Driver"); dataSource.setUrl("jdbc:mysql://localhost:3306/javacode2018_slave?characterEncoding=UTF-8"); dataSource.setUsername("root"); dataSource.setPassword("root123"); dataSource.setInitialSize(5); return dataSource; } //讀寫分離路由數(shù)據(jù)源 @Bean public ReadWriteDataSource dataSource() { ReadWriteDataSource dataSource = new ReadWriteDataSource(); //設(shè)置主庫為默認(rèn)的庫,當(dāng)路由的時候沒有在datasource那個map中找到對應(yīng)的數(shù)據(jù)源的時候,會使用這個默認(rèn)的數(shù)據(jù)源 dataSource.setDefaultTargetDataSource(this.masterDs()); //設(shè)置多個目標(biāo)庫 Map<Object, Object> targetDataSources = new HashMap<>(); targetDataSources.put(DsType.MASTER, this.masterDs()); targetDataSources.put(DsType.SLAVE, this.slaveDs()); dataSource.setTargetDataSources(targetDataSources); return dataSource; } //JdbcTemplate,dataSource為上面定義的注入讀寫分離的數(shù)據(jù)源 @Bean public JdbcTemplate jdbcTemplate(@Qualifier("dataSource") DataSource dataSource) { return new JdbcTemplate(dataSource); } //定義事務(wù)管理器,dataSource為上面定義的注入讀寫分離的數(shù)據(jù)源 @Bean public PlatformTransactionManager transactionManager(@Qualifier("dataSource") DataSource dataSource) { return new DataSourceTransactionManager(dataSource); } }
UserService
這個類就相當(dāng)于我們平時寫的 service,我是為了方法,直接在里面使用了 JdbcTemplate 來操作數(shù)據(jù)庫,真實的項目操作 db 會放在 dao 里面。
getUserNameById 方法:通過 id 查詢 name。
insert 方法:插入數(shù)據(jù),這個內(nèi)部的所有操作都會走主庫,為了驗證是不是查詢也會走主庫,插入數(shù)據(jù)之后,我們會調(diào)用 this.userService.getUserNameById(id, DsType.SLAVE)方法去執(zhí)行查詢操作,第二個參數(shù)故意使用 SLAVE,如果查詢有結(jié)果,說明走的是主庫,否則走的是從庫,這里為什么需要通過 this.userService 來調(diào)用 getUserNameById?
this.userService 最終是個代理對象,通過代理對象訪問其內(nèi)部的方法,才會被讀寫分離的攔截器攔截。
package com.javacode2018.readwritesplit.demo1; import com.javacode2018.readwritesplit.base.DsType; import com.javacode2018.readwritesplit.base.IService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.stereotype.Component; import org.springframework.transaction.annotation.Propagation; import org.springframework.transaction.annotation.Transactional; import java.util.List; @Component public class UserService implements IService { @Autowired private JdbcTemplate jdbcTemplate; @Autowired private UserService userService; @Transactional(propagation = Propagation.SUPPORTS, readOnly = true) public String getUserNameById(long id, DsType dsType) { String sql = "select name from t_user where id=?"; List<String> list = this.jdbcTemplate.queryForList(sql, String.class, id); return (list != null && list.size() > 0) ? list.get(0) : null; } //這個insert方法會走主庫,內(nèi)部的所有操作都會走主庫 @Transactional public void insert(long id, String name) { System.out.println(String.format("插入數(shù)據(jù){id:%s, name:%s}", id, name)); this.jdbcTemplate.update("insert into t_user (id,name) values (?,?)", id, name); String userName = this.userService.getUserNameById(id, DsType.SLAVE); System.out.println("查詢結(jié)果:" + userName); } }
測試用例
package com.javacode2018.readwritesplit.demo1; import com.javacode2018.readwritesplit.base.DsType; import org.junit.Before; import org.junit.Test; import org.springframework.context.annotation.AnnotationConfigApplicationContext; public class Demo1Test { UserService userService; @Before public void before() { AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(); context.register(MainConfig.class); context.refresh(); this.userService = context.getBean(UserService.class); } @Test public void test1() { System.out.println(this.userService.getUserNameById(1, DsType.MASTER)); System.out.println(this.userService.getUserNameById(1, DsType.SLAVE)); } @Test public void test2() { long id = System.currentTimeMillis(); System.out.println(id); this.userService.insert(id, "張三"); } }
test1 方法執(zhí)行 2 次查詢,分別查詢主庫和從庫,輸出:
master庫
slave庫
是不是很爽,由開發(fā)者自己控制具體走主庫還是從庫。
test2 執(zhí)行結(jié)果如下,可以看出查詢到了剛剛插入的數(shù)據(jù),說明 insert 中所有操作都走的是主庫。
1604905117467
插入數(shù)據(jù){id:1604905117467, name:張三}
查詢結(jié)果:張三
到此這篇關(guān)于Spring實現(xiàn)數(shù)據(jù)庫讀寫分離詳解的文章就介紹到這了,更多相關(guān)Spring讀寫分離內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
HttpClient的RedirectStrategy重定向處理核心機(jī)制
這篇文章主要為大家介紹了HttpClient的RedirectStrategy重定向處理核心機(jī)制源碼解讀,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-10-10SpringBoot入坑筆記之spring-boot-starter-web 配置文件的使用
本篇向小伙伴介紹springboot配置文件的配置,已經(jīng)全局配置參數(shù)如何使用的。需要的朋友跟隨腳本之家小編一起學(xué)習(xí)吧2018-01-01Java synchronized底層的實現(xiàn)原理
這篇文章主要介紹了Java synchronized底層的實現(xiàn)原理,文章基于Java來介紹 synchronized 是如何運行的,內(nèi)容詳細(xì)具有一定的參考價值,需要的小伙伴可以參考一下2022-05-05mybatisplus報Invalid bound statement (not found)錯誤的解決方法
搭建項目時使用了mybatisplus,項目能夠正常啟動,但在調(diào)用mapper方法查詢數(shù)據(jù)庫時報Invalid bound statement (not found)錯誤。本文給大家分享解決方案,感興趣的朋友跟隨小編一起看看吧2020-08-08