最近在调研 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 项目仓库的代码进行对比。
这里通过商品库的商品表和旅馆库的旅馆表来模拟多数据源的场景,具体建库以及建表 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;
搭建 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 配置均在定义多个数据源的配置类进行。
自定义动态路由数据源是整个操作中最为重要的环节,因为整个切换数据源过程都是通过操作它来完成的。
第一步,创建动态数据源状态类以及动态路由状态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;
}
}
第一步,配置商品库数据源、旅馆库数据源、动态数据源、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切面类,通过方法名前缀来配置其事务。
@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());
}
}
指定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);
}
}
到这里多数据源配置操作介绍完毕!
正所谓没有测试的代码都是耍流氓,接下来通过分别定义访问商品库和旅馆的 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
*/
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
*/
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
本文介绍了 Spring 通过继承抽象 AbstractRoutingDataSource
定义动态路由来完成多数据源切换的实战以及代码执行流程。
Spring 提供的动态数据源的机制就是将多个数据源通过 Map 进行维护,具体使用哪个数据源通过 determineCurrentLookupKey
方法返回的 Key 来确定。通过 AOP 动态修改 determineCurrentLookupKey 方法返回的Key,来完成切换数据源的操作。
本文介绍了访问不同数据库业务的实现,通过这种方式也可以搭建相同业务多个一致的数据库的读写分离。你可以尝试使用这种方式来实现,欢迎大家在评论区说说你实现读写分离的方案。
操作过程如出现问题可以在我的GitHub 仓库 springbootexamples 中模块名为 spring-boot-2.x-mybaties-multipleDataSource 项目中进行对比查看
GitHub:https://github.com/zhuoqianmingyue/springbootexamples
如果您对这些感兴趣,欢迎 star、或转发给予支持!转发请标明出处!