dynamic-datasource-spring-boot-starter 是一个基于springboot的快速集成多数据源的启动器。
这里整理出文档。
特性
- 支持
数据源分组
,适用于多种场景,纯粹多库、读写分离、一主多从、混合模式。 - 支持数据库敏感配置信息
加密(可自定义)
ENC()。 - 支持每个数据库独立初始化表结构schema和数据库database。
- 支持无数据源启动,支持
懒加载数据源
(需要的时候再创建连接)。 - 支持
自定义注解
,需继承DS(3.2.0+)。 - 提供并简化对Druid,HikariCp,BeeCp,Dbcp2的快速集成。
- 提供对
Mybatis-Plus
,Quartz,ShardingJdbc,P6sy,Jndi等组件的集成方案。 - 提供
自定义数据源来源
方案(如全从数据库加载)。 - 提供项目启动后
动态增加移除数据源
方案。 - 提供Mybatis环境下的
纯读写分离
方案。 - 提供使用
spel动态参数
解析数据源方案。内置spel,session,header,支持自定义。 - 支持
多层数据源嵌套切换
。(ServiceA >>> ServiceB >>> ServiceC)。 - 提供
基于seata的分布式事务方案
。 - 提供
本地多数据源事务方案
。
约定
- dynamic-datasource只做
切换数据源
这件核心的事情,并不限制你的具体操作
,切换了数据源可以做任何CRUD。 - 配置文件所有以下划线
_
分割的数据源首部
即为组的名称,相同组名称的数据源会放在一个组下。 - 切换数据源可以是组名,也可以是具体数据源名称。
组名则切换时采用负载均衡算法切换
。 - 默认的数据源名称为
master
,你可以通过spring.datasource.dynamic.primary
修改。 方法上的注解优先于类上注解
。- DS支持继承抽象类上的DS,暂不支持继承接口上的DS。
使用方法
1、引入dynamic-datasource-spring-boot-starter
spring-boot 1.5.x、2.x.x
1 | <dependency> |
spring-boot3及以上
1 | <dependency> |
2、配置数据源
1 | spring: |
1 | # 多主多从 |
1 | # 纯粹多库(记得设置primary) |
1 | # 混合配置 |
3、使用 @DS
切换数据源
@DS
可以注解在方法上或类上,同时存在就近原则
,方法上注解
优先于 类上注解
。
注解 | 结果 |
---|---|
没有@DS | 默认数据源 |
@DS(“dsName”) | dsName可以为组名也可以为具体某个库的名称,组名则切换时采用负载均衡算法切换。 |
1 |
|
DS放在哪里合适
DS作为切换数据源核心注解,我应该把他注解在哪里合适?
这其实是初次接触多数据源的人常问的问题。
这其实没有一定的要求,只是有一些经验之谈。
首先开发者要了解的基础知识是,DS注解是基于AOP的原理实现的,aop的常见失效场景应清楚,比如内部调用失效,shiro代理失效。
通常建议DS放在serviceImpl的方法上,如事务注解一样。
注解在Controller的方法上或类上
并不是不可以,作者并不建议的原因主要是controller主要作用是参数的检验等一些基础逻辑的处理,这部分操作常常并不涉及数据库。
注解在service的实现类的方法或类上
这是作者建议的方式,service主要是对业务的处理, 在复杂的场景涉及连续切换不同的数据库。
如果你的方法有通用性,其他service也会调用你的方法。 这样别人就不用重复处理切换数据源。
注解在Mapper上
通常如果你某个Mapper对应的表只在确定的一个库,也是可以的,但是建议只注解在Mapper的类上。
我之前出现多线程失效场景的时候,就是在Mapper上加了注解解决的。
其他使用方式
继承抽象类上的DS
比如我有一个抽象Service,我想实现继承我这个抽象Service下的子Service的所有方法除非重新指定,都用我抽象Service上注解的数据源。是否支持?
答:支持。
继承接口上的DS
3.4.1开始支持, 但是需要注意的是,一个类能实现多个接口,如果多个接口都有DS会如何?
不知道,别这么干。一般不会有人这么干吧,想一想都知道会出问题。
连接池集成
传统多数据源集成连接池如果需要配置的参数较多,则手动编码量大,编程复杂。
dynamic-datasource实现了常见数据源的参数配置,支持全局配置每个数据源继承。
通过本章您可以快速掌握不同连接池的集成配置方案和继承中可能遇见的问题。
连接池必读
每个数据源都有一个type来指定连接池。
每个数据源甚至可以使用不同的连接池,如无特殊需要并不建议。
type
不是必填字段
。
在没有设置type的时候系统会自动按以下顺序查找并使用连接池:
Druid
> HikariCp
> BeeCp
> DBCP2
> Spring Basic
。
1 | spring: |
集成Druid
基础介绍
Druid Github:点我跳转
Druid 文档:点我跳转
dynamic-datasource能简单高效
完成对Druid的集成并完成其参数的多元化配置。
各个库可以使用不同的数据库连接池,如
master使用Druid,slave使用HikariCP
。如果项目同时存在Druid和HikariCP并且未配置连接池type类型,
默认Druid优先于HikariCP
。
集成步骤
1、项目引入 druid-spring-boot-starter
依赖
1 | <dependency> |
2、排除原生Druid的快速配置类
注意:v3.3.3及以上
版本不用排除了。
1 | .class) (exclude = DruidDataSourceAutoConfigure |
某些SpringBoot的版本上面可能无法排除可用以下方式排除。
1 | spring: |
3、参数配置
- 如果参数都未配置,则保持原组件默认值。
- 如果配置了全局参数,则每一个数据源都会继承对应参数。
- 每一个数据源可以单独设置参数覆盖全局参数。
1 | spring: |
以下是我曾经配置过的示例:
1 | # 数据源配置 |
如上即可配置访问用户和密码,访问 http://ip:端口/druid/index.html
查看druid监控。
示例项目
核心源码
Druid数据源创建器
:点我跳转
Druid参数源码
:点我跳转
集成HikariCP
基础介绍
HikariCP Github:点我跳转
HikariCP 文档:点我跳转
集成步骤
1、项目引入 HikariCP
依赖
SpringBoot2.x.x
默认引入了HikariCP,除非对版本有要求无需再次引入。
SpringBoot 1.5.x
需手动引入,对应的版本请根据自己环境和HikariCP官方文档自行选择。
1 | <dependency> |
2、参数配置
- 如果参数都未配置,则保持原组件默认值。
- 如果配置了全局参数,则每一个数据源都会继承对应参数。
- 每一个数据源可以单独设置参数覆盖全局参数。
特别注意,hikaricp原生设置某些字段名和dynamic-datasource不一致,dynamic-datasource是根据参数反射设置,而原生hikaricp字段名称和set名称不一致。
1 | spring: |
常见问题
Failed to validate connection com.mysql.jdbc.JDBC4Connection@xxxxx (No operations allowed after connection closed.)
https://github.com/brettwooldridge/HikariCP/issues/1034
核心意思就是HikariCP要设置 connection-test-query 并且max-lifetime要小于mysql的默认时间。
核心源码
HikariCP数据源创建器
:点我跳转
HikariCP参数配置类
:点我跳转
第三方集成
通过本章您可以掌握数据源在集成MybatisPlus,Quartz,ShardingJdbc的方案。
集成MybatisPlus
基础介绍
MybatisPlus Github:点我跳转
MybatisPlus 文档 点我跳转
只要引入了MybatisPlus相关jar包,项目自动集成,兼容MybatisPlus2.x和3.x的版本。
集成步骤
1、项目引入 Mybatis-Plus
依赖
1 | <dependency> |
2、使用DS
注解进行切换数据源
1 |
|
3、分页配置
1 |
|
注意事项
MybatisPlus内置的ServiceImpl在新增,更改,删除等一些方法上自带事物导致不能切换数据源。
解决方法:
方法1:复制ServiceImpl出来为自己的MyServiceImpl,并去掉所有事务注解。
方法2:创建一个新方法,并在此方法上加DS注解. 如上面的addUser
方法。
FAQ
为什么要单独拿出来说和MybatisPlus的集成?
因为MybatisPlus重写了一些核心类,必须通过解析获得真实的代理对象。
如果自己写多数据源,则很难完成与mp的集成。
核心解析源码:点我跳转
示例项目
其他集成
集成P6spy、集成Quartz、集成ShardingJdbc
由于以上场景暂时没用到,文章中暂时不体现了,如有用到,后面会补充上。
进阶使用
动态添加移除数据源
:指在系统运行过程中动态的添加数据源,删除数据源,多使用于基于数据库的多租户系统。
动态解析数据源
:指数据源切换是不固定的,可以根据域名,根据header参数,根据session参数,根据方法变量等来动态切换。多使用于多租户系统,支持扩展。
数据库加密
:指数据库的url,username,password需要加密,长用于安全性较高系统,不让普通开发通过配置知道生产库的连接配置。
启动初始化脚本
:指数据源启动的时候可以执行schema和data的脚本来初始化结构和数据,dynamic-datasource组件支持每个数据源加载不同的schema和data。
自动读写分离
:适用于mybatis环境,基于mybatis插件实现无注解自动读写分离。
懒启动数据源
:指配置的数据源不立即启动,等需要建立连接的时候再真正初始化连接池。
无数据源启动
:指启动的时候不配置任何数据源,全靠后期系统动态添加。
手动切换数据源
:指某些情况无法根据注解切换,通过工具类手动切换数据源。
动态添加移除数据源
基础介绍
主要在多租户场景中,常常新的一个租户进来需要动态的添加一个数据源到库中,使得系统不用重启即可切换数据源。
使用步骤
1 | import com.baomidou.dynamic.datasource.DynamicRoutingDataSource; |
示例项目
源码分析
1 | public interface DataSourceCreator { |
DataSourceCreator是一个接口,定义了根据参数创建数据源的接口。
其他creator实现此接口,dynamic-datasource项目暂时实现了Druid和Hikaricp的等连接池的实现。
BasicDataSourceCreator 是调用Spring原生的创建方式,只支持最最原始的基础配置。
DefaultDataSourceCreator 是一个通用的创建器,其根据环境自动选择连接池。
动态解析数据源
基础介绍
默认有三个职责链来处理动态参数解析器 header -> session -> spel
所有以 #
开头的参数都会从参数中获取数据源
1 | "#session.tenantName") // 从session获取 ( |
如何扩展
- 我想从cookie中获取参数解析?
- 我想从其他环境属性中来计算?
参考header解析器,继承DsProcessor,如果matches返回true则匹配成功,调用doDetermineDatasource返回匹配到的数据源,否则跳到下一个解析器。
1、自定义一个处理器
1 | public class DsHeaderProcessor extends DsProcessor { |
2、重写完后重新注入一个根据自己解析顺序的解析处理器
1 |
|
注意
如果在Mapper接口下面的方法使用:
1 | public interface UserMapper{ |
对于在接口下面的使用, 由于编译器的默认配置没有将接口参数的元数据写入字节码文件中
所以spring el会无法识别参数名称, 只能用默认的参数命名方式
- 第一个参数: p0,a0,(加入-parameters后,可以使用参数具体的名字,例如这里的#user)
- 第二个参数: p1,a1
- 第三个参数: P2,,a2
可以通过修改maven配置和java编译配置将接口参数信息写入字节码文件
1 | <plugin> |
idea java编译配置: -parameters
java 支持-parameters的最低版本为 1.8
数据库加密
基础介绍
在一些项目中,有对数据库关键字段加密的需求,大家熟悉的是Druid的加密方式。
在连接池集成中的Druid章节里有对应的加密方式,但是如果我不用Druid也想用加密呢?
所以作者copy了Druid的加密相关源码,嘿嘿。
dynamic-datasource项目也支持支持url , username, password
的加密。
使用的RAS加密,相关原理文章 https://www.cnblogs.com/pcheng/p/9629621.html。
简单来说就是生成两把钥匙,私钥加密,公钥解密。
公钥可以发布出去,解密也是用的公钥。
具体使用
1、获得加密字符串
1 | import com.baomidou.dynamic.datasource.toolkit.CryptoUtils; |
2、配置加密yml
ENC(xxx)
中包裹的xxx
即为使用上面加密方法后生成的字符串
1 | spring: |
自定义解密
一些公司要求使用自己的方式加密,解密。
从3.5.0版本开始,扩展了一个event,用户自行实现注入即可。
1 | public interface DataSourceInitEvent { |
默认的实现是EncDataSourceInitEvent,即ENC方式的。
为什么不是公钥加密,私钥解密
根据RSA的设计,大部分人会认为应该是公钥加密,私钥解密。 为什么Druid设计相反?
建议更高的安全,可以把publicKey在启动时候传进去,或者配置中心配好,不让普通开发接触到就好。
查询了Druid的ISSUE和一些文章:
1、Druid作者wenshao自己的回答:点我跳转
2、知乎一些文章片段
启动初始化执行脚本
3.5.0 之前版本
1 | spring: |
3.5.0(含) 之后版本 (层级多了一层init,保持和新版springboot一致)
1 | spring: |
懒启动数据源
懒启动:连接池创建出来后并不会立即初始化连接池,等需要使用connection的时候再初始化。
暂时只支持Druid和HikariCp和BeeCp连接池。
主要场景可能适合于数据源很多,又不需要启动立即初始化的情况,可以减少系统启动时间。
缺点
在于,如果参数配置有误,则启动的时候不知道,初始化的时候失败,可能一直抛异常。
配置使用
1 | spring: |
无数据源启动
基础介绍
场景:部分系统可能启动的时候完全不需要数据源,全靠启动后动态添加。
配置方式
即基本不需要配置,可能根据需要配置一些类似Druid和HikariCp全局参数。
如需配置Druid和HikariCp全局参数可参考对应章节文档。
1 | spring: |
因为没有匹配的主数据源,启动的时候你会见到类似如下日志。
1 | dynamic-datasource initial loaded 0 datasource,Please add your primary datasource or check your configuration |
手动切换数据源
在某些情况您可能需要手动切换数据源。
1 | import com.baomidou.dynamic.datasource.toolkit.DynamicDataSourceContextHolder; |
需要注意的是手动切换的数据源,最好自己在合适的位置调用DynamicDataSourceContextHolder.clear()清空当前线程的数据源信息,防止内存泄漏,因为一层使用的是ThreadLocal
如果你不太清楚什么时候调用,那么可以参考下面写一个拦截器,注册进spring里即可。
1 | 4j |
手动注入多数据源
什么时候需要手动注入多数据源?
绝大部分是因为您的系统需要和其他数据源共同存在使用。
如Quartz和ShardingJdbc等都需要使用独立的数据源。
dynamic-datasource 3.4.0及以上版本和老版本注入方式有一定差别,根据自己版本注入。
注意一定要让多数据源使用 @Primary ,让其成为主数据源。
1 | // 3.4.0版本以下 |
主要变更是因为3.4.0支持了多个provider同时生效,采取了内部注入,具体源码改动。
自定义
自定义注解
:不满足于默认DS注解,希望自定义注解。
自定义数据源来源
:默认的数据源来源是基于yml或者properties配置来的,组件支持同时从多个地方来加载初始化数据源。常用于多租户系统,从一个租户信息库多加载不同租户的数据源。
自定义负载均衡策略
:常用的有轮询,随机。支持扩展。
自定义切面
:支持不使用注解,通过配置来切换数据源。如某个包下所有service方法以select*
开头的都走slave库,以add*
开头的都走master库。
自定义注解
基础介绍
如果你只有几个确定的库,可以尝试自定义注解替换掉@DS
。
建议从3.2.1版本开始使用自定义注解,另外组件自带了@Master
和@Slave
注解。
使用方法
下面我们自定义一个产品库的注解,方便我们后面程序的使用。
1、需要自己定义一个注解并继承自DS
1 | import com.baomidou.dynamic.datasource.annotation.DS; |
2、注解在需要切换数据源的方法上或类上
1 |
|
自定义数据源来源
基础介绍
数据源来源的默认实现是YmlDynamicDataSourceProvider
,其从yaml或properties中读取信息并解析出所有数据源信息。
1 | public interface DynamicDataSourceProvider { |
自定义示例
可以参考 AbstractJdbcDataSourceProvider
(仅供参考)来实现从JDBC数据库中获取数据库连接信息。
1 |
|
PS: 从3.4.0开始,可以注入多个DynamicDataSourceProvider的Bean以实现同时从多个不同来源加载数据源,注意同名会被覆盖。
自定义负载均衡策略
基础介绍
如下slave
组下有三个数据源,当用户使用slave
切换数据源时会使用负载均衡算法。
系统自带了两个负载均衡算法:
LoadBalanceDynamicDataSourceStrategy
轮询,是默认的。RandomDynamicDataSourceStrategy
随机的。
1 | spring: |
如何自定义
如果默认的两个都不能满足要求,可以参考源码自定义,暂时只能全局更改。
1 | import java.util.List; |
无注解方案
不管是出于注解侵入性还是代码整洁等理由,我们都有需求不想使用注解的需求。
但是需要理解多数据源实现的核心。
1 | public class DynamicRoutingDataSource { |
所以我们就可以根据需求,选择合适的时机调用DynamicDataSourceContextHolder.push("对应数据源")
。
默认的@DS
注解来切换数据源是根据spring AOP的特性,在方法开启前设置数据源KEY,方法执行完成后移除对应数据源KEY。
filter切换数据源
目标:拦截以filter/**开头的所有请求,如果后面的方法以a开头就切换到db1,以b开头就切换到db2,其余使用默认数据源。
实现如下:
1 | 4j |
1 |
|
扩展:从request中还能获取到很多东西,如header和session,同理可以根据自己业务需求根据header值和session里对应的用户来动态设置和切换数据源。
intercepror切换数据源
目标:拦截以interceptor/**开头的所有请求,如果后面的方法以a开头就切换到db1,以b开头就切换到db2,其余使用默认数据源。
实现如下:
1 | import com.baomidou.dynamic.datasource.toolkit.DynamicDataSourceContextHolder; |
1 | import org.springframework.context.annotation.Configuration; |
自定义切面
从3.4.0开始支持自定义切面。
- 使用这个方式,原注解方式并不会失效。
- 注意:不要在同一个切面同时使用注解又使用自定义切面。
1 |
|
以上实现com.baomidou.samples.pattern包service下所有类的add/update/delete开头的方法使用master数据源,select使用slave数据源。
更复杂的切点表达式语法需自行学习。
mybatis下读写分离
场景:
- 在纯的读写分离环境,写操作全部是master,读操作全部是slave。
- 不想通过注解配置完成以上功能。
答:在mybatis环境下可以基于mybatis插件结合dynamic-datasource完成以上功能。
手动注入插件:
1 |
|
默认主库名称master,从库名称slave。
暂不支持更多,源码简单可参考重写。
1 | .class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}), ({ (type = Executor |
事务专栏
使用多数据源的你一定会遇到事务问题:
- 为什么使用了事务就切换不了数据源了啊?
- 有没有什么解决方案啊?
- 有没有使用案例和注意事项啊。
以上问题都能在本章节得到解答。
方案一:dynamic-datasource集成了alibaba分布式事务组件seata。
方案二:dynamic-datasource提供了无需外部组件的本地多数据源事务。
各有各的优劣,总有一款适合你。
事务概念
什么是事务?
事务:
是数据库操作的最小工作单元,是作为单个逻辑工作单元执行的一系列操作;这些操作作为一个整体一起向系统提交,要么都执行、要么都不执行;事务是一组不可再分割的操作集合(工作逻辑单元)。
事务的四大特性:
原子性
事务是数据库的逻辑工作单位,事务中包含的各操作要么都做,要么都不做 。
一致性
事务执行的结果必须是使数据库从一个一致性状态变到另一个一致性状态。因此当数据库只包含成功事务提交的结果时,就说数据库处于一致性状态。如果数据库系统 运行中发生故障,有些事务尚未完成就被迫中断,这些未完成事务对数据库所做的修改有一部分已写入物理数据库,这时数据库就处于一种不正确的状态,或者说是 不一致的状态。
隔离性
一个事务的执行不能其它事务干扰。即一个事务内部的操作及使用的数据对其它并发事务是隔离的,并发执行的各个事务之间不能互相干扰。
持续性
也称永久性,指一个事务一旦提交,它对数据库中的数据的改变就应该是永久性的。接下来的其它操作或故障不应该对其执行结果有任何影响。
原生JDBC处理事务
1 | try { |
以上是事务的核心,所有操作要一起成功,只要出错就回滚。
Spring处理事务
spring开启事务很简单,在需要事务的方法和类上添加@Transactional
注解。
1 |
|
使用了@Transactional
,spring会保证整个事务下都复用同一个connection
。
在默认配置下,只要事务中发生RuntimeException
,就会回滚。
基础知识
问:使用了事务如@Transational无法切换数据源?
答: 是的,本组件是基于springAop的方案来进行多数据源的管理和切换的,要想保证多个库的整体事务则需要分布式事务。
问:为什么使用了事务如@Transational就无法切换数据源?
答:开启了事务后,spring事务管理器会保证在事务下整个线程后续拿到的都是同一个connection。
问:事务下无法切换数据源我知道了,那我单库的事务的可以用吗?
答:完全可以的。只要事务下不切换数据源就OK。
问:我的业务必须要保证事务,还有什么解决办法?
方案一:seata就是解决此问题,本组件已完成和seata的自动集成。
方案二:本组件从3.3.0开始支持本地多数据源事务,无需第三方。
本地事务
背景
多数据源事务方案一直是一个难题,通常的解决方案有以下二种。
- 利用atomiks手动构建多数据源事务,适合数据源较少,配置的参数也不太多,性能要求不高的项目。难点就是手动配置量大,需要耗费一定时间。
- 用seata类似的分布式事务解决方案,难点就是需要搭建维护如seata-server的统一管理中心。
每一种方案都有其适用场景。你可能遇到最多的问题就是:
- 为什么我加了事务注解,切换数据源失败?
- 我了解涉及了分布式事务了,但我不想用seata,我场景简单,有没有不依赖第三方的方案?
基础介绍
自从3.3.0开始,由seata的核心贡献者 https://github.com/a364176773 贡献了基于connection代理的方案。
建议从3.4.0版本开始使用,其修复了一个功能,老版本不加@DS
只加@DSTransactional
会报错。
注意事项
本地事务实现很简单,就是循环提交,发生错误,循环回滚。
我们默认的前提是数据库本身不会异常,比如宕机。
如数据在回滚的过程突然宕机,本地事务就会有问题。如果你需要完整分布式方案请使用seata方案。
- 不支持spring原生事务,不支持spring事务,不支持spring事务,可分别使用,
千万不能混用
。 - 再次强调不支持spring事务注解,可理解成独立写了一套事务方案。
- 只适合简单本地多数据源场景, 如果涉及异步和微服务等完整事务场景,请使用seata方案。
- 暂时不支持更多配置,如只读,如spring的传播特性。 后续会根据反馈考虑支持。
使用方法
在最外层的方法添加 @DSTransactional
,底下调用的各个类该切数据源就正常使用DS
切换数据源即可,就是这么简单。
1 | // 如AService调用BService和CService的方法,A,B,C分别对应不同数据源。 |
只要@DSTransactional
注解下任一环节发生异常,则全局多数据源事务回滚。
如果BC上也有@DSTransactional
会有影响吗?答:没有影响的。
示例项目
完整示例项目,数据库都已准备好,可以直接运行测试。
示例项目A,B,C分别对应OrderService,ProductService,AccountService,分别是独立的数据库。
用户下单分别调用产品库扣库存,账户库扣余额。
如果库存不足,或用户余额不足都抛出RuntimeException,触发整体回滚。
1 | 4j |
1 | 4j |
1 | 4j |
原理介绍
核心原理就是代理connection,并根据不同数据库获取到一个connection放入ConnectionFactory。
如果成功了整体提交,失败了整体回滚。
seata事务
基础介绍
seata Github地址:点我跳转
seata 文档:点我跳转
seata 示例:点我跳转
PS:一般需要分布式事务的场景大多数都是微服务化,个人并不建议在单体项目引入多数据源+分布式事务,有能力尽早拆开,可为过度方案。
注意事项
dynamic-datasource-sring-boot-starter
组件内部开启seata后会自动使用DataSourceProxy来包装DataSource,所以需要以下方式来保持兼容。
1、如果你引入的是seata-all,请不要使用@EnableAutoDataSourceProxy注解。
2、如果你引入的是seata-spring-boot-starter 请关闭自动代理。
1 | seata: |
示例项目
此工程为 多数据源+druid+seata+mybatisPlus的版本。
模拟用户下单,扣商品库存,扣用户余额,初步可分为订单服务+商品服务+用户服务。
环境准备
为了快速演示相关环境都采用docker部署,生产上线请参考seata官方文档使用。
1、准备seata-server。
1 | docker run --name seata-server -p 8091:8091 -d seataio/seata-server |
2、准备mysql数据库,账户root密码123456。
1 | docker run --name mysql -p 3306:3306 -e MYSQL_ROOT_PASSWORD=123456 -d mysql:5.7 |
3、创建相关数据库。
创建 seata-order
seata-product
seata-account
模拟连接不同的数据库。
1 | CREATE DATABASE IF NOT EXIST seata-order; |
4、准备相关数据库脚本。
每个数据库下脚本相关的表,seata需要undo_log来监测和回滚。
相关的seata脚本可到seata官方获取,另外配合多数据源的自动执行脚本功能,应用启动后会自动执行。
工程准备
1、引入相关依赖,seata+druid+MybatisPlus+dynamic-datasource+mysql+lombok。
1 | <dependency> |
2、编写相关yaml配置。
1 | spring: |
代码编写
参考工程下面的代码完成controller,service,maaper,entity,dto等。
订单服务
1 | 4j |
商品服务
1 | 4j |
用户服务
1 | 4j |
测试
自行编写接口测试,注意观察运行日志,至此分布式事务集成案例全流程完毕。
事务常见问题
建议先阅读基础知识。
核心知识: spring原生事务会保证在事务下整个线程后续拿到的都是同一个connection。
核心知识: spring原生事务会保证在事务下整个线程后续拿到的都是同一个connection。
核心知识: spring原生事务会保证在事务下整个线程后续拿到的都是同一个connection。
原生Spring中@Transational能和@DS一起使用吗?
1 | public class AService { |
可以的,其会先切换使用数据源a再开启事务,整个原生事务内部不管是注解切换还是手动调用代码切换都不能切换,会一直使用a数据源。
所以确认整个事务下不再切换其他数据源,用原生 @Transational
是建议的,毕竟其更完善。
本地事务和Spring原生事务不能混用是什么意思?
场景一:先使用的Spring@Transational
内部方法调用了本地@DSTransactional
。
1 | public class AService { |
基于核心知识: 事务下都是a数据源,内部无论做什么都改变不了使用a数据源。
场景二: 先使用的本地@DSTransactional
内部方法调用了Spring@Transational
。
1 | public class AService { |
B和C都是独立的事务。C发生错误B会回滚吗?不会B已经提交了。A会回滚吗?会。
不建议混用,除非你确实非常理解事务,能随心所欲掌握你代码的执行流程。
本地事务标准使用
1 | public class AService { |
调试源码
1、开启动态数据源的debug日志。
1 | logging: |
检查日志输出是否正确。
2、断点调试DynamicDataSourceAnnotationInterceptor。
1 | public class DynamicDataSourceAnnotationInterceptor implements MethodInterceptor { |
3、断点调试DynamicRoutingDataSource。
1 | public class DynamicRoutingDataSource extends AbstractRoutingDataSource { |
常见问题之-切换数据源失败
开启了spring的事务
原因: spring开启事务后会维护一个ConnectionHolder,保证在整个事务下,都是用同一个数据库连接。
请检查整个调用链路涉及的类的方法和类本身还有继承的抽象类上是否有@Transactional
注解。
如强烈需要事务保证多个库同时执行成功或者失败,请查看事务专栏的解决办法。
方法内部调用
查看以下示例
外部调用 userservice.test1()
能在执行到 test2()
切换到second数据源吗?
1 | public UserService { |
答案:!!!不能不能不能!!!
数据源核心原理是基于aop代理实现切换,内部方法调用不会使用aop。
解决方法:
把test2()方法提到另外一个service,单独调用。
PostConstruct初始化顺序
初始化包括: @PostConstruct 注解, InitializingBean接口, 自定义init-method
1 |
|
解决方法:监听容器启动完成事件, 在容器完成后做初始化。
1 |
|
相关spring源码 : org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory#initializeBean
在初始化完成后, bean 进入增强阶段, 所以这个阶段的任何AOP都是无效的。
Druid版本过低
请升级druid1.1.22及以上版本,这个版本修复了在高并发下偶发的切换失效问题。
@Async或者java8的ParallelStream并行流之类方法
这种情况都是新开了线程去异步处理,不受当前线程管控了。
可以在新开的方法上加对应的DS注解解决。
知识库
如何注入多数据源?
1 | public class A{ |
如何获取当前线程数据源名称?
1 | DynamicDataSourceContextHolder.peek() |
如何获取当前线程数据源?
1 | public void dosomething{ |