一个支持监听SQL、感知事务状态、回溯数据源的动态数据源框架

数据库 MySQL
在easymulti-datasource-spring-boot-starter之后笔者又开发了hotkit-r2dbc,这两个项目都支持动态数据源切换,前者支持mybatis框架,后者支持响应式编程spring-data-r2dbc框架,既然都是ORM框架,不如合并到一个项目中维护。

[[386751]]

项目更名

在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中使用:

  1. <dependency> 
  2.     <groupId>com.github.wujiuye</groupId> 
  3.     <artifactId>easymulti-datasource-mybatis</artifactId> 
  4.     <version>${version}</version> 
  5. </dependency> 

 

旧版本为:

  1. <dependency> 
  2.     <groupId>com.github.wujiuye</groupId> 
  3.     <artifactId>easymulti-datasource-spring-boot-starter</artifactId> 
  4.     <version>${version}</version> 
  5. </dependency> 

 

版本选择注意事项说明如下图所示。 

动态切换数据源

  • 使用注解切换数据源:@EasyMutiDataSource;
  • 使用API切换数据源:DataSourceContextHolder#setDataSource。

AOP中注册事务监听器

在application配置文件中打开追踪事务方法调用链路的开关,配置如下。

  1. ## 监控事务方法调用链路 
  2. easymuti: 
  3.   transaction
  4.     open-chain: true 

定义切面,拦截Mapper方法,在环绕方法中实现更新缓存的逻辑,代码如下。 

  • TransactionInvokeContext.currentExistTransaction:判断当前调用链路上是否存在事务;
  • TransactionInvokeContext.addCurrentTransactionMethodPopListener:给当前事务绑定一个监听器(PopTransactionListener),当事务提交或者回滚时监听器被调用。

如上代码所示,首先是判断当前调用链路上是否存在事务,如果存在,则给当前事务注入一个监听器,由监听器完成缓存更新逻辑,如果不存在事务,在目标方法执行完成后且无异常抛出时执行更新缓存逻辑。

监听SQL

easymulti-datasource-mybatis支持sql埋点监听功能,并且支持监听事务状态,如果当前sql执行存在事务中,则会在事务提交后才会回调sql监听者。

第一步:启用sql埋点监听功能,并且启用事务调用链路追踪功能。

  1. easymuti:  
  2.   transaction:  
  3.     open-chain: true 
  4.   sql-watcher: 
  5.     enable: true 

第二步:编写观察者,可以有n多个,并且多个观察者也可观察同一个表、甚至相同字段。

  1. @Component 
  2. @Slf4j 
  3. public class TestTableFieldObserver implements TableFieldObserver , InitializingBean { 
  4.  
  5.     @Override 
  6.     public Set<WatchMetadata> observeMetadatas() { 
  7.        // 在这里注册要监听哪些表的哪些字段 
  8.     } 
  9.  
  10.     /** 
  11.      * 监听到sql时被同步调用 
  12.      * 
  13.      * @param commandType 事件类型 
  14.      * @param matchResult 匹配的ITEM 
  15.      * @return 返回异步消费者 
  16.      */ 
  17.     @Override 
  18.     public AsyncConsumer observe(CommandType commandType, MatchItem matchResult) { 
  19.         // 同步消费 
  20.         // 这里是sql执行之前,可在sql执行之前做一些事情,比如新旧数据的对比,这里查出旧数据 
  21.  
  22.         // 异步消费,再sql执行完成时,或者在事务方法执行完成时(如果存在事务),完成指:正常执行完成 or 方法异常退出 
  23.         return throwable -> { 
  24.             // sql执行抛出异常不处理 
  25.             if (throwable != null) { 
  26.                 return
  27.             } 
  28.             // 消费事件 
  29.             // .... 
  30.         }; 
  31.     } 
  32.  

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的依赖,如下。

  1. <dependency> 
  2.     <groupId>com.github.wujiuye</groupId> 
  3.     <artifactId>easymulti-datasource-r2dbc</artifactId> 
  4.     <version>${version}</version> 
  5. </dependency> 

 

此时,只需要额外添加用到的数据库类型对应的驱动依赖即可,例如,添加mysql的r2dbc驱动。

  1. <dependency> 
  2.     <groupId>dev.miku</groupId> 
  3.     <artifactId>r2dbc-mysql</artifactId> 
  4.     <version>0.8.2.RELEASE</version> 
  5. </dependency> 

 

如果使用主从模式,则使用如下配置。

  1. easymuti: 
  2.   database
  3.     r2dbc: 
  4.       master-slave-mode: 
  5.         master: 
  6.           url: r2dbc:mysql://127.0.0.1:3306/r2dbc_stu 
  7.           username: root 
  8.           password
  9.           pool: 
  10.             max-size: 5 
  11.             idel-timeout: 60 
  12.         slave: 
  13.           url: r2dbc:mysql://127.0.0.1:3306/r2dbc_stu 
  14.           username: root 
  15.           password
  16.           pool: 
  17.             max-size: 5 
  18.             idel-timeout: 60 

master会被设置为默认使用的数据源,slave有则配置,没有也可以为空。虽然slave允许为空,但如果真的不需要多数据源,也是没有必要使用easymulti-datasource-r2dbc的。

如果使用Cluster模式,则使用如下配置。

  1. easymuti: 
  2.   database
  3.     r2dbc: 
  4.       cluster-mode: 
  5.         first
  6.           url: r2dbc:mysql://127.0.0.1:3306/r2dbc_stu 
  7.           username: root 
  8.           password
  9.           pool: 
  10.             max-size: 5 
  11.             idel-timeout: 60 
  12.         second
  13.           url: r2dbc:mysql://127.0.0.1:3306/r2dbc_stu 
  14.           username: root 
  15.           password
  16.           pool: 
  17.             max-size: 5 
  18.             idel-timeout: 60 
  19.         third: 
  20.           url: r2dbc:mysql://127.0.0.1:3306/r2dbc_stu 
  21.           username: root 
  22.           password
  23.           pool: 
  24.             max-size: 5 
  25.             idel-timeout: 60 

其中first会被设置为默认使用的数据源,second与third可以为空。

声明式动态切换数据源

声明式动态切换数据源即使用注解方式动态切换数据源,只需要在spring bean的public方法或者类上添加@R2dbcDataBase注解,将注解的value属性指定为使用的数据源。

示例代码如下。

  1. @Service 
  2. public class PersonService { 
  3.  
  4.     @Resource 
  5.     private PersonRepository personRepository; 
  6.    
  7.     // 方法返回值类型为Mono测试 
  8.     @R2dbcDataBase(MasterSlaveMode.Master) 
  9.     @Transactional(rollbackFor = Throwable.class) 
  10.     public Mono<Integer> addPerson(Person... persons) { 
  11.         Mono<Integer> txOp = null
  12.         for (Person person : persons) { 
  13.             if (txOp == null) { 
  14.                 txOp = personRepository.insertPerson(person.getId(), person.getName(), person.getAge()); 
  15.             } else { 
  16.                 txOp = txOp.then(personRepository.insertPerson(person.getId(), person.getName(), person.getAge())); 
  17.             } 
  18.         } 
  19.         return txOp; 
  20.     } 
  21.  
  22.     // 方法返回值类型为Flux测试 
  23.     @R2dbcDataBase(MasterSlaveMode.Master) 
  24.     @Transactional(rollbackFor = Throwable.class) 
  25.     public Flux<Integer> addPersons(Flux<Person> persons) { 
  26.         return persons.flatMap(person -> personRepository.insertPerson(person.getId(), person.getName(), person.getAge())); 
  27.     } 
  28.  
  • 如果是主从模式,@R2dbcDataBase注解的value属性可选值参见MasterSlaveMode接口声明的常量;
  • 如果是Cluster模式,@R2dbcDataBase注解的value属性可选值参见ClusterMode接口声明的常量;

编程式动态切换数据源

声明式切换数据源的实现是依赖编程式切换数据源实现的,因此,我们也可以直接编写代码切换数据源,而不需要将方法改为public暴露出去。

只需要调用EasyMutiR2dbcRoutingConnectionFactory提供的静态方法putDataSource为Context写入使用的数据源,代码如下。

  1. public class RoutingTest extends SupporSpringBootTest { 
  2.  
  3.     @Resource 
  4.     private DatabaseClient client; 
  5.     @Resource 
  6.     private ReactiveTransactionManager reactiveTransactionManager; 
  7.  
  8.     @Test 
  9.     public void test() throws InterruptedException { 
  10.         TransactionalOperator operator = TransactionalOperator.create(reactiveTransactionManager); 
  11.         Mono<Void> atomicOperation = client.execute("INSERT INTO person (id, name, age) VALUES(:id, :name, :age)"
  12.                 .bind("id""joe"
  13.                 .bind("name""Joe"
  14.                 .bind("age", 34) 
  15.                 .fetch().rowsUpdated() 
  16.                 .then(client.execute("INSERT INTO person (id, name) VALUES(:id, :name)"
  17.                         .bind("id""joe"
  18.                         .bind("name""Joe"
  19.                         .fetch().rowsUpdated()) 
  20.                 .then(); 
  21.         // 包装事务 
  22.         Mono<Void> txOperation = operator.transactional(atomicOperation); 
  23.         // 包装切换数据源 
  24.         EasyMutiR2dbcRoutingConnectionFactory.putDataSource(txOperation, MasterSlaveMode.Slave).subscribe(); 
  25.         TimeUnit.SECONDS.sleep(Integer.MAX_VALUE); 
  26.     } 
  27.  

 

需要注意,如果需要使用事务,必须先调用TransactionalOperator对象的transactional方法,再调用EasyMutiR2dbcRoutingConnectionFactory的putDataSource方法。

本文转载自微信公众号「Java艺术」,可以通过以下二维码关注。转载本文请联系Java艺术公众号。

 

责任编辑:武晓燕 来源: Java艺术
相关推荐

2017-09-04 14:52:51

Tomcat线程数据源

2023-10-18 15:25:29

数据源数据库

2009-06-15 13:24:46

JBoss数据源

2010-12-27 09:59:11

ODBC数据源

2023-11-27 09:16:53

Python数据源类型

2017-06-14 23:42:27

大数据数据源架构

2009-07-21 17:41:58

JDBC数据源

2009-09-15 17:15:33

Linq排序

2009-09-08 11:09:39

LINQ数据源

2023-11-27 07:33:55

2013-06-07 10:05:18

2011-07-13 15:32:38

ODBC数据库驱动程序系统DSN

2022-05-10 10:43:35

数据源动态切换Spring

2014-11-20 09:47:06

Java

2023-09-07 08:39:39

copy属性数据源

2009-07-28 14:22:05

数据源控件ASP.NET

2020-12-31 07:55:33

spring bootMybatis数据库

2013-06-09 10:15:09

2021-10-18 06:54:47

数据源数据预处理

2009-12-28 14:19:51

WPF更新数据源
点赞
收藏

51CTO技术栈公众号