Spring Boot +Mybatis plus多数据源实践
随着业务及客户的不断壮大,单数据库已经不足以支撑程序业务的完美运行(响应快、高吞吐),所以数据库往往都会进行分表分库/读写分离,那么问题来了,分库后程序如何从不同URL数据库中读取数据呢?
这篇文章只讲如何配置/使用多数据源,不讲分表分库/读写分离,也不讲主键生成策略及读取策略。
如何实现多数据源呢?原理很简单:Spring的AOP.只需要mybatis plus及spring boot的基础依赖,不需要引入其他依赖
说明:多数据源不仅指同类不同地址的数据源,也可以是异构关系型数据库
自定义注解
@Documented @Inherited @Target({ElementType.TYPE, ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) public @interface DataSource { DataSourceType value() default DataSourceType.MESH; enum DataSourceType { /** * 数据源类型 **/ MASTER, SLAVE, } }
yml文件
spring: jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT+8 datasource: driver-class-name: org.postgresql.Driver type: com.zaxxer.hikari.HikariDataSource hikari: minimum-idle: 5 maximum-pool-size: 10 db: conn: str: useUnicode=true&characterEncoding=UTF-8 master: jdbc-url: jdbc:postgresql://127.0.01:54320/master?${spring.datasource.db.conn.str} username: postgres password: 123456 salve: jdbc-url: jdbc:postgresql://127.0.0.1:54321/salve?${spring.datasource.db.conn.str} username: postgres password: 123456
动态数据源上下文
@Slf4j public class DynamicDataSourceContextHolder { /** * 数据源标识,保存在线程变量中,避免多线程操作数据源时互相干扰 */ private static final ThreadLocal<String> CONTEXT_HOLDER = new ThreadLocal<>(); /** * 设置数据源 * * @param dataSource 数据源名称 */ public static void setDataSource(String dataSource) { log.info("切换到{}数据源", Assert.isEmpty(dataSource) ? DataSource.DataSourceType.MASTER.name() : dataSource); CONTEXT_HOLDER.set(Assert.isEmpty(dataSource) ? DataSource.DataSourceType.MASTER.name() : dataSource); } /** * 获取数据源 * * @return java.lang.String * @author Jackpot * @date 2021/4/30 4:38 下午 */ public static String getDataSource() { return CONTEXT_HOLDER.get(); } /** * 清除数据源 */ public static void clearDataSource() { CONTEXT_HOLDER.remove(); } }
动态数据源
继承AbstractRoutingDataSource
类,该类是能够实现数据源切换的关键所在。实现determineCurrentLookupKey()
,返回数据源的key值。
@Slf4j public class DynamicDataSource extends AbstractRoutingDataSource { public DynamicDataSource(DataSource defaultTargetDataSource, Map<Object, Object> targetDataSources) { super.setDefaultTargetDataSource(defaultTargetDataSource); super.setTargetDataSources(targetDataSources); super.afterPropertiesSet(); } @Override protected Object determineCurrentLookupKey() { String dataSource = DynamicDataSourceContextHolder.getDataSource(); log.info("当前数据源:{}", Assert.isEmpty(dataSource) ? com.**.config.datasource.DataSource.DataSourceType.MASTE.name() : dataSource); return dataSource; } }
多数据源配置
@Slf4j @Configuration public class DataSourceConfig { final MybatisPlusInterceptor interceptor; public DataSourceConfig(MybatisPlusInterceptor interceptor) { this.interceptor = interceptor; } /** * 主数据源配置 * Primary 表示当前数据源为主数据源 * * @return javax.sql.DataSource * @author Jackpot * @date 2021/5/12 9:32 上午 */ @Primary @Bean("master") @ConfigurationProperties(prefix = "spring.datasource.master") public DataSource masterDataSource() { return DataSourceBuilder.create().build(); } /** * 从数据源配置 * * @return javax.sql.DataSource * @author Jackpot * @date 2021/5/12 9:32 上午 */ @Bean("salve") @ConfigurationProperties(prefix = "spring.datasource.salve") public DataSource salveDataSource() { return DataSourceBuilder.create().build(); } /** * 多数据源定义 * * @param masterDataSource 主数据源 * @param salveDataSource 从数据源 * @return com.**.config.datasource.DynamicDataSource * @author Jackpot * @date 2021/5/12 9:33 上午 */ @Bean(name = "dynamicDataSource") public DynamicDataSource dataSource(@Qualifier("master") DataSource meshDataSource, @Qualifier("salve") DataSource walleDataSource) { Map<Object, Object> targetDataSources = new HashMap<>(4); targetDataSources.put(com.**.config.datasource.DataSource.DataSourceType.MASTER.name(), masterDataSource); targetDataSources.put(com.**.config.datasource.DataSource.DataSourceType.SALVE.name(), salveDataSource); return new DynamicDataSource(masterDataSource, targetDataSources); } /** * 将动态数据源注入到SqlSessionFactory * 同时解决分页失效 * * @param dynamicDataSource 多数据源 * @return org.apache.ibatis.session.SqlSessionFactory * @author Jackpot * @date 2021/4/30 4:30 下午 */ @Bean("sqlSessionFactory") public SqlSessionFactory getSqlSessionFactory(@Qualifier("dynamicDataSource") DataSource dynamicDataSource) throws Exception { final MybatisSqlSessionFactoryBean bean = new MybatisSqlSessionFactoryBean(); bean.setDataSource(dynamicDataSource); //分页插件 bean.setPlugins(interceptor); bean.setMapperLocations( new PathMatchingResourcePatternResolver().getResources("classpath*:/mapper/*Mapper.xml")); return bean.getObject(); } /** * sqlSessionTemplate定义 * * @param sessionFactory session工厂 * @return org.mybatis.spring.SqlSessionTemplate * @author Jackpot * @date 2021/5/12 9:34 上午 */ @Bean("sqlSessionTemplate") public SqlSessionTemplate sqlSessionTemplate(@Qualifier("sqlSessionFactory") SqlSessionFactory sessionFactory) { return new SqlSessionTemplate(sessionFactory); } /** * 多数据源事务管理 * 防止事务绑定主数据源无法对数据源进行切换及事务失效 * * @param dataSource 多数据源 * @return org.springframework.jdbc.datasource.DataSourceTransactionManager * @author Jackpot * @date 2021/5/12 9:39 上午 */ @Bean("dataSourceTransactionManager") public DataSourceTransactionManager dataSourceTransactionManager(@Qualifier("dynamicDataSource") DataSource dataSource) { return new DataSourceTransactionManager(dataSource); } }
AOP切面实现。
注:@annotation和@within怎么区分?通俗点说就是@annotation作用与方法(method)之上,@within作用于controller/service上,也就是对象
@Slf4j @Order(0) @Aspect @Component public class DataSourceAspect { /** * 通过自定义注解@DataSource定义切点 */ @Pointcut("@annotation(com.**.config.datasource.DataSource)" + "|| @within(com.**.config.datasource.DataSource)") public void dsPointCut() { } /** * 切点环绕 * * @param point 切入点 * @return java.lang.Object * @author Jackpot * @date 2021/4/30 4:34 下午 */ @Around("dsPointCut()") public Object around(ProceedingJoinPoint point) throws Throwable { DataSource dataSource = getDataSource(point); if (dataSource != null) { DynamicDataSourceContextHolder.setDataSource(dataSource.value().name()); } try { return point.proceed(); } finally { DynamicDataSourceContextHolder.clearDataSource(); } } /** * 获取需要切换的数据源 * * @param point 切入点 * @return com.iot.mesh.common.config.datasource.DataSource * @author Jackpot * @date 2021/4/30 4:34 下午 */ public DataSource getDataSource(ProceedingJoinPoint point) { MethodSignature signature = (MethodSignature) point.getSignature(); Class<?> targetClass = point.getTarget().getClass(); DataSource targetDataSource = targetClass.getAnnotation(DataSource.class); if (targetDataSource != null) { return targetDataSource; } else { Method method = signature.getMethod(); return method.getAnnotation(DataSource.class); } } }
启动类,需要剔除spring boot的自动数据源配置
@EnableScheduling @EnableTransactionManagement @SpringBootApplication(exclude = {DataSourceAutoConfiguration.class}) public class ApiApplication { public static void main(String[] args) { SpringApplication.run(ApiApplication.class, args); } }
使用:粗粒度使用在service类对象之上,细粒度作用于方法之上,同时存在方法注解优先于类上注解。推荐使用第二种方式。
注:同一个方法里面如果涉及到多个数据源操作,事务会失效。
附:其实mybatis plus提供了多数据的依赖
<dependency> <groupId>com.baomidou</groupId> <artifactId>dynamic-datasource-spring-boot-starter</artifactId> <version>3.5.1</version> </dependency>
想偷懒的同学可以直接使用,上面主要讲的是核心内容,dynamic-datasource-spring-boot-starter实现动态数据源的思想也是AOP,把上面的代码弄懂了,dynamic-datasource的源码看起来也就很简单了.
mybatis plus的多数据源的使用也很简单,对象或方法上使用@DS(**)即可。
注:使用mybatis plus动态数据源,yml文件格式和上面有点区别,我这里也把它贴出来。启动类上也不需要将DataSourceAutoConfiguration.class剔除
spring: jackson: date-format: yyyy-MM-dd HH:mm:ss locale: zh_CN datasource: dynamic: primary: master #设置默认的数据源或者数据源组,默认值为master strict: false #严格匹配数据源,默认false. true未匹配到指定数据源时抛异常,false使用默认数据源 datasource: master: url: jdbc:postgresql://127.0.0.1:54320/master?useUnicode=true&characterEncoding=utf-8&reWriteBatchedInserts=true username: postgres password: 123456 salve: url: jdbc:postgresql://127.0.0.1:54321/salve?useUnicode=true&characterEncoding=utf-8&reWriteBatchedInserts=true username: postgres password: 123456