对于springboot使用多数据源,有一个点就是对于数据源的切换是值得分析的。
数据源切换方式有几种?
对于同一个方法中使用不同数据源时配合spring的事务为何会切换数据源失败?那如何解决?如果解决了那会不会有分布式事务的问题(解决的方案可能是分不同事务去解决的,那不同事务就存在事务一致性(分布式事务的问题))
一、实现多数据源的方式
实现多数据源的方式主要有三种
1、不同包(实际不常用)
2、AOP(基于 Spring AbstractRoutingDataSource 做拓展)
3、数据库中间件(比较完美解决)
其中第二种和第三种较多人用
这里对于第二种情况进行分析,由于在第二种情况下结合spring事务会产生数据源切换不成功情况,此时则使用开源 baomidou 的一个项目
dynamic-datasource-spring-boot-starter来实现
多数据源的功能。
二、baomidou 多数据源(dynamic-datasource-spring-boot-starter
)
关于
dynamic-datasource-spring-boot-starter
的介绍,胖友自己看 官方文档 。😈 它和 MyBatis-Plus 都是开发者 baomidou 提供的。1、pom文件:
<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.1.3.RELEASE</version> <relativePath/> <!-- lookup parent from repository --> </parent> <dependencies> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!-- 实现对数据库连接池的自动化配置 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-jdbc</artifactId> </dependency> <dependency> <!-- 本示例,我们使用 MySQL --> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>5.1.48</version> </dependency> <!-- 实现对 MyBatis 的自动化配置 --> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>2.1.1</version> </dependency> <!-- 实现对 dynamic-datasource 的自动化配置 --> <dependency> <groupId>com.baomidou</groupId> <artifactId>dynamic-datasource-spring-boot-starter</artifactId> <version>2.5.7</version> </dependency> <!-- 不造为啥 dynamic-datasource-spring-boot-starter 会依赖这个 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-actuator</artifactId> </dependency> <!-- 方便等会写单元测试 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> </dependencies>
yml (可以自己加上druid等连接池,这里为了简化容易看就不加了)
spring: datasource: # dynamic-datasource-spring-boot-starter 动态数据源的配置内容 dynamic: primary: users # 设置默认的数据源或者数据源组,默认值即为 master datasource: # 订单 orders 数据源配置 orders: url: jdbc:mysql://127.0.0.1:3306/test_orders?useSSL=false&useUnicode=true&characterEncoding=UTF-8 driver-class-name: com.mysql.jdbc.Driver username: root password: 123456 # 用户 users 数据源配置 users: url: jdbc:mysql://127.0.0.1:3306/test_users?useSSL=false&useUnicode=true&characterEncoding=UTF-8 driver-class-name: com.mysql.jdbc.Driver username: root password: 123456 # mybatis 配置内容 mybatis: #config-location: classpath:mybatis-config.xml # 配置 MyBatis 配置文件路径 mapper-locations: classpath:mapper/*.xml # 配置 Mapper XML 地址 type-aliases-package: com.datasource.entity # 配置数据库实体包路径 configuration: map-underscore-to-camel-case: true #开启驼峰 server: port: 9001
启动类:
@SpringBootApplication @MapperScan(basePackages = "com.datasource.mapper")//扫描mapper接口 @EnableAspectJAutoProxy(exposeProxy = true)//配置 Spring AOP 能将当前代理对象设置到 AopContext 中,后面我们需要用AopContext拿到aop的代理对象 public class Application { public static void main(String[] args) { SpringApplication.run(Application.class,args); } }
枚举类:
//枚举类 public class DBConstants { /** * 数据源分组 - 订单库 */ public static final String DATASOURCE_ORDERS = "orders"; /** * 数据源分组 - 用户库 */ public static final String DATASOURCE_USERS = "users"; }
新建order库和order表
新建user库和表
实体类:
order
@Data public class Order implements Serializable { private int id; private int userId; }
user
@Data public class User implements Serializable { private int id; private String username; private String password; private Date createTime; }
mapper接口:
//order的mapper接口 @Repository @DS(DBConstants.DATASOURCE_ORDERS) public interface OrderMapper { Order selectById(@Param("id") Integer id); } //user的mapper接口 @Repository @DS(DBConstants.DATASOURCE_USERS)//dynamic-datasource-spring-boot-starter,数据源名 public interface UserMapper { User selectById(@Param("id") Integer id); }
xml:
//order的xml文件 <mapper namespace="com.datasource.mapper.OrderMapper"> <sql id="FIELDS"> id, user_id </sql> <select id="selectById" parameterType="integer" resultType="com.datasource.entity.Order"> SELECT <include refid="FIELDS" /> FROM orders WHERE id = #{id} </select> </mapper> //user的xml文件 <mapper namespace="com.datasource.mapper.UserMapper"> <sql id="FIELDS"> id, username,password,create_time </sql> <select id="selectById" parameterType="integer" resultType="com.datasource.entity.User"> SELECT <include refid="FIELDS" /> FROM users WHERE id = #{id} </select> </mapper>
service层(这里设置了5个场景,等下分别来分析各个场景的问题)
@Service public class OrderService { @Autowired private OrderMapper orderMapper; @Autowired private UserMapper userMapper; //获取到aop的代理对象(如果用this调用此时拿到的不是aop的代理对象) private OrderService self() { return (OrderService) AopContext.currentProxy(); } //-------------------------场景1---------------------------- //未开启事务,则可以自己使用自己的数据源,并不会报异常 public void method01() { // 查询订单 Order order = orderMapper.selectById(1); System.out.println(order); // 查询用户 User user = userMapper.selectById(1); System.out.println(user); } //-------------------------场景2---------------------------- //此时开启事务(经过aop代理),会抛出找不到表的情况,因为此时事务开启就会默认拿到默认的数据源user, // 而第一次查询是要使用order数据源的,此时因为spring事务则无法从默认的user切换到order数据源上 //这里就是结合spring事务出现的数据源切换不成功的情况(具体原因后面分析) @Transactional public void method02() { // 查询订单 Order order = orderMapper.selectById(1); System.out.println(order); // 查询用户 User user = userMapper.selectById(1); System.out.println(user); } //-------------------------场景3---------------------------- public void method03() { // 查询订单 self().method031(); // 查询用户 self().method032(); } @Transactional // 报错,因为此时获取的是 primary 对应的 DataSource ,即 users 。 public void method031() { Order order = orderMapper.selectById(1); System.out.println(order); } @Transactional public void method032() { User user = userMapper.selectById(1); System.out.println(user); } //-------------------------场景4---------------------------- //未报异常,正常结束(两个方法各自执行各自的事务,并且根据@DS获取到各自的数据源) public void method04() { // 查询订单 self().method041(); // 查询用户 self().method042(); } @Transactional @DS(DBConstants.DATASOURCE_ORDERS) public void method041() { Order order = orderMapper.selectById(1); System.out.println(order); } @Transactional @DS(DBConstants.DATASOURCE_USERS) public void method042() { User user = userMapper.selectById(1); System.out.println(user); } //-------------------------场景5---------------------------- //正常执行,05方法中调用052方法,在一个事务中调用另一个事务方法, // 另一个事务方法采用,重新建立一个事务的方法并使用自己的数据源 // 此时在05方法中则会出现两个事务,这时就涉及到分布式事务 @Transactional @DS(DBConstants.DATASOURCE_ORDERS)//声明自己要用到的数据源,这样就不会去拿默认数据源了 public void method05() { // 查询订单 Order order = orderMapper.selectById(1); System.out.println(order); // 查询用户 self().method052(); } @Transactional(propagation = Propagation.REQUIRES_NEW) @DS(DBConstants.DATASOURCE_USERS) public void method052() { User user = userMapper.selectById(1); System.out.println(user); } }
此时用controller的接口来测试各个场景
@RestController public class DatasourceController { @Autowired OrderService orderService; //场景1 @GetMapping("/method1") public void getMethod1(){ orderService.method01(); } //场景2 @GetMapping("/method2") public void getMethod2(){ orderService.method02(); } //场景3 @GetMapping("/method3") public void getMethod3(){ orderService.method03(); } //场景4 @GetMapping("/method4") public void getMethod4(){ orderService.method04(); } //场景5 @GetMapping("/method5") public void getMethod5(){ orderService.method05(); } }
下面针对各个场景进行分析
1、场景1
此时场景1未使用spring的事务,即没开启事务,访问场景1接口http://localhost:9001/method1
此时运行正常,控制台打印出正常信息
原因:
- 方法未使用
@Transactional
注解,不会开启事务。- 对于 OrderMapper 和 UserMapper 的查询操作,分别使用其接口上的
@DS
注解,找到对应的数据源,执行操作。- 这样一看,在未开启事务的情况下,我们已经能够自由的使用多数据源进行切换。
public void method01() { // 查询订单 Order order = orderMapper.selectById(1); System.out.println(order); // 查询用户 User user = userMapper.selectById(1); System.out.println(user); }
2、场景2
@Transactional public void method02() { // 查询订单 Order order = orderMapper.selectById(1); System.out.println(order); // 查询用户 User user = userMapper.selectById(1); System.out.println(user); }
此时报错:Table 'test_users.orders' doesn't exist (即在tset_users数据库中找不到orders表)
为什么?我们不是在mapper接口各自的使用的各自的数据源?
注意,此时的错误发生在orderMapper查询该行代码上,而此时从错误中我们可以看出他是在user库中找order表?
那不就是说我们在执行orderMapper接口查询时拿到的是user的数据源信息(所以才会去user库)
那么这是为什么呢?我们不是在 OrderMapper 上,声明使用
orders
数据源了么?结果为什么会使用users
数据库,路由到test_users
库上呢。原因如下:
- 这里,就和 Spring 事务的实现机制有关系。因为方法添加了
@Transactional
注解,Spring 事务就会生效。此时,Spring TransactionInterceptor 会通过 AOP 拦截该方法,创建事务。而创建事务,势必就会获得数据源。那么,TransactionInterceptor 会使用 Spring DataSourceTransactionManager 创建事务,并将事务信息通过 ThreadLocal 绑定在当前线程。- 而事务信息,就包括事务对应的 Connection 连接。那也就意味着,还没走到 OrderMapper 的查询操作,Connection 就已经被创建出来了。并且,因为事务信息会和当前线程绑定在一起,在 OrderMapper 在查询操作需要获得 Connection 时,就直接拿到当前线程绑定的 Connection ,而不是 OrderMapper 添加
@DS
注解所对应的 DataSource 所对应的 Connection 。- OK ,那么我们现在可以把问题聚焦到 DataSourceTransactionManager 是怎么获取 DataSource 从而获得 Connection 的了。对于每个 DataSourceTransactionManager 数据库事务管理器,创建时都会传入其需要管理的 DataSource 数据源。在使用
dynamic-datasource-spring-boot-starter
时,它创建了一个 DynamicRoutingDataSource ,传入到 DataSourceTransactionManager 中。- 而 DynamicRoutingDataSource 负责管理我们配置的多个数据源。例如说,本示例中就管理了
orders
、users
两个数据源,并且默认使用users
数据源。那么在当前场景下,DynamicRoutingDataSource 需要基于@DS
获得数据源名,从而获得对应的 DataSource ,结果因为我们在 Service 方法上,并没有添加@DS
注解,所以它只好返回默认数据源,也就是users
。故此,就发生了Table 'test_users.orders' doesn't exist
的异常。也就是说此时要切换数据源,需要在service层(即 @Transactional的方法上设置是哪个数据源,这样DynamicRoutingDataSource才会去拿到相应的数据源)设置使用哪个数据源,如果不设置此时DynamicRoutingDataSource会去拿到我们默认的数据源(即user),而后面的场景4就是根据这样实现的。
(那么这里提出一个疑问:mapper接口上设置的数据源不就不起作用?那是为什么呢?这个问题需要我再去学学然后再回来解释)(亲测在场景4中使用了在service设置数据源后删掉mapper接口的数据源设置后没有影响结果)
3、场景3
public void method03() { // 查询订单 self().method031(); // 查询用户 self().method032(); } @Transactional // 报错,因为此时获取的是 primary 对应的 DataSource ,即 users 。 public void method031() { OrderDO order = orderMapper.selectById(1); System.out.println(order); } @Transactional public void method032() { UserDO user = userMapper.selectById(1); System.out.println(user); }
抛出异常:Table 'test_users.orders' doesn't exist
和场景2等价的。
其中self()是获取到aop的代理对象,如果此时,我们将
#self()
代码替换成this
之后,诶,结果就正常执行。这又是为什么呢?其实,这样调整后,因为this
不是代理对象,所以#method031()
和#method032()
方法上的@Transactional
直接没有作用,Spring 事务根本没有生效。所以,最终结果和场景一是等价的。4、场景4
public void method04() { // 查询订单 self().method041(); // 查询用户 self().method042(); } @Transactional @DS(DBConstants.DATASOURCE_ORDERS) public void method041() { OrderDO order = orderMapper.selectById(1); System.out.println(order); } @Transactional @DS(DBConstants.DATASOURCE_USERS) public void method042() { UserDO user = userMapper.selectById(1); System.out.println(user); }
正常执行,原因:
- 和
@method03()
方法,差异在于,#method041()
和#method042()
方法上,添加@DS
注解,声明对应使用的 DataSource 。- 执行方法,正常结束,未抛出异常。是不是觉得有点奇怪?
- 在执行
#method041()
方法前,因为有@Transactional
注解,所以 Spring 事务机制触发。DynamicRoutingDataSource 根据@DS
注解,获得对应的orders
的 DataSource ,从而获得 Connection 。所以后续 OrderMapper 执行查询操作时,即使使用的是线程绑定的 Connection ,也可能不会报错。😈 嘿嘿,实际上,此时 OrderMapper 上的@DS
注解,也没有作用。- 对于
#method042()
,也是同理。但是,我们上面不是提了 Connection 会绑定在当前线程么?那么,在#method042()
方法中,应该使用的是#method041()
的orders
对应的 Connection 呀。在 Spring 事务机制中,在一个事务执行完成后,会将事务信息和当前线程解绑。所以,在执行#method042()
方法前,又可以执行一轮事务的逻辑。- 【重要】总的来说,对于声明了
@Transactional
的 Service 方法上,也同时通过@DS
声明对应的数据源。这时就解决了场景2、3的数据源切换不成功的问题了,即在各个@Transactional设置不同的数据源,但这里要明白,设置不同的数据源,此时场景4的method4方法是启动了两个事务,则此时就会有分布式事务问题(即可能事务1执行完成,事务2回滚了,而method4实际要达到的目的是事务1、2要么都完成,要么都回滚,所以这里的分布式事务需要进一步解决)
5、场景5
@Transactional @DS(DBConstants.DATASOURCE_ORDERS) public void method05() { // 查询订单 OrderDO order = orderMapper.selectById(1); System.out.println(order); // 查询用户 self().method052(); } @Transactional(propagation = Propagation.REQUIRES_NEW) @DS(DBConstants.DATASOURCE_USERS) public void method052() { UserDO user = userMapper.selectById(1); System.out.println(user); }
在一个事务中重开一个事务,这时也解决了事务中切换不成功的情况
执行正常,原因:
- 和
@method04()
方法,差异在于,我们直接在#method05()
方法中,此时处于一个事务中,直接调用了#method052()
方法。- 执行方法,正常结束,未抛出异常。是不是觉得有点奇怪?
- 我们仔细看看
#method052()
方法,我们添加的@Transactionl
注解,使用的事务传播级别是Propagation.REQUIRES_NEW
。此时,在执行#method052()
方法之前,TransactionInterceptor 会将原事务挂起,暂时性的将原事务信息和当前线程解绑。
- 所以,在执行
#method052()
方法前,又可以执行一轮事务的逻辑。- 之后,在执行
#method052()
方法完成后,会将原事务恢复,重新将原事务信息和当前线程绑定。注意:这里由于开启了两个事务,也会存在分布式事务问题。
而这个场景则说明如何在一个事务方法中切换数据源
上面是对数据源源切换使用做了介绍,而多数据源也涉及了读写分离、分库分表的实现,虽然上面这种方案也可以实现,但是每次都要去配置一个数据源和其他配置信息1,这会非常的麻烦,那么下面会介绍一款开源的数据库中间件来解决这些事情
我在看见网上还有其他的解决方案:大概是不使用spring的声明式事务,自己提交回滚事务,从而达到切换数据源成功(即编程式事务):https://www.cnblogs.com/cq-yangzhou/p/10945779.html
https://blog.csdn.net/u010928589/article/details/91348761
https://blog.csdn.net/qq1010830256/article/details/106652663
(这个方法也值得我分析实现)
sharedingsphere
上面对于基于 Spring AbstractRoutingDataSource 做拓展(aop)的方案结合开源包进行了分析,虽然上面可以进行数据源切换,但是其解决方法中开启了多个事务,此时则会面临分布式事务的问题,现在对于分布式事务的解决方案,主要有下面几个方案:
刚性事务/低并发:2/3pc柔性事务/长事务/高并发:tcc、saga、mq等
那么我们选择什么呢?
此时我们就需要选择一个阿帕奇的开源项目sharedingsphere,其中包含shareding jdbc(client模式),sharding proxy(proxy代理模式)、和正在开发的shareding sidecar。
我们可以使用其中的shareding jdbc或者sharding proxy,这里我们主要选择前者,其一少了一层代理层性能更高,但是配置可能会麻烦一点(现在比较多的大厂还是在用client模式的)。而且对于分布式事务,sharedingsphere也支持刚性柔性事务(seata等框架),可以完美的解决多数据源、分库分表、读写分离、分布式事务的解决方案。
其实还有一款比较多人使用的数据库中间件mycat(不太适合高并发)(client模式)
下面有关一个分库分表技术说明文章:https://www.bilibili.com/read/cv7536093?spm_id_from=333.788.b_636f6d6d656e74.23
对于sharedingsphere我不打算在这篇文章讲,后面会写关于怎么使用sharedingsphere来实现多数据源切换、分库分表、读写分离、分布式事务问题的实现。
本文主要参考这位大哥的文章:http://www.iocoder.cn/Spring-Boot/dynamic-datasource/#