Spring Boot 2系列(五十):Spring AOP 实现动态数据源切换

项目可能会有多个数据源,例如读写分离至少有两个数据库,或一个主库多个从库,或一个系统里面不同业务拆分有各自的数据库,在使用时需要确定使用正确的数据源。

Spring 多数据源实现方式大概有2种,一种是新建多个 MapperScan 扫描不同 Mapper 包,另一种是继承 AbstractRoutingDataSource 实现动态路由。

本篇是基于 Spring AOP 实现数据源动态路由,多个 MapperScan 扫描不同的 Mapper 包的方式可参考 Spring Boot 2实践系列(三十九):Spring Boot 2.x + Mybatis + Druid + Common Mapper 配置多数据源

相关知识可参考 Spring Boot 2实践系列(四十八):Spring AOP详解与应用Spring(二十四) Spring Transaction 事务管理机制

动态数据源

实现原理分析

Spring JDBC 为实现多数据源动态路由提供了 AbstractRoutingDataSource 抽象类,该类继承了 AbstractDataSource ,同时实现了 InitializingBeanAbstractDataSourceDataSource 接口的抽象实现,见下类图。

AbstractDataSource

AbstractRoutingDataSource 是 DataSource 的间接抽象实现,是基于指定的 Key 来选择(路由)目标的数据源,在运行时切换数据源,通常是将 Key 绑定到当前线程上下文中来传递;

AbstractRoutingDataSource 实现了 InitializingBean 接口,重写了 afterPropertiesSet() 方法,其具体子类在 Spring 启动时会被 BeanFactory 初始化为 Bean,并在设置了所有属性后,调用 afterPropertiesSet() 方法对其总体配置和最终初始化执行验证 。

  1. afterPropertiesSet() 方法

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    @Override
    public void afterPropertiesSet() {
    if (this.targetDataSources == null) {
    throw new IllegalArgumentException("Property 'targetDataSources' is required");
    }
    this.resolvedDataSources = new HashMap<>(this.targetDataSources.size());
    this.targetDataSources.forEach((key, value) -> {
    Object lookupKey = resolveSpecifiedLookupKey(key);
    DataSource dataSource = resolveSpecifiedDataSource(value);
    // 将 targetDataSources 转为 resolvedDataSources
    this.resolvedDataSources.put(lookupKey, dataSource);
    });
    if (this.defaultTargetDataSource != null) {
    // 将 defaultTargetDataSource 转为 resolvedDefaultDataSource
    this.resolvedDefaultDataSource = resolveSpecifiedDataSource(this.defaultTargetDataSource);
    }
    }

    targetDataSourcesdefaultTargetDataSource 是 AbstractRoutingDataSource 的两个属性,其具体子类在初始化为 Bean 时设置这两个属性值。

    此方法将 targetDataSources 转换为 resolvedDataSources,将 defaultTargetDataSource 转换为 resolvedDefaultDataSource,内部实际是从 resolvedDataSourcesresolvedDefaultDataSource 来获取数据源的 。

    如果数据源是可通过后台管理页面动态配置的,获取自定义的动态数据源 Bean,设置 targetDataSourcesdefaultTargetDataSource ,但仅仅将数据源维护在 targetDataSources 是不会生效的,还得手动调用 afterPropertiesSet() 方法再次转换。

  2. getConnection()determineTargetDataSource() 确定数据源创建链接

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    @Override
    public Connection getConnection() throws SQLException {
    // 下面调用的 getConnection() 是具体的数据源实现 DataSource 接口重写的方法
    // 例如, DruidDataSource 或 HikariDataSource 重写了 getConnection() 方法
    // 最终是从数据库连接池中取出连接
    return determineTargetDataSource().getConnection();
    }
    @Override
    public Connection getConnection(String username, String password) throws SQLException {
    return determineTargetDataSource().getConnection(username, password);
    }

    /**
    * 检查当前目标数据源,调用 determineCurrentLookupKey() 确定要使用的数据源 Key
    * 根据 Key 从 Map 中取出数据源或使用默认数据源返回
    */
    protected DataSource determineTargetDataSource() {
    Assert.notNull(this.resolvedDataSources, "DataSource router not initialized");
    Object lookupKey = determineCurrentLookupKey();
    DataSource dataSource = this.resolvedDataSources.get(lookupKey);
    if (dataSource == null && (this.lenientFallback || lookupKey == null)) {
    //如果为空,使用默认数据源
    dataSource = this.resolvedDefaultDataSource;
    }
    if (dataSource == null) {
    throw new IllegalStateException("Cannot determine target DataSource for lookup key [" + lookupKey + "]");
    }
    return dataSource;
    }
  3. determineCurrentLookupKey() 数据源 Key

    1
    2
    @Nullable
    protected abstract Object determineCurrentLookupKey();

    这是一个抽象方法,AbstractRoutingDataSource 的具体子类必须实现此方法,且不为空,返回的是数据源 Map( Map<Object, DataSource> resolvedDataSources ) 的 Key,即确定要使用的数据源的 Key。

实现与使用

基于以上分析,应该对动态数据源实现原理有个初步了解。

下面是基于 Spring Boot + Druid + Mybatis + MySQL 配置动态数据源来实现读写分离。

动态数据源实现

数据库

准备至少两个数据库

sakila_mastersakila_slave 两个库,数据库及表结构和表数据使用 MySQL 的测试数据库 Github -> datacharmer / test_db -> sakila

创建项目

创建 Spring Boot 项目,排除掉 Spring Boot 默认启用数据源自动配置

1
2
3
4
5
6
@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class})
public class DatasourceApplication {
public static void main(String[] args) {
SpringApplication.run(DatasourceApplication.class, args);
}
}

引入依赖

pom.xml 文件导入依赖包

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.2.2.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.springboot.datasource</groupId>
<artifactId>datasource</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>datasource</name>
<description>Demo project for Spring Boot</description>

<properties>
<java.version>1.8</java.version>
</properties>

<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jdbc</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.1.1</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.1.9</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.62</version>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
</dependencies>

<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>

</project>

配置数据源属性值

application.properties 添加数据源 和 Mybatis 属性配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#debug=true

#=========== master datasource ===========
spring.datasource.master.name=master
spring.datasource.master.url=jdbc:mysql://localhost:3306/sakila_master?useUnicode=true&characterEncoding=utf-8&allowMultiQueries=true&autoReconnect=true&serverTimezone=GMT%2B8
spring.datasource.master.username=master
spring.datasource.master.password=123456
#=========== slave datasource ===========
spring.datasource.slave.name=slave
spring.datasource.slave.url=jdbc:mysql://localhost:3306/sakila_slave?useUnicode=true&characterEncoding=utf-8&allowMultiQueries=true&autoReconnect=true&serverTimezone=GMT%2B8
spring.datasource.slave.username=slave
spring.datasource.slave.password=123456
#=========== mybatis configure ===========
#mybatis.mapper-locations=classpath:mapper/*.xml
#mybatis.type-aliases-package=com.springboot.datasource.entity
#mybatis.configuration.map-underscore-to-camel-case=true
#=========== mapper log ===========
logging.level.com.springboot.datasource.mapper=debug
#mybatis.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl

定义当前线程变量

定义当前线程绑定数据源 Key

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/**
* 存放线程数据源
*/
public class DataSourceHolder {

private static final ThreadLocal<DataSourceEnum> threadLocal = new ThreadLocal<>();

public static void setDataSource(DataSourceEnum key){
threadLocal.set(key);
}

public static DataSourceEnum getDataSource() {
return threadLocal.get();
}

public static void cleanDataSource(){
threadLocal.remove();
}
}

定义动态数据源

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
/**
* 数据源配置类
*/
@Configuration
@MapperScan(basePackages = "com.springboot.datasource.mapper")
public class DataSourceConfig {

/**
* master datasource
*
* @return DataSource
*/
@Bean(name = "dataSourceMaster")
@ConfigurationProperties("spring.datasource.master")
public DataSource dataSourceMaster() {
return DruidDataSourceBuilder.create().build();
}

/**
* slave datasource
*
* @return DataSource
*/
@Bean(name = "dataSourceSlave")
@ConfigurationProperties("spring.datasource.slave")
public DataSource dataSourceSlave() {
return DruidDataSourceBuilder.create().build();
}

/**
* DynamicDataSource
*
* @param dataSourceMaster
* @param dataSourceSlave
* @return DataSource
*/
@Primary
@Bean(name = "dynamicDataSource")
public DynamicDataSource dataSource(@Qualifier("dataSourceMaster") DataSource dataSourceMaster,
@Qualifier("dataSourceSlave") DataSource dataSourceSlave) {
DynamicDataSource dynamicDataSource = new DynamicDataSource();
dynamicDataSource.setDefaultTargetDataSource(dataSourceMaster);
Map<Object, Object> targetDataSources = new HashMap<>();
targetDataSources.put(DataSourceEnum.MASTER, dataSourceMaster);
targetDataSources.put(DataSourceEnum.SLAVE, dataSourceSlave);
dynamicDataSource.setTargetDataSources(targetDataSources);
return dynamicDataSource;
}

/**
* Mybatis Configuration
*
* @return Configuration
*/
@Bean(name = "mybatisConfiguration")
public org.apache.ibatis.session.Configuration mybatisConfiguration() {
org.apache.ibatis.session.Configuration configuration = new org.apache.ibatis.session.Configuration();
//开启驼峰映射
configuration.setMapUnderscoreToCamelCase(true);
return configuration;
}

/**
* SqlSession Factory
*
* @param dataSource
* @return SqlSessionFactory
* @throws Exception
*/
@Primary
@Bean(name = "sqlSessionFactory")
public SqlSessionFactory sqlSessionFactory(@Qualifier("dynamicDataSource") DataSource dataSource) throws Exception {
SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
sqlSessionFactoryBean.setDataSource(dataSource);
// sqlSessionFactoryBean.setTransactionFactory(new MultiDataSourceTransactionFactory());
//指定mapper xml目录
ResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
sqlSessionFactoryBean.setMapperLocations(resolver.getResources("classpath:mapper/*.xml"));
sqlSessionFactoryBean.setTypeAliasesPackage("com.springboot.datasource.entity");
sqlSessionFactoryBean.setConfiguration(mybatisConfiguration());
return sqlSessionFactoryBean.getObject();
}

/**
* SqlSession Template
*
* @param sqlSessionFactory
* @return SqlSessionTemplate
*/
@Primary
@Bean(name = "sqlSessionTemplate")
public SqlSessionTemplate sqlSessionTemplate(@Qualifier("sqlSessionFactory") SqlSessionFactory sqlSessionFactory) {
return new SqlSessionTemplate(sqlSessionFactory);
}

/**
* Transaction Manager
*
* @param dynamicDataSource
* @return DataSourceTransactionManager
*/
@Bean
public DataSourceTransactionManager transactionManager(@Qualifier("dynamicDataSource") DynamicDataSource dynamicDataSource) {
return new DataSourceTransactionManager(dynamicDataSource);
}
}

定义查找数据源 Key

继承 AbstractRoutingDataSource,重写 determineCurrentLookupKey 方法,获取数据源 Key

1
2
3
4
5
6
7
8
9
10
/**
* 动态数据源
*/
public class DynamicDataSource extends AbstractRoutingDataSource {

@Override
protected Object determineCurrentLookupKey() {
return DataSourceHolder.getDataSource();
}
}

定义数据源枚举

也可以使用 String 类型的静态变量。

1
2
3
public enum  DataSourceEnum {
MASTER,SLAVE
}

定义数据源注解

用户可以使用注解来显式指定要使用的数据源。

1
2
3
4
5
6
7
8
/**
* 数据源选择注解
*/
@Retention(value = RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface DataSourceSelector {
DataSourceEnum name();
}

创建数据源切换AOP

下面示例使用的是环绕通知,最终必须清空当前线程中的数据源。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
/**
* 数据源动态切换 AOP
*/
@Order(Integer.MAX_VALUE - 2000)
@Aspect
@Component
public class DataSourceAspect {
private static final Logger logger = LogManager.getLogger(DataSourceAspect.class);

@Pointcut("execution(* com.springboot.datasource.mapper..*(..))")
public void pointcut() {
}

@Around("pointcut()")
public Object switchDataSource(ProceedingJoinPoint joinPoint) throws Throwable {
MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
Method method = methodSignature.getMethod();
String methodName = method.getName();

DataSourceSelector dsSelector = method.getAnnotation(DataSourceSelector.class);
Transactional transactional = method.getAnnotation(Transactional.class);

if (null != dsSelector) {
//显式指定数据源优先
DataSourceEnum dataSourceKey = dsSelector.name();
DataSourceHolder.setDataSource(dataSourceKey);
logger.info("DataSourceSelector name:{}, DataSource:{}", dsSelector.name(), dataSourceKey);
} else if (null != transactional && transactional.readOnly()) {
//读事务路由到从库
DataSourceHolder.setDataSource(DataSourceEnum.SLAVE);
logger.info("Transactional readOnly:{}, DataSource:{}", transactional.readOnly(), DataSourceEnum.SLAVE);
} else if (methodName.startsWith("get") || methodName.startsWith("query") || methodName.startsWith("find")
|| methodName.startsWith("select") || methodName.startsWith("list")) {
//根据方法前缀判断路由到从库
DataSourceHolder.setDataSource(DataSourceEnum.SLAVE);
logger.info("Method Name:{}, DataSource:{}", methodName, DataSourceEnum.SLAVE);
} else {
//其它到主库
DataSourceHolder.setDataSource(DataSourceEnum.MASTER);
logger.info("Method Name:{}, Datasource:{}", methodName, DataSourceEnum.MASTER);
}

try {
return joinPoint.proceed();
} catch (Throwable throwable) {
//这里必须抛出异常才会触发事务回滚
throw throwable;
} finally {
DataSourceHolder.cleanDataSource();
}
}
}

动态数据源事务

在实际开发中,开启事务通常是在业务(Service)层,Spring 事务管理(DataSourceTransactionManager)只支持单库事务,开启事务,会将数据源缓存到 DataSourceTransactionObject 对象中进行后续的 commitrollback 等操作。即开启了事务后是不能切换数据源的,切换数据源会无效,也就是说,切换数据源要在开启事务之前执行。

本示例中动态切换数据源的 AOP 和注解是作用在 Mapper 层的方法上,最简单的修改是将 AOP 的切点或注释作用在 Controller 中的方法上。不过此方式就使得 Controller 层的责职有些混乱。

完美解决思路:最完美的方式在业务层,开启事务之前就完成数据源切换。自定义事务切面,使用 Order 注解,属性值大于数据源切换切面的值,即 事务切面数据源切换切面 后面执行。

注解事务控制AOP

此方式是关闭 Spring Boot 的事务自动配置,创建自定义的事务切面(AOP),通过数据源获取事务状态控制事务。

  1. 关闭 Spring Boot 事务自动配置

    1
    2
    3
    4
    5
    6
    @SpringBootApplication(exclude = {DataSourceAutoConfiguration.class, DataSourceTransactionManagerAutoConfiguration.class})
    public class DatasourceApplication {
    public static void main(String[] args) {
    SpringApplication.run(DatasourceApplication.class, args);
    }
    }
  2. 修改数据源切换 AOP 的切点表达式指向 service 层

    1
    2
    3
    @Pointcut("execution(* com.springboot.datasource.service..*(..))")
    public void pointcut() {
    }
  3. 创建事务控制 AOP

    自定义事务控制AOP 使用了前置通知开启事务,后置通知提交事务,异常通知回滚事务,使用了组合切点,精确定位到 service 层的 Transactional 注解,即关闭了 Spring Boot 事务自动配置后,Transactional 注解在这里仍然有效(此时,这个 Transactional 相当于是自定义的注解了)。

    注意:数据源切换AOP的 @Order 注解值(*@Order(Integer.MAX_VALUE - 2000)*)

    TransactionalAop.class

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    @Order(Integer.MAX_VALUE - 1000)//此是重点,Order值必须大于动态数据源切换AOP的Order值
    @Aspect
    @Component
    @Scope("prototype")//线程安全,每次调用都是一个新的实例
    public class TransactionalAop {

    private TransactionStatus transactionStatus;

    @Autowired
    private TransactionUtils transactionUtils;

    /**
    * 切点 Service 层
    */
    @Pointcut("execution(* com.springboot.datasource.service..*(..))")
    public void servicePointcut() {
    }

    /**
    * 切点 Transactional 注解
    */
    @Pointcut("@annotation(transactional)")
    public void txAnnotationPointcut(Transactional transactional) {
    }

    /**
    * 开启事务
    * @param joinPoint
    * @param transactional
    * @throws Throwable
    */
    @Before(value = "servicePointcut() && txAnnotationPointcut(transactional)", argNames = "joinPoint,transactional")
    public void before(JoinPoint joinPoint, Transactional transactional) throws Throwable {
    // 1 找到对应的方法,通过方法名和参数类型 因为会有重载
    this.transactionStatus = transactionUtils.begin(transactional);
    }

    /**
    * 正常返回,提交事务
    * @param transactional
    */
    @AfterReturning(value = "servicePointcut() && txAnnotationPointcut(transactional)", argNames = "transactional")
    public void afterReturning(Transactional transactional) {
    transactionUtils.commit(transactionStatus);
    }

    /**
    * 异常通知,回滚事务
    * @param transactional
    */
    @AfterThrowing(value = "servicePointcut() && txAnnotationPointcut(transactional)", argNames = "transactional")
    public void afterThrowing(Transactional transactional) {
    transactionUtils.rollback(transactionStatus);
    }
    }

    从上面代码可以看出,AOP 常用的几种细粒度 通知类型 可以与事务的操作完美契合,可以说 AOP 是为事务而生的,再而扩展适配到其它领域(个人理解,不接受反驳)。

    注意:网上有些文章中的事务通知使用的是 @Around 环绕通知,但没有返回值,此方式是不恰当的,有比较大的局限性,不能在 AOP 层忽略业务层的返回数据;有的有返回值,在返回值前提交事务,是否有其它问题待验证。

    TransactionUtils.class

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    @Component
    public class TransactionUtils {
    private static final Logger logger = LogManager.getLogger(TransactionUtils.class);

    @Autowired
    private DataSourceTransactionManager dataSourceTransactionManager;

    /**
    * begin transactional
    *
    * @param transactional
    * @return TransactionStatus
    */
    public TransactionStatus begin(Transactional transactional) {
    DefaultTransactionAttribute txAttribute = new DefaultTransactionAttribute();

    txAttribute.setQualifier(transactional.value());
    txAttribute.setReadOnly(transactional.readOnly());
    txAttribute.setTimeout(transactional.timeout());
    txAttribute.setIsolationLevel(transactional.isolation().value());
    txAttribute.setPropagationBehavior(transactional.propagation().value());
    //注意: 注解Transactional.rollbackFor() 返回的是一个继承 Throwable 的 Class 对象数组
    //而下面方法只能设置单个继承 Throwable 的 Class 对象,且只对 RuntimeException 和 Error 异常有效
    // txAttribute.rollbackOn()

    TransactionStatus transactionStatus = dataSourceTransactionManager.getTransaction(txAttribute);
    logger.info("begin transactional");
    return transactionStatus;
    }

    /**
    * commit transactional
    *
    * @param transactionStatus
    */
    public void commit(TransactionStatus transactionStatus) {
    dataSourceTransactionManager.commit(transactionStatus);
    logger.info("commit transactional");
    }

    /**
    * rollback transactional
    *
    * @param transactionStatus
    */
    public void rollback(TransactionStatus transactionStatus) {
    dataSourceTransactionManager.rollback(transactionStatus);
    logger.info("rollback transactional");
    }
    }

    注意: TransactionUtils 类可以在业务实现类中注入,在方法的业务逻辑开始前使用此工具类手动开启事务,正常执行完后提交事务,抛出异常后回滚事务,这就是编程式事务,但这是不可取的。

全局事务控制AOP

此方式是采用 Spring AOP 方法名称匹配方法来开启事务,此方式相对就没有注解事务灵活。

重点还是在 @Order 注解的值要大于数据源切换AOP 的 @Order

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
/**
* Service 层全局事务
*/
@Order(Integer.MAX_VALUE - 1000)
@Aspect
@Component
public class TransactionAspect {
private static final Logger logger = LogManager.getLogger(TransactionAspect.class);

private static final int TX_METHOD_TIMEOUT = 5;
private static final String AOP_POINTCUT_EXPRESSION = "execution(* com.springboot.datasource.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<>();
//事务方法前缀
txMap.put("add*", requiredTx);
txMap.put("save*", requiredTx);
txMap.put("insert*", requiredTx);
txMap.put("update*", requiredTx);
txMap.put("delete*", requiredTx);
txMap.put("remove*", requiredTx);
//非事务方法前缀
txMap.put("get*", readOnlyTx);
txMap.put("find*", readOnlyTx);
txMap.put("select*", readOnlyTx);
txMap.put("query*", readOnlyTx);
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());
}
}

动态数据源使用

定义实体类

1
2
3
4
5
6
7
8
9
10
public class Actor implements Serializable {
private static final long serialVersionUID = -1623523874495502121L;

private Long actorId;
private String firstName;
private String lastName;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8" )
private Date lastUpdate;
//---------set/get 方法--------------
}

定义 Controller

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
@RestController
@RequestMapping("/actor")
public class ActorController {
private static final Logger logger = LogManager.getLogger(ActorController.class);

@Autowired
private ActorService actorService;

@GetMapping("/getById")
public Actor getById(Long id) {
Actor actor = actorService.getById(id);
return actor;
}
@GetMapping("/getActor")
public List<Actor> getActor(Actor actor) {
List<Actor> actorList = actorService.getActor(actor);
return actorList;
}

@PostMapping("/save")
public Actor save(Actor actor) {
actor.setLastUpdate(new Date());
actor = actorService.save(actor);
return actor;
}

@PostMapping("/saveActorList")
public void saveActorList(Boolean errorFlag) {
actorService.saveActorList(errorFlag);
}
}

定义 Service

Service 接口

1
2
3
4
5
6
7
8
9
public interface ActorService {
Actor getById(Long id);

Actor save(Actor actor);

List<Actor> getActor(Actor actor);

void saveActorList(Boolean errorFlag);
}

Service 接口实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
@Service
public class ActorServiceImpl implements ActorService {

@Autowired
private ActorMapper actorMapper;

@Override
public Actor getById(Long id) {
Actor actor = actorMapper.getById(id);
return actor;
}

@Override
@Transactional
public Actor save(Actor actor) {
actorMapper.save(actor);
return actor;
}

@Override
@DataSourceSelector(name = DataSourceEnum.MASTER)
public List<Actor> getActor(Actor actor) {
return actorMapper.getActor(actor);
}

@Override
@Transactional
@DataSourceSelector(name = DataSourceEnum.SLAVE)
public void saveActorList(Boolean errorFlag) {
List<Actor> actorList = new ArrayList<>();
actorList.add(new Actor("张","飞", new Date()));
actorList.add(new Actor("关","羽", new Date()));
actorList.add(new Actor("刘","备", new Date()));
actorMapper.saveActorList(actorList);

//抛异常触发事务回滚
if(null != errorFlag && errorFlag){
int i = 1/0;
}

actorMapper.save(new Actor("曹","操",new Date()));
}
}

定义 Mapper

Mapper 接口

1
2
3
4
5
6
7
8
9
10
11
12
@Repository
public interface ActorMapper {

// @Select("select * from actor where actor_id = #{id}")
Actor getById(@Param("id") Long id);

void save(Actor actor);

List<Actor> getActor(Actor actor);

void saveActorList(List<Actor> actorList);
}

Mapper XML 文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
<?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="com.springboot.datasource.mapper.ActorMapper">

<select id="getById" parameterType="long" resultType="actor">
select * from actor where actor_id = #{id}
</select>

<insert id="save" parameterType="actor" useGeneratedKeys="true" keyColumn="actor_id" keyProperty="actorId">
insert into actor (first_name, last_name, last_update) values (#{firstName}, #{lastName}, #{lastUpdate})
</insert>

<select id="getActor" parameterType="actor" resultType="actor">
select * from actor where 1= 1
<if test="null != firstName and '' != firstName">
and first_name = #{firstName}
</if>
<if test="null != lastName and '' != lastName">
and last_name = #{lastName}
</if>
</select>

<insert id="saveActorList" parameterType="java.util.List">
insert into actor (first_name, last_name, last_update) values
<foreach collection="list" index="index" item="item" separator=",">
(#{item.firstName}, #{item.lastName}, #{item.lastUpdate})
</foreach>
</insert>

</mapper>

调用接口测试

  1. 调用保存数据的接口,在两个数据库查看保存的数据查询。

  2. 调用获取数据的接口,注意数据来自于哪个数据库。

  3. 查询日志输出,读 / 写操作是否有切换数据源。

    1
    2
    2019-12-14 16:36:11.327  INFO 15036 --- [nio-8080-exec-1] c.s.d.c.d.aspect.DataSourceAspect        : DataSourceSelector name:SLAVE, DataSource:SLAVE
    2019-12-14 16:36:23.927 INFO 15036 --- [nio-8080-exec-1] com.alibaba.druid.pool.DruidDataSource : {dataSource-1,slave} inited
  4. 调用保存接口,抛出异常触发事务回滚,查看数据是否有写入库,预期是事务回滚,数据库没有脏数据。

动态数据源扩展

需求:假如读写分离数据库是 多主多从 的架构,并要满足数据源可动态配置即时生效,且不修改源码和配置文件。或 Saas 系统,每个客户有自己独自的数据库,业务需要根据客户对应的数据库来进行动态切换。

思路:

  1. 将多个数据源信息存入到 MySQL 中。

  2. 基于动态数据源实现,配置默认数据源,在项目启动时从 MySQL 中查询数据源信息。

    可通过实现 CommandLineRunner 接口,重写 run(String… args) 方法来处理。

  3. 调用数据源配置,创建多个数据源并注册为 Bean,加入到一个集合中。

  4. 定义使用数据源策略,轮循或随机访问,或 根据用户 ID 与 数据源的 映射来选择对应数据源。

上面示例源码

GitHub -> spring-boot-datasource

其它参考

  1. SpringBoot多数据源
  2. Spring 动态切换数据源
  3. Spring Boot 配置动态数据源
  4. Spring 动态切换数据源及事务
  5. SpringBoot数据源加载及其多数据源
  6. Spring中实现多数据源事务管理
  7. SpringBoot+MyBatis+MySQL读写分离
  8. Spring的事务管理入门:编程式事务管理(TransactionTemplate)
  9. 手写 Spring 事务,IoC 和 MVC
  10. Spring JtaTransactionManager 实现多数据源使事务
  11. Spring TransactionManager 源码分析

Spring Boot 2系列(五十):Spring AOP 实现动态数据源切换

http://blog.gxitsky.com/2019/12/11/SpringBoot-50-aop-datasource-switch/

作者

光星

发布于

2019-12-11

更新于

2022-06-17

许可协议

评论