写点什么

Java 踩坑 1|Spring 事务导致多数据源切换失败

作者:itschenxiang
  • 2023-07-12
    广东
  • 本文字数:2884 字

    阅读完需:约 9 分钟

背景

在我的日常开发中,遇到了同一个应用需要接入多数据源,因此需要指定不同数据访问层的数据源,而同时又需要在服务层保证事务,因此在服务层方法上使用了 @Transactional 注解,然而在实际执行时却没有切换数据源。

场景复现

这里为了简便,直接使用开源组件 dynamic-datasource-spring-boot-starter 实现多数据源切换,大家也可以自己动手实现。

1)创建 SpringBoot 工程;

2)引入 dynamic-datasource 依赖:

<dependency>  <groupId>com.baomidou</groupId>  <artifactId>dynamic-datasource-spring-boot-starter</artifactId>  <version>${version}</version></dependency>
复制代码

3)按照 dynamic-datasource 规范添加数据源配置;

spring:  datasource:    dynamic:      datasource:        master:          url: jdbc:h2:mem:master          driver-class-name: org.h2.Driver          init:            schema: classpath:master-schema-h2.sql            data: classpath:master-data-h2.sql        slave_1:          url: jdbc:h2:mem:slave_1          driver-class-name: org.h2.Driver          init:            schema: classpath:slave_1-schema-h2.sql            data: classpath:slave_1-data-h2.sql      strict: false      primary: master
复制代码

4)添加 DDL 及数据:

-- master-schema-h2.sqlDROP TABLE IF EXISTS master_user;
CREATE TABLE master_user( id BIGINT NOT NULL COMMENT '主键ID', user_name VARCHAR(30) NULL DEFAULT NULL COMMENT '姓名', age INT NULL DEFAULT NULL COMMENT '年龄', email VARCHAR(50) NULL DEFAULT NULL COMMENT '邮箱', PRIMARY KEY (id));-- master-data-h2.sqlDELETE FROM master_user;
INSERT INTO master_user (id, user_name, age, email) VALUES (1, 'Jone', 18, 'test1@baomidou.com'), (2, 'Jack', 20, 'test2@baomidou.com'), (3, 'Tom', 28, 'test3@baomidou.com'), (4, 'Sandy', 21, 'test4@baomidou.com'), (5, 'Billie', 24, 'test5@baomidou.com');-- slave_1-schema-h2.sqlDROP TABLE IF EXISTS slave_user;
CREATE TABLE slave_user( id BIGINT NOT NULL COMMENT '主键ID', user_name VARCHAR(30) NULL DEFAULT NULL COMMENT '姓名', age INT NULL DEFAULT NULL COMMENT '年龄', email VARCHAR(50) NULL DEFAULT NULL COMMENT '邮箱', PRIMARY KEY (id));-- slave_1-data-h2.sqlDELETE FROM slave_user;
INSERT INTO slave_user (id, user_name, age, email) VALUES (6, 'Jone', 18, 'test1@baomidou.com'), (7, 'Jack', 20, 'test2@baomidou.com'), (8, 'Tom', 28, 'test3@baomidou.com'), (9, 'Sandy', 21, 'test4@baomidou.com'), (10, 'Billie', 24, 'test5@baomidou.com');
复制代码

4)创建数据对象及 Mapper,这里只列出了 Master 数据源数据对象及 Mapper;

package com.itschenxiang.multidatasource.entity;
import lombok.Data;
@Datapublic class MasterUser { private Long id; private String userName; private Integer age; private String email;}
复制代码


package com.itschenxiang.multidatasource.dao;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;import com.itschenxiang.multidatasource.entity.MasterUser;import org.springframework.stereotype.Repository;
@Repositorypublic interface MasterUserMapper extends BaseMapper<MasterUser> {
}
复制代码

5)新增服务层构建场景;

@Servicepublic class MultiDataSourceService {
@Autowired private MasterUserMapper masterUserMapper;
@Autowired private SlaveUserMapper slaveUserMapper; @Autowired @Lazy private MultiDataSourceService multiDataSourceService; // 单master数据源 @DS("master") public List<MasterUser> accessPrimaryDataSource() { return masterUserMapper.selectList(null); }
// 单slave_1数据源 @DS("slave_1") public List<SlaveUser> accessNotPrimaryDataSource() { return slaveUserMapper.selectList(null); }
// 多数据源,无@Transactional注解 public void multiDataSourceWithoutTransactional() { multiDataSourceService.accessPrimaryDataSource(); multiDataSourceService.accessNotPrimaryDataSource(); }
// 多数据源,有@Transactional注解,mapper执行出错 @Transactional(rollbackFor = Exception.class) public void multiDataSourceWithTransactional() { accessPrimaryDataSource(); accessNotPrimaryDataSource(); }}
复制代码

6)添加单元测试复现问题;这里仅列举了 @Transactional 导致异常的 UT;

@SpringBootTest@ActiveProfiles("ut")@RunWith(SpringRunner.class)public class MultiDataSourceServiceTest {
@Autowired private MultiDataSourceService multiDataSourceService; @Test public void multiDataSourceWithTransactionalTest() { try { multiDataSourceService.multiDataSourceWithTransactional(); } catch (Exception e) { e.printStackTrace(); Assert.assertTrue(e instanceof BadSqlGrammarException); } } }
复制代码

复现代码链接:https://github.com/itschenxiang/spring-boot-examples/tree/main/multi-datasource

根因分析

Spring 开启事务后会维护一个 ConnectionHolder,保证在整个事务下,都是用同一个数据库连接。也就是说:使用了 @Transactional,Spring 会保证整个事务下都复用同一个 connection

需要额外注意的是:单库的事务仍然可用,只要事务下不切换数据源即可。

解决方案

对于确实需要单事务多数据源的场景,解决方案包括:

  1. 从文章标题就可以看到的解决方案:删除事务注解;

  2. Seata 事务;

其实对于大部分合理业务场景,应用可能有多个数据源,但基本都是单数据源事务。我实际遇到的也是单数据源事务,数据源切换失败的问题,根本原因是自定义数据源切换切面执行顺序在 @Transactional 之后,导致无法切换数据源(参考链接 1)。

参考链接


发布于: 刚刚阅读数: 4
用户头像

itschenxiang

关注

还未添加个人签名 2019-05-04 加入

还未添加个人简介

评论

发布
暂无评论
Java 踩坑 1|Spring 事务导致多数据源切换失败_itschenxiang_InfoQ写作社区