基础

简介

dynamic-datasource-spring-boot-starter 是一个基于springboot的快速集成多数据源的启动器。

其支持 Jdk 1.7+, SpringBoot 1.4.x 1.5.x 2.x.x

特性

  • 支持 数据源分组 ,适用于多种场景 纯粹多库 读写分离 一主多从 混合模式。
  • 支持数据库敏感配置信息 加密 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的分布式事务方案。
  • 提供 本地多数据源事务方案。 附:不能和原生spring事务混用。

约定

  1. 本框架只做 切换数据源 这件核心的事情,并不限制你的具体操作,切换了数据源可以做任何CRUD。
  2. 配置文件所有以下划线 _ 分割的数据源 首部 即为组的名称,相同组名称的数据源会放在一个组下。
  3. 切换数据源可以是组名,也可以是具体数据源名称。组名则切换时采用负载均衡算法切换。
  4. 默认的数据源名称为 master ,你可以通过 spring.datasource.dynamic.primary 修改。
  5. 方法上的注解优先于类上注解。
  6. DS支持继承抽象类上的DS,暂不支持继承接口上的DS。

使用方法

  1. 引入dynamic-datasource-spring-boot-starter。
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>dynamic-datasource-spring-boot-starter</artifactId>
<version>${version}</version>
</dependency>
  1. 配置数据源。
spring:
datasource:
dynamic:
primary: master #设置默认的数据源或者数据源组,默认值即为master
strict: false #严格匹配数据源,默认false. true未匹配到指定数据源时抛异常,false使用默认数据源
datasource:
master:
url: jdbc:mysql://xx.xx.xx.xx:3306/dynamic
username: root
password: 123456
driver-class-name: com.mysql.jdbc.Driver # 3.2.0开始支持SPI可省略此配置
slave_1:
url: jdbc:mysql://xx.xx.xx.xx:3307/dynamic
username: root
password: 123456
driver-class-name: com.mysql.jdbc.Driver
slave_2:
url: ENC(xxxxx) # 内置加密,使用请查看详细文档
username: ENC(xxxxx)
password: ENC(xxxxx)
driver-class-name: com.mysql.jdbc.Driver

#......省略
#以上会配置一个默认库master,一个组slave下有两个子库slave_1,slave_2
# 多主多从 纯粹多库(记得设置primary) 混合配置
spring: spring: spring:
datasource: datasource: datasource:
dynamic: dynamic: dynamic:
datasource: datasource: datasource:
master_1: mysql: master:
master_2: oracle: slave_1:
slave_1: sqlserver: slave_2:
slave_2: postgresql: oracle_1:
slave_3: h2: oracle_2:
  1. 使用 @DS 切换数据源。

@DS 可以注解在方法上或类上,同时存在就近原则 方法上注解 优先于 类上注解

注解 结果
没有@DS 默认数据源
@DS(“dsName”) dsName可以为组名也可以为具体某个库的名称
@Service
@DS("slave")
public class UserServiceImpl implements UserService {

@Autowired
private JdbcTemplate jdbcTemplate;

public List selectAll() {
return jdbcTemplate.queryForList("select * from user");
}

@Override
@DS("slave_1")
public List selectByCondition() {
return jdbcTemplate.queryForList("select * from user where age >10");
}
}

连接池集成

连接池必读

  1. 每个数据源都有一个type来指定连接池。
  2. 每个数据源甚至可以使用不同的连接池,如无特殊需要并不建议。
  3. type 不是必填字段 ,在没有设置type的时候系统会自动按以下顺序查找并使用连接池。
    Druid>HikariCp>BeeCp>DBCP2>Spring Basic。
spring:
datasource:
dynamic:
primary: db1
datasource:
db1:
url: jdbc:mysql://xx.xx.xx.xx:3306/dynamic
username: root
password: 123456
driver-class-name: com.mysql.jdbc.Driver
type: com.zaxxer.hikari.HikariDataSource #使用Hikaricp
db2:
url: jdbc:mysql://xx.xx.xx.xx:3307/dynamic
username: root
password: 123456
driver-class-name: com.mysql.jdbc.Driver
type: com.alibaba.druid.pool.DruidDataSource #使用Druid
db3:
url: jdbc:mysql://xx.xx.xx.xx:3308/dynamic
username: root
password: 123456
driver-class-name: com.mysql.jdbc.Driver
type: cn.beecp.BeeDataSource #使用beecp

集成Druid

基础介绍

本组件能简单高效 完成对Druid的集成并完成其参数的多元化配置。

1.各个库可以使用不同的数据库连接池,如 master使用Druid监控,从库使用HikariCP
2.如果项目同时存在Druid和HikariCP并且未配置连接池type类型,默认 Druid优先于HikariCP

集成步骤

1. 项目引入 druid-spring-boot-starter 依赖。

img

<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>${version}</version>
</dependency>

2. 排除原生Druid的快速配置类。

注意:v3.3.3及以上版本不用排除了。

@SpringBootApplication(exclude = DruidDataSourceAutoConfigure.class)
public class Application {

public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}

某些springBoot的版本上面可能无法排除可用以下方式排除。

spring:
autoconfigure:
exclude: com.alibaba.druid.spring.boot.autoconfigure.DruidDataSourceAutoConfigure

3.参数配置。

  1. 如果参数都未配置,则保持原组件默认值。
  2. 如果配置了全局参数,则每一个数据源都会继承对应参数。
  3. 每一个数据源可以单独设置参数覆盖全局参数。
spring:
datasource:
druid:
stat-view-servlet:
enabled: true
loginUsername: admin
loginPassword: 123456
dynamic:
druid: #以下是支持的全局默认值
initial-size:
max-active:
min-idle:
max-wait:
time-between-eviction-runs-millis:
time-between-log-stats-millis:
stat-sqlmax-size:
min-evictable-idle-time-millis:
max-evictable-idle-time-millis:
test-while-idle:
test-on-borrow:
test-on-return:
validation-query:
validation-query-timeout:
use-global-datasource-stat:
async-init:
clear-filters-enable:
reset-stat-enable:
not-full-timeout-retry-count:
max-wait-thread-count:
fail-fast:
phyTimeout-millis:
keep-alive:
pool-prepared-statements:
init-variants:
init-global-variants:
use-unfair-lock:
kill-when-socket-read-timeout:
connection-properties:
max-pool-prepared-statement-per-connection-size:
init-connection-sqls:
share-prepared-statements:
connection-errorretry-attempts:
break-after-acquire-failure:
filters: stat # 注意这个值和druid原生不一致,默认启动了stat
wall:
noneBaseStatementAllow:
callAllow:
selectAllow:
selectIntoAllow:
selectIntoOutfileAllow:
selectWhereAlwayTrueCheck:
selectHavingAlwayTrueCheck:
selectUnionCheck:
selectMinusCheck:
selectExceptCheck:
selectIntersectCheck:
createTableAllow:
dropTableAllow:
alterTableAllow:
renameTableAllow:
hintAllow:
lockTableAllow:
startTransactionAllow:
blockAllow:
conditionAndAlwayTrueAllow:
conditionAndAlwayFalseAllow:
conditionDoubleConstAllow:
conditionLikeTrueAllow:
selectAllColumnAllow:
deleteAllow:
deleteWhereAlwayTrueCheck:
deleteWhereNoneCheck:
updateAllow:
updateWhereAlayTrueCheck:
updateWhereNoneCheck:
insertAllow:
mergeAllow:
minusAllow:
intersectAllow:
replaceAllow:
setAllow:
commitAllow:
rollbackAllow:
useAllow:
multiStatementAllow:
truncateAllow:
commentAllow:
strictSyntaxCheck:
constArithmeticAllow:
limitZeroAllow:
describeAllow:
showAllow:
schemaCheck:
tableCheck:
functionCheck:
objectCheck:
variantCheck:
mustParameterized:
doPrivilegedAllow:
dir:
tenantTablePattern:
tenantColumn:
wrapAllow:
metadataAllow:
conditionOpXorAllow:
conditionOpBitwseAllow:
caseConditionConstAllow:
completeInsertValuesCheck:
insertValuesCheckSize:
selectLimit:
stat:
merge-sql:
log-slow-sql:
slow-sql-millis:
datasource:
master:
username: root
password: 123456
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://xx.xx.xx.xx:3306/dynamic?characterEncoding=utf8&useSSL=false
druid: # 以下是独立参数,每个库可以重新设置
initial-size: 20
validation-query: select 1 FROM DUAL #比如oracle就需要重新设置这个
public-key: #(非全局参数)设置即表示启用加密,底层会自动帮你配置相关的连接参数和filter,推荐使用本项目自带的加密方法。
# ......

# 生成 publickey 和密码,推荐使用本项目自带的加密方法。
# java -cp druid-1.1.10.jar com.alibaba.druid.filter.config.ConfigTools youpassword

如上即可配置访问用户和密码,访问 http://localhost:8080/druid/index.html 查看druid监控。

核心源码

Druid数据源创建器https://github.com/baomidou/dynamic-datasource-spring-boot-starter/blob/master/src/main/java/com/baomidou/dynamic/datasource/creator/DruidDataSourceCreator.java

Druid参数源码https://github.com/baomidou/dynamic-datasource-spring-boot-starter/blob/master/src/main/java/com/baomidou/dynamic/datasource/spring/boot/autoconfigure/druid/DruidConfig.java

如有参数缺失可参考源码提交PR。

集成HikariCP

基础介绍

集成步骤

1. 项目引入 HikariCP 依赖。

img

<dependency>
<groupId>com.zaxxer</groupId>
<artifactId>HikariCP</artifactId>
<version>${version}</version>
</dependency>

SpringBoot2.x.x默认引入了HikariCP,除非对版本有要求无需再次引入。

SpringBoot 1.5.x需手动引入,对应的版本请根据自己环境和HikariCP官方文档自行选择。

2.参数配置。

  1. 如果参数都未配置,则保持原组件默认值。
  2. 如果配置了全局参数,则每一个数据源都会继承对应参数。
  3. 每一个数据源可以单独设置参数覆盖全局参数。

特别注意,hikaricp原生设置某些字段名和本组件不一致,本组件是根据参数反射设置,而原生hikaricp字段名称和set名称不一致。 所以大家理解,以本组件字段名称为准。

spring:
datasource:
dynamic:
hikari: # 全局hikariCP参数,所有值和默认保持一致。(现已支持的参数如下,不清楚含义不要乱设置)
catalog:
connection-timeout:
validation-timeout:
idle-timeout:
leak-detection-threshold:
max-lifetime:
max-pool-size:
min-idle:
initialization-fail-timeout:
connection-init-sql:
connection-test-query:
dataSource-class-name:
dataSource-jndi-name:
schema:
transaction-isolation-name:
is-auto-commit:
is-read-only:
is-isolate-internal-queries:
is-register-mbeans:
is-allow-pool-suspension:
data-source-properties:
health-check-properties:
datasource:
master:
username: root
password: 123456
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://xx.xx.xx.xx:3306/dynamic?characterEncoding=utf8&useSSL=false
hikari: # 以下参数针对每个库可以重新设置hikari参数
max-pool-size:
idle-timeout:
# ......

常见问题

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数据源创建器https://github.com/baomidou/dynamic-datasource-spring-boot-starter/blob/master/src/main/java/com/baomidou/dynamic/datasource/creator/HikariDataSourceCreator.java

HikariCP参数配置类https://github.com/baomidou/dynamic-datasource-spring-boot-starter/blob/master/src/main/java/com/baomidou/dynamic/datasource/spring/boot/autoconfigure/hikari/HikariCpConfig.java

集成BeeCP

基础介绍

集成步骤

1. 项目引入 BeeCp 依赖。

img

<dependency>
<groupId>com.github.chris2018998</groupId>
<artifactId>beecp</artifactId>
<version>${version}</version>
</dependency>

2.参数配置。

  1. 如果参数都未配置,则保持原组件默认值。
  2. 如果配置了全局参数,则每一个数据源都会继承对应参数。
  3. 每一个数据源可以单独设置参数覆盖全局参数。
spring:
datasource:
dynamic:
beecp: # 全局beeCp参数,所有值和默认保持一致。(现已支持的参数如下,不清楚含义不要乱设置)
default-catalog:
default-schema:
default-readOnly:
default-auto-commit:
default-transaction-isolation-code:
default-transaction-isolation-name:
fair-mode:
initial-size:
max-active:
borrow-semaphore-size:
max-wait:
idle-timeout:
hold-timeout:
connection-test-sql:
connection-test-timeout:
connection-test-integererval:
idle-check-time-integererval:
force-close-using-on-clear:
delay-time-for-next-clear:
connection-factory-class-name:
xa-connection-factory-class-name:
pool-implement-class-name:
enable-jmx:
connect-properties:
datasource:
master:
username: root
password: 123456
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://xx.xx.xx.xx:3306/dynamic?characterEncoding=utf8&useSSL=false
beecp: # 以下参数针对每个库可以重新设置beeCp参数
initial-size:
max-active:
# ......

核心源码

BeeCp数据源创建器https://github.com/baomidou/dynamic-datasource-spring-boot-starter/blob/master/src/main/java/com/baomidou/dynamic/datasource/creator/BeeCpDataSourceCreator.java

BeeCp参数配置类https://github.com/baomidou/dynamic-datasource-spring-boot-starter/blob/master/src/main/java/com/baomidou/dynamic/datasource/spring/boot/autoconfigure/beecp/BeeCpConfig.java

集成DBCP2

基础介绍

集成步骤

1. 项目引入 Dbcp2 依赖。

img

<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-dbcp2</artifactId>
<version>${version}</version>
</dependency>

2.参数配置。

  1. 如果参数都未配置,则保持原组件默认值。
  2. 如果配置了全局参数,则每一个数据源都会继承对应参数。
  3. 每一个数据源可以单独设置参数覆盖全局参数。
spring:
datasource:
dynamic:
dbcp2: # 全局dbcp2参数,所有值和默认保持一致。(现已支持的参数如下,不清楚含义不要乱设置)
initial-size:
datasource:
master:
username: root
password: 123456
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://xx.xx.xx.xx:3306/dynamic?characterEncoding=utf8&useSSL=false
dbcp2: # 以下参数针对每个库可以重新设置dbcp2参数
initial-size:
# ......

核心源码

dbcp2数据源创建器https://github.com/baomidou/dynamic-datasource-spring-boot-starter/blob/master/src/main/java/com/baomidou/dynamic/datasource/creator/Dbcp2DataSourceCreator.java

dbcp2参数配置类https://github.com/baomidou/dynamic-datasource-spring-boot-starter/blob/master/src/main/java/com/baomidou/dynamic/datasource/spring/boot/autoconfigure/dbcp2/Dbcp2Config.java

集成Jndi

JNDI是什么? https://blog.csdn.net/zhaosg198312/article/details/3979435

用到JNDI大部分应该是老项目了。 如无需求不建议继续使用JNDI数据源。

spring:
datasource:
dynamic:
datasource:
db1:
jndi_name: xxx
db2:
jndi_name: xxx

第三方集成

通过本章您可以掌握数据源在集成MybatisPlus,Quartz,ShardingJdbc的方案。

集成MybatisPlus

基础介绍

只要引入了mybatisPlus相关jar包,项目自动集成,兼容mybatisPlus 2.x和3.x的版本。

集成步骤

1. 项目引入 mybatis-Plus 依赖。

img

<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>${version}</version>
</dependency>

2. 使用DS注解进行切换数据源。

@Service
@DS("mysql")
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {

@DS("oracle")
publid void addUser(User user){
//do something
baseMapper.insert(user);
}
}

3.分页配置。

@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
//如果是不同类型的库,请不要指定DbType,其会自动判断。
interceptor.addInnerInterceptor(new PaginationInnerInterceptor());
// interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
return interceptor;
}

4.注意事项。

mp内置的ServiceImpl在新增,更改,删除等一些方法上自带事物导致不能切换数据源。

解决办法:

  1. 继承ServiceImpl写自己的MyServiceImpl方法,并去掉所有事务。
  2. 创建一个新方法,并在此方法上加DS注解. 如上面的addUser方法。

FAQ

为什么要单独拿出来说和mybatisPlus的集成

因为mybatisPlus重写了一些核心类,必须通过解析获得真实的代理对象。

如果自己写多数据源,则很难完成与mp的集成。

核心解析源码 https://github.com/baomidou/dynamic-datasource-spring-boot-starter/blob/master/src/main/java/com/baomidou/dynamic/datasource/support/DataSourceClassResolver.java

示例项目

https://github.com/dynamic-datasource/dynamic-datasource-samples/tree/master/mybatisplus3-sample

集成P6spy

基础介绍

# before
select * from user where age>?
# after enabled p6spy
select * from user where age>6

集成步骤

1. 项目引入p6spy依赖。

img

<dependency>
<groupId>p6spy</groupId>
<artifactId>p6spy</artifactId>
<version>${version}</version>
</dependency>

2. 启用p6spy相关配置。

spring:
datasource:
dynamic:
p6spy: true # 默认false,建议线上关闭。
datasource:
product:
username: sa
password: ""
url: jdbc:h2:mem:test
driver-class-name: org.h2.Driver
p6spy: false # 如果这个库不需要可单独关闭。
order:
username: sa
password: ""
url: jdbc:h2:mem:test
driver-class-name: org.h2.Driver

3. 引入相关配置文件。

在classPath下创建spy.properties

# 一个最简单配置,定义slf4j日志输出。 更多参数请自行了解。
appender=com.p6spy.engine.spy.appender.Slf4JLogger

集成Quartz

基础介绍


Quartz是一个定时任务框架,常常用于解决分布式系统下的定时任务协调问题。

Quartz常常需要独立运行在主业务数据库外,在springboot场景中可以以下面方式运行。

使用方法

1. 项目引用 quartz-starter

img

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-quartz</artifactId>
</dependency>

2. 配置数据源参数。

根据需要可配置独立数据源的参数,或把quartz数据源也作为动态数据源的一个数据源。

一般来说二种方式根据需要选择一种即可,如果没有在动态数据源里需要切到quartz的的场景,建议久独立配吧。

spring:
datasource: #独立quartz配置
username: root
password: 123456
url: jdbc:mysql://39.108.158.138:3306/quartz
driver-class-name: com.mysql.cj.jdbc.Driver
dynamic:
datasource:
master:
username: sa
password: ""
url: jdbc:h2:mem:test
driver-class-name: org.h2.Driver
quartz: #把quartz数据源也作为动态数据源的一个数据源
username: root
password: 123456
url: jdbc:mysql://39.108.158.138:3306/quartz
driver-class-name: com.mysql.cj.jdbc.Driver
quartz:
job-store-type: jdbc
jdbc:
initialize-schema: always

3. 创建任务。

创建一个每秒打印一次hello world的任务。

@Slf4j
public class HelloworldJob extends QuartzJobBean {

private static int time = 0;

@Override
protected void executeInternal(JobExecutionContext jobExecutionContext) throws JobExecutionException {
log.info("Hello world!:" + jobExecutionContext.getJobDetail().getKey() + "-" + (time++));
}
}

对应的自动配置。

@Configuration
public class QuartzCommonAutoConfiguration {

@Bean
public JobDetail job() {
return JobBuilder.newJob(HelloworldJob.class)
.withIdentity("myJob1", "myJobGroup1")
//JobDataMap可以给任务execute传递参数
.usingJobData("job_param", "job_param1")
.storeDurably()
.build();
}

@Bean
public Trigger myTrigger(JobDetail jobDetail) {
return TriggerBuilder.newTrigger()
.forJob(jobDetail)
.withIdentity("myTrigger1", "myTriggerGroup1")
.usingJobData("job_trigger_param", "job_trigger_param1")
.startNow()
.withSchedule(CronScheduleBuilder.cronSchedule("0/1 * * * * ? *"))
.build();
}
}

4 配置Quartz实际使用的数据源。

具体方式有二种,随意选择一种即可。建议方式一,简单点。

4.1 方式一: 自定义SchedulerFactoryBeanCustomizer。
spring:
datasource: #独立quartz配置
username: root
password: 123456
url: jdbc:mysql://39.108.158.138:3306/quartz
driver-class-name: com.mysql.cj.jdbc.Driver
@Configuration
public class MyQuartzAutoConfigurationMode1 {

@Autowired
private DataSourceProperties dataSourceProperties;

@Order(1)
@Bean
public SchedulerFactoryBeanCustomizer schedulerFactoryBeanCustomizer() {
DataSource dataSource = dataSourceProperties.initializeDataSourceBuilder().build();
return schedulerFactoryBean -> {
schedulerFactoryBean.setDataSource(dataSource);
schedulerFactoryBean.setTransactionManager(new DataSourceTransactionManager(dataSource));
};
}

//如果需要使用动态数据源里的某个数据源则打开以下配置,关闭上面配置。
// @Order(1)
// @Bean
// public SchedulerFactoryBeanCustomizer schedulerFactoryBeanCustomizer(DataSource dataSource) {
// DynamicRoutingDataSource ds = (DynamicRoutingDataSource) dataSource;
// DataSource quartz = ds.getDataSource("quartz");
// return schedulerFactoryBean -> {
// schedulerFactoryBean.setDataSource(quartz);
// schedulerFactoryBean.setTransactionManager(new DataSourceTransactionManager(quartz));
// };
// }

}
4.2 方式二: 使用@QuartzDataSource来指明quartz数据源。
@Configuration
public class MyQuartzAutoConfigurationMode2 {

@Autowired
private DataSourceProperties dataSourceProperties;
@Autowired
private DynamicDataSourceProperties properties;

//3.4.0版本及以上使用以下方式注入,老版本请阅读文档 进阶-手动注入多数据源
@Primary
@Bean
public DataSource dataSource() {
DynamicRoutingDataSource dataSource = new DynamicRoutingDataSource();
dataSource.setPrimary(properties.getPrimary());
dataSource.setStrict(properties.getStrict());
dataSource.setStrategy(properties.getStrategy());
dataSource.setP6spy(properties.getP6spy());
dataSource.setSeata(properties.getSeata());
return dataSource;
}

@QuartzDataSource
@Bean
public DataSource quartzDataSource() {
return dataSourceProperties.initializeDataSourceBuilder().build();
}

//如果需要使用动态数据源里的某个数据源则打开以下配置,关闭上面配置。
// @QuartzDataSource
// @Bean
// public DataSource quartzDataSource(DataSource dataSource) {
// DynamicRoutingDataSource ds = (DynamicRoutingDataSource) dataSource;
// return ds.getDataSource("quartz");
// }
}

示例项目

https://github.com/dynamic-datasource/dynamic-datasource-samples/tree/master/quartz-sample

集成ShardingJdbc

基础介绍


多数据源与shardingsphere集成的场景:部分表比较大需要分表由shardingsphere完成,同时又有多库的需求。

可参考文章 https://zhuanlan.zhihu.com/p/166105923

使用方法

1. 项目引入 shardingsphere 依赖。

img

<dependency>
<groupId>org.apache.shardingsphere</groupId>
<artifactId>sharding-jdbc-spring-boot-starter</artifactId>
<version>${version}</version>
</dependency>
<dependency>
<groupId>org.apache.shardingsphere</groupId>
<artifactId>sharding-jdbc-spring-namespace</artifactId>
<version>${version}</version>
</dependency>

2. 分别配置shardingjdbc和多数据源。

spring:
# shardingjdbc 配置
shardingsphere:
datasource:
names: shardingmaster,shardingslave0,shardingslave1
shardingmaster:
type: com.zaxxer.hikari.HikariDataSource
driver-class-name: org.h2.Driver
jdbc-url: jdbc:h2:mem:test
username: sa
password: ""
shardingslave0:
type: com.zaxxer.hikari.HikariDataSource
driver-class-name: org.h2.Driver
jdbc-url: jdbc:h2:mem:test
username: sa
password: ""
shardingslave1:
type: com.zaxxer.hikari.HikariDataSource
driver-class-name: org.h2.Driver
jdbc-url: jdbc:h2:mem:test
username: sa
password: ""
masterslave:
name: ms
load-balance-algorithm-type: round_robin
master-data-source-name: shardingmaster
slave-data-source-names: shardingslave0,shardingslave1
sharding:
tables:
t_order:
actualDataNodes: shardingmaster.t_order${0..1}
tableStrategy:
inline:
shardingColumn: order_id
algorithmExpression: t_order${order_id % 2}
keyGenerator:
type: SNOWFLAKE
column: order_id
# 动态数据源配置
datasource:
dynamic:
datasource:
master:
username: sa
password: ""
url: jdbc:h2:mem:master
driver-class-name: org.h2.Driver
schema: db/schema.sql
test:
username: sa
password: ""
url: jdbc:h2:mem:test
driver-class-name: org.h2.Driver
schema: db/schema.sql

3. 通过自定义DynamicDataSourceProvider完成与shardingsphere的集成。

@Configuration
public class MyDataSourceConfiguration {

@Resource
private DynamicDataSourceProperties properties;

/**
* 未使用分片, 脱敏的名称(默认): shardingDataSource
* 使用了主从: masterSlaveDataSource
* 根据自己场景修改注入
*/
@Resource(name = "masterSlaveDataSource")
@Lazy //特别注意,有的spring版本不用加Lazy一样能注入,有的会空指针。 根据自己测试选择,但是如果加了lazy就没强转了,也就无法添加其内部的数据源了。 有办法请联系作者。
private DataSource masterSlaveDataSource;

@Bean
public DynamicDataSourceProvider dynamicDataSourceProvider() {
return new AbstractDataSourceProvider() {

@Override
public Map<String, DataSource> loadDataSources() {
Map<String, DataSource> dataSourceMap = new HashMap<>();
dataSourceMap.put("sharding", masterSlaveDataSource);
//打开下面的代码可以把 shardingJdbc 内部管理的子数据源也同时添加到动态数据源里 (根据自己需要选择开启)
// dataSourceMap.putAll(((MasterSlaveDataSource) masterSlaveDataSource).getDataSourceMap());
return dataSourceMap;
}
};
}

/**
* 将动态数据源设置为首选的
* 当spring存在多个数据源时, 自动注入的是首选的对象
* 设置为主要的数据源之后,就可以支持shardingjdbc原生的配置方式了
* 3.4.0版本及以上使用以下方式注入,老版本请阅读文档 进阶-手动注入多数据源
*/
@Primary
@Bean
public DataSource dataSource() {
DynamicRoutingDataSource dataSource = new DynamicRoutingDataSource();
dataSource.setPrimary(properties.getPrimary());
dataSource.setStrict(properties.getStrict());
dataSource.setStrategy(properties.getStrategy());
dataSource.setP6spy(properties.getP6spy());
dataSource.setSeata(properties.getSeata());
return dataSource;
}
}

示例项目

https://github.com/dynamic-datasource/dynamic-datasource-samples/tree/master/sharding-jdbc-sample

进阶使用

动态添加移除数据源:指在系统运行过程中动态的添加数据源,删除数据源,多使用于基于数据库的多租户系统。


动态解析数据源:指数据源切换是不固定的,可以根据域名,根据header参数,根据session参数,根据方法变量等来动态切换。 多使用于多租户系统,支持扩展。


数据库加密:指数据库的url,username,password需要加密,长用于安全性较高系统,不让普通开发通过配置知道生产库的连接配置。


启动初始化脚本:指数据源启动的时候可以执行schema和data的脚本来初始化结构和数据,本组件支持每个数据源加载不同的schema和data。


自动读写分离:适用于mybatis环境,基于mybatis插件实现无注解自动读写分离。


懒启动数据源:指配置的数据源不立即启动,等需要建立连接的时候再真正初始化连接池。


无数据源启动:指启动的时候不配置任何数据源,全靠后期系统动态添加。


手动切换数据源:指某些情况无法根据注解切换,通过工具类手动切换数据源。

动态添加移除数据源

基础介绍

主要在多租户场景中,常常新的一个租户进来需要动态的添加一个数据源到库中,使得系统不用重启即可切换数据源。

使用步骤

import com.baomidou.dynamic.datasource.DynamicRoutingDataSource;
import com.baomidou.dynamic.datasource.creator.*;
import com.baomidou.dynamic.datasource.spring.boot.autoconfigure.DataSourceProperty;
import com.baomidou.samples.ds.dto.DataSourceDTO;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;

import javax.sql.DataSource;
import java.util.Set;

@RestController
@RequestMapping("/datasources")
@Api(tags = "添加删除数据源")
public class DataSourceController {

@Autowired
private DataSource dataSource;
// private final DataSourceCreator dataSourceCreator; //3.3.1及以下版本使用这个通用
@Autowired
private DefaultDataSourceCreator dataSourceCreator;
@Autowired
private BasicDataSourceCreator basicDataSourceCreator;
@Autowired
private JndiDataSourceCreator jndiDataSourceCreator;
@Autowired
private DruidDataSourceCreator druidDataSourceCreator;
@Autowired
private HikariDataSourceCreator hikariDataSourceCreator;
@Autowired
private BeeCpDataSourceCreator beeCpDataSourceCreator;
@Autowired
private Dbcp2DataSourceCreator dbcp2DataSourceCreator;

@GetMapping
@ApiOperation("获取当前所有数据源")
public Set<String> now() {
DynamicRoutingDataSource ds = (DynamicRoutingDataSource) dataSource;
return ds.getCurrentDataSources().keySet();
}

//通用数据源会根据maven中配置的连接池根据顺序依次选择。
//默认的顺序为druid>hikaricp>beecp>dbcp>spring basic
@PostMapping("/add")
@ApiOperation("通用添加数据源(推荐)")
public Set<String> add(@Validated @RequestBody DataSourceDTO dto) {
DataSourceProperty dataSourceProperty = new DataSourceProperty();
BeanUtils.copyProperties(dto, dataSourceProperty);
DynamicRoutingDataSource ds = (DynamicRoutingDataSource) dataSource;
DataSource dataSource = dataSourceCreator.createDataSource(dataSourceProperty);
ds.addDataSource(dto.getPollName(), dataSource);
return ds.getCurrentDataSources().keySet();
}

@PostMapping("/addBasic(强烈不推荐,除了用了马上移除)")
@ApiOperation(value = "添加基础数据源", notes = "调用Springboot内置方法创建数据源,兼容1,2")
public Set<String> addBasic(@Validated @RequestBody DataSourceDTO dto) {
DataSourceProperty dataSourceProperty = new DataSourceProperty();
BeanUtils.copyProperties(dto, dataSourceProperty);
DynamicRoutingDataSource ds = (DynamicRoutingDataSource) dataSource;
DataSource dataSource = basicDataSourceCreator.createDataSource(dataSourceProperty);
ds.addDataSource(dto.getPollName(), dataSource);
return ds.getCurrentDataSources().keySet();
}

@PostMapping("/addJndi")
@ApiOperation("添加JNDI数据源")
public Set<String> addJndi(String pollName, String jndiName) {
DynamicRoutingDataSource ds = (DynamicRoutingDataSource) dataSource;
DataSource dataSource = jndiDataSourceCreator.createDataSource(jndiName);
ds.addDataSource(pollName, dataSource);
return ds.getCurrentDataSources().keySet();
}

@PostMapping("/addDruid")
@ApiOperation("基础Druid数据源")
public Set<String> addDruid(@Validated @RequestBody DataSourceDTO dto) {
DataSourceProperty dataSourceProperty = new DataSourceProperty();
BeanUtils.copyProperties(dto, dataSourceProperty);
dataSourceProperty.setLazy(true);
DynamicRoutingDataSource ds = (DynamicRoutingDataSource) dataSource;
DataSource dataSource = druidDataSourceCreator.createDataSource(dataSourceProperty);
ds.addDataSource(dto.getPollName(), dataSource);
return ds.getCurrentDataSources().keySet();
}

@PostMapping("/addHikariCP")
@ApiOperation("基础HikariCP数据源")
public Set<String> addHikariCP(@Validated @RequestBody DataSourceDTO dto) {
DataSourceProperty dataSourceProperty = new DataSourceProperty();
BeanUtils.copyProperties(dto, dataSourceProperty);
dataSourceProperty.setLazy(true);//3.4.0版本以下如果有此属性,需手动设置,不然会空指针。
DynamicRoutingDataSource ds = (DynamicRoutingDataSource) dataSource;
DataSource dataSource = hikariDataSourceCreator.createDataSource(dataSourceProperty);
ds.addDataSource(dto.getPollName(), dataSource);
return ds.getCurrentDataSources().keySet();
}

@PostMapping("/addBeeCp")
@ApiOperation("基础BeeCp数据源")
public Set<String> addBeeCp(@Validated @RequestBody DataSourceDTO dto) {
DataSourceProperty dataSourceProperty = new DataSourceProperty();
BeanUtils.copyProperties(dto, dataSourceProperty);
dataSourceProperty.setLazy(true);//3.4.0版本以下如果有此属性,需手动设置,不然会空指针。
DynamicRoutingDataSource ds = (DynamicRoutingDataSource) dataSource;
DataSource dataSource = beeCpDataSourceCreator.createDataSource(dataSourceProperty);
ds.addDataSource(dto.getPollName(), dataSource);
return ds.getCurrentDataSources().keySet();
}

@PostMapping("/addDbcp")
@ApiOperation("基础Dbcp数据源")
public Set<String> addDbcp(@Validated @RequestBody DataSourceDTO dto) {
DataSourceProperty dataSourceProperty = new DataSourceProperty();
BeanUtils.copyProperties(dto, dataSourceProperty);
dataSourceProperty.setLazy(true);//3.4.0版本以下如果有此属性,需手动设置,不然会空指针。
DynamicRoutingDataSource ds = (DynamicRoutingDataSource) dataSource;
DataSource dataSource = dbcp2DataSourceCreator.createDataSource(dataSourceProperty);
ds.addDataSource(dto.getPollName(), dataSource);
return ds.getCurrentDataSources().keySet();
}

@DeleteMapping
@ApiOperation("删除数据源")
public String remove(String name) {
DynamicRoutingDataSource ds = (DynamicRoutingDataSource) dataSource;
ds.removeDataSource(name);
return "删除成功";
}
}

示例项目

https://github.com/dynamic-datasource/dynamic-datasource-samples/tree/master/add-remove-datasource-sample

源码分析

img

public interface DataSourceCreator {

/**
* 通过属性创建数据源
*
* @param dataSourceProperty 数据源属性
* @return 被创建的数据源
*/
DataSource createDataSource(DataSourceProperty dataSourceProperty);

/**
* 当前创建器是否支持根据此属性创建
*
* @param dataSourceProperty 数据源属性
* @return 是否支持
*/
boolean support(DataSourceProperty dataSourceProperty);
}

DataSourceCreator是一个接口,定义了根据参数创建数据源的接口。
其他creator实现此接口,本项目暂时实现了Druid和Hikaricp的等连接池的实现。
BasicDataSourceCreator 是调用Spring原生的创建方式,只支持最最原始的基础配置。
DefaultDataSourceCreator 是一个通用的创建器,其根据环境自动选择连接池,

动态解析数据源

基础介绍

默认有三个职责链来处理动态参数解析器 header->session->spel
所有以 # 开头的参数都会从参数中获取数据源。

@DS("#session.tenantName")//从session获取
public List selectSpelBySession() {
return userMapper.selectUsers();
}

@DS("#header.tenantName")//从header获取
public List selectSpelByHeader() {
return userMapper.selectUsers();
}

@DS("#tenantName")//使用spel从参数获取
public List selectSpelByKey(String tenantName) {
return userMapper.selectUsers();
}

@DS("#user.tenantName")//使用spel从复杂参数获取
public List selecSpelByTenant(User user) {
return userMapper.selectUsers();
}

如何扩展?

  1. 我想从cookie中获取参数解析?
  2. 我想从其他环境属性中来计算?

参考header解析器,继承DsProcessor,如果matches返回true则匹配成功,调用doDetermineDatasource返回匹配到的数据源,否则跳到写一个解析器.

1. 自定义一个处理器。

public class DsHeaderProcessor extends DsProcessor {

private static final String HEADER_PREFIX = "#header";

@Override
public boolean matches(String key) {
return key.startsWith(HEADER_PREFIX);
}

@Override
public String doDetermineDatasource(MethodInvocation invocation, String key) {
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
return request.getHeader(key.substring(8));
}
}

2. 重写完后重新注入一个根据自己解析顺序的解析处理器.

@Configuration
public class MyDynamicDataSourceConfig{

@Bean
public DsProcessor dsProcessor() {
DsHeaderProcessor headerProcessor = new DsHeaderProcessor();
DsSessionProcessor sessionProcessor = new DsSessionProcessor();
DsSpelExpressionProcessor spelExpressionProcessor = new DsSpelExpressionProcessor();
headerProcessor.setNextProcessor(sessionProcessor);
sessionProcessor.setNextProcessor(spelExpressionProcessor);
return headerProcessor;
}
}

注意

如果

在Mapper接口下面的方法使用

public interface UserMapper{
// 前缀可以是p0,a0
@DS("#p0.tenantName")
public List selecSpelByTenant(User user);
}

对于在接口下面的使用, 由于编译器的默认配置没有将接口参数的元数据写入字节码文件中.
所以spring el会无法识别参数名称, 只能用默认的参数命名方式

  1. 第一个参数: p0,a0,(加入-parameter后,可以使用参数具体的名字,例如这里的#user)
  2. 第二个参数: p1,a1
  3. 第三个参数: P2,a2

可以通过修改maven配置和java编译配置将接口参数信息写入字节码文件
maven配置:

<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<!-- 想启用 <parameters>true</parameters> 的maven编译最低版本为:3.6.2 -->
<version>3.6.2</version>
<configuration>
<source>${java.version}</source>
<target>${java.version}</target>
<parameters>true</parameters>
</configuration>
</plugin>

idea java编译配置: -parameters

java 支持-parameters的最低版本为 1.8

数据库加密

基础介绍

在一些项目中,有对数据库关键字段加密的需求,大家熟悉的是Druid的加密方式。

在连接池集成中的Druid章节里有对应的加密方式,但是如果我不用Druid也想用加密呢?

所以作者copy了Druid的加密相关源码,嘿嘿。

本项目也支持支持url , username, password 的加密。

使用的RAS加密,相关原理文章https://www.cnblogs.com/pcheng/p/9629621.html。
简单来说就是生成两把钥匙,私钥加密,公钥解密。 公钥可以发布出去,解密也是用的公钥。

具体使用

1. 获得加密字符串。

import com.baomidou.dynamic.datasource.toolkit.CryptoUtils;

public class Demo {

public static void main(String[] args) throws Exception {
String password = "123456";
//使用默认的publicKey ,建议还是使用下面的自定义
String encodePassword = CryptoUtils.encrypt(password);
System.out.println(encodePassword);
}

//自定义publicKey
public static void main(String[] args) throws Exception {
String[] arr = CryptoUtils.genKeyPair(512);
System.out.println("privateKey: " + arr[0]);
System.out.println("publicKey: " + arr[1]);
System.out.println("url: " + CryptoUtils.encrypt(arr[0], "jdbc:mysql://127.0.0.1:3306/order"));
System.out.println("username: " + CryptoUtils.encrypt(arr[0], "root"));
System.out.println("password: " + CryptoUtils.encrypt(arr[0], "123456"));
}
}

img

2. 配置加密yml。

ENC(xxx)` 中包裹的xxx即为使用加密方法后生成的字符串。

spring:
datasource:
dynamic:
public-key: #有默认值,强烈建议更换
datasource:
master:
url: ENC(xxxxx)
username: ENC(xxxxx)
password: ENC(xxxxx)
driver-class-name: com.mysql.jdbc.Driver
public-key: #每个数据源可以独立设置,没有就继承上面的。

启动初始化执行脚本

启动初始化执行脚本

spring:
datasource:
dynamic:
primary: order
datasource:
order:
# 基础配置省略...
schema: db/order/schema.sql # 配置则生效,自动初始化表结构
data: db/order/data.sql # 配置则生效,自动初始化数据
continue-on-error: true # 默认true,初始化失败是否继续
separator: ";" # sql默认分号分隔符,一般无需更改
product:
schema: classpath*:db/product/schema.sql
data: classpath*:db/product/data.sql
user:
schema: classpath*:db/user/schema/**/*.sql
data: classpath*:db/user/data/**/*.sql

懒启动数据源

基础介绍

懒启动:连接池创建出来后并不会立即初始化连接池,等需要使用connection的时候再初始化。

暂时只支持Druid和HikariCp和BeeCp连接池。

主要场景可能适合于数据源很多,又不需要启动立即初始化的情况,可以减少系统启动时间。

缺点在于,如果参数配置有误,则启动的时候不知道,初始化的时候失败,可能一直抛异常。

配置使用

spring:
datasource:
dynamic:
primary: master #设置默认的数据源或者数据源组,默认值即为master
strict: false #设置严格模式,默认false不启动. 启动后在未匹配到指定数据源时候会抛出异常,不启动则使用默认数据源.
lazy: true #默认false非懒启动,系统加载到数据源立即初始化连接池
datasource:
master:
url: jdbc:mysql://xx.xx.xx.xx:3306/dynamic
username: root
password: 123456
driver-class-name: com.mysql.jdbc.Driver
lazy: true #表示这个数据源懒启动
db1:
url: jdbc:mysql://xx.xx.xx.xx:3307/dynamic
username: root
password: 123456
driver-class-name: com.mysql.jdbc.Driver
db2:
url: jdbc:mysql://xx.xx.xx.xx:3307/dynamic
username: root
password: 123456
driver-class-name: com.mysql.jdbc.Driver

无数据源启动

基础介绍

场景:部分系统可能启动的时候完全不需要数据源,全靠启动后动态添加。

配置方式

即基本不需要配置,可能根据需要配置一些类似Druid和HikariCp全局参数。

如需配置Druid和HikariCp全局参数可参考对应章节文档。

spring:
datasource:
dynamic:
primary: master #设置默认的数据源或者数据源组,默认值即为master
strict: false #设置严格模式,默认false不启动. 启动后在未匹配到指定数据源时候会抛出异常,不启动则使用默认数据源.

因为没有匹配的主数据源,启动的时候你会见到类似如下日志。

dynamic-datasource initial loaded 0 datasource,Please add your primary datasource or check your configuration

手动切换数据源

在某些情况您可能需要手动切换数据源。

import com.baomidou.dynamic.datasource.toolkit.DynamicDataSourceContextHolder;

@Service
public class UserServiceImpl implements UserService {

@Autowired
private JdbcTemplate jdbcTemplate;

public List selectAll() {
DynamicDataSourceContextHolder.push("slave");//手动切换
return jdbcTemplate.queryForList("select * from user");
}

}

手动注入多数据源

什么时候需要手动注入多数据源?

绝大部分是因为您的系统需要和其他数据源共同存在使用。如Quartz和ShardingJdbc等都需要使用独立的数据源。


dynamic-datasource 3.4.0及以上版本和老版本注入方式有一定差别,根据自己版本注入。

注意一定要让多数据源使用 @Primary ,让其成为主数据源。

//3.4.0版本以下
@Primary
@Bean
public DataSource dataSource(DynamicDataSourceProvider dynamicDataSourceProvider) {
DynamicRoutingDataSource dataSource = new DynamicRoutingDataSource();
dataSource.setPrimary(properties.getPrimary());
dataSource.setStrict(properties.getStrict());
dataSource.setStrategy(properties.getStrategy());
dataSource.setProvider(dynamicDataSourceProvider);
dataSource.setP6spy(properties.getP6spy());
dataSource.setSeata(properties.getSeata());
return dataSource;
}

//3.4.0版本及以上
@Primary
@Bean
public DataSource dataSource() {
DynamicRoutingDataSource dataSource = new DynamicRoutingDataSource();
dataSource.setPrimary(properties.getPrimary());
dataSource.setStrict(properties.getStrict());
dataSource.setStrategy(properties.getStrategy());
dataSource.setP6spy(properties.getP6spy());
dataSource.setSeata(properties.getSeata());
return dataSource;
}

主要变更是因为3.4.0支持了多个provider同时生效,采取了内部注入。 源码改动参考https://github.com/baomidou/dynamic-datasource-spring-boot-starter/commit/6e8d2954499f83269302d23b58f8832c31e07ef7

自定义

自定义注解:不满足于默认DS注解,希望自定义注解。


自定义数据源来源:默认的数据源来源是基于yml或者properties配置来的,组件支持同时从多个地方来加载初始化数据源。 常用于多租户系统,从一个租户信息库多加载不同租户的数据源。


自定义负载均衡策略:常用的有轮询,随机。支持扩展。


自定义切面:支持不使用注解,通过配置来切换数据源。
如某个包下所有service方法以select*开头的都走slave库,以add*开头的都走master库。

自定义注解

基础介绍

如果你只有几个确定的库,可以尝试自定义注解替换掉@DS

建议从3.2.1版本开始使用自定义注解,另外组件自带了@Master@Slave注解。

使用方法

下面我们自定义一个产品库的注解,方便我们后面程序的使用。

1. 需要自己定义一个注解并继承自DS。

import com.baomidou.dynamic.datasource.annotation.DS;
import java.lang.annotation.*;

@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@DS("product")
public @interface Product {
}

2. 注解在需要切换数据源的方法上或类上。

@Service
@Product
public class ProductServiceImpl implements ProductService {

@Product
public List selectAll() {
return jdbcTemplate.queryForList("select * from products");
}
}

自定义数据源来源

自定义数据源来源

基础介绍

数据源来源的默认实现是YmlDynamicDataSourceProvider,其从yaml或properties中读取信息并解析出所有数据源信息。

public interface DynamicDataSourceProvider {

/**
* 加载所有数据源
*
* @return 所有数据源,key为数据源名称
*/
Map<String, DataSource> loadDataSources();
}

自定义示例

可以参考 AbstractJdbcDataSourceProvider (仅供参考)来实现从JDBC数据库中获取数据库连接信息。

@Bean
public DynamicDataSourceProvider jdbcDynamicDataSourceProvider() {
return new AbstractJdbcDataSourceProvider("org.h2.Driver", "jdbc:h2:mem:test", "sa", "") {
@Override
protected Map<String, DataSourceProperty> executeStmt(Statement statement) throws SQLException {
ResultSet rs = statement.executeQuery("select * from DB");
while (rs.next()) {
String name = rs.getString("name");
String username = rs.getString("username");
String password = rs.getString("password");
String url = rs.getString("url");
String driver = rs.getString("driver");
DataSourceProperty property = new DataSourceProperty();
property.setUsername(username);
property.setPassword(password);
property.setUrl(url);
property.setDriverClassName(driver);
map.put(name, property);
}
return map;
}
};
}

PS: 从3.4.0开始,可以注入多个DynamicDataSourceProvider的Bean以实现同时从多个不同来源加载数据源,注意同名会被覆盖。

自定义负载均衡策略

自定义负载均衡策略

基础介绍

如下图slave组下有三个数据源,当用户使用slave切换数据源时会使用负载均衡算法。

系统自带了两个负载均衡算法

  • LoadBalanceDynamicDataSourceStrategy 轮询,是默认的。
  • RandomDynamicDataSourceStrategy 随机的。
spring:
datasource:
dynamic:
datasource:
master:
username: sa
password: ""
url: jdbc:h2:mem:test
driver-class-name: org.h2.Driver
schema: db/schema.sql
slave_1:
username: sa
password: ""
url: jdbc:h2:mem:test
driver-class-name: org.h2.Driver
slave_2:
username: sa
password: ""
url: jdbc:h2:mem:test
driver-class-name: org.h2.Driver
slave_3:
username: sa
password: ""
url: jdbc:h2:mem:test
driver-class-name: org.h2.Driver
strategy: com.baomidou.dynamic.datasource.strategy.LoadBalanceDynamicDataSourceStrategy

如何自定义

如果默认的两个都不能满足要求,可以参考源码自定义。 暂时只能全局更改。

import java.util.List;
import java.util.concurrent.ThreadLocalRandom;
import javax.sql.DataSource;

public class RandomDynamicDataSourceStrategy implements DynamicDataSourceStrategy {
public RandomDynamicDataSourceStrategy() {
}

public DataSource determineDataSource(List<DataSource> dataSources) {
return (DataSource)dataSources.get(ThreadLocalRandom.current().nextInt(dataSources.size()));
}
}

无注解方案

无注解必读

不管是出于注解侵入性还是代码整洁等理由,我们都有需求不想使用注解的需求。

但是需要理解本多数据源实现的核心。

public class DynamicRoutingDataSource {
@Override
public DataSource determineDataSource() {
//这里是核心,是从ThreadLocal中获取当前数据源。
String dsKey = DynamicDataSourceContextHolder.peek();
return getDataSource(dsKey);
}

所以我们就可以根据需求,选择合适的时机调用DynamicDataSourceContextHolder.push(“对应数据源”)。

默认的@DS注解来切换数据源是根据spring AOP的特性,在方法开启前设置数据源KEY,方法执行完成后移除对应数据源KEY。

filter切换数据源

目标:拦截以filter/**开头的所有请求,如果后面的方法以a开头就切换到db1,以b开头就切换到db2,其余使用默认数据源。

实现如下:

@Slf4j
@WebFilter(filterName = "dsFilter", urlPatterns = {"/filter/*"})
public class DynamicDatasourceFilter implements Filter {

@Override
public void init(FilterConfig filterConfig) throws ServletException {
log.info("loading filter {}", filterConfig.getFilterName());
}

@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) servletRequest;
HttpServletResponse response = (HttpServletResponse) servletResponse;

String requestURI = request.getRequestURI();
log.info("经过多数据源filter,当前路径是{}", requestURI);
// String headerDs = request.getHeader("ds");
// Object sessionDs = request.getSession().getAttribute("ds");
String s = requestURI.replaceFirst("/filter/", "");

String dsKey = "master";
if (s.startsWith("a")) {
dsKey = "db1";
} else if (s.startsWith("b")) {
dsKey = "db2";
} else {

}

//执行
try {
DynamicDataSourceContextHolder.push(dsKey);
filterChain.doFilter(servletRequest, servletResponse);
} finally {
DynamicDataSourceContextHolder.poll();
}
}

@Override
public void destroy() {

}
}
@SpringBootApplication
@ServletComponentScan //filter必须使用这个
@MapperScan("com.baomidou.samples.ds.mapper")
public class AllDataSourceApplication {

public static void main(String[] args) {
SpringApplication.run(AllDataSourceApplication.class, args);
}
}

对应源码: https://github.com/dynamic-datasource/dynamic-datasource-samples/tree/master/all-datasource-sample
扩展:从request中还能获取到很多东西,如header和session,是否同理可以根据自己业务需求根据header值和session里对应的用户来动态设置和切换数据源?

intercepror切换数据源

目标:拦截以filter/**开头的所有请求,如果后面的方法以a开头就切换到db1,以b开头就切换到db2,其余使用默认数据源。

实现如下:

@Slf4j
public class DynamicDatasourceInterceptor implements HandlerInterceptor {

/**
* 在请求处理之前进行调用(Controller方法调用之前)
*/
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
String requestURI = request.getRequestURI();
log.info("经过多数据源Interceptor,当前路径是{}", requestURI);
// String headerDs = request.getHeader("ds");
// Object sessionDs = request.getSession().getAttribute("ds");
String s = requestURI.replaceFirst("/interceptor/", "");

String dsKey = "master";
if (s.startsWith("a")) {
dsKey = "db1";
} else if (s.startsWith("b")) {
dsKey = "db2";
} else {

}

DynamicDataSourceContextHolder.push(dsKey);
return true;
}

/**
* 请求处理之后进行调用,但是在视图被渲染之前(Controller方法调用之后)
*/
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) {

}

/**
* 在整个请求结束之后被调用,也就是在DispatcherServlet 渲染了对应的视图之后执行(主要是用于进行资源清理工作)
*/
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
DynamicDataSourceContextHolder.clear();
}

}
@Configuration
public class MyWebAutoConfig implements WebMvcConfigurer {

@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new DynamicDatasourceInterceptor()).addPathPatterns("/interceptor/**");
}
}

对应源码: https://github.com/dynamic-datasource/dynamic-datasource-samples/tree/master/all-datasource-sample

扩展:从request中还能获取到很多东西,如header和session,是否同理可以根据自己业务需求根据header值和session里对应的用户来动态设置和切换数据源?

自定义切面

从3.4.0开始支持自定义切面。

  1. 使用这个方式,原注解方式并不会失效。
  2. 注意:不要在同一个切面同时使用注解又使用自定义切面。
@Configuration
public class MyConfig {

@Bean
public DynamicDatasourceNamedInterceptor dsAdvice(DsProcessor dsProcessor) {
DynamicDatasourceNamedInterceptor interceptor = new DynamicDatasourceNamedInterceptor(dsProcessor);
Map<String, String> patternMap = new HashMap<>();
patternMap.put("select*", "slave");
patternMap.put("add*", "master");
patternMap.put("update*", "master");
patternMap.put("delete*", "master");
interceptor.addPatternMap(patternMap);
return interceptor;
}

@Bean
public Advisor dsAdviceAdvisor(DynamicDatasourceNamedInterceptor dsAdvice) {
AspectJExpressionPointcut pointcut = new AspectJExpressionPointcut();
pointcut.setExpression("execution (* com.baomidou.samples.pattern..service.*.*(..))");
return new DefaultPointcutAdvisor(pointcut, dsAdvice);
}
}

以上实现com.baomidou.samples.pattern包service下所有类的add和update,delete开头的方法使用master数据源,select使用slave数据源。

更复杂的切点表达式语法需自行学习。

示例项目 https://github.com/dynamic-datasource/dynamic-datasource-samples/tree/master/name-pattern-sample

mybatis下读写分离

自动读写分离

场景:

  1. 在纯的读写分离环境,写操作全部是master,读操作全部是slave。
  2. 不想通过注解配置完成以上功能。

答:在mybatis环境下可以基于mybatis插件结合本数据源完成以上功能。

手动注入插件。

@Bean
public MasterSlaveAutoRoutingPlugin masterSlaveAutoRoutingPlugin(){
return new MasterSlaveAutoRoutingPlugin();
}

默认主库名称master,从库名称slave。

暂不支持更多,源码简单可参考重写。

@Intercepts({@Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}),
@Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class})})
@Slf4j
public class MasterSlaveAutoRoutingPlugin implements Interceptor {

private static final String MASTER = "master";

private static final String SLAVE = "slave";

@Override
public Object intercept(Invocation invocation) throws Throwable {
Object[] args = invocation.getArgs();
MappedStatement ms = (MappedStatement) args[0];
try {
DynamicDataSourceContextHolder.push(SqlCommandType.SELECT == ms.getSqlCommandType() ? SLAVE : MASTER);
return invocation.proceed();
} finally {
DynamicDataSourceContextHolder.clear();
}
}

@Override
public Object plugin(Object target) {
return target instanceof Executor ? Plugin.wrap(target, this) : target;
}

@Override
public void setProperties(Properties properties) {
}
}

事务专栏

使用多数据源的你一定会遇到事务问题。


  1. 为什么使用了事务就切换不了数据源了啊?
  2. 有没有什么解决方案啊?
  3. 有没有使用案例和注意事项啊。

以上问题都能在本章节得到解答。


方案一:本组件集成了alibaba分布式事务组件seata。

方案二:本组件提供了无需外部组件的本地多数据源事物。

各有自己的优劣,总有一款适合你。

事务概念

什么是事务?

事务:是数据库操作的最小工作单元,是作为单个逻辑工作单元执行的一系列操作;这些操作作为一个整体一起向系统提交,要么都执行、要么都不执行;事务是一组不可再分割的操作集合(工作逻辑单元)。

事务的四大特性:

  1. 原子性
    事务是数据库的逻辑工作单位,事务中包含的各操作要么都做,要么都不做 。
  2. 一致性
    事务执行的结果必须是使数据库从一个一致性状态变到另一个一致性状态。因此当数据库只包含成功事务提交的结果时,就说数据库处于一致性状态。如果数据库系统 运行中发生故障,有些事务尚未完成就被迫中断,这些未完成事务对数据库所做的修改有一部分已写入物理数据库,这时数据库就处于一种不正确的状态,或者说是 不一致的状态。
  3. 隔离性
    一个事务的执行不能其它事务干扰。即一个事务内部的操作及使用的数据对其它并发事务是隔离的,并发执行的各个事务之间不能互相干扰。
  4. 持续性
    也称永久性,指一个事务一旦提交,它对数据库中的数据的改变就应该是永久性的。接下来的其它操作或故障不应该对其执行结果有任何影响。

原生JDBC处理事务。

try{
connection.setAutoCommit( false);
//这里用connection对数据库做了一系列操作,CRUD
connection.commit();//没有异常就提交
}catch(Exception ex){
connection.rollback(); //异常回滚
}finally{
connection.setAutoCommit( true);
}

以上是事务的核心,所有操作要一起成功,只要出错就回滚。

Spring处理事务。

spring开启事务很简单,在需要事务的方法和类上添加 @Transactional注解。

@Transactional
public void test() {
Aservice.dosometing();
Bservice.dosometing();
}

使用了 @Transactionalspring会保证整个事务下都复用同一个connection。
在默认配置下,只要事务中发生RuntimeException,就会回滚。

基础知识

基础知识

问:使用了事务如@Transational 无法切换数据源?

答: 是的,本组件是基于springAop的方案来进行多数据源的管理和切换的,要想保证多个库的整体事务则需要分布式事务。

问:为什么使用了事务如@Transational就无法切换数据源?

答:开启了事务后,spring事务管理器会保证在事务下整个线程后续拿到的都是同一个connection。

问:事务下无法切换数据源我知道了,那我单库的事务的可以用吗?

答:完全可以的。 只要事务下不切换数据源就OK。

问:我的业务必须要保证事务,还有什么解决办法?

方案一:seata就是解决此问题,本组件已完成和seata的自动集成。

方案二:本组件从3.3.0开始支持本地多数据源事务,无需第三方。

查看左侧边栏对应的详细文档。

还有没有其他方案?

答: 如果数据源只有几个,又不怕复杂,可以放弃本组件。

尝试手动构建数据源结合JTA方案 如 https://www.cnblogs.com/cicada-smile/p/13289306.html。

本文不做详解,如果连接失效或者还是不清楚可以自行各渠道搜索 多数据源JTA

另外建议不理解或不清楚分布式事务的同学,自行先通过搜索引擎 分布式事务CAP定理 学习相关概念。

程序员小灰的分布式事务 https://blog.csdn.net/bjweimengshu/article/details/79607522

程序员小灰的CAP定理 https://blog.csdn.net/bjweimengshu/article/details/79338859

本地事务

背景

多数据源事务方案一直是一个难题,通常的解决方案有以下二种。

  1. 利用atomiks手动构建多数据源事务,适合数据源较少,配置的参数也不太多,性能要求不高的项目。难点就是手动配置量大,需要耗费一定时间。
  2. 用seata类似的分布式事务解决方案,难点就是需要搭建维护如seata-server的统一管理中心。

每一种方案都有其适用场景,本项目作者常常收到的问题就是。

  1. 为什么我加了事务注解,切换数据源失败?
  2. 我了解涉及了分布式事务了,但我不想用seata。我场景简单,有没有不依赖第三方的方案?

基础介绍

自从3.3.0开始,由seata的核心贡献者https://github.com/a364176773 贡献了基于connection代理的方案。
建议从3.4.0版本开始使用,其修复了一个功能,老版本不加@DS只加@DSTransactional会报错。

注意事项

  1. 不支持spring原生事务,不支持spring事务,不支持spring事务,可分别使用,千万不能混用
  2. 再次强调不支持spring事务注解,可理解成独立写了一套事务方案。
  3. 只适合简单本地多数据源场景, 如果涉及异步和微服务等完整事务场景,请使用seata方案。
  4. 暂时不支持更多配置,如只读,如spring的传播特性。 后续会根据反馈考虑支持。

使用方法

在最外层的方法添加 @DSTransactional,底下调用的各个类该切数据源就正常使用DS切换数据源即可。 就是这么简单。~

//如AService调用BService和CService的方法,A,B,C分别对应不同数据源。

public class AService {

@DS("a")//如果a是默认数据源则不需要DS注解。
@DSTransactional
public void dosomething(){
BService.dosomething();
CService.dosomething();
}
}

public class BService {

@DS("b")
public void dosomething(){
//dosomething
}
}

public class CService {

@DS("c")
public void dosomething(){
//dosomething
}
}

只要@DSTransactional注解下任一环节发生异常,则全局多数据源事务回滚。
如果BC上也有@DSTransactional会有影响吗?答:没有影响的。

示例项目

示例项目A,B,C分别对应OrderService,ProductService,AccountService。分别是独立的数据库。

用户下单分别调用产品库扣库存,账户库扣余额。
如果库存不足,或用户余额不足都抛出RuntimeException,触发整体回滚。

@Slf4j
@Service
@AllArgsConstructor
public class OrderService {

private final OrderMapper orderMapper;
private final AccountService accountService;
private final ProductService productService;

//@DS("order") 这里不需要,因为order是默认库,如果开启事务的不是默认库则必须加
@DSTransactional //注意这里开启事务
public void placeOrder(PlaceOrderRequest request) {
log.info("=============ORDER START=================");
Long userId = request.getUserId();
Long productId = request.getProductId();
Integer amount = request.getAmount();
log.info("收到下单请求,用户:{}, 商品:{},数量:{}", userId, productId, amount);

log.info("当前 XID: {}", TransactionContext.getXID());

Order order = Order.builder()
.userId(userId)
.productId(productId)
.status(OrderStatus.INIT)
.amount(amount)
.build();

orderMapper.insert(order);
log.info("订单一阶段生成,等待扣库存付款中");
// 扣减库存并计算总价
Double totalPrice = productService.reduceStock(productId, amount);
// 扣减余额
accountService.reduceBalance(userId, totalPrice);

order.setStatus(OrderStatus.SUCCESS);
order.setTotalPrice(totalPrice);
orderMapper.updateById(order);
log.info("订单已成功下单");
log.info("=============ORDER END=================");
}
}
@Slf4j
@Service
@RequiredArgsConstructor
public class ProductService {

private final ProductMapper productMapper;

@DS("product")
public Double reduceStock(Long productId, Integer amount) {
log.info("=============PRODUCT START=================");
log.info("当前 XID: {}", TransactionContext.getXID());

// 检查库存
Product product = productMapper.selectById(productId);
Assert.notNull(product, "商品不存在");
Integer stock = product.getStock();
log.info("商品编号为 {} 的库存为{},订单商品数量为{}", productId, stock, amount);

if (stock < amount) {
log.warn("商品编号为{} 库存不足,当前库存:{}", productId, stock);
throw new RuntimeException("库存不足");
}
log.info("开始扣减商品编号为 {} 库存,单价商品价格为{}", productId, product.getPrice());
// 扣减库存
int currentStock = stock - amount;
product.setStock(currentStock);
productMapper.updateById(product);
double totalPrice = product.getPrice() * amount;
log.info("扣减商品编号为 {} 库存成功,扣减后库存为{}, {} 件商品总价为 {} ", productId, currentStock, amount, totalPrice);
log.info("=============PRODUCT END=================");
return totalPrice;
}
}
@Slf4j
@Service
@RequiredArgsConstructor
public class AccountService {

private final AccountMapper accountMapper;

@DS("account")
public void reduceBalance(Long userId, Double price) {
log.info("=============ACCOUNT START=================");
log.info("当前 XID: {}", TransactionContext.getXID());

Account account = accountMapper.selectById(userId);
Assert.notNull(account, "用户不存在");
Double balance = account.getBalance();
log.info("下单用户{}余额为 {},商品总价为{}", userId, balance, price);

if (balance < price) {
log.warn("用户 {} 余额不足,当前余额:{}", userId, balance);
throw new RuntimeException("余额不足");
}
log.info("开始扣减用户 {} 余额", userId);
double currentBalance = account.getBalance() - price;
account.setBalance(currentBalance);
accountMapper.updateById(account);
log.info("扣减用户 {} 余额成功,扣减后用户账户余额为{}", userId, currentBalance);
log.info("=============ACCOUNT END=================");
}
}

完整示例项目 https://github.com/dynamic-datasource/dynamic-datasource-samples/tree/master/tx-local-sample 数据库都已准备好,可以直接运行测试。http://localhost:8080/doc.html

img

原理介绍

完整支持代码:https://github.com/baomidou/dynamic-datasource-spring-boot-starter/commit/f0cbad193528296eeb64faa76c79743afbdd811d

核心原理就是代理connection,并根据不同数据库获取到一个connection放入ConnectionFactory。 如果成功了整体提交,失败了整体回滚。

seata事务

基础介绍

PS:一般需要分布式事务的场景大多数都是微服务化,个人并不建议在单体项目引入多数据源+分布式事务,有能力尽早拆开,可为过度方案。

注意事项

dynamic-datasource-sring-boot-starter 组件内部开启seata后会自动使用DataSourceProxy来包装DataSource,所以需要以下方式来保持兼容。

1.如果你引入的是seata-all,请不要使用@EnableAutoDataSourceProxy注解。

2.如果你引入的是seata-spring-boot-starter 请关闭自动代理。

seata:
enable-auto-data-source-proxy: false

示例项目

此工程为 多数据源+druid+seata+mybatisPlus的版本。

模拟用户下单,扣商品库存,扣用户余额,初步可分为订单服务+商品服务+用户服务。

环境准备

为了快速演示相关环境都采用docker部署,生产上线请参考seata官方文档使用。

  1. 准备seata-server。
docker run --name seata-server -p 8091:8091 -d seataio/seata-server
  1. 准备mysql数据库,账户root密码123456。
docker run --name mysql -p 3306:3306 -e MYSQL_ROOT_PASSWORD=123456 -d mysql:5.7
  1. 创建相关数据库。

创建 seata-order``seata-product``seata-account 模拟连接不同的数据库。

CREATE DATABASE IF NOT EXIST seata-order;
CREATE DATABASE IF NOT EXIST seata-product;
CREATE DATABASE IF NOT EXIST seata-account;
  1. 准备相关数据库脚本。

每个数据库下脚本相关的表,seata需要undo_log来监测和回滚。

相关的脚本不用自行准备,本工程已在resources/db下面准备好,另外配合多数据源的自动执行脚本功能,应用启动后会自动执行。

工程准备

  1. 引入相关依赖,seata+druid+mybatisPlus+dynamic-datasource+mysql+lombok。
<dependency>
<groupId>io.seata</groupId>
<artifactId>seata-spring-boot-starter</artifactId>
<version>1.4.2</version>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>dynamic-datasource-spring-boot-starter</artifactId>
<version>3.4.0</version>
</dependency>
# 省略,查看示例项目
  1. 编写相关yaml配置。
spring:
application:
name: dynamic
datasource:
dynamic:
primary: order
strict: true
seata: true #开启seata代理,开启后默认每个数据源都代理,如果某个不需要代理可单独关闭
seata-mode: AT #支持XA及AT模式,默认AT
datasource:
order:
username: root
password: 123456
url: jdbc:mysql://39.108.158.138:3306/seata_order?useUnicode=true&characterEncoding=utf8&allowMultiQueries=true&useSSL=false
driver-class-name: com.mysql.cj.jdbc.Driver
schema: classpath:db/schema-order.sql
account:
username: root
password: 123456
url: jdbc:mysql://39.108.158.138:3306/seata_account?useUnicode=true&characterEncoding=utf8&allowMultiQueries=true&useSSL=false
driver-class-name: com.mysql.cj.jdbc.Driver
schema: classpath:db/schema-account.sql
product:
username: root
password: 123456
url: jdbc:mysql://39.108.158.138:3306/seata_product?useUnicode=true&characterEncoding=utf8&allowMultiQueries=true&useSSL=false
driver-class-name: com.mysql.cj.jdbc.Driver
schema: classpath:db/schema-product.sql
test:
username: sa
password: ""
url: jdbc:h2:mem:test
driver-class-name: org.h2.Driver
seata: false #这个数据源不需要seata
seata:
enabled: true
application-id: applicationName
tx-service-group: my_test_tx_group
enable-auto-data-source-proxy: false #一定要是false
service:
vgroup-mapping:
my_test_tx_group: default #key与上面的tx-service-group的值对应
grouplist:
default: 39.108.158.138:8091 #seata-server地址仅file注册中心需要
config:
type: file
registry:
type: file

代码编写

参考工程下面的代码完成controller,service,maaper,entity,dto等。

订单服务

@Slf4j
@Service
public class OrderServiceImpl implements OrderService {

@Resource
private OrderDao orderDao;
@Autowired
private AccountService accountService;
@Autowired
private ProductService productService;

@DS("order")//每一层都需要使用多数据源注解切换所选择的数据库
@Override
@Transactional
@GlobalTransactional //重点 第一个开启事务的需要添加seata全局事务注解
public void placeOrder(PlaceOrderRequest request) {
log.info("=============ORDER START=================");
Long userId = request.getUserId();
Long productId = request.getProductId();
Integer amount = request.getAmount();
log.info("收到下单请求,用户:{}, 商品:{},数量:{}", userId, productId, amount);

log.info("当前 XID: {}", RootContext.getXID());

Order order = Order.builder()
.userId(userId)
.productId(productId)
.status(OrderStatus.INIT)
.amount(amount)
.build();

orderDao.insert(order);
log.info("订单一阶段生成,等待扣库存付款中");
// 扣减库存并计算总价
Double totalPrice = productService.reduceStock(productId, amount);
// 扣减余额
accountService.reduceBalance(userId, totalPrice);

order.setStatus(OrderStatus.SUCCESS);
order.setTotalPrice(totalPrice);
orderDao.updateById(order);
log.info("订单已成功下单");
log.info("=============ORDER END=================");
}
}

商品服务

@Slf4j
@Service
public class ProductServiceImpl implements ProductService {

@Resource
private ProductDao productDao;

/**
* 事务传播特性设置为 REQUIRES_NEW 开启新的事务 重要!!!!一定要使用REQUIRES_NEW
*/
@DS("product")
@Transactional(propagation = Propagation.REQUIRES_NEW)
@Override
public Double reduceStock(Long productId, Integer amount) {
log.info("=============PRODUCT START=================");
log.info("当前 XID: {}", RootContext.getXID());

// 检查库存
Product product = productDao.selectById(productId);
Integer stock = product.getStock();
log.info("商品编号为 {} 的库存为{},订单商品数量为{}", productId, stock, amount);

if (stock < amount) {
log.warn("商品编号为{} 库存不足,当前库存:{}", productId, stock);
throw new RuntimeException("库存不足");
}
log.info("开始扣减商品编号为 {} 库存,单价商品价格为{}", productId, product.getPrice());
// 扣减库存
int currentStock = stock - amount;
product.setStock(currentStock);
productDao.updateById(product);
double totalPrice = product.getPrice() * amount;
log.info("扣减商品编号为 {} 库存成功,扣减后库存为{}, {} 件商品总价为 {} ", productId, currentStock, amount, totalPrice);
log.info("=============PRODUCT END=================");
return totalPrice;
}
}

用户服务

@Slf4j
@Service
public class AccountServiceImpl implements AccountService {

@Resource
private AccountDao accountDao;

/**
* 事务传播特性设置为 REQUIRES_NEW 开启新的事务 重要!!!!一定要使用REQUIRES_NEW
*/
@DS("account")
@Override
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void reduceBalance(Long userId, Double price) {
log.info("=============ACCOUNT START=================");
log.info("当前 XID: {}", RootContext.getXID());

Account account = accountDao.selectById(userId);
Double balance = account.getBalance();
log.info("下单用户{}余额为 {},商品总价为{}", userId, balance, price);

if (balance < price) {
log.warn("用户 {} 余额不足,当前余额:{}", userId, balance);
throw new RuntimeException("余额不足");
}
log.info("开始扣减用户 {} 余额", userId);
double currentBalance = account.getBalance() - price;
account.setBalance(currentBalance);
accountDao.updateById(account);
log.info("扣减用户 {} 余额成功,扣减后用户账户余额为{}", userId, currentBalance);
log.info("=============ACCOUNT END=================");
}

}

测试

在schema自动执行的脚本里,默认设置了商品价格为10,商品总数量为20,用户余额为50。

启动项目后通过命令行执行。

  1. 模拟正常下单,买一个商品。
curl -X POST \
http://localhost:8080/order/placeOrder \
-H 'Content-Type: application/json' \
-d '{
"userId": 1,
"productId": 1,
"amount": 1
}'
  1. 模拟库存不足,事务回滚。
curl -X POST \
http://localhost:8080/order/placeOrder \
-H 'Content-Type: application/json' \
-d '{
"userId": 1,
"productId": 1,
"amount": 22
}'
  1. 模拟用户余额不足,事务回滚。
curl -X POST \
http://localhost:8080/order/placeOrder \
-H 'Content-Type: application/json' \
-d '{
"userId": 1,
"productId": 1,
"amount": 6
}'

注意观察运行日志,至此分布式事务集成案例全流程完毕。

事务常见问题

建议先阅读基础知识。

核心知识: spring原生事务会保证在事务下整个线程后续拿到的都是同一个connection。
核心知识: spring原生事务会保证在事务下整个线程后续拿到的都是同一个connection。
核心知识: spring原生事务会保证在事务下整个线程后续拿到的都是同一个connection。

原生Spring@Transational能和@DS一起使用吗?

public class AService {

@DS("a")
@Transational
public void dosomething(){
//some code
}
}

可以的,其会先切换使用数据源a再开启事务,整个原生事务内部不管是注解切换还是手动调用代码切换都不能切换,会一直使用a数据源。

所以确认整个事务下不再切换其他数据源,用原生 @Transational 是建议的,毕竟其更完善。

本地事务和Spring原生事务不能混用是什么意思?

场景一:先使用的Spring@Transational 内部方法调用了本地@DSTransactional

public class AService {

@DS("a")
@Transational
public void dosomething(){
Bservice.dosomething(); //B是另外数据源,然后注解了@DS("b")和@DSTransactional
Cservice.dosomething(); //C是另外数据源,然后注解了@DS("c")和@DSTransactional
}
}

基于核心知识: 整理事务下都是a数据源,内部无论做什么都改变不了使用a数据源。

场景二: 先使用的本地@DSTransactional内部方法调用了Spring@Transational

public class AService {

@DS("a")
@DSTransactional
public void dosomething(){
Amapper.updateSomeThing();
Bservice.dosomething(); //B是另外数据源,然后注解了@DS("b")和@Transational
Cservice.dosomething(); //C是另外数据源,然后注解了@DS("b")和@Transational
}
}

B和C都是独立的事务。C发生错误B会回滚吗?不会B已经提交了。A会回滚吗?会。

不建议混用,除非你确实非常理解事务,能随心所欲掌握你代码的执行流程。

本地事务标准使用。

请结合阅读本地事务章节。

public class AService {

@DS("a")
@DSTransactional//最外层开启即可
public void dosomething(){
Amapper.updateSomeThing();
Bservice.dosomething(); //B是另外数据源,然后注解了@DS("b")
Cservice.dosomething(); //C是另外数据源,然后注解了@DS("b")
}
}

调试源码

  1. 开启动态数据源的debug日志。
logging:
level:
com.baomidou.dynamic: debug

检查日志输出是否正确。

  1. 断点调试DynamicDataSourceAnnotationInterceptor。
public class DynamicDataSourceAnnotationInterceptor implements MethodInterceptor {

private static final String DYNAMIC_PREFIX = "#";

private final DataSourceClassResolver dataSourceClassResolver;
private final DsProcessor dsProcessor;

public DynamicDataSourceAnnotationInterceptor(Boolean allowedPublicOnly, DsProcessor dsProcessor) {
dataSourceClassResolver = new DataSourceClassResolver(allowedPublicOnly);
this.dsProcessor = dsProcessor;
}

@Override
public Object invoke(MethodInvocation invocation) throws Throwable {
try {
//这里把获取到的数据源标识如master存入本地线程
String dsKey = determineDatasourceKey(invocation);
● DynamicDataSourceContextHolder.push(dsKey);
return invocation.proceed();
} finally {
DynamicDataSourceContextHolder.poll();
}
}

private String determineDatasourceKey(MethodInvocation invocation) {
String key = dataSourceClassResolver.findDSKey(invocation.getMethod(), invocation.getThis());
return (!key.isEmpty() && key.startsWith(DYNAMIC_PREFIX)) ? dsProcessor.determineDatasource(invocation, key) : key;
}
}
  1. 断点调试DynamicRoutingDataSource。
public class DynamicRoutingDataSource extends AbstractRoutingDataSource {

private Map<String, DataSource> dataSourceMap = new LinkedHashMap<>();

private Map<String, DynamicGroupDataSource> groupDataSources = new ConcurrentHashMap<>();

@Override
public DataSource determineDataSource() {
//从本地线程获取key解析最终真实的数据源
● String dsKey = DynamicDataSourceContextHolder.peek();
return getDataSource(dsKey);
}

private DataSource determinePrimaryDataSource() {
log.debug("从默认数据源中返回数据");
return groupDataSources.containsKey(primary) ? groupDataSources.get(primary).determineDataSource() : dataSourceMap.get(primary);
}

public DataSource getDataSource(String ds) {
if (StringUtils.isEmpty(ds)) {
return determinePrimaryDataSource();
} else if (!groupDataSources.isEmpty() && groupDataSources.containsKey(ds)) {
log.debug("从 {} 组数据源中返回数据源", ds);
return groupDataSources.get(ds).determineDataSource();
} else if (dataSourceMap.containsKey(ds)) {
log.debug("从 {} 单数据源中返回数据源", ds);
return dataSourceMap.get(ds);
}
return determinePrimaryDataSource();
}
}

切换数据源失败

  • 1.开启了spring的事务。

原因: spring开启事务后会维护一个ConnectionHolder,保证在整个事务下,都是用同一个数据库连接。

请检查整个调用链路涉及的类的方法和类本身还有继承的抽象类上是否有@Transactional注解。

如强烈需要事务保证多个库同时执行成功或者失败,请查看事务专栏的解决办法。

2.方法内部调用。

查看以下示例 回答 外部调用 userservice.test1() 能在执行到 test2() 切换到second数据源吗?

public UserService {

@DS("first")
public void test1() {
// do something
test2();
}

@DS("second")
public void test2() {
// do something
}
}

答案:!!!不能不能不能!!!! 数据源核心原理是基于aop代理实现切换,内部方法调用不会使用aop。

解决方法:

把test2()方法提到另外一个service,单独调用。

3.shiroAop代理。

在shiro框架中(UserRealm)使用@Autowire 注入的类, 缓存注解和事务注解都失效。

@Component
public class UserRealm extends AuthorizingRealm {

@Lazy
@Autowired
private IUserService userService;
//... 省略其他无关的内容
}

解决方法:
1.在Shiro框架中注入Bean时, 不使用@Autowire, 使用ApplicationContextRegister.getBean()方法,手动注入bean。

2.使用@Autowire+@Lazy注解,设置注入到Shiro框架的Bean延时加载(推荐)。

4.PostConstruct初始化顺序。

初始化包括: @PostConstruct 注解, InitializingBean接口, 自定义init-method

@Component
public class MyConfiguration {
@Resource
private UserMapper userMapper;
@DS("slave")
@PostConstruct
public void init(){
// 无法选择正确的数据源
userMapper.selectById(1);
}
}

解决方法:监听容器启动完成事件, 在容器完成后做初始化。

@Component
public class MyConfiguration {

@DS("slave")
@EventListener
public void onApplicationEvent(ContextRefreshedEvent event) {
// 成功选择正确的数据源
userMapper.selectById(1);
}
}

相关spring源码 : `org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory#initializeBean

在初始化完成后, bean 进入增强阶段, 所以这个阶段的任何AOP都是无效的。

5.Druid版本过低。

请升级druid1.1.22及以上版本,这个版本修复了在高并发下偶发的切换失效问题。

6.@Async或者java8的ParallelStream并行流之类方法。

这种情况都是新开了线程去处理,不受当前线程管控了。 可以在新开的方法上加对应的DS注解解决。


扩展阅读

shiro失效原因

在spring初始化bean的阶段中,大致上分为三段: BeanFactoryPostProcessor -> BeanPostProcessor -> Bean

这三种都是单例bean. 只不过会按照这个优先级进行初始化, shiro引起AOP失效的原因:

ShiroFilterFactoryBean 是一个 BeanPostProcessor , 比普通的单例Bean都优先加载, 所以他依赖注入的bean都无法正确的进行切面。

ShiroFilterFactoryBean 实际的依赖情况:
ShiroFilterFactoryBean -> SecurityManager -> UserRealm -> IUserService

IUserService依赖的其他 service 也会失效
IUserService-> MenuService -> RoleService

辅助知识

如何注入多数据源?

public class A{
@Autowired
private DataSource dataSource;

public void dosomething{
//使用的时候需要强转
DynamicRoutingDataSource ds = (DynamicRoutingDataSource) dataSource;
}
}

如何获取当前线程数据源名称?

DynamicDataSourceContextHolder.peek()

如何获取当前线程数据源?

public void dosomething{
DynamicRoutingDataSource ds = (DynamicRoutingDataSource) dataSource;
DataSource datasource=ds.determineDataSource();
}

DynamicRoutingDataSource 其他常用方法

img