Spring實(shí)現(xiàn)數(shù)據(jù)庫(kù)讀寫(xiě)分離詳解
1、背景
大多數(shù)系統(tǒng)都是讀多寫(xiě)少,為了降低數(shù)據(jù)庫(kù)的壓力,可以對(duì)主庫(kù)創(chuàng)建多個(gè)從庫(kù),從庫(kù)自動(dòng)從主庫(kù)同步數(shù)據(jù),程序中將寫(xiě)的操作發(fā)送到主庫(kù),將讀的操作發(fā)送到從庫(kù)去執(zhí)行。
今天的主要目標(biāo):通過(guò) spring 實(shí)現(xiàn)讀寫(xiě)分離。
讀寫(xiě)分離需實(shí)現(xiàn)下面 2 個(gè)功能:
1、讀的方法,由調(diào)用者來(lái)控制具體是讀從庫(kù)還是主庫(kù)
2、有事務(wù)的方法,內(nèi)部的所有讀寫(xiě)操作都走主庫(kù)
2、思考 3 個(gè)問(wèn)題
讀的方法,由調(diào)用者來(lái)控制具體是讀從庫(kù)還是主庫(kù),如何實(shí)現(xiàn)?
可以給所有讀的方法添加一個(gè)參數(shù),來(lái)控制讀從庫(kù)還是主庫(kù)。
數(shù)據(jù)源如何路由?
spring-jdbc 包中提供了一個(gè)抽象類(lèi):AbstractRoutingDataSource,實(shí)現(xiàn)了 javax.sql.DataSource 接口,我們用這個(gè)類(lèi)來(lái)作為數(shù)據(jù)源類(lèi),重點(diǎn)是這個(gè)類(lèi)可以用來(lái)做數(shù)據(jù)源的路由,可以在其內(nèi)部配置多個(gè)真實(shí)的數(shù)據(jù)源,最終用哪個(gè)數(shù)據(jù)源,由開(kāi)發(fā)者來(lái)決定。
AbstractRoutingDataSource 中有個(gè) map,用來(lái)存儲(chǔ)多個(gè)目標(biāo)數(shù)據(jù)源
private Map<Object, DataSource> resolvedDataSources;
比如主從庫(kù)可以這么存儲(chǔ)
resolvedDataSources.put("master",主庫(kù)數(shù)據(jù)源);
resolvedDataSources.put("salave",從庫(kù)數(shù)據(jù)源);AbstractRoutingDataSource 中還有抽象方法determineCurrentLookupKey,將這個(gè)方法的返回值作為 key 到上面的 resolvedDataSources 中查找對(duì)應(yīng)的數(shù)據(jù)源,作為當(dāng)前操作 db 的數(shù)據(jù)源
protected abstract Object determineCurrentLookupKey();
讀寫(xiě)分離在哪控制?
讀寫(xiě)分離屬于一個(gè)通用的功能,可以通過(guò) spring 的 aop 來(lái)實(shí)現(xiàn),添加一個(gè)攔截器,攔截目標(biāo)方法的之前,在目標(biāo)方法執(zhí)行之前,獲取一下當(dāng)前需要走哪個(gè)庫(kù),將這個(gè)標(biāo)志存儲(chǔ)在 ThreadLocal 中,將這個(gè)標(biāo)志作為 AbstractRoutingDataSource.determineCurrentLookupKey()方法的返回值,攔截器中在目標(biāo)方法執(zhí)行完畢之后,將這個(gè)標(biāo)志從 ThreadLocal 中清除。
3、代碼實(shí)現(xiàn)
DsType
表示數(shù)據(jù)源類(lèi)型,有 2 個(gè)值,用來(lái)區(qū)分是主庫(kù)還是從庫(kù)。
package com.javacode2018.readwritesplit.base;
public enum DsType {
MASTER, SLAVE;
}DsTypeHolder
內(nèi)部有個(gè) ThreadLocal,用來(lái)記錄當(dāng)前走主庫(kù)還是從庫(kù),將這個(gè)標(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 接口
這個(gè)接口起到標(biāo)志的作用,當(dāng)某個(gè)類(lèi)需要啟用讀寫(xiě)分離的時(shí)候,需要實(shí)現(xiàn)這個(gè)接口,實(shí)現(xiàn)這個(gè)接口的類(lèi)都會(huì)被讀寫(xiě)分離攔截器攔截。
package com.javacode2018.readwritesplit.base;
//需要實(shí)現(xiàn)讀寫(xiě)分離的service需要實(shí)現(xiàn)該接口
public interface IService {
}ReadWriteDataSource
讀寫(xiě)分離數(shù)據(jù)源,繼承 ReadWriteDataSource,注意其內(nèi)部的 determineCurrentLookupKey 方法,從上面的 ThreadLocal 中獲取當(dāng)前需要走主庫(kù)還是從庫(kù)的標(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
讀寫(xiě)分離攔截器,需放在事務(wù)攔截器前面執(zhí)行,通過(guò)@1 代碼我們將此攔截器的順序設(shè)置為 Integer.MAX_VALUE - 2,稍后我們將事務(wù)攔截器的順序設(shè)置為 Integer.MAX_VALUE - 1,事務(wù)攔截器的執(zhí)行順序是從小到達(dá)的,所以,ReadWriteInterceptor 會(huì)在事務(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í)行之前,獲取具體要用哪個(gè)數(shù)據(jù)源就可以了,所以下面代碼中會(huì)在第一次進(jìn)入這個(gè)攔截器的時(shí)候,記錄一下走主庫(kù)還是從庫(kù)。
下面方法中會(huì)獲取當(dāng)前目標(biāo)方法的最后一個(gè)參數(shù),最后一個(gè)參數(shù)可以是 DsType 類(lèi)型的,開(kāi)發(fā)者可以通過(guò)這個(gè)參數(shù)來(lái)控制具體走主庫(kù)還是從庫(kù)。
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)方法的最后一個(gè)參數(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)來(lái),用于處理事務(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 {
//退出的時(shí)候,清理
if (isFirst) {
DsTypeHolder.clearDsType();
}
}
}
}ReadWriteConfiguration
spring 配置類(lèi),作用
1、@3:用來(lái)將 com.javacode2018.readwritesplit.base 包中的一些類(lèi)注冊(cè)到 spring 容器中,比如上面的攔截器 ReadWriteInterceptor
2、@1:開(kāi)啟 spring aop 的功能
3、@2:開(kāi)啟 spring 自動(dòng)管理事務(wù)的功能,@EnableTransactionManagement 的 order 用來(lái)指定事務(wù)攔截器 org.springframework.transaction.interceptor.TransactionInterceptor 順序,在這里我們將 order 設(shè)置為 Integer.MAX_VALUE - 1,而上面 ReadWriteInterceptor 的 order 是 Integer.MAX_VALUE - 2,所以 ReadWriteInterceptor 會(huì)在事務(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
這個(gè)注解用倆開(kāi)啟讀寫(xiě)分離的功能,@1 通過(guò)@Import 將 ReadWriteConfiguration 導(dǎo)入到 spring 容器了,這樣就會(huì)自動(dòng)啟用讀寫(xiě)分離的功能。業(yè)務(wù)中需要使用讀寫(xiě)分離,只需要在 spring 配置類(lèi)中加上@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、案例
讀寫(xiě)分離的關(guān)鍵代碼寫(xiě)完了,下面我們來(lái)上案例驗(yàn)證一下效果。
執(zhí)行 sql 腳本
下面準(zhǔn)備 2 個(gè)數(shù)據(jù)庫(kù):javacode2018_master(主庫(kù))、javacode2018_slave(從庫(kù))
2 個(gè)庫(kù)中都創(chuàng)建一個(gè) t_user 表,分別插入了一條數(shù)據(jù),稍后用這個(gè)數(shù)據(jù)來(lái)驗(yàn)證走的是主庫(kù)還是從庫(kù)。
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庫(kù)');
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庫(kù)');spring 配置類(lèi)
@1:?jiǎn)⒂米x寫(xiě)分離
masterDs()方法:定義主庫(kù)數(shù)據(jù)源
slaveDs()方法:定義從庫(kù)數(shù)據(jù)源
dataSource():定義讀寫(xiě)分離路由數(shù)據(jù)源
后面還有 2 個(gè)方法用來(lái)定義 JdbcTemplate 和事務(wù)管理器,方法中都通過(guò)@Qualifier(“dataSource”)限定了注入的 bean 名稱為 dataSource:即注入了上面 dataSource()返回的讀寫(xiě)分離路由數(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 {
//主庫(kù)數(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;
}
//從庫(kù)數(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;
}
//讀寫(xiě)分離路由數(shù)據(jù)源
@Bean
public ReadWriteDataSource dataSource() {
ReadWriteDataSource dataSource = new ReadWriteDataSource();
//設(shè)置主庫(kù)為默認(rèn)的庫(kù),當(dāng)路由的時(shí)候沒(méi)有在datasource那個(gè)map中找到對(duì)應(yīng)的數(shù)據(jù)源的時(shí)候,會(huì)使用這個(gè)默認(rèn)的數(shù)據(jù)源
dataSource.setDefaultTargetDataSource(this.masterDs());
//設(shè)置多個(gè)目標(biāo)庫(kù)
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為上面定義的注入讀寫(xiě)分離的數(shù)據(jù)源
@Bean
public JdbcTemplate jdbcTemplate(@Qualifier("dataSource") DataSource dataSource) {
return new JdbcTemplate(dataSource);
}
//定義事務(wù)管理器,dataSource為上面定義的注入讀寫(xiě)分離的數(shù)據(jù)源
@Bean
public PlatformTransactionManager transactionManager(@Qualifier("dataSource") DataSource dataSource) {
return new DataSourceTransactionManager(dataSource);
}
}
UserService
這個(gè)類(lèi)就相當(dāng)于我們平時(shí)寫(xiě)的 service,我是為了方法,直接在里面使用了 JdbcTemplate 來(lái)操作數(shù)據(jù)庫(kù),真實(shí)的項(xiàng)目操作 db 會(huì)放在 dao 里面。
getUserNameById 方法:通過(guò) id 查詢 name。
insert 方法:插入數(shù)據(jù),這個(gè)內(nèi)部的所有操作都會(huì)走主庫(kù),為了驗(yàn)證是不是查詢也會(huì)走主庫(kù),插入數(shù)據(jù)之后,我們會(huì)調(diào)用 this.userService.getUserNameById(id, DsType.SLAVE)方法去執(zhí)行查詢操作,第二個(gè)參數(shù)故意使用 SLAVE,如果查詢有結(jié)果,說(shuō)明走的是主庫(kù),否則走的是從庫(kù),這里為什么需要通過(guò) this.userService 來(lái)調(diào)用 getUserNameById?
this.userService 最終是個(gè)代理對(duì)象,通過(guò)代理對(duì)象訪問(wèn)其內(nèi)部的方法,才會(huì)被讀寫(xiě)分離的攔截器攔截。
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;
}
//這個(gè)insert方法會(huì)走主庫(kù),內(nèi)部的所有操作都會(huì)走主庫(kù)
@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);
}
}測(cè)試用例
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 次查詢,分別查詢主庫(kù)和從庫(kù),輸出:
master庫(kù)
slave庫(kù)
是不是很爽,由開(kāi)發(fā)者自己控制具體走主庫(kù)還是從庫(kù)。
test2 執(zhí)行結(jié)果如下,可以看出查詢到了剛剛插入的數(shù)據(jù),說(shuō)明 insert 中所有操作都走的是主庫(kù)。
1604905117467
插入數(shù)據(jù){id:1604905117467, name:張三}
查詢結(jié)果:張三
到此這篇關(guān)于Spring實(shí)現(xiàn)數(shù)據(jù)庫(kù)讀寫(xiě)分離詳解的文章就介紹到這了,更多相關(guān)Spring讀寫(xiě)分離內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
- SpringBoot+MyBatis-Plus實(shí)現(xiàn)數(shù)據(jù)庫(kù)讀寫(xiě)分離的代碼示例
- 在SpringBoot項(xiàng)目中實(shí)現(xiàn)讀寫(xiě)分離的流程步驟
- SpringBoot配置主從數(shù)據(jù)庫(kù)實(shí)現(xiàn)讀寫(xiě)分離
- Java基于SpringBoot和tk.mybatis實(shí)現(xiàn)事務(wù)讀寫(xiě)分離代碼實(shí)例
- SpringBoot+MySQL實(shí)現(xiàn)讀寫(xiě)分離的多種具體方案
- SpringBoot詳解MySQL如何實(shí)現(xiàn)讀寫(xiě)分離
相關(guān)文章
HttpClient的RedirectStrategy重定向處理核心機(jī)制
這篇文章主要為大家介紹了HttpClient的RedirectStrategy重定向處理核心機(jī)制源碼解讀,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-10-10
SpringBoot入坑筆記之spring-boot-starter-web 配置文件的使用
本篇向小伙伴介紹springboot配置文件的配置,已經(jīng)全局配置參數(shù)如何使用的。需要的朋友跟隨腳本之家小編一起學(xué)習(xí)吧2018-01-01
Java synchronized底層的實(shí)現(xiàn)原理
這篇文章主要介紹了Java synchronized底層的實(shí)現(xiàn)原理,文章基于Java來(lái)介紹 synchronized 是如何運(yùn)行的,內(nèi)容詳細(xì)具有一定的參考價(jià)值,需要的小伙伴可以參考一下2022-05-05
mybatisplus報(bào)Invalid bound statement (not found)錯(cuò)誤的解決方法
搭建項(xiàng)目時(shí)使用了mybatisplus,項(xiàng)目能夠正常啟動(dòng),但在調(diào)用mapper方法查詢數(shù)據(jù)庫(kù)時(shí)報(bào)Invalid bound statement (not found)錯(cuò)誤。本文給大家分享解決方案,感興趣的朋友跟隨小編一起看看吧2020-08-08

