Dynamic-Datasource 文档

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

特性

  1. 支持数据源分组,适用于多种场景,纯粹多库、读写分离、一主多从、混合模式。
  2. 支持数据库敏感配置信息加密(可自定义)ENC()。
  3. 支持每个数据库独立初始化表结构schema和数据库database
  4. 支持无数据源启动,支持懒加载数据源(需要的时候再创建连接)。
  5. 支持自定义注解,需继承DS(3.2.0+)
  6. 提供并简化对DruidHikariCPBeeCPDBCP2的快速集成。
  7. 提供对Mybatis-PlusQuartzShardingJdbcP6syJNDI等组件的集成方案。
  8. 提供自定义数据源来源方案(如全从数据库加载)。
  9. 提供项目启动后动态增加移除数据源方案。
  10. 提供Mybatis环境下的纯读写分离方案。
  11. 提供使用spel动态参数解析数据源方案。内置spelsessionheader,支持自定义。
  12. 支持多层数据源嵌套切换。(ServiceA >>> ServiceB >>> ServiceC)。
  13. 提供基于seata的分布式事务方案。
  14. 提供本地多数据源事务方案。

约定

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

使用方法

1、引入dynamic-datasource-spring-boot-starter

spring-boot 1.5.x、2.x.x

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

spring-boot3及以上

<dependency><groupId>com.baomidou</groupId><artifactId>dynamic-datasource-spring-boot3-starter</artifactId><version>${version}</version>
</dependency>

2、配置数据源

spring:datasource:dynamic:primary: master # 设置默认的数据源或者数据源组,默认值即为masterstrict: false # 严格匹配数据源,默认false. true未匹配到指定数据源时抛异常,false使用默认数据源datasource:master:url: jdbc:mysql://xx.xx.xx.xx:3306/dynamicusername: rootpassword: 123456driver-class-name: com.mysql.cj.jdbc.Driver # 3.2.0开始支持SPI可省略此配置slave_1:url: jdbc:mysql://xx.xx.xx.xx:3307/dynamicusername: rootpassword: 123456driver-class-name: com.mysql.cj.jdbc.Driverslave_2:url: ENC(xxxxx) # 内置加密,使用请查看详细文档username: ENC(xxxxx)password: ENC(xxxxx)driver-class-name: com.mysql.cj.jdbc.Driver# ......省略# 以上会配置一个默认库master,一个组slave下有两个子库slave_1,slave_2
# 多主多从
spring: datasource:dynamic:datasource:master_1:master_2:slave_1:slave_2:slave_3:
# 纯粹多库(记得设置primary)
spring: datasource:   dynamic:datasource:mysql: oracle:sqlserver: postgresql:h2:
# 混合配置
spring: datasource:   dynamic:datasource:master: slave_1:slave_2: oracle_1:oracle_2:

3、使用@DS切换数据源

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

注解结果
没有@DS默认数据源
@DS("dsName")dsName可以为组名也可以为具体某个库的名称,组名则切换时采用负载均衡算法切换。
@Service
@DS("slave")
public class UserServiceImpl implements UserService {@Autowired private JdbcTemplate jdbcTemplate;// 使用slave数据源public List selectAll() {return  jdbcTemplate.queryForList("select * from user");}// 使用slave_1数据源@Override@DS("slave_1")public List selectByCondition() {return  jdbcTemplate.queryForList("select * from user where age >10");}
}

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

spring:datasource:dynamic:primary: db1 datasource:db1:url: jdbc:mysql://xx.xx.xx.xx:3306/dynamicusername: rootpassword: 123456driver-class-name: com.mysql.cj.jdbc.Drivertype: com.zaxxer.hikari.HikariDataSource     # 使用Hikaricpdb2:url: jdbc:mysql://xx.xx.xx.xx:3307/dynamicusername: rootpassword: 123456driver-class-name: com.mysql.cj.jdbc.Drivertype: com.alibaba.druid.pool.DruidDataSource # 使用Druiddb3:url: jdbc:mysql://xx.xx.xx.xx:3308/dynamicusername: rootpassword: 123456driver-class-name: com.mysql.cj.jdbc.Drivertype: cn.beecp.BeeDataSource                 # 使用beecp

集成Druid

基础介绍

Druid Github:点我跳转

Druid 文档:点我跳转

dynamic-datasource能简单高效完成对Druid的集成并完成其参数的多元化配置。

各个库可以使用不同的数据库连接池,如master使用Druidslave使用HikariCP

如果项目同时存在DruidHikariCP并且未配置连接池type类型,默认Druid优先于HikariCP

集成步骤

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

<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: trueloginUsername: adminloginPassword: 12345678dynamic: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原生不一致,默认启动了statwall: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: rootpassword: 123456driver-class-name: com.mysql.cj.jdbc.Driverurl: jdbc:mysql://xx.xx.xx.xx:3306/dynamic?characterEncoding=utf8&useSSL=falsedruid: # 以下是独立参数,每个库可以重新设置initial-size: 20validation-query: select 1 FROM DUAL # 比如oracle就需要重新设置这个public-key: #(非全局参数)设置即表示启用加密,底层会自动帮你配置相关的连接参数和filter,推荐使用本项目自带的加密方法。

以下是我曾经配置过的示例:

# 数据源配置
spring:# 排除掉druid原生的自动配置autoconfigure:exclude: com.alibaba.druid.spring.boot.autoconfigure.DruidDataSourceAutoConfiguredatasource:druid:# 初始连接数initialSize: 5# 最小连接池数量minIdle: 10# 最大连接池数量maxActive: 20# 配置获取连接等待超时的时间maxWait: 60000# 配置连接超时时间connectTimeout: 30000# 配置网络超时时间socketTimeout: 60000# 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒timeBetweenEvictionRunsMillis: 60000# 配置一个连接在池中最小生存的时间,单位是毫秒minEvictableIdleTimeMillis: 300000# 配置一个连接在池中最大生存的时间,单位是毫秒maxEvictableIdleTimeMillis: 900000# 配置检测连接是否有效validationQuery: SELECT 1 FROM DUALtestWhileIdle: truetestOnBorrow: falsetestOnReturn: falsewebStatFilter:enabled: truestatViewServlet:enabled: true# 设置白名单,不填则允许所有访问allow:url-pattern: /druid/*# 控制台管理用户名和密码login-username: adminlogin-password: 123456filter:stat:enabled: true# 慢SQL记录log-slow-sql: trueslow-sql-millis: 2000merge-sql: truewall:config:multi-statement-allow: truedynamic:druid:# 初始连接数initial-size: 5# 最小连接池数量min-idle: 10# 最大连接池数量max-active: 20# 配置获取连接等待超时的时间max-wait: 60000# 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒time-between-eviction-runs-millis: 60000# 配置一个连接在池中最小生存的时间,单位是毫秒min-evictable-idle-time-millis: 300000# 配置一个连接在池中最大生存的时间,单位是毫秒max-evictable-idle-time-millis: 900000# 配置检测连接是否有效validation-query: SELECT 1 FROM DUALtest-while-idle: truetest-on-borrow: falsetest-on-return: false# 打开PSCache,并指定每个连接上PSCache的大小。# oracle设为true,mysql设为false。分库分表较多推荐设置为falsepool-prepared-statements: falsemax-pool-prepared-statement-per-connection-size: 20filters: stat # 注意这个值和druid原生不一致,默认启动了statstat:merge-sql: truelog-slow-sql: trueslow-sql-millis: 2000primary: master # 设置默认的数据源或者数据源组strict: false # 设置严格模式,默认false不启动,启动后在未匹配到指定数据源时候回抛出异常,不启动会使用默认数据源datasource:master:url: jdbc:mysql://192.168.56.101:3306/master?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8username: rootpassword: rootdriver-class-name: com.mysql.cj.jdbc.Driverslave_1:url: jdbc:mysql://192.168.56.102:3306/slave_1?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8&rewriteBatchedStatements=trueusername: rootpassword: rootdriver-class-name: com.mysql.cj.jdbc.Driverslave_2:url: jdbc:mysql://192.168.56.103:3306/slave_2?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8&rewriteBatchedStatements=trueusername: rootpassword: rootdriver-class-name: com.mysql.cj.jdbc.Driverdruid: # 以下参数针对每个库可以重新设置druid参数initial-size:validation-query: select 1 FROM DUAL # 比如oracle就需要重新设置这个public-key: #(非全局参数)设置即表示启用加密,底层会自动帮你配置相关的连接参数和filter。

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

示例项目

点我跳转

核心源码

Druid数据源创建器:点我跳转

Druid参数源码:点我跳转

集成HikariCP

基础介绍

HikariCP Github:点我跳转

HikariCP 文档:点我跳转

集成步骤

1、项目引入HikariCP依赖

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

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

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

2、参数配置

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

特别注意,hikaricp原生设置某些字段名和dynamic-datasource不一致,dynamic-datasource是根据参数反射设置,而原生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: rootpassword: 123456driver-class-name: com.mysql.cj.jdbc.Driverurl: jdbc:mysql://xx.xx.xx.xx:3306/dynamic?characterEncoding=utf8&useSSL=falsehikari: # 以下参数针对每个库可以重新设置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数据源创建器:点我跳转

HikariCP参数配置类:点我跳转

第三方集成

通过本章您可以掌握数据源在集成MybatisPlusQuartzShardingJdbc的方案。

集成MybatisPlus

基础介绍

MybatisPlus Github:点我跳转

MybatisPlus 文档 点我跳转

只要引入了MybatisPlus相关jar包,项目自动集成,兼容MybatisPlus2.x3.x的版本。

集成步骤

1、项目引入Mybatis-Plus依赖

<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 somethingbaseMapper.insert(user);}
}

3、分页配置

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

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

解决方法:

方法1:复制ServiceImpl出来为自己的MyServiceImpl,并去掉所有事务注解。

方法2:创建一个新方法,并在此方法上加DS注解. 如上面的addUser方法。

FAQ

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

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

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

核心解析源码:点我跳转

示例项目

点我跳转

其他集成

集成P6spy、集成Quartz、集成ShardingJdbc

由于以上场景暂时没用到,文章中暂时不体现了,如有用到,后面会补充上。

进阶使用

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

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

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

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

自动读写分离:适用于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 {@Autowiredprivate DataSource dataSource;// private final DataSourceCreator dataSourceCreator;// 3.3.1及以下版本使用这个通用,强烈推荐sb2用户至少升级到3.5.2版本@Autowiredprivate DefaultDataSourceCreator dataSourceCreator;@Autowiredprivate BasicDataSourceCreator basicDataSourceCreator;@Autowiredprivate JndiDataSourceCreator jndiDataSourceCreator;@Autowiredprivate DruidDataSourceCreator druidDataSourceCreator;@Autowiredprivate HikariDataSourceCreator hikariDataSourceCreator;@Autowiredprivate BeeCpDataSourceCreator beeCpDataSourceCreator;@Autowiredprivate Dbcp2DataSourceCreator dbcp2DataSourceCreator;@GetMapping@ApiOperation("获取当前所有数据源")public Set<String> now() {DynamicRoutingDataSource ds = (DynamicRoutingDataSource) dataSource;return ds.getDataSources().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.getPoolName(), dataSource);return ds.getDataSources().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.getPoolName(), dataSource);return ds.getDataSources().keySet();}@PostMapping("/addJndi")@ApiOperation("添加JNDI数据源")public Set<String> addJndi(String pollName, String jndiName) {DynamicRoutingDataSource ds = (DynamicRoutingDataSource) dataSource;DataSource dataSource = jndiDataSourceCreator.createDataSource(jndiName);ds.addDataSource(poolName, dataSource);return ds.getDataSources().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.getPoolName(), dataSource);return ds.getDataSources().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.getPoolName(), dataSource);return ds.getDataSources().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.getPoolName(), dataSource);return ds.getDataSources().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.getPoolName(), dataSource);return ds.getDataSources().keySet();}@DeleteMapping@ApiOperation("删除数据源")public String remove(String name) {DynamicRoutingDataSource ds = (DynamicRoutingDataSource) dataSource;ds.removeDataSource(name);return "删除成功";}
}
示例项目

点我跳转

源码分析
public interface DataSourceCreator {/*** 通过属性创建数据源** @param dataSourceProperty 数据源属性* @return 被创建的数据源*/DataSource createDataSource(DataSourceProperty dataSourceProperty);/*** 当前创建器是否支持根据此属性创建** @param dataSourceProperty 数据源属性* @return 是否支持*/boolean support(DataSourceProperty dataSourceProperty);
}

DataSourceCreator是一个接口,定义了根据参数创建数据源的接口。

其他creator实现此接口,dynamic-datasource项目暂时实现了DruidHikaricp的等连接池的实现。

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";@Overridepublic boolean matches(String key) {return key.startsWith(HEADER_PREFIX);}@Overridepublic String doDetermineDatasource(MethodInvocation invocation, String key) {HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();return request.getHeader(key.substring(8));}
}

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

@Configuration
public class MyDynamicDataSourceConfig{@Beanpublic 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,(加入-parameters后,可以使用参数具体的名字,例如这里的#user)
  2. 第二个参数: p1,a1
  3. 第三个参数: P2,,a2

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

    <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也想用加密呢?

所以作者copyDruid的加密相关源码,嘿嘿。

dynamic-datasource项目也支持支持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);}// 自定义publicKeypublic 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"));}
}

2、配置加密yml

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

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

一些公司要求使用自己的方式加密,解密。

3.5.0版本开始,扩展了一个event,用户自行实现注入即可。

public interface DataSourceInitEvent {/*** 连接池创建前执行(可用于参数解密)** @param dataSourceProperty 数据源基础信息*/void beforeCreate(DataSourceProperty dataSourceProperty);/*** 连接池创建后执行** @param dataSource 连接池*/void afterCreate(DataSource dataSource);
}

默认的实现是EncDataSourceInitEvent,即ENC方式的。

为什么不是公钥加密,私钥解密

根据RSA的设计,大部分人会认为应该是公钥加密,私钥解密。 为什么Druid设计相反?

建议更高的安全,可以把publicKey在启动时候传进去,或者配置中心配好,不让普通开发接触到就好。

查询了DruidISSUE和一些文章:

1、Druid作者wenshao自己的回答:点我跳转

2、知乎一些文章片段

!

启动初始化执行脚本

3.5.0之前版本

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

3.5.0(含) 之后版本 (层级多了一层init,保持和新版springboot一致)

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

懒启动数据源

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

暂时只支持DruidHikariCpBeeCp连接池。

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

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

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

无数据源启动

基础介绍

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

配置方式

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

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

spring:datasource:dynamic:primary: master # 设置默认的数据源或者数据源组,默认值即为masterstrict: 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 {@Autowiredprivate JdbcTemplate jdbcTemplate;public List selectAll() {DynamicDataSourceContextHolder.push("slave"); // 手动切换return  jdbcTemplate.queryForList("select * from user");}}

需要注意的是手动切换的数据源,最好自己在合适的位置调用DynamicDataSourceContextHolder.clear()清空当前线程的数据源信息,防止内存泄漏,因为一层使用的是ThreadLocal

如果你不太清楚什么时候调用,那么可以参考下面写一个拦截器,注册进spring里即可。

@Slf4j
public class DynamicDatasourceClearInterceptor implements HandlerInterceptor {@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {// 入口处清空看个人,有的新人在异步里切了数据源但是忘记清除了,下一个请求遇到这个线程就会带进来。// DynamicDataSourceContextHolder.clear(); return true;}@Overridepublic void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) {}@Overridepublic void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {DynamicDataSourceContextHolder.clear();}}

手动注入多数据源

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

绝大部分是因为您的系统需要和其他数据源共同存在使用。

QuartzShardingJdbc等都需要使用独立的数据源。

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同时生效,采取了内部注入,具体源码改动。

自定义

自定义注解:不满足于默认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 {@Productpublic List selectAll() {return  jdbcTemplate.queryForList("select * from products");}}

自定义数据源来源

基础介绍

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

public interface DynamicDataSourceProvider {/*** 加载所有数据源** @return 所有数据源,key为数据源名称*/Map<String, DataSource> loadDataSources();
}
自定义示例

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

@Bean
public DynamicDataSourceProvider jdbcDynamicDataSourceProvider() {return new AbstractJdbcDataSourceProvider("com.mysql.cj.jdbc.Driver", "jdbc:mysql://xx.xx.xx.xx:3306/dynamic", "root", "root") {@Overrideprotected 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开始,可以注入多个DynamicDataSourceProviderBean以实现同时从多个不同来源加载数据源,注意同名会被覆盖。

自定义负载均衡策略

基础介绍

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

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

LoadBalanceDynamicDataSourceStrategy轮询,是默认的。

RandomDynamicDataSourceStrategy随机的。

spring:datasource:dynamic:datasource:master:username: rootpassword: rooturl: jdbc:mysql://xx.xx.xx.xx:3306/dynamicdriver-class-name: com.mysql.cj.jdbc.Driverschema: db/schema.sqlslave_1:username: rootpassword: rooturl: jdbc:mysql://xx.xx.xx.xx:3307/dynamicdriver-class-name: com.mysql.cj.jdbc.Driverslave_2:username: rootpassword: rooturl: jdbc:mysql://xx.xx.xx.xx:3308/dynamicdriver-class-name: com.mysql.cj.jdbc.Driverslave_3:username: rootpassword: rooturl: jdbc:mysql://xx.xx.xx.xx:3309/dynamicdriver-class-name: com.mysql.cj.jdbc.Driverstrategy: 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 {@Overridepublic void init(FilterConfig filterConfig) throws ServletException {log.info("loading filter {}", filterConfig.getFilterName());}@Overridepublic 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();}}@Overridepublic 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);}
}

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

intercepror切换数据源

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

实现如下:

import com.baomidou.dynamic.datasource.toolkit.DynamicDataSourceContextHolder;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;@Slf4j
public class DynamicDatasourceInterceptor implements HandlerInterceptor {/*** 在请求处理之前进行调用(Controller方法调用之前)*/@Overridepublic 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方法调用之后)*/@Overridepublic void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) {}/*** 在整个请求结束之后被调用,也就是在DispatcherServlet渲染了对应的视图之后执行(主要是用于进行资源清理工作)*/@Overridepublic void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {DynamicDataSourceContextHolder.clear();}}
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;@Configuration
public class MyWebAutoConfig implements WebMvcConfigurer {@Overridepublic void addInterceptors(InterceptorRegistry registry) {registry.addInterceptor(new DynamicDatasourceInterceptor()).addPathPatterns("/interceptor/**");}
}

自定义切面

3.4.0开始支持自定义切面。

  1. 使用这个方式,原注解方式并不会失效。
  2. 注意:不要在同一个切面同时使用注解又使用自定义切面。
@Configuration
public class MyConfig {@Beanpublic 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;}@Beanpublic Advisor dsAdviceAdvisor(DynamicDatasourceNamedInterceptor dsAdvice) {AspectJExpressionPointcut pointcut = new AspectJExpressionPointcut();pointcut.setExpression("execution (* com.baomidou.samples.pattern..service.*.*(..))");return new DefaultPointcutAdvisor(pointcut, dsAdvice);}
}

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

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

mybatis下读写分离

场景:

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

答:在mybatis环境下可以基于mybatis插件结合dynamic-datasource完成以上功能。

手动注入插件:

@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";@Overridepublic 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();}}@Overridepublic Object plugin(Object target) {return target instanceof Executor ? Plugin.wrap(target, this) : target;}@Overridepublic void setProperties(Properties properties) {}
}

事务专栏

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

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

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

方案一:dynamic-datasource集成了alibaba分布式事务组件seata

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

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

事务概念

什么是事务?

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

事务的四大特性

原子性

事务是数据库的逻辑工作单位,事务中包含的各操作要么都做,要么都不做 。

一致性

事务执行的结果必须是使数据库从一个一致性状态变到另一个一致性状态。因此当数据库只包含成功事务提交的结果时,就说数据库处于一致性状态。如果数据库系统 运行中发生故障,有些事务尚未完成就被迫中断,这些未完成事务对数据库所做的修改有一部分已写入物理数据库,这时数据库就处于一种不正确的状态,或者说是 不一致的状态。

隔离性

一个事务的执行不能其它事务干扰。即一个事务内部的操作及使用的数据对其它并发事务是隔离的,并发执行的各个事务之间不能互相干扰。

持续性

也称永久性,指一个事务一旦提交,它对数据库中的数据的改变就应该是永久性的。接下来的其它操作或故障不应该对其执行结果有任何影响。

原生JDBC处理事务
try {connection.setAutoCommit(false);  // 这里用connection对数据库做了一系列操作,CRUDconnection.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开始支持本地多数据源事务,无需第三方。

本地事务

背景

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

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

每一种方案都有其适用场景。你可能遇到最多的问题就是:

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

自从3.3.0开始,由seata的核心贡献者 https://github.com/a364176773 贡献了基于connection代理的方案。

建议从3.4.0版本开始使用,其修复了一个功能,老版本不加@DS只加@DSTransactional会报错。

注意事项

本地事务实现很简单,就是循环提交,发生错误,循环回滚。

我们默认的前提是数据库本身不会异常,比如宕机。

如数据在回滚的过程突然宕机,本地事务就会有问题。如果你需要完整分布式方案请使用seata方案。

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

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

// 如AService调用BService和CService的方法,A,B,C分别对应不同数据源。
@Service
public class AService {@DS("a") // 如果a是默认数据源则不需要DS注解。@DSTransactionalpublic void dosomething(){BService.dosomething();CService.dosomething();}
}@Service
public class BService {@DS("b")public void dosomething(){// dosomething}
}@Service
public class CService {@DS("c")public void dosomething(){// dosomething}
}

只要@DSTransactional注解下任一环节发生异常,则全局多数据源事务回滚。

如果BC上也有@DSTransactional会有影响吗?答:没有影响的。

示例项目

点我跳转

完整示例项目,数据库都已准备好,可以直接运行测试。

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

用户下单分别调用产品库扣库存,账户库扣余额。

如果库存不足,或用户余额不足都抛出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=================");}
}
原理介绍

点我跳转

核心原理就是代理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请关闭自动代理。

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

2、准备mysql数据库,账户root密码123456。

docker run --name mysql -p 3306:3306 -e MYSQL_ROOT_PASSWORD=123456 -d mysql:5.7

3、创建相关数据库。

创建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;

4、准备相关数据库脚本。

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

相关的seata脚本可到seata官方获取,另外配合多数据源的自动执行脚本功能,应用启动后会自动执行。

工程准备

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>
# 省略,查看示例项目

2、编写相关yaml配置。

spring:application:name: dynamicdatasource:dynamic:primary: orderstrict: trueseata: true    # 开启seata代理,开启后默认每个数据源都代理,如果某个不需要代理可单独关闭seata-mode: AT # 支持XA及AT模式,默认ATdatasource:order:username: rootpassword: 123456url: jdbc:mysql://192.168.56.101:3306/seata_order?useUnicode=true&characterEncoding=utf8&allowMultiQueries=true&useSSL=falsedriver-class-name: com.mysql.cj.jdbc.Driverschema: classpath:db/schema-order.sqlaccount:username: rootpassword: 123456url: jdbc:mysql://192.168.56.101:3306/seata_account?useUnicode=true&characterEncoding=utf8&allowMultiQueries=true&useSSL=falsedriver-class-name: com.mysql.cj.jdbc.Driverschema: classpath:db/schema-account.sqlproduct:username: rootpassword: 123456url: jdbc:mysql://192.168.56.101:3306/seata_product?useUnicode=true&characterEncoding=utf8&allowMultiQueries=true&useSSL=falsedriver-class-name: com.mysql.cj.jdbc.Driverschema: classpath:db/schema-product.sqltest:username: lijingpassword: ""url: jdbc:h2:mem:testdriver-class-name: org.h2.Driverseata: false # 这个数据源不需要seata
seata:enabled: trueapplication-id: applicationNametx-service-group: my_test_tx_groupenable-auto-data-source-proxy: false   #一定要是falseservice:vgroup-mapping:my_test_tx_group: default  # key与上面的tx-service-group的值对应grouplist:default: 192.168.56.101:8091 # seata-server地址仅file注册中心需要config:type: fileregistry:type: file
代码编写

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

订单服务

@Slf4j
@Service
public class OrderServiceImpl implements OrderService {@Resourceprivate OrderDao orderDao;@Autowiredprivate AccountService accountService;@Autowiredprivate 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 {@Resourceprivate ProductDao productDao;/*** 事务传播特性设置为 REQUIRES_NEW 开启新的事务  重要!!!!一定要使用REQUIRES_NEW*/@DS("product")@Transactional(propagation = Propagation.REQUIRES_NEW)@Overridepublic 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 {@Resourceprivate 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=================");}}
测试

自行编写接口测试,注意观察运行日志,至此分布式事务集成案例全流程完毕。

事务常见问题

建议先阅读基础知识。

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

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

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

原生Spring@Transational能和@DS一起使用吗?
public class AService {@DS("a")@Transationalpublic void dosomething(){// some code}
}

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

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

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

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

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

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

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

public class AService {@DS("a")@DSTransactionalpublic void dosomething(){Amapper.updateSomeThing();Bservice.dosomething(); // B是另外数据源,然后注解了@DS("b")和@TransationalCservice.dosomething(); // C是另外数据源,然后注解了@DS("c")和@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("c")}
}

调试源码

1、开启动态数据源的debug日志。

logging:level:com.baomidou.dynamic: debug

检查日志输出是否正确。

2、断点调试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;}@Overridepublic 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;}
}

3、断点调试DynamicRoutingDataSource

public class DynamicRoutingDataSource extends AbstractRoutingDataSource {private Map<String, DataSource> dataSourceMap = new LinkedHashMap<>();private Map<String, DynamicGroupDataSource> groupDataSources = new ConcurrentHashMap<>();@Overridepublic 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();}
}

常见问题之-切换数据源失败

开启了spring的事务

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

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

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

方法内部调用

查看以下示例

外部调用userservice.test1()能在执行到test2()切换到second数据源吗?

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

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

解决方法:

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

PostConstruct初始化顺序

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

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

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

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

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

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

Druid版本过低

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

@Async或者java8ParallelStream并行流之类方法

这种情况都是新开了线程去异步处理,不受当前线程管控了。

可以在新开的方法上加对应的DS注解解决。

知识库

如何注入多数据源?

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

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

DynamicDataSourceContextHolder.peek()

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

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/web/65479.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

P10424 [蓝桥杯 2024 省 B] 好数

题目描述 一个整数如果按从低位到高位的顺序&#xff0c;奇数位&#xff08;个位、百位、万位……&#xff09;上的数字是奇数&#xff0c;偶数位&#xff08;十位、千位、十万位……&#xff09;上的数字是偶数&#xff0c;我们就称之为“好数”。 给定一个正整数 N&#xf…

Spring Boot教程之五十二:CrudRepository 和 JpaRepository 之间的区别

Spring Boot – CrudRepository 和 JpaRepository 之间的区别 Spring Boot建立在 Spring 之上&#xff0c;包含 Spring 的所有功能。由于其快速的生产就绪环境&#xff0c;使开发人员能够直接专注于逻辑&#xff0c;而不必费力配置和设置&#xff0c;因此如今它正成为开发人员…

LLM的MoE由什么构成:门控网络,专家网络

LLM的MoE由什么构成:门控网络,专家网络 目录 LLM的MoE由什么构成:门控网络,专家网络专家网络门控网络MoE在联邦学习中的使用及原理专家网络 定义与特点:是一组独立的模型,每个模型都负责处理某个特定的子任务或学习输入空间的特定部分。这些专家可以是简单的线性回归模型…

DeepSeek-V3与GPT-4o的对比详解

DeepSeek-V3&#xff0c;作为一款引人注目的开源大型语言模型&#xff0c;自其诞生以来&#xff0c;便以卓越的性能和高效的性价比&#xff0c;在AI界掀起了一股新的浪潮。本文将详细介绍DeepSeek-V3的诞生背景、技术优势&#xff0c;以及与顶尖闭源模型GPT-4o的对比&#xff0…

Mysql 性能优化:覆盖索引

概述 覆盖索引&#xff08;Covering Index&#xff09;是一个 MySQL 查询优化技术&#xff0c;它指的是一个索引包含了查询所需的所有字段的数据&#xff0c;因此不需要回表&#xff08;访问数据表的行&#xff09;就可以完成查询。使用覆盖索引可以显著提高查询性能&#xff…

python注意事项:range遍历越索引现象、列表边遍历边修改出现的问题

文章目录 前言一、range遍历越索引现象QS1:遍历range(2,2)会发生什么&#xff1f;不会报错&#xff0c;但是也不会遍历到任何内容QS1:遍历range(3,2)会发生什么&#xff1f;不会报错&#xff0c;但是也不会遍历到任何内容 二、列表边遍历边修改注意事项&#xff08;Java的List系…

【SQL】Delete使用

目录 语法 需求 示例 分析 代码 语法 DELETE删除表中所需内容 删除表中满足特点条件的行&#xff1a;DELETE FROM 表名 WHERE 条件; 删除表中所有行&#xff1a;DELETE FROM 表名; WHERE子句 WHERE子句用于指定从表中选取记录的条件。允许筛选数据&#xff0c;只返回满足…

【express-generator】05-路由中间件和错误处理(第一阶段收尾)

一、前言 上篇文章我们介绍了express-generator的请求体解析&#xff0c;重点讲了常用的请求体数据格式&#xff08;JSON/URL 编码的表单数据&#xff09;以及一个FILE文件上传&#xff0c;同时搭配代码示范进行辅助理解。 二、本篇重点 我们继续第一阶段的知识&#xff0c;…

Vue3(elementPlus) el-table替换/隐藏行箭头,点击整行展开

element文档链接&#xff1a; https://element-plus.org/zh-CN/component/form.html 一、el-table表格行展开关闭箭头替换成加减号 注&#xff1a;Vue3在样式中修改箭头图标无效&#xff0c;可能我设置不对&#xff0c;欢迎各位来交流指导 转变思路&#xff1a;隐藏箭头&…

【Javascript Day1】javascript基础

javascript编程规则 弹窗&#xff08;举例&#xff09; alert("内容")&#xff0c;直接写在控制区生效 三种写法 1、行内js语法 &#xff1a;需要注意引号的问题 <input type"button" value"提示窗" οnclick alert("消息") &…

GitLab创建用户,设置访问SSH Key

继上一篇 Linux Red Hat 7.9 Server安装GitLab-CSDN博客 安装好gitlab&#xff0c;启用管理员root账号后&#xff0c;开始创建用户账户 1、创建用户账户 进入管理后台页面 点击 New User 输入用户名、邮箱等必填信息和登录密码 密码最小的8位&#xff0c;不然会不通过 拉到…

QT鼠标、键盘事件

一、鼠标 鼠标点击 mousePressEvent 鼠标释放 mouseReleaseEvent 鼠标移动 mouseMoveEvent 鼠标双击 mouseDoubleClickEvent 鼠标滚轮 QWheelEvent 二、键盘 键盘按下 keyPressEvent 键盘松开keyReleaseEvent 一、鼠标 #include <QMouseEvent> 鼠标点击 mouse…

数据库环境安装(day1)

网址&#xff1a;MySQL 下载&#xff08;环境准备&#xff09;&#xff1a; &#xff08;2-5点击此处&#xff0c;然后选择合适的版本&#xff09; 1.linux在线YUM仓库 下载/安装: wget https://repo.mysql.com//mysql84-community-release-el9-1.noarch.rpm rpm -i https://r…

Qt 5.14.2 学习记录 —— 칠 QWidget 常用控件(2)

文章目录 1、Window Frame2、windowTitle3、windowIcon4、qrc机制5、windowOpacity 1、Window Frame 在运行Qt程序后&#xff0c;除了用户做的界面&#xff0c;最上面还有一个框&#xff0c;这就是window frame框。对于界面的元素&#xff0c;它们的原点是Qt界面的左上角或win…

以太网协议在汽车应用中的动与静

为了让网络中的设备能够随时或随地接入网络或离开网络&#xff0c;做到&#xff1a;接入时无需多余的配置就能和其他设备互相通信&#xff1b;离开时又不会导致设备或网络崩溃。以太网从物理层到协议层展现出多方面的灵活性&#xff0c;&#xff0c;使其成为连接各种设备和系统…

牛客网刷题 ——C语言初阶(6指针)——BC105 矩阵相等判定

1. 题目描述&#xff1a;BC105 矩阵相等判定 牛客网OJ题链接 描述&#xff1a; KiKi得到了两个n行m列的矩阵&#xff0c;他想知道两个矩阵是否相等&#xff0c;请你回答他。(当两个矩阵对应数组元素都相等时两个矩阵相等)。 示例1 输入&#xff1a; 2 2 1 2 3 4 1 2 3 4 输出…

外观设计模式学习

1.介绍 外观模式&#xff08;Facade Pattern&#xff09; 是一种结构型设计模式&#xff0c;通过提供一个统一的接口&#xff0c;用于访问子系统中的一组接口&#xff0c;从而简化客户端与复杂系统之间的交互。它隐藏了系统的复杂性&#xff0c;使得客户端只需与一个简单的接口…

基于单片机的数字气压计设计

摘要:在嵌入式技术快速发展过程中&#xff0c;智能测量仪器被广泛应用于工业生产以及人们日常生活领域。数字气压计在实际应用中&#xff0c;利用气压传感器检测环境中的压力大小&#xff0c;便于实现对设备进行智能化的控制操作。数字气压计在气象监测、矿产开采、科学实验等环…

嵌入式软件C语言面试常见问题及答案解析(三)

嵌入式软件C语言面试常见问题及答案解析(三) 上一篇已经足够长了,再长也就有点不礼貌了,所以在这儿继续来总结分享那个面试中遇到的题目,文中的问题和提供的答案或者代码均代表个人的理解,如有不合理或者错误的地方,欢迎大家批评指正。 本文中题目列表 1. 编码实现子串定…

HTML5实现好看的中秋节网页源码

HTML5实现好看的中秋节网页源码 前言一、设计来源1.1 网站首页界面1.2 登录注册界面1.3 节日由来界面1.4 节日习俗界面1.5 节日文化界面1.6 节日美食界面1.7 节日故事界面1.8 节日民谣界面1.9 联系我们界面 二、效果和源码2.1 动态效果2.2 源代码 源码下载结束语 HTML5实现好看…