1、快速理解 Spring 多數據源操作
最近在調研 Spring 如何配置多數據源的操作,結果被媳婦吐槽,整天就坐在那打電腦,啥都不幹。於是我靈光一現,跟我媳婦說了一下調研結果,第一版本原話如下:
Spring 提供了一套多數據源的解決方案,通過繼承抽象 AbstractRoutingDataSource
定義動態路由數據源,然後可以通過AOP, 動態切換配置好的路由Key,來跳轉不同的數據源。
Spring ?春天 ?我在這幹活你還想有春天,還有那個什麼什麼抽象,我現在有點想抽你。好好說話 !
媳婦,莫急莫急,嗯… 等我重新組織一下語言先。我想了一會緩緩的對媳婦說:
生活中我們去景點買票或者購買火車票,一般都有軍人和殘疾人專門的售票窗口,普通人通過正常的售票窗口進行購票,軍人和殘疾人通過專門的優惠售票窗口進行購票。
爲了防止有人冒充軍人或殘疾人去優惠售票窗口進行購票,這就需要你提供相關的證件來證明。沒有證件的走正常售票窗口,有證件的走優惠售票窗口。
那如何判斷誰有證件,誰沒有證件呢? 這就需要辛苦檢查證件的工作人員。默認情況下,正常售票窗口通道是打開的,大家可以直接去正常的售票窗口進行購票。
如果有軍人或殘疾人來購票,工作人員檢查相關證件後,則關閉正常售票窗口通道,然後打開優惠窗口通道。
在理解了購票的流程後,我們在來看 Spring 動態數據源切換的解決方案就會容易很多。Spring 動態數據源解決方案與購票流程中的節點的對應如下:
- 具體 Dao 訪問數據庫獲取的數據 = 景點票或火車票
- 具體 Dao = 購票人員。
- 具體 Dao目標數據源註解類的value值 = 證明是否是軍人或殘疾人的證件。
- Spring 動態數據源 = 售票點。
- 不同的數據源 = 正常售票窗口和優惠售票窗口。
- AOP 動態修改動態數據源狀態類Key值 = 檢查證件工作人員去關閉和打開正常售票窗口和優惠售票窗口通道。
- 動態數據源狀態類 = 不同購票窗口通道門打開或關閉狀態。
- 動態路由狀態Key值的枚舉類 = 不同購票窗口通道的門。
具體執行流程圖如下:
你要這麼說:我就明白了,媳婦這會的語氣緩和了很多。但是你講這麼多有個毛線用 ! 拖地去 !
好嘞 ! 我拿起拖把瘋狂的拖了起來。
到這裏Spring 多數據源操作流程介紹完畢! 如果你想了解代碼的實現請接着往下看,如果您就想看看操作流程那麼感謝您的閱讀。記得關注加點贊哈 😁
正所謂光說不練假把式,說了這麼多操作流程的介紹,接下來開始正式的實戰操作。在實戰操作前我先說一下實戰操作內容以及注意事項:
實戰操作的主要內容介紹瞭如何在 SpringBoot 項目下,通過 MyBatis + Durid 數據庫連接池來配置不同數據源的操作。
閱讀本文需要你熟悉 SpringBoot 和 MyBatis 的基本操作即可,另外需要注意的是實戰操作的代碼環境如下:
- SpringBoot:2.1.0.RELEASE
- MyBatis:3.4.0 (mybatis-spring-boot-starter:1.1.1)
- JDK:1.8.0_251
- Durid:1.1.10 (druid-spring-boot-starter:1.1.10 )
- Maven:3.6.2
按照本文進行操作的過程中,請儘量保持你的環境版本和上述一致,如出現問題可以查看本文末尾處的 GitHub 項目倉庫的代碼進行對比。
2、整合多數源實戰操作
2.1、數據庫準備
這裏通過商品庫的商品表和旅館庫的旅館表來模擬多數據源的場景,具體建庫以及建表 SQL 如下:
需要注意的是,我本地環境使用的是 MySql 5.6 。
創建商品庫以及商品表的 Sql。
SET NAMES utf8;
SET FOREIGN_KEY_CHECKS = 0;
-- ----------------------------
-- Table structure for `product`
-- ----------------------------
DROP TABLE IF EXISTS `product`;
CREATE TABLE `product` (
`id` bigint(10) NOT NULL AUTO_INCREMENT COMMENT '商品id',
`product_name` varchar(25) DEFAULT NULL COMMENT '商品名稱',
`price` decimal(8,3) DEFAULT NULL COMMENT '價格',
`product_brief` varchar(125) DEFAULT NULL COMMENT '商品簡介',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8;
-- ----------------------------
-- Records of `product`
-- ----------------------------
BEGIN;
INSERT INTO `product` VALUES ('2', '蘋果', '20.000', '好喫的蘋果,紅富士大蘋果');
COMMIT;
SET FOREIGN_KEY_CHECKS = 1;
創建旅館庫和旅館表 Sql
SET NAMES utf8;
SET FOREIGN_KEY_CHECKS = 0;
-- ----------------------------
-- Table structure for `hotel`
-- ----------------------------
DROP TABLE IF EXISTS `hotel`;
CREATE TABLE `hotel` (
`id` bigint(10) NOT NULL AUTO_INCREMENT COMMENT '旅館id',
`city` varchar(125) DEFAULT NULL COMMENT '城市',
`name` varchar(125) DEFAULT NULL COMMENT '旅館名稱',
`address` varchar(256) DEFAULT NULL COMMENT '旅館地址',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8;
-- ----------------------------
-- Records of `hotel`
-- ----------------------------
BEGIN;
INSERT INTO `hotel` VALUES ('1', '北京', '漢庭', '朝陽區富明路112號');
COMMIT;
SET FOREIGN_KEY_CHECKS = 1;
2.2、SpringBoot 項目多數據源yml 配置
搭建 SpringBoot 項目這裏不在進行介紹,搭建好SpringBoot 項目的第一步就是進行項目的 yml 配置。
application.yml 的配置代碼如下:
server:
port: 8080
#mybatis:
#config-location: classpath:mybatis-config.xml
#mapper-locations: classpath*:mapper/**/*Mapper.xml
spring:
datasource:
#初始化時建立物理連接的個數
#type: com.alibaba.druid.pool.DruidDataSource
druid:
product:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/product?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
username: root
password: 123456
initial-size: 15
#最小連接池數量
min-idle: 10
#最大連接池數量
max-active: 50
#獲取連接時最大等待時間
max-wait: 60000
# 配置間隔多久才進行一次檢測,檢測需要關閉的空閒連接,單位是毫秒
timeBetweenEvictionRunsMillis: 60000
# 配置一個連接在池中最小生存的時間,單位是毫秒
minEvictableIdleTimeMillis: 300000
# 配置一個連接在池中最大生存的時間,單位是毫秒
maxEvictableIdleTimeMillis: 9000000
# 配置檢測連接是否有效
validationQuery: SELECT 1 FROM DUAL
#配置監控頁面訪問登錄名稱
stat-view-servlet.login-username: admin
#配置監控頁面訪問密碼
stat-view-servlet.login-password: admin
#是否開啓慢sql查詢監控
filter.stat.log-slow-sql: true
#慢SQL執行時間
filter.stat.slow-sql-millis: 1000
hotel:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/hotel?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
username: root
password: 123456
initial-size: 15
#最小連接池數量
min-idle: 10
#最大連接池數量
max-active: 50
#獲取連接時最大等待時間
max-wait: 60000
# 配置間隔多久才進行一次檢測,檢測需要關閉的空閒連接,單位是毫秒
timeBetweenEvictionRunsMillis: 60000
# 配置一個連接在池中最小生存的時間,單位是毫秒
minEvictableIdleTimeMillis: 300000
# 配置一個連接在池中最大生存的時間,單位是毫秒
maxEvictableIdleTimeMillis: 9000000
# 配置檢測連接是否有效
validationQuery: SELECT 1 FROM DUAL
#配置監控頁面訪問登錄名稱
stat-view-servlet.login-username: admin
#配置監控頁面訪問密碼
stat-view-servlet.login-password: admin
#是否開啓慢sql查詢監控
filter.stat.log-slow-sql: true
#慢SQL執行時間
filter.stat.slow-sql-millis: 1000
多數據源情況下,原先的 Mybatis 相關配置不會起作用。Mybaies 配置均在定義多個數據源的配置類進行。
2.3、自定義動態路由數據源
自定義動態路由數據源是整個操作中最爲重要的環節,因爲整個切換數據源過程都是通過操作它來完成的。
第一步,創建動態數據源狀態類以及動態路由狀態Key值的枚舉類,具體代碼如下:
動態路由狀態Key值的枚舉類
public enum DataSourceKeyEnum {
HOTEL, PRODUCT
}
動態數據源狀態類
public class DynamicDataSourceRoutingKeyState {
private static Logger log = LoggerFactory.getLogger(DynamicDataSourceRoutingKeyState.class);
// 使用ThreadLocal保證線程安全
private static final ThreadLocal<DataSourceKeyEnum> TYPE = new ThreadLocal<DataSourceKeyEnum>();
// 往當前線程裏設置數據源類型
public static void setDataSourceKey(DataSourceKeyEnum dataSourceKey) {
if (dataSourceKey == null) {
throw new NullPointerException();
}
log.info("[將當前數據源改爲]:{}",dataSourceKey);
TYPE.set(dataSourceKey);
}
// 獲取數據源類型
public static DataSourceKeyEnum getDataSourceKey() {
DataSourceKeyEnum dataSourceKey = TYPE.get();
log.info("[獲取當前數據源的類型爲]:{}",dataSourceKey);
System.err.println("[獲取當前數據源的類型爲]:" + dataSourceKey);
return dataSourceKey;
}
// 清空數據類型
public static void clearDataSourceKey() {
TYPE.remove();
}
}
第二步,通過繼承 Spring 提供的抽象類 AbstractRoutingDataSource
來創建動態數據源,具體代碼如下:
public class DynamicDataSourceRouting extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
DataSourceKeyEnum dataSourceKey = DynamicDataSourceRoutingKeyState.getDataSourceKey();
return dataSourceKey;
}
}
2.4、定義多個數據源的配置類
第一步,配置商品庫數據源、旅館庫數據源、動態數據源、MyBatis SqlSessionFactory 。
配置商品庫數據源代碼。
@Configuration
public class DataSourceConfig {
/**
* 商品庫的數據源
* @return
*/
@Bean(name = "dataSourceForProduct")
@ConfigurationProperties(prefix="spring.datasource.druid.product")
public DruidDataSource dataSourceForProduct() {
return DruidDataSourceBuilder.create().build();
}
}
配置旅館庫的數據源代碼。
/**
* 旅館庫的數據源
* @return
*/
@Bean(name = "dataSourceForHotel")
@ConfigurationProperties(prefix="spring.datasource.druid.hotel")
public DruidDataSource dataSourceForHotel() {
return DruidDataSourceBuilder.create().build();
}
配置動態路由的數據源代碼。
/**
* 動態切換的數據源
* @return
*/
@Bean(name = "dynamicDataSource")
public DataSource dynamicDataSource() {
Map<Object, Object> targetDataSource = new HashMap<>();
targetDataSource.put(DataSourceKeyEnum.PRODUCT, dataSourceForProduct());
targetDataSource.put(DataSourceKeyEnum.HOTEL, dataSourceForHotel());
//設置默認的數據源和以及多數據源的Map信息
DynamicDataSourceRouting dataSource = new DynamicDataSourceRouting();
dataSource.setTargetDataSources(targetDataSource);
dataSource.setDefaultTargetDataSource(dataSourceForProduct());
return dataSource;
}
配置 MyBatis SqlSessionFactory 並指定動態數據源代碼。
@Bean(name = "sqlSessionFactory")
public SqlSessionFactory sqlSessionFactory(@Qualifier("dynamicDataSource") DataSource dynamicDataSource)
throws Exception {
SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
bean.setDataSource(dynamicDataSource);
//設置數據數據源的Mapper.xml路徑
bean.setMapperLocations(
new PathMatchingResourcePatternResolver().getResources("classpath*:mapper/*.xml"));
//設置Mybaties查詢數據自動以駝峯式命名進行設值
org.apache.ibatis.session.Configuration configuration = new org.apache.ibatis.session
.Configuration();
configuration.setMapUnderscoreToCamelCase(true);
bean.setConfiguration(configuration);
return bean.getObject();
}
配置數據源注入事務 DataSourceTransactionManager 代碼。
/**
* 注入 DataSourceTransactionManager 用於事務管理
*/
@Bean
public PlatformTransactionManager transactionManager(@Qualifier("dynamicDataSource") DataSource dynamicDataSource) {
DataSourceTransactionManager dataSourceTransactionManager = new DataSourceTransactionManager(dynamicDataSource);
return new DataSourceTransactionManager(dynamicDataSource);
}
配置商品庫數據源、配置旅館庫的數據源、配置動態路由的數據源、配置MyBatis SqlSessionFactory 、配置數據源注入事務 代碼均在 DataSourceConfig 配置類中。
第二步,配置數據源的事務 AOP 切面類。
添加事務AOP切面類,通過方法名前綴來配置其事務。
- add、save、insert、update、delete、其他前綴的方法:讀寫事務。
- get、find、query前綴的方法:只讀事務。
@Aspect
@Configuration
public class TransactionConfiguration {
private static final int TX_METHOD_TIMEOUT = 5;
private static final String AOP_POINTCUT_EXPRESSION = "execution( * cn.lijunkui.service.*.*(..))";
@Autowired
private PlatformTransactionManager transactionManager;
@Bean
public TransactionInterceptor txAdvice() {
NameMatchTransactionAttributeSource source = new NameMatchTransactionAttributeSource();
/* 只讀事務,不做更新操作 */
RuleBasedTransactionAttribute readOnlyTx = new RuleBasedTransactionAttribute();
readOnlyTx.setReadOnly(true);
readOnlyTx.setPropagationBehavior(TransactionDefinition.PROPAGATION_NOT_SUPPORTED);
/* 當前存在事務就使用當前事務,當前不存在事務就創建一個新的事務 */
RuleBasedTransactionAttribute requiredTx = new RuleBasedTransactionAttribute();
requiredTx.setRollbackRules(Collections.singletonList(new RollbackRuleAttribute(Exception.class)));
requiredTx.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);
//requiredTx.setTimeout(TX_METHOD_TIMEOUT);
Map<String, TransactionAttribute> txMap = new HashMap<String, TransactionAttribute>();
txMap.put("add*", requiredTx);
txMap.put("save*", requiredTx);
txMap.put("insert*", requiredTx);
txMap.put("update*", requiredTx);
txMap.put("delete*", requiredTx);
txMap.put("get*", readOnlyTx);
txMap.put("find*", readOnlyTx);
txMap.put("query*", readOnlyTx);
txMap.put("*", requiredTx);
source.setNameMap(txMap);
TransactionInterceptor txAdvice = new TransactionInterceptor(transactionManager, source);
return txAdvice;
}
@Bean
public Advisor txAdviceAdvisor() {
AspectJExpressionPointcut pointcut = new AspectJExpressionPointcut();
pointcut.setExpression(AOP_POINTCUT_EXPRESSION);
return new DefaultPointcutAdvisor(pointcut, txAdvice());
}
}
2.5、定義AOP動態切換配置數據源的操作
指定Dao目標數據源註解類。
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface TargetDataSource {
DataSourceKeyEnum value() default DataSourceKeyEnum.PRODUCT;
}
Dao 訪問數據庫進行攔截的 Aop 切面類。
@Aspect
@Component
public class DataSourceAop {
Logger log = LoggerFactory.getLogger(DataSourceAop.class);
@Pointcut("execution( * cn.lijunkui.dao.*.*(..))")
public void daoAspect() {
}
@Before(value="daoAspect()")
public void switchDataSource(JoinPoint joinPoint) throws NoSuchMethodException {
log.info("開始切換數據源");
//獲取HotelMapper or ProductMapper 類上聲明的TargetDataSource的數據源註解的值
MethodSignature methodSignature = (MethodSignature)joinPoint.getSignature();
Class<?> declaringClass = methodSignature.getMethod().getDeclaringClass();
TargetDataSource annotation = declaringClass.getAnnotation(TargetDataSource.class);
DataSourceKeyEnum value = annotation.value();
log.info("數據源爲:{}",value);
//根據TargetDataSource的value設置要切換的數據源
DynamicDataSourceRoutingKeyState.setDataSourceKey(value);
}
}
到這裏多數據源配置操作介紹完畢!
3、測試
正所謂沒有測試的代碼都是耍流氓,接下來通過分別定義訪問商品庫和旅館的 Controller、Service、Dao。並進行驗證上述配置是否有效。
旅館 Controller
@RestController
public class HotelController {
@Autowired
private HotelService hotelService;
/**
* 查詢所有的旅館信息
* @return
*/
@GetMapping("/hotel")
public List<Hotel> findAll(){
List<Hotel> hotelList = hotelService.findAll();
return hotelList;
}
}
旅館 Service
@Service
public class HotelService {
@Autowired
private HotelMapper hotelMapper;
public List<Hotel> findAll(){
List<Hotel> hotels = hotelMapper.selectList();
return hotels;
}
}
旅館 Dao
@Mapper
@TargetDataSource(value = DataSourceKeyEnum.HOTEL )
public interface HotelMapper {
/**
* 查詢所有
* @return List<Hotel>
*/
List<Hotel> selectList();
}
旅館 Mapper.xml 文件配置。
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="cn.lijunkui.dao.HotelMapper">
<sql id="baseSelect">
select id, city, name, address from hotel
</sql>
<select id="selectList" resultType="cn.lijunkui.domain.Hotel">
<include refid="baseSelect"/>
</select>
</mapper>
商品 Controller
@RestController
public class ProductController {
@Autowired
private ProductService productService;
/**
* 查詢所有的商品信息
* @return
*/
@GetMapping("/product")
public List<Product> findAll() {
List<Product> productList = productService.findAll();
return productList;
}
}
商品Service
@Service
public class ProductService {
@Autowired
private ProductMapper productMapper;
public List<Product> findAll(){
return productMapper.selectList();
}
}
商品Dao
@Mapper
@TargetDataSource(value = DataSourceKeyEnum.PRODUCT )
public interface ProductMapper {
/**
* 查詢所有
* @param
* @return List<Hotel>
*/
List<Product> selectList();
}
商品的Mapper.xml 配置
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="cn.lijunkui.dao.ProductMapper">
<select id="selectList" resultType="cn.lijunkui.domain.Product">
select * from product
</select>
</mapper>
通過 http://localhost:8080/product 訪問獲取所有的商品信息,具體效果如下圖
後臺日誌信息如下:
2020-06-20 11:44:07.927 INFO 1234 --- [nio-8080-exec-1] cn.lijunkui.config.DataSourceAop : 開始切換數據源
2020-06-20 11:44:07.928 INFO 1234 --- [nio-8080-exec-1] cn.lijunkui.config.DataSourceAop : 數據源爲:PRODUCT
2020-06-20 11:44:07.930 INFO 1234 --- [nio-8080-exec-1] c.l.c.DynamicDataSourceRoutingKeyState : [將當前數據源改爲]:PRODUCT
2020-06-20 11:44:07.942 INFO 1234 --- [nio-8080-exec-1] c.l.c.DynamicDataSourceRoutingKeyState : [獲取當前數據源的類型爲]:PRODUCT
通過 http://localhost:8080/hotel 訪問獲取所有的旅館信息,具體效果如下圖
後臺日誌信息如下:
[獲取當前數據源的類型爲]:PRODUCT
2020-06-20 11:44:08.252 INFO 1234 --- [nio-8080-exec-1] com.alibaba.druid.pool.DruidDataSource : {dataSource-1} inited
2020-06-20 11:45:15.256 INFO 1234 --- [nio-8080-exec-4] cn.lijunkui.config.DataSourceAop : 開始切換數據源
2020-06-20 11:45:15.256 INFO 1234 --- [nio-8080-exec-4] cn.lijunkui.config.DataSourceAop : 數據源爲:HOTEL
2020-06-20 11:45:15.256 INFO 1234 --- [nio-8080-exec-4] c.l.c.DynamicDataSourceRoutingKeyState : [將當前數據源改爲]:HOTEL
2020-06-20 11:45:15.256 INFO 1234 --- [nio-8080-exec-4] [獲取當前數據源的類型爲]:HOTEL
c.l.c.DynamicDataSourceRoutingKeyState : [獲取當前數據源的類型爲]:HOTEL
4、小結
本文介紹了 Spring 通過繼承抽象 AbstractRoutingDataSource
定義動態路由來完成多數據源切換的實戰以及代碼執行流程。
Spring 提供的動態數據源的機制就是將多個數據源通過 Map 進行維護,具體使用哪個數據源通過 determineCurrentLookupKey
方法返回的 Key 來確定。通過 AOP 動態修改 determineCurrentLookupKey 方法返回的Key,來完成切換數據源的操作。
本文介紹了訪問不同數據庫業務的實現,通過這種方式也可以搭建相同業務多個一致的數據庫的讀寫分離。你可以嘗試使用這種方式來實現,歡迎大家在評論區說說你實現讀寫分離的方案。
5、代碼示例
操作過程如出現問題可以在我的GitHub 倉庫 springbootexamples 中模塊名爲 spring-boot-2.x-mybaties-multipleDataSource 項目中進行對比查看
GitHub:https://github.com/zhuoqianmingyue/springbootexamples
如果您對這些感興趣,歡迎 star、或轉發給予支持!轉發請標明出處!