github :https://github.com/lanchengx/dynamic
MyBatis-Plus(简称 MP)是一个 MyBatis 的增强工具,在 MyBatis 的基础上只做增强不做改变,为简化开发、提高效率而生
dynamic-datasource-spring-boot-starter(简称 DS) 是一个基于springboot的快速集成多数据源的启动器。
特性
- 支持 数据源分组 ,适用于多种场景 纯粹多库 读写分离 一主多从 混合模式。
- 支持数据库敏感配置信息 加密(可自定义) ENC()。
- 支持每个数据库独立初始化表结构schema和数据库database。
- 支持无数据源启动,支持懒加载数据源(需要的时候再创建连接)。
- 支持 自定义注解 ,需继承DS(3.2.0+)。
- 提供并简化对Druid,HikariCp,BeeCp,Dbcp2的快速集成。
- 提供对Mybatis-Plus,Quartz,ShardingJdbc,P6sy,Jndi等组件的集成方案。
- 提供 自定义数据源来源 方案(如全从数据库加载)。
- 提供项目启动后 动态增加移除数据源 方案。
- 提供Mybatis环境下的 纯读写分离 方案。
- 提供使用 spel动态参数 解析数据源方案。内置spel,session,header,支持自定义。
- 支持 多层数据源嵌套切换 。(ServiceA >>> ServiceB >>> ServiceC)。
- 提供 基于seata的分布式事务方案 。
- 提供 本地多数据源事务方案。
约定
- 本框架只做 切换数据源 这件核心的事情,并不限制你的具体操作,切换了数据源可以做任何CRUD。
- 配置文件所有以下划线 _ 分割的数据源 首部 即为组的名称,相同组名称的数据源会放在一个组下。
- 切换数据源可以是组名,也可以是具体数据源名称。组名则切换时采用负载均衡算法切换。
- 默认的数据源名称为 master ,你可以通过 spring.datasource.dynamic.primary 修改。
- 方法上的注解优先于类上注解。
- DS支持继承抽象类上的DS,暂不支持继承接口上的DS。
使用方法
引入dynamic-datasource-spring-boot-starter。
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>dynamic-datasource-spring-boot-starter</artifactId>
<version>${version}</version>
</dependency>
配置数据源。
spring:
datasource:
dynamic:
primary: master #设置默认的数据源或者数据源组,默认值即为master
strict: false #严格匹配数据源,默认false. true未匹配到指定数据源时抛异常,false使用默认数据源
datasource:
master:
url: jdbc:mysql://xx.xx.xx.xx:3306/dynamic
username: root
password: 123456
driver-class-name: com.mysql.jdbc.Driver # 3.2.0开始支持SPI可省略此配置
slave_1:
url: jdbc:mysql://xx.xx.xx.xx:3307/dynamic
username: root
password: 123456
driver-class-name: com.mysql.jdbc.Driver
slave_2:
url: ENC(xxxxx) # 内置加密,使用请查看详细文档
username: ENC(xxxxx)
password: ENC(xxxxx)
driver-class-name: com.mysql.jdbc.Driver
#......省略
#以上会配置一个默认库master,一个组slave下有两个子库slave_1,slave_2
# 多主多从 纯粹多库(记得设置primary) 混合配置
spring: spring: spring:
datasource: datasource: datasource:
dynamic: dynamic: dynamic:
datasource: datasource: datasource:
master_1: mysql: master:
master_2: oracle: slave_1:
slave_1: sqlserver: slave_2:
slave_2: postgresql: oracle_1:
slave_3: h2: oracle_2:
使用 @DS 切换数据源。
@DS 可以注解在方法上或类上,同时存在就近原则 方法上注解 优先于 类上注解。
强烈建议只注解在service实现上。
注解 | 结果 |
---|---|
没有@DS | 默认数据源 |
@DS(“dsName”) | dsName可以为组名也可以为具体某个库的名称 |
实例
@Service
@DS("slave")
public class UserServiceImpl implements UserService {
@Autowired
private JdbcTemplate jdbcTemplate;
public List selectAll() {
return jdbcTemplate.queryForList("select * from user");
}
@Override
@DS("slave_1")
public List selectByCondition() {
return jdbcTemplate.queryForList("select * from user where age >10");
}
}
增加一个aop配置,通过方法名/类名 设置不同的数据源
/**
* @Author: lancx
* @Date: 2020/5/10 0010
*/
@Aspect
@Component
@Order(0)
@Lazy(false)
@Log
public class DataSourceAop {
private static final String MASTER = "master";
private static final String SLAVE = "slave";
@Pointcut(" execution(* com.example.dynamic.service..*.*(..)) " +
"|| execution(* com.baomidou.mybatisplus.extension.service..*.*(..)) " +
"|| @annotation(org.springframework.transaction.annotation.Transactional)")
public void pushDataSource() {
}
// 这里切到你的方法目录
@Before("pushDataSource()")
public void process(JoinPoint joinPoint) throws NoSuchMethodException, SecurityException {
String methodName = joinPoint.getSignature().getName();
Class clazz = joinPoint.getTarget().getClass();
if (clazz.isAnnotationPresent(DS.class)) {
//获取类上注解
return;
}
String targetName = clazz.getSimpleName();
Class[] parameterTypes =
((MethodSignature) joinPoint.getSignature()).getMethod().getParameterTypes();
Method methdo = clazz.getMethod(methodName, parameterTypes);
if (methdo.isAnnotationPresent(DS.class)) {
return;
}
if (methodName.startsWith("get")
|| methodName.startsWith("count")
|| methodName.startsWith("find")
|| methodName.startsWith("list")
|| methodName.startsWith("select")
|| methodName.startsWith("check")
|| methodName.startsWith("page")) {
log.info("当前执行的库:" + SLAVE);
DynamicDataSourceContextHolder.push(SLAVE);
} else {
log.info("当前执行的库:" + MASTER);
DynamicDataSourceContextHolder.push(MASTER);
}
}
@After("pushDataSource()")
public void afterAdvice() {
DynamicDataSourceContextHolder.clear();
}
}
事务问题:
@Transactional
public void method1(){
service.getById(...);
...
doSomeThing();
}
@DS("master")
@Transactional
public void doSomeThing(){
service.save()
service.update()
}
在一个事务内先执行查询getById()方法(使用Slave数据源),后执行doSomeThing()方法,在执行doSomeThing()方法时会使用getById()所使用的数据源。
原因分析:
在org.springframework.jdbc.datasource.DataSourceTransactionManager#doBegin 这个类的源代码中
在开始一个事务前,如果当前上下文的连接对象为空,获取一个连接对象,然后保存起来,下次doBegin再调用时,就直接用这个连接了,根本不做任何切换(类似于缓存命中!)
doSomeThing()方法被调用前,加了一段select方法,相当于已经切换到了slave从库,然后再进入doBegin方法时,就直接拿这个从库的链接了,不再进行切换。
解决办法:
在doSomeThing() 之前手动切换数据库
@Transactional
public void method1(){
service.getById(...);
...
DBContext.setDBKey("master");//先切换到主库
doSomeThing();
}
在自定义切面中增加对@Transactional注解的判断,提前使用master库
if (methdo.isAnnotationPresent(Transactional.class)) {
log.info("当前执行的库:" + MASTER);
DynamicDataSourceContextHolder.push(MASTER);
return;
}
在doSomeThing()上使用@Transactional(propagation = Propagation.REQUIRES_NEW)
@DS("master")
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void doSomeThing(){
service.save()
service.update()
}
自定义切面时间测试
dynamic-datasource 强烈建议只注解在service实现上 , 故测试自定义aop在切换数据源时所消耗的时间。
测试过程:100 get操作的基础上使用不同的切换数据源方式,比较aop切换数据源的耗时情况
最后编辑:Jeebiz 更新时间:2024-08-22 10:22