Spring多數(shù)據(jù)源切換失敗,發(fā)現(xiàn)與事務(wù)相關(guān)問(wèn)題
背景
一個(gè)方法里,A數(shù)據(jù)源需要進(jìn)行查詢和更新操作,B數(shù)據(jù)源進(jìn)行查詢操作。
詳細(xì)業(yè)務(wù)是查詢用戶基礎(chǔ)信息數(shù)據(jù)(A數(shù)據(jù)源)的時(shí)候,需要查詢登錄行為數(shù)據(jù)(B數(shù)據(jù)源),同時(shí)對(duì)用戶信息進(jìn)行修改,所以該方法最后還需要更新用戶基礎(chǔ)信息數(shù)據(jù)(A數(shù)據(jù)源)。
原來(lái)的A實(shí)現(xiàn)思路
controller調(diào)用serviceImpl方法,在serviceImpl方法內(nèi)某一段代碼去切換B數(shù)據(jù)源查詢結(jié)果,根據(jù)查詢結(jié)果更新A數(shù)據(jù)源。
實(shí)現(xiàn)A思路的結(jié)果
表面上一切調(diào)試都顯示切換了B數(shù)據(jù)源,應(yīng)該是還沒(méi)有深入源碼去debug、
應(yīng)該某個(gè)地方拿的數(shù)據(jù)源連接還是默認(rèn)的A數(shù)據(jù)源,導(dǎo)致報(bào)錯(cuò),提示沒(méi)有找到對(duì)應(yīng)數(shù)據(jù)庫(kù)表,A數(shù)據(jù)源肯定沒(méi)有B數(shù)據(jù)源的表啊,郁悶。
后來(lái)的B實(shí)現(xiàn)思路
查詢帖子后發(fā)現(xiàn)事務(wù)的干預(yù),導(dǎo)致數(shù)據(jù)源切換失敗,決定換個(gè)思路,把切換數(shù)據(jù)源的方法放在了controller,因?yàn)槲沂莾H對(duì)一個(gè)數(shù)據(jù)源進(jìn)行更新操作,另一個(gè)數(shù)據(jù)源只作查詢操作,此時(shí)整個(gè)事務(wù)其實(shí)只在A數(shù)據(jù)源進(jìn)行,所以我就單獨(dú)把對(duì)A數(shù)據(jù)源的操作聲明為A方法,對(duì)B數(shù)據(jù)源的操作聲明為B方法,在controller先調(diào)用B方法獲取查詢結(jié)果,作為入?yún)⑷フ{(diào)用A方法,這樣就解決了數(shù)據(jù)源切換問(wèn)題也解決了A方法的事務(wù)問(wèn)題。
實(shí)現(xiàn)B思路的結(jié)果
達(dá)到了預(yù)期。
切換數(shù)據(jù)源成功,A數(shù)據(jù)源查詢、更新、事務(wù)都沒(méi)問(wèn)題,B數(shù)據(jù)源查詢沒(méi)問(wèn)題。
??注意:
此思路是把service的方法一分為二,在controller分別調(diào)用,只適用于對(duì)其中單一數(shù)據(jù)源作修改數(shù)據(jù)操作,并不適用于對(duì)多數(shù)據(jù)源同時(shí)進(jìn)行修改數(shù)據(jù)操作,因?yàn)閱螖?shù)據(jù)源進(jìn)行數(shù)據(jù)操作是普通數(shù)據(jù)源事務(wù),并不復(fù)雜,就和我們平時(shí)使用@Transactional一樣。
但是如果你對(duì)多數(shù)據(jù)源進(jìn)行修改數(shù)據(jù)操作的話!事情就變得復(fù)雜起來(lái)了,多數(shù)據(jù)源事務(wù),可讓你頭疼的了,因?yàn)榛貪L非常麻煩,類似于分布式事務(wù)了,阿里的分布式事務(wù)有SEATA支撐,這個(gè)我了解,但是以后我再講這方面的,因?yàn)檫@個(gè)單體系統(tǒng)的多數(shù)據(jù)源事務(wù)還需要深入研究一下。
原因
由于默認(rèn)使用的是主數(shù)據(jù)源master,只有在mapper接口方法上標(biāo)注從數(shù)據(jù)源slave才會(huì)切換數(shù)據(jù)源過(guò)去,但是要注意事務(wù)(因?yàn)橹翱匆粋€(gè)帖子,說(shuō)一個(gè)事物里,緩存了默認(rèn)的數(shù)據(jù)庫(kù)連接,即使代碼里切換了數(shù)據(jù)源,重新去建立連接時(shí)候發(fā)現(xiàn)有緩存一個(gè)數(shù)據(jù)庫(kù)連接耶,直接拿這個(gè),導(dǎo)致我們切換數(shù)據(jù)源失敗,因?yàn)槟玫倪€是默認(rèn)的數(shù)據(jù)庫(kù)連接。
配置文件
spring:
datasource:
type: com.alibaba.druid.pool.DruidDataSource
driverClassName: com.mysql.jdbc.Driver
master: #主庫(kù)A
type: com.alibaba.druid.pool.DruidDataSource
driverClassName: com.mysql.jdbc.Driver
url: jdbc:mysql://127.0.0.1:3306/A?useUnicode=true&characterEncoding=UTF-8&useOldAliasMetadataBehavior=true&useSSL=false&serverTimezone=GMT%2B8
username: root
password: 123456
slave: #從庫(kù)B
type: com.alibaba.druid.pool.DruidDataSource
driverClassName: com.mysql.jdbc.Driver
url: jdbc:mysql://192.168.1.12:3306/B?useUnicode=true&characterEncoding=UTF-8&useOldAliasMetadataBehavior=true&useSSL=true&serverTimezone=GMT%2B8
username: root
password: 123456配置類(蠻多的,注意,請(qǐng)復(fù)制完整)
import com.alibaba.druid.pool.DruidDataSource;
import com.alibaba.druid.spring.boot.autoconfigure.DruidDataSourceBuilder;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.jdbc.datasource.LazyConnectionDataSourceProxy;
import javax.sql.DataSource;
import java.util.HashMap;
// 主從數(shù)據(jù)源配置
@Configuration
public class DataSourceConfiguration {
DataSourceProperties masterDataSource = new DataSourceProperties();
DataSourceProperties slaveDataSource = new DataSourceProperties();
@Bean
@ConfigurationProperties(prefix = "spring.datasource.master")
public DataSource masterDataSource() {
DruidDataSource druidDataSource = masterDataSource.setDataSource(DruidDataSourceBuilder.create().build());
return druidDataSource;
}
@Bean
@ConfigurationProperties(prefix = "spring.datasource.slave")
public DataSource slaveDataSource() {
DruidDataSource druidDataSource = slaveDataSource.setDataSource(DruidDataSourceBuilder.create().build());
return druidDataSource;
}
@Bean
public DataSource routeDataSource() {
RoutingDataSource routingDataSource = new RoutingDataSource() {{
setDefaultTargetDataSource(masterDataSource());
setTargetDataSources(new HashMap<Object, Object>() {{
put(DbType.MASTER, masterDataSource());
put(DbType.SLAVE, slaveDataSource());
}});
}};
return routingDataSource;
}
@Bean
@Primary
public LazyConnectionDataSourceProxy lazyConnectionDataSourceProxy() {
return new LazyConnectionDataSourceProxy(routeDataSource());
}
}import com.alibaba.druid.pool.DruidDataSource;
import lombok.Setter;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
/**
* 數(shù)據(jù)源配置文件
*/
@Setter
@Configuration
@ConfigurationProperties(prefix = "spring.datasource.druid")
public class DataSourceProperties {
private int initialSize = 10;
private int minIdle = 10;
private int maxActive = 50;
private int maxWait;
private int timeBetweenEvictionRunsMillis = 300000;
private int minEvictableIdleTimeMillis = 60000;
private int maxEvictableIdleTimeMillis = 7200000;
private String validationQuery = "SELECT 1 FROM DUAL";
private boolean testWhileIdle = true;
private boolean testOnBorrow = true;
private boolean testOnReturn = true;
public DruidDataSource setDataSource(DruidDataSource datasource) {
/** 配置初始化大小、最小、最大 */
datasource.setInitialSize(initialSize); //優(yōu)先級(jí):application的spring.datasource.master.initialSize > application的spring.datasource.druid.initialSize > datasource.setInitialSize(20)和datasource.setInitialSize(initialSize)
datasource.setMaxActive(maxActive);
datasource.setMinIdle(minIdle);
/** 配置獲取連接等待超時(shí)的時(shí)間 */
// datasource.setMaxWait(maxWait);
/** 配置間隔多久才進(jìn)行一次檢測(cè),檢測(cè)需要關(guān)閉的空閑連接,單位是毫秒 */
datasource.setTimeBetweenEvictionRunsMillis(timeBetweenEvictionRunsMillis);
/** 配置一個(gè)連接在池中最小、最大生存的時(shí)間,單位是毫秒 */
datasource.setMinEvictableIdleTimeMillis(minEvictableIdleTimeMillis);
datasource.setMaxEvictableIdleTimeMillis(maxEvictableIdleTimeMillis);
/**
* 用來(lái)檢測(cè)連接是否有效的sql,要求是一個(gè)查詢語(yǔ)句,常用select 'x'。如果validationQuery為null,testOnBorrow、testOnReturn、testWhileIdle都不會(huì)起作用。
*/
datasource.setValidationQuery(validationQuery);
/** 建議配置為true,不影響性能,并且保證安全性。申請(qǐng)連接的時(shí)候檢測(cè),如果空閑時(shí)間大于timeBetweenEvictionRunsMillis,執(zhí)行validationQuery檢測(cè)連接是否有效。 */
datasource.setTestWhileIdle(testWhileIdle);
/** 申請(qǐng)連接時(shí)執(zhí)行validationQuery檢測(cè)連接是否有效,做了這個(gè)配置會(huì)降低性能。 */
datasource.setTestOnBorrow(testOnBorrow);
/** 歸還連接時(shí)執(zhí)行validationQuery檢測(cè)連接是否有效,做了這個(gè)配置會(huì)降低性能。 */
datasource.setTestOnReturn(testOnReturn);
return datasource;
}
}import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;
// aop環(huán)繞增強(qiáng)切換數(shù)據(jù)源
@Slf4j
@Aspect
@Component
public class DBConnectionAOP {
@Around("@annotation(connectToDB)")
public Object proceed(ProceedingJoinPoint pjp, ConnectToDB connectToDB) throws Throwable {
try {
if (connectToDB.value().equals("MASTER")) {
log.info("Master DB 配置");
DBContextHolder.setDbType(DbType.MASTER);
} else if (connectToDB.value().equals("SLAVE")) {
log.info("Slave DB 配置");
DBContextHolder.setDbType(DbType.SLAVE);
} else {
log.info("默認(rèn) DB 配置");
}
Object result = pjp.proceed();
DBContextHolder.clearDbType();
return result;
} finally {
DBContextHolder.clearDbType();
}
}
}public class DBContextHolder {
private static final ThreadLocal<DbType> contextHolder = new ThreadLocal<DbType>();
public static void setDbType(DbType dbType) {
if (dbType == null) {
throw new NullPointerException();
}
contextHolder.set(dbType);
}
public static DbType getDbType() {
return (DbType) contextHolder.get();
}
public static void clearDbType() {
contextHolder.remove();
}
}import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
// 動(dòng)態(tài)數(shù)據(jù)源路由配置
public class RoutingDataSource extends AbstractRoutingDataSource {
// 決定使用哪個(gè)數(shù)據(jù)源
@Override
protected Object determineCurrentLookupKey() {
return DBContextHolder.getDbType();
}
}// 數(shù)據(jù)源枚舉
public enum DbType {
MASTER,
SLAVE
}import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
// 數(shù)據(jù)源選擇注解
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface ConnectToDB {
// 默認(rèn)主數(shù)據(jù)庫(kù)
String value() default "primary";
}異步線程池配置
如果有異步需求的話,可以借鑒,所以也放上來(lái)吧。
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import java.util.concurrent.Executor;
import java.util.concurrent.ThreadPoolExecutor;
/**
* 異步線程池的配置類
*/
@Configuration
@EnableAsync
public class ExecutorConfig {
private static final Logger logger = LoggerFactory.getLogger(ExecutorConfig.class);
@Bean
public Executor asyncServiceExecutor() {
logger.info("注冊(cè)asyncServiceExecutor");
// ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
ThreadPoolTaskExecutor executor = new VisibleThreadPoolTaskExecutor();
//配置核心線程數(shù)
executor.setCorePoolSize(5);
//配置最大線程數(shù)
executor.setMaxPoolSize(50);
//配置隊(duì)列大小
// Set the capacity for the ThreadPoolExecutor's BlockingQueue. Default is Integer.MAX_VALUE.
// Any positive value will lead to a LinkedBlockingQueue instance; any other value will lead to a SynchronousQueue instance.
executor.setQueueCapacity(100);
// 設(shè)置允許的空閑時(shí)間(秒)
executor.setKeepAliveSeconds(60);
//配置線程池中的線程的名稱前綴
executor.setThreadNamePrefix("async-service-");
// rejection-policy:當(dāng)pool已經(jīng)達(dá)到max size的時(shí)候,如何處理新任務(wù);CALLER_RUNS:不在新線程中執(zhí)行任務(wù),而是有調(diào)用者所在的線程來(lái)執(zhí)行
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.DiscardOldestPolicy());
//執(zhí)行初始化
executor.initialize();
return executor;
}
}import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import org.springframework.util.concurrent.ListenableFuture;
import java.util.concurrent.Callable;
import java.util.concurrent.Future;
import java.util.concurrent.ThreadPoolExecutor;
/**
* ThreadPoolTaskExecutor的子類,在父類的基礎(chǔ)上加入了日志信息,查看線程池的信息
*/
public class VisibleThreadPoolTaskExecutor extends ThreadPoolTaskExecutor {
private static final Logger logger = LoggerFactory.getLogger(VisibleThreadPoolTaskExecutor.class);
@Override
public void execute(Runnable task) {
showThreadPoolInfo("1. do execute");
super.execute(task);
}
@Override
public void execute(Runnable task, long startTimeout) {
showThreadPoolInfo("2. do execute");
super.execute(task, startTimeout);
}
@Override
public Future<?> submit(Runnable task) {
showThreadPoolInfo("1. do submit");
return super.submit(task);
}
@Override
public <T> Future<T> submit(Callable<T> task) {
showThreadPoolInfo("2. do submit");
return super.submit(task);
}
@Override
public ListenableFuture<?> submitListenable(Runnable task) {
showThreadPoolInfo("1. do submitListenable");
return super.submitListenable(task);
}
@Override
public <T> ListenableFuture<T> submitListenable(Callable<T> task) {
showThreadPoolInfo("2. do submitListenable");
return super.submitListenable(task);
}
private void showThreadPoolInfo(String prefix) {
ThreadPoolExecutor executor = getThreadPoolExecutor();
if (null == executor) {
return;
}
logger.info("MaximumPoolSize:" + executor.getMaximumPoolSize() +
",CorePoolSize:" + executor.getCorePoolSize() +
",ThreadNamePrefix:" + this.getThreadNamePrefix() +
",prefix:" + prefix +
",TaskCount:" + executor.getTaskCount() +
",CompletedTaskCount:" + executor.getCompletedTaskCount() +
",ActiveCount:" + executor.getActiveCount() +
",PoolSize:" + executor.getPoolSize() +
",QueueSize:" + executor.getQueue().size()
);
}
}A思路業(yè)務(wù)代碼(反面教材,請(qǐng)勿模仿)
controller
@PostMapping("queryUserData.do")
@VerifyLogin
public RetInfo<Object> queryUserData(String temp, String channelId) throws JsonProcessingException {
String mobile = UserUtil.getMobile();
User user = userService.queryUserData(mobile, temp, channelId);
return new RetInfo<>(RetEnum.SUCCESS.getCode(), RetEnum.SUCCESS.getMsg(), user);
}@Slf4j
@Service
@Transactional(rollbackFor = Exception.class)
public class UserServiceImpl implements UserService {
@Autowired
private UserLogService userLogService;
@Override
public User queryUserData(String mobile, String temp, String channelId) throws JsonProcessingException {
// 切換slave從數(shù)據(jù)源查詢
Date loginTime = userLogService.validateLoginByToday(mobile); // 查詢當(dāng)天登錄時(shí)間
Date date = new Date();
Integer userStatus = 0;
// 以下所有操作都切換回master主數(shù)據(jù)源查詢和更新
User user = userMapper.selectUserByMobile(mobile);
// 業(yè)務(wù)代碼.....
userMapper.insert(user);
}
}B思路業(yè)務(wù)代碼(正確!有相同業(yè)務(wù)場(chǎng)景的可以借鑒)
controller
@PostMapping("queryUserData.do")
public RetInfo<Object> queryUserData(String temp, String channelId) throws JsonProcessingException {
long start = System.currentTimeMillis();
String mobile = UserUtil.getMobile();
HashMap<String, Object> map = new HashMap<>();
// 切換slave從數(shù)據(jù)源查詢
Date loginTime = userLogService.validateLoginByToday(mobile); // 查詢用戶當(dāng)天登錄時(shí)間
// 切換回master主數(shù)據(jù)源查詢和更新
User user = userService.queryUserData(mobile, loginTime, temp, channelId);
return new RetInfo<>(RetEnum.SUCCESS.getCode(), RetEnum.SUCCESS.getMsg(), user);
}service 只是貼出了從數(shù)據(jù)源查詢代碼,主數(shù)據(jù)源的代碼就和平常寫(xiě)的一樣就行了。
@Service
public class UserLogServiceImpl implements UserLogService {
private UserBehaviorService userBehaviorService;
public UserLogServiceImpl(UserBehaviorService userBehaviorService) {
this.userBehaviorService = userBehaviorService;
}
@Override
public Date validateLoginByToday(String mobile) {
return userBehaviorService.validateLoginByToday(mobile);
}
}@Slf4j
@Service
public class UserBehaviorServiceImpl implements UserBehaviorService {
private UserBehaviorMapper userBehaviorMapper;
public UserBehaviorServiceImpl(UserBehaviorMapper userBehaviorMapper) {
this.userBehaviorMapper = userBehaviorMapper;
}
@Transactional
@ConnectToDB(value = "SLAVE") // 這就是切換數(shù)據(jù)源最重要注解
@Override
public Date validateLoginByToday(String mobile) {
return userBehaviorMapper.validateLoginByToday(mobile, DateUtil.dateToString(new Date(), "yyyyMMdd"));
}
}mapper(和平常一樣,沒(méi)有什么特別的)
@Mapper
public interface UserBehaviorMapper {
/**
* 查詢用戶當(dāng)天是否活躍
* @param mobile
* @param dateStr
* @return Integer
*/
Integer validateActive(@Param("mobile") String mobile, @Param("dateStr") String dateStr);
}總結(jié)
以上為個(gè)人經(jīng)驗(yàn),希望能給大家一個(gè)參考,也希望大家多多支持腳本之家。
相關(guān)文章
java volatile關(guān)鍵字的含義詳細(xì)介紹
這篇文章主要介紹了java volatile關(guān)鍵字的含義詳解的相關(guān)資料,需要的朋友可以參考下2016-12-12
SpringBoot+Mybatis-plus+shardingsphere實(shí)現(xiàn)分庫(kù)分表的方案
實(shí)現(xiàn)億級(jí)數(shù)據(jù)量分庫(kù)分表的項(xiàng)目是一個(gè)挑戰(zhàn)性很高的任務(wù),下面是一個(gè)基于Spring Boot的簡(jiǎn)單實(shí)現(xiàn)方案,感興趣的朋友一起看看吧2024-03-03
java調(diào)用微信現(xiàn)金紅包接口的心得與體會(huì)總結(jié)
這篇文章主要介紹了java調(diào)用微信現(xiàn)金紅包接口的心得與體會(huì)總結(jié),有需要的朋友可以了解一下。2016-11-11
詳解mybatis插入數(shù)據(jù)后返回自增主鍵ID的問(wèn)題
這篇文章主要介紹了mybatis插入數(shù)據(jù)后返回自增主鍵ID詳解,本文通過(guò)場(chǎng)景分析示例代碼相結(jié)合給大家介紹的非常詳細(xì),需要的朋友可以參考下2021-07-07
Linux環(huán)境下的Java(JDBC)連接openGauss數(shù)據(jù)庫(kù)實(shí)踐記錄
這篇文章主要介紹了Linux環(huán)境下的Java(JDBC)連接openGauss數(shù)據(jù)庫(kù)實(shí)踐記錄,需要的朋友可以參考下2022-11-11
Java使用Apache Batik實(shí)現(xiàn)將SVG轉(zhuǎn)換為PNG
在項(xiàng)目中經(jīng)常需要將 SVG 圖標(biāo)或圖形轉(zhuǎn)換為 PNG 格式,Apache Batik 是一個(gè)強(qiáng)大的 SVG 處理庫(kù),可以輕松實(shí)現(xiàn) SVG → PNG 的轉(zhuǎn)換,下面我們就來(lái)看看具體實(shí)現(xiàn)方法吧2026-01-01
Spring Boot2與Spring Boot3的區(qū)別小結(jié)
SpringBoot2和SpringBoot3之間有一些重要的區(qū)別,本文就來(lái)探討SpringBoot2和SpringBoot3之間的區(qū)別,具有一定的參考價(jià)值,感興趣的可以了解一下2023-10-10

