Spring Boot 動態(tài)數(shù)據(jù)源在事務(wù)中切庫失效問題排查
最近在項目里遇到一個比較典型的多數(shù)據(jù)源問題:代碼里明明給 Mapper 標了 @DS("db2"),但是運行時 SQL 還是打到了主庫 db1,最終報錯:
Table 'vnfinance.invest_target_info' doesn't exist
或者某些配置查詢返回 null,導(dǎo)致后續(xù)空指針:
java.lang.NullPointerException at InvestAssetServiceImpl.syncInvestAsset(...)
這個問題本質(zhì)不是表不存在,也不是數(shù)據(jù)丟了,而是事務(wù)中動態(tài)數(shù)據(jù)源切換失效。
背景
項目使用了動態(tài)數(shù)據(jù)源:
spring:
datasource:
dynamic:
primary: db1
datasource:
db1:
url: jdbc:mysql://.../vnfinance
db2:
url: jdbc:mysql://.../vncashloan其中:
db1:投資業(yè)務(wù)庫,比如invest_assetdb2:借款業(yè)務(wù)庫,比如invest_target_info、sys_config
Mapper 上也加了數(shù)據(jù)源注解:
@DS("db2")
public interface LoanerMapper {
InvestAssetPOJO getInvest(@Param("investId") Long investId);
}
@DS("db2")
public interface SysConfigMapper extends BaseMapper<SysConfig> {
}
看起來應(yīng)該沒問題,但實際在某些調(diào)用鏈里還是查到了 db1。
問題代碼
原來的邏輯大概是這樣:
@Override
@Transactional(rollbackFor = Exception.class)
public InvestAsset syncInvestAsset(Long investId) {
InvestAsset investAsset = baseMapper.getByInvestId(investId);
if (investAsset == null || investAsset.getInvestId() == null) {
InvestAssetPOJO invest = loanerMapper.getInvest(investId);
SysConfig config = sysConfigService.getByConfigKey(INVEST_ASSET_TITLE_PREFIX);
String titlePrefix = config.getConfigValue("??u t? linh ho?t: K? th? ");
// ...
}
return investAsset;
}
這段代碼的問題在于:
baseMapper.getByInvestId(investId)
是查主庫 db1 的。
而整個方法又加了:
@Transactional(rollbackFor = Exception.class)
所以方法一開始進入事務(wù)后,第一次數(shù)據(jù)庫訪問就拿到了 db1 的連接,并綁定到當(dāng)前線程。
后面再執(zhí)行:
loanerMapper.getInvest(investId) sysConfigService.getByConfigKey(...)
雖然 Mapper 上標了 @DS("db2"),但是當(dāng)前線程已經(jīng)在同一個事務(wù)里綁定了 db1 連接,動態(tài)數(shù)據(jù)源就切不過去了。
于是本來應(yīng)該查:
vncashloan.invest_target_info
結(jié)果查成了:
vnfinance.invest_target_info
最終報錯:
Table 'vnfinance.invest_target_info' doesn't exist
為什么之前沒問題
這個問題很容易讓人疑惑:之前數(shù)據(jù)一直正常,為什么現(xiàn)在突然出問題?
原因通常不是“之前真的沒問題”,而是之前沒有觸發(fā)這個隱患。
常見情況有幾種。
第一,之前查詢?nèi)肟跊]有事務(wù)。
比如列表頁直接調(diào)用:
loanerMapper.queryAllInvestAsset(...)
外面沒有先開啟 db1 事務(wù),@DS("db2") 可以正常生效。
第二,資產(chǎn)包已經(jīng)同步過。
如果 invest_asset 里已經(jīng)有數(shù)據(jù):
InvestAsset investAsset = baseMapper.getByInvestId(investId);
后面就不會進入同步分支,也不會查 db2 的 invest_target_info 和 sys_config。
第三,舊代碼吞掉了異常。
有些 MQ 消費代碼原來是這樣:
try {
// 業(yè)務(wù)處理
} catch (Exception e) {
log.error("change invest asset status fail:", e);
}
異常只打印,不繼續(xù)拋出。RocketMQ 可能認為消費成功,問題不會持續(xù)重試暴露出來。
后來改成異常繼續(xù)拋出后,MQ 會不斷重試,問題就變明顯了。
核心原因
一句話總結(jié):
動態(tài)數(shù)據(jù)源切換依賴當(dāng)前線程上下文,但 Spring 事務(wù)一旦開啟并拿到連接,連接會綁定到當(dāng)前線程。后續(xù)同一個事務(wù)里再切換數(shù)據(jù)源,可能不會生效。
尤其是這種代碼非常危險:
@Transactional
public void business() {
db1Mapper.selectById(id); // 先綁定 db1
db2Mapper.selectById(id); // 期望切 db2,但可能仍然走 db1
}
Mapper 上的 @DS("db2") 并不能保證在已經(jīng)存在事務(wù)連接的情況下強制切庫。
解決辦法
解決思路是:跨庫查詢不要直接混在同一個事務(wù)里,要把 db2 查詢拆到獨立 Service,并開啟新的只讀事務(wù)。
1. 新增 db2 查詢 Service
@Service
@DS("db2")
public class LoanerDb2Service {
private final LoanerMapper loanerMapper;
public LoanerDb2Service(LoanerMapper loanerMapper) {
this.loanerMapper = loanerMapper;
}
/**
* 從借款庫查詢資產(chǎn)包基礎(chǔ)信息。
* 外層可能已有 db1 事務(wù),這里單獨開啟 db2 只讀事務(wù),避免查到主庫。
*/
@Transactional(readOnly = true, propagation = Propagation.REQUIRES_NEW)
public InvestAssetPOJO getInvest(Long investId) {
return loanerMapper.getInvest(investId);
}
/**
* 從借款庫查詢資產(chǎn)包關(guān)聯(lián)的借款人信息。
*/
@Transactional(readOnly = true, propagation = Propagation.REQUIRES_NEW)
public List<InvestLoanerPOJO> findByInvestIdFromLoan(Long investId) {
return loanerMapper.findByInvestIdFromLoan(investId);
}
}
重點是這兩個注解:
@DS("db2")
@Transactional(readOnly = true, propagation = Propagation.REQUIRES_NEW)
REQUIRES_NEW 會掛起外層事務(wù),重新開啟一個新的事務(wù),從而重新獲取 db2 連接。
2. SysConfigService 也要處理
配置表如果也在 db2,同樣不能只依賴 Mapper 上的 @DS("db2")。
@Service
@DS("db2")
public class SysConfigServiceImpl extends ServiceImpl<SysConfigMapper, SysConfig>
implements SysConfigService {
private final SysConfigMapper sysConfigMapper;
public SysConfigServiceImpl(SysConfigMapper sysConfigMapper) {
this.sysConfigMapper = sysConfigMapper;
}
@Override
@Transactional(readOnly = true, propagation = Propagation.REQUIRES_NEW)
public SysConfig getByConfigKey(String configKey) {
LambdaQueryWrapper<SysConfig> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(SysConfig::getConfigKey, configKey);
return sysConfigMapper.selectOne(wrapper);
}
}
3. 業(yè)務(wù) Service 不再直接調(diào) db2 Mapper
原來:
InvestAssetPOJO invest = loanerMapper.getInvest(investId);
改成:
InvestAssetPOJO invest = loanerDb2Service.getInvest(investId);
原來:
List<InvestLoanerPOJO> loaners = loanerMapper.findByInvestIdFromLoan(investId);
改成:
List<InvestLoanerPOJO> loaners = loanerDb2Service.findByInvestIdFromLoan(investId);
這樣即使當(dāng)前方法在 db1 事務(wù)中,db2 查詢也會進入獨立事務(wù),不會被外層連接污染。
順手補充:配置讀取要兜底
這次還暴露了另一個問題:配置缺失時直接空指針。
原代碼:
SysConfig config = sysConfigService.getByConfigKey(INVEST_ASSET_TITLE_PREFIX);
String value = config.getConfigValue("??u t? linh ho?t: K? th? ");
如果 config 為 null,直接 NPE。
建議封裝:
private String getConfigValue(String configKey, String defaultValue) {
SysConfig config = sysConfigService.getByConfigKey(configKey);
return config == null ? defaultValue : config.getConfigValue(defaultValue);
}
使用:
String titlePrefix = getConfigValue(
INVEST_ASSET_TITLE_PREFIX,
"??u t? linh ho?t: K? th? "
);
這樣即使配置缺失,也不會把 MQ 消費卡死。
最佳實踐總結(jié)
多數(shù)據(jù)源項目里,建議遵守幾個原則:
- 不要在一個事務(wù)里混查多個庫。
@DS盡量加在 Service 方法上,而不是只加 Mapper。- 跨庫查詢拆成獨立 Service。
- 外層已有事務(wù)時,跨庫查詢使用
Propagation.REQUIRES_NEW。 - 只讀查詢加
readOnly = true。 - MQ 消費不要吞異常,否則業(yè)務(wù)失敗但消息被確認,會留下臟數(shù)據(jù)。
- 配置類數(shù)據(jù)讀取要有默認值兜底,避免配置缺失導(dǎo)致核心流程失敗。
結(jié)論
這個問題看起來像是“表不存在”或者“配置為空”,但真正原因是:
外層 db1 事務(wù)已經(jīng)綁定連接,導(dǎo)致后續(xù) db2 Mapper 的動態(tài)數(shù)據(jù)源切換沒有生效。
解決方式不是簡單給 Mapper 加 @DS("db2"),而是把 db2 查詢下沉到獨立 Service,并使用:
@DS("db2")
@Transactional(readOnly = true, propagation = Propagation.REQUIRES_NEW)
這樣才能保證在復(fù)雜事務(wù)鏈路、MQ 消費、定時任務(wù)中,數(shù)據(jù)源切換穩(wěn)定可靠。
到此這篇關(guān)于Spring Boot 動態(tài)數(shù)據(jù)源在事務(wù)中切庫失效問題排查的文章就介紹到這了,更多相關(guān)Spring Boot 動態(tài)數(shù)據(jù)源切庫失效內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
- Spring Boot 動態(tài)數(shù)據(jù)源示例(多數(shù)據(jù)源自動切換)
- Spring Boot + Mybatis多數(shù)據(jù)源和動態(tài)數(shù)據(jù)源配置方法
- 詳解Spring Boot + Mybatis 實現(xiàn)動態(tài)數(shù)據(jù)源
- SpringBoot實現(xiàn)動態(tài)數(shù)據(jù)源切換的項目實踐
- SpringBoot配置動態(tài)數(shù)據(jù)源的實戰(zhàn)詳解
- SpringBoot動態(tài)數(shù)據(jù)源連接測試的操作詳解
- SpringBoot實現(xiàn)動態(tài)數(shù)據(jù)源切換的方法總結(jié)
相關(guān)文章
基于Spring定時任務(wù)的fixedRate和fixedDelay的區(qū)別
這篇文章主要介紹了基于Spring定時任務(wù)的fixedRate和fixedDelay的區(qū)別,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2021-10-10
Java自動化實現(xiàn)將HTML轉(zhuǎn)換為Word
在日常的軟件開發(fā)和企業(yè)應(yīng)用中,我們經(jīng)常會遇到需要將動態(tài)生成的 HTML 內(nèi)容轉(zhuǎn)化為規(guī)范的 Word 文檔,本文將介紹如何利用 Java 輕松實現(xiàn) HTML 到 Word 的高效轉(zhuǎn)換,需要的可以了解下2025-10-10
Java如何判斷一個IP是否在給定的網(wǎng)段內(nèi)
這篇文章主要介紹了Java如何判斷一個IP是否在給定的網(wǎng)段內(nèi)問題,具有很好的參考價值,希望對大家有所幫助,如有錯誤或未考慮完全的地方,望不吝賜教2025-04-04
關(guān)于兩個BeanUtils.copyProperties()的用法及區(qū)別
這篇文章主要介紹了關(guān)于兩個BeanUtils.copyProperties()的用法及區(qū)別說明,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2022-06-06
Spring依賴注入多種類型數(shù)據(jù)的示例代碼
這篇文章主要介紹了Spring依賴注入多種類型數(shù)據(jù),本文通過實例代碼給大家介紹的非常詳細,對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下2022-03-03
idea 無法創(chuàng)建Scala class 選項的原因分析及解決辦法匯總
這篇文章主要介紹了idea 無法創(chuàng)建Scala class 選項的解決辦法匯總,本文給大家介紹的非常詳細,對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下2020-09-09

