项目更名
在easymulti-datasource-spring-boot-starter
之后笔者又开发了hotkit-r2dbc
,这两个项目都支持动态数据源切换,前者支持mybatis
框架,后者支持响应式编程spring-data-r2dbc
框架,既然都是ORM
框架,不如合并到一个项目中维护。
GitHub
上原easymulti-datasource-spring-boot-starter
项目已更名为easymulti-datasource
,而原easymulti-datasource-spring-boot-starter
模块已经更名为easymulti-datasource-mybatis
,版本号从3.0.1
开始。新版本增加了easymulti-datasource-r2dbc
(也就是原hotkit-r2dbc
)。
项目背景
多数据源动态切换似乎已经成了微服务的标配,做过那么多项目发现每个项目都要配一个动态数据源,都要写一个切面去实现动态切换,因此,我将这些繁琐的配置封装为starter
,拿来即用。
easymulti-datasource
两个模块:
easymulti-datasource-mybatis
(原easymulti-datasource-spring-boot-starter
)easymulti-datasource-r2dbc
(原hotkit-r2dbc
)
easymulti-datasource-mybatis
mybatis
版多数据源框架,提供声明式和编程式动态切换数据源功能。
easymulti-datasource-mybatis
自动整合了mybatis-plus
,提供两种动态多数据源模式,分别是主从数据源模式、非主从的多数据源模式,每个数据源使用独立的连接池配置,可针对每个数据源单独配置连接池。
支持多数据源动态切换并不是easymulti-datasource-mybatis
框架的最大亮点,easymulti-datasource-mybatis
区别于其它动态数据源切换框架的主要特色如下:
- 支持监听
SQL
,监听修改某个表的某些字段的sql
,用于实现埋点事件; - 支持事务状态监听、注册事务监听器,用于在事务回滚/提交时再完成一些后台操作;
详细使用可参见wiki
。
依赖配置
maven
中使用:
<dependency>
<groupId>com.github.wujiuye</groupId>
<artifactId>easymulti-datasource-mybatis</artifactId>
<version>${version}</version>
</dependency>
旧版本为:
<dependency>
<groupId>com.github.wujiuye</groupId>
<artifactId>easymulti-datasource-spring-boot-starter</artifactId>
<version>${version}</version>
</dependency>
版本选择注意事项说明如下图所示。
动态切换数据源
- 使用注解切换数据源:
@EasyMutiDataSource
; - 使用
API
切换数据源:DataSourceContextHolder#setDataSource
。
AOP中注册事务监听器
在application
配置文件中打开追踪事务方法调用链路的开关,配置如下。
## 监控事务方法调用链路
easymuti:
transaction:
open-chain: true
定义切面,拦截Mapper
方法,在环绕方法中实现更新缓存的逻辑,代码如下。
TransactionInvokeContext.currentExistTransaction
:判断当前调用链路上是否存在事务;TransactionInvokeContext.addCurrentTransactionMethodPopListener
:给当前事务绑定一个监听器(PopTransactionListener
),当事务提交或者回滚时监听器被调用。
如上代码所示,首先是判断当前调用链路上是否存在事务,如果存在,则给当前事务注入一个监听器,由监听器完成缓存更新逻辑,如果不存在事务,在目标方法执行完成后且无异常抛出时执行更新缓存逻辑。
监听SQL
easymulti-datasource-mybatis
支持sql
埋点监听功能,并且支持监听事务状态,如果当前sql
执行存在事务中,则会在事务提交后才会回调sql
监听者。
第一步:启用sql
埋点监听功能,并且启用事务调用链路追踪功能。
easymuti:
transaction:
open-chain: true
sql-watcher:
enable: true
第二步:编写观察者,可以有n
多个,并且多个观察者也可观察同一个表、甚至相同字段。
@Component
@Slf4j
public class TestTableFieldObserver implements TableFieldObserver , InitializingBean {
@Override
public Set<WatchMetadata> observeMetadatas() {
// 在这里注册要监听哪些表的哪些字段
}
/**
* 监听到sql时被同步调用
*
* @param commandType 事件类型
* @param matchResult 匹配的ITEM
* @return 返回异步消费者
*/
@Override
public AsyncConsumer observe(CommandType commandType, MatchItem matchResult) {
// 同步消费
// 这里是sql执行之前,可在sql执行之前做一些事情,比如新旧数据的对比,这里查出旧数据
// 异步消费,再sql执行完成时,或者在事务方法执行完成时(如果存在事务),完成指:正常执行完成 or 方法异常退出
return throwable -> {
// sql执行抛出异常不处理
if (throwable != null) {
return;
}
// 消费事件
// ....
};
}
}
observe
方法在监听到sql
时被同步调用,该方法返回的AsyncConsumer
则在事务提交后被回调调用,如果事务回滚了则不会被调用。
如果调用链路上出现多个事务,那么根据事务的传播机制,只在当前方法所在事务提交时才会回调注册在该事务上的所有AsyncConsumer
。
easymulti-datasource-r2dbc
spring-data-r2dbc
版多数据源组件,用于响应式编程。
easymulti-datasource-r2dbc
为spring-data-r2dbc
实现动态路由接口,为反应式编程提供声明式和编程式多数据源动态切换提供支持。同样支持两种多数据源模式,覆盖常见的多数据源使用场景,分别是主从模式和Cluster
模式,Cluster
模式支持最多配置3
个数据源,而主从模式支持一主一从。
添加依赖与配置数据源
使用easymulti-datasource-r2dbc
后,无需再在项目中添加spring-boot-starter-data-r2dbc
的依赖,也不需要添加spring-data-r2dbc
的依赖。
easymulti-datasource-r2dbc
版本号对应spring-data-r2dbc
的版本号:
easymulti-datasource-r2dbc | spring-data-r2dbc |
---|---|
3.0.1-RELEASE | 1.1.0.RELEASE |
在项目中添加easymulti-datasource-r2dbc
的依赖,如下。
<dependency>
<groupId>com.github.wujiuye</groupId>
<artifactId>easymulti-datasource-r2dbc</artifactId>
<version>${version}</version>
</dependency>
此时,只需要额外添加用到的数据库类型对应的驱动依赖即可,例如,添加mysql
的r2dbc
驱动。
<dependency>
<groupId>dev.miku</groupId>
<artifactId>r2dbc-mysql</artifactId>
<version>0.8.2.RELEASE</version>
</dependency>
如果使用主从模式,则使用如下配置。
easymuti:
database:
r2dbc:
master-slave-mode:
master:
url: r2dbc:mysql://127.0.0.1:3306/r2dbc_stu
username: root
password:
pool:
max-size: 5
idel-timeout: 60
slave:
url: r2dbc:mysql://127.0.0.1:3306/r2dbc_stu
username: root
password:
pool:
max-size: 5
idel-timeout: 60
master
会被设置为默认使用的数据源,slave
有则配置,没有也可以为空。虽然slave
允许为空,但如果真的不需要多数据源,也是没有必要使用easymulti-datasource-r2dbc
的。
如果使用Cluster
模式,则使用如下配置。
easymuti:
database:
r2dbc:
cluster-mode:
first:
url: r2dbc:mysql://127.0.0.1:3306/r2dbc_stu
username: root
password:
pool:
max-size: 5
idel-timeout: 60
second:
url: r2dbc:mysql://127.0.0.1:3306/r2dbc_stu
username: root
password:
pool:
max-size: 5
idel-timeout: 60
third:
url: r2dbc:mysql://127.0.0.1:3306/r2dbc_stu
username: root
password:
pool:
max-size: 5
idel-timeout: 60
其中first
会被设置为默认使用的数据源,second
与third
可以为空。
声明式动态切换数据源
声明式动态切换数据源即使用注解方式动态切换数据源,只需要在spring bean
的public
方法或者类上添加@R2dbcDataBase
注解,将注解的value
属性指定为使用的数据源。
示例代码如下。
@Service
public class PersonService {
@Resource
private PersonRepository personRepository;
// 方法返回值类型为Mono测试
@R2dbcDataBase(MasterSlaveMode.Master)
@Transactional(rollbackFor = Throwable.class)
public Mono<Integer> addPerson(Person... persons) {
Mono<Integer> txOp = null;
for (Person person : persons) {
if (txOp == null) {
txOp = personRepository.insertPerson(person.getId(), person.getName(), person.getAge());
} else {
txOp = txOp.then(personRepository.insertPerson(person.getId(), person.getName(), person.getAge()));
}
}
return txOp;
}
// 方法返回值类型为Flux测试
@R2dbcDataBase(MasterSlaveMode.Master)
@Transactional(rollbackFor = Throwable.class)
public Flux<Integer> addPersons(Flux<Person> persons) {
return persons.flatMap(person -> personRepository.insertPerson(person.getId(), person.getName(), person.getAge()));
}
}
- 如果是主从模式,
@R2dbcDataBase
注解的value
属性可选值参见MasterSlaveMode
接口声明的常量; - 如果是
Cluster
模式,@R2dbcDataBase
注解的value
属性可选值参见ClusterMode
接口声明的常量;
编程式动态切换数据源
声明式切换数据源的实现是依赖编程式切换数据源实现的,因此,我们也可以直接编写代码切换数据源,而不需要将方法改为public
暴露出去。
只需要调用EasyMutiR2dbcRoutingConnectionFactory
提供的静态方法putDataSource
为Context
写入使用的数据源,代码如下。
public class RoutingTest extends SupporSpringBootTest {
@Resource
private DatabaseClient client;
@Resource
private ReactiveTransactionManager reactiveTransactionManager;
@Test
public void test() throws InterruptedException {
TransactionalOperator operator = TransactionalOperator.create(reactiveTransactionManager);
Mono<Void> atomicOperation = client.execute("INSERT INTO person (id, name, age) VALUES(:id, :name, :age)")
.bind("id", "joe")
.bind("name", "Joe")
.bind("age", 34)
.fetch().rowsUpdated()
.then(client.execute("INSERT INTO person (id, name) VALUES(:id, :name)")
.bind("id", "joe")
.bind("name", "Joe")
.fetch().rowsUpdated())
.then();
// 包装事务
Mono<Void> txOperation = operator.transactional(atomicOperation);
// 包装切换数据源
EasyMutiR2dbcRoutingConnectionFactory.putDataSource(txOperation, MasterSlaveMode.Slave).subscribe();
TimeUnit.SECONDS.sleep(Integer.MAX_VALUE);
}
}
需要注意,如果需要使用事务,必须先调用TransactionalOperator
对象的transactional
方法,再调用EasyMutiR2dbcRoutingConnectionFactory
的putDataSource
方法。