赞
踩
提示:多数据源实现方式、多数据源的使用场景。AbstractRoutingDataSource、DynamicDataSource框架、mybatisplus的Intercepter插件、java中多数据源的几种实现方式、mybatisPlus的插件实现多数据源
最近工作中有一张表,实际数据量超过1亿了,导致一条普通的insert语句也耗时15秒,因此需要分表。在使用shardingSphere分表时,需要切换多数据源,因此特意调研了一下多数据源的几种实现方式。再次记录一下,感兴趣的同学可以下载代码,这样看起来更加清晰。gitee代码
java中实现多数据源,比较常见的方式有3种:
其实最底层的核心原理,就是abstractRootingDataSource,剩下的两种,肯定也是以第一种为基础的,只不过封装了一层而已。
一般来说,多数据源有以下两种使用场景:
最核心的类就是AbstractRootingDataSource,因此我们着重介绍一下。
这个抽象类中,有3个比较重要的成员变量:
实现多数据源大概需要3部,(AbstractRoutingDataSource)
1.继承 abstractRootingDataSource
2.返回当前数据源标识 重写 determineCurrentLookupKey 方法
3.获取全部的数据源map super.setTargetDataSources(targetDataSources);
<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.6.2</version> </parent> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-jdbc</artifactId> <version>2.6.2</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> <version>2.6.2</version> </dependency> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>2.2.1</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <scope>runtime</scope> <version>8.0.27</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <version>2.6.2</version> <scope>test</scope> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <version>1.18.22</version> </dependency> <!--Druid连接池--> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid-spring-boot-starter</artifactId> <version>1.2.3</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-aop</artifactId> <version>2.6.2</version> </dependency> </dependencies>
spring: datasource: type: com.alibaba.druid.pool.DruidDataSource datasource1: url: jdbc:mysql://localhost:3306/mytest?serverTimezone=UTC&useUnicode=true&zeroDateTimeBehavior=convertToNull&autoReconnect=true&characterEncoding=utf-8 username: root password: 123456 initial-size: 1 min-idle: 1 max-active: 20 test-on-borrow: true driver-class-name: com.mysql.cj.jdbc.Driver datasource2: url: jdbc:mysql://localhost:3306/mytest2?serverTimezone=UTC&useUnicode=true&zeroDateTimeBehavior=convertToNull&autoReconnect=true&characterEncoding=utf-8 username: root password: 123456 initial-size: 1 min-idle: 1 max-active: 20 test-on-borrow: true driver-class-name: com.mysql.cj.jdbc.Driver server: port: 9001 mybatis: mapper-locations: classpath:mapper/**/*.xml
package zheng.config; import com.alibaba.druid.spring.boot.autoconfigure.DruidDataSourceBuilder; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import javax.sql.DataSource; /** * @author: ztl * @date: 2024/02/06 22:59 * @desc: */ @Configuration public class DataSourceConfig { @Bean @ConfigurationProperties(prefix = "spring.datasource.datasource1") public DataSource dataSource1() { // 底层会自动拿到spring.datasource中的配置, 创建一个DruidDataSource return DruidDataSourceBuilder.create().build(); } @Bean @ConfigurationProperties(prefix = "spring.datasource.datasource2") public DataSource dataSource2() { // 底层会自动拿到spring.datasource中的配置, 创建一个DruidDataSource return DruidDataSourceBuilder.create().build(); } }
相关解释:
package zheng.config; import org.springframework.beans.factory.InitializingBean; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Primary; import org.springframework.stereotype.Component; import javax.sql.DataSource; import java.io.PrintWriter; import java.sql.Connection; import java.sql.SQLException; import java.sql.SQLFeatureNotSupportedException; import java.util.logging.Logger; /** * @author: ztl * @date: 2024/02/08 22:29 * @desc: */ @Component @Primary // 将动态数据源作为核心返回。 // (datasource1、datasource2、DynamicDataSource都会被spring扫描出来,如果只返回datasource1、2就没法动态切换了) public class DynamicDataSource implements DataSource, InitializingBean { // 当前使用的数据源 public static ThreadLocal<String > name = new ThreadLocal<>(); // 写 @Autowired DataSource dataSource1; // 读 @Autowired DataSource dataSource2; @Override public Connection getConnection() throws SQLException { if (name.get().equals("W")){ return dataSource1.getConnection(); }else { return dataSource2.getConnection(); } } @Override public Connection getConnection(String username, String password) throws SQLException { return null; } @Override public <T> T unwrap(Class<T> iface) throws SQLException { return null; } @Override public boolean isWrapperFor(Class<?> iface) throws SQLException { return false; } @Override public PrintWriter getLogWriter() throws SQLException { return null; } @Override public void setLogWriter(PrintWriter out) throws SQLException { } @Override public void setLoginTimeout(int seconds) throws SQLException { } @Override public int getLoginTimeout() throws SQLException { return 0; } @Override public Logger getParentLogger() throws SQLFeatureNotSupportedException { return null; } @Override public void afterPropertiesSet() throws Exception { // todo: 这个是初始化的操作:(默认数据库是W库) name.set("W"); } }
package zheng.controller; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import zheng.config.DynamicDataSource; import zheng.entity.Frend; import zheng.service.FrendService; import java.util.List; /** * @author: ztl * @date: 2024/02/06 23:06 * @desc: */ @RestController @RequestMapping("frend") @Slf4j public class FrendController { @Autowired private FrendService frendService; @GetMapping(value = "select") public List<Frend> select(){ log.info("select start ..."); // 读的操作,我们用读库 DynamicDataSource.name.set("R"); return frendService.list(); } @GetMapping(value = "insert") public void in(){ log.info("in start ..."); // 写的操作,我们用写库 DynamicDataSource.name.set("W"); Frend frend = new Frend(); frend.setName("ztl"); frendService.save(frend); } }
package zheng.mapper; import org.apache.ibatis.annotations.Insert; import org.apache.ibatis.annotations.Select; import zheng.entity.Frend; import java.util.List; public interface FrendMapper { @Select("SELECT * FROM Frend") List<Frend> list(); @Insert("INSERT INTO frend(`name`) VALUES (#{name})") void save(Frend frend); }
在上面的代码上,加一个注解,然后直接在注解上指定具体的数据源,比如说1就是a数据源,2就是b数据源。
针对于在controller层写数据源源的,代码侵入量大,不方便。
我们有两种解决方案:
1、aop。 更加适用于 大数据量,业务复杂的场景。(有多个不同的库,不同业务导致的)
2、mybatis插件。 更加适用于,读写分离的操作。 因为mybatis的插件可以很方便的知道我们现在是查询操作还是增删改操作。(只适用于mybaits持久层框架,如果是hibernate就不行了)
当然,你也可以判断sql,如果sql中包含某个表,用a库,不包含某个表,用b库。不过像一个表还行,几十张表,通过表名去判断查不同的库的话,太费劲了
除了aop以外,我们还有另一种实现方式,就是mybatisPlus的插件。因为通过插件,我们可以知道这个sql是查询、还是insert,像那种读写分离的数据源,是非常的适合的。
就是,切换数据源时的注解,真实开发中,一般一个service只代表一个类的增删改查,所以可以直接把这个注解写在service上,而不是metnhod上
package zheng.annotation; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; /** * @author: ztl * @date: 2024/02/21 22:34 * @desc: */ @Target({ElementType.TYPE,ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) public @interface WR { String value() default "W"; }
service,带了 @WR注解了
package zheng.service.impl; import org.springframework.aop.framework.AopContext; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import zheng.annotation.WR; import zheng.entity.Frend; import zheng.mapper.FrendMapper; import zheng.service.FrendService; import java.util.List; /** * @author: ztl * @date: 2022/12/27 11:18 * @desc: */ @Service public class FrendImplService implements FrendService { @Autowired FrendMapper frendMapper; @Override @WR("R") public List<Frend> list() { return frendMapper.list(); } @Override @WR("W") public void save(Frend frend) { //如果你想获取当前类的代理类(比如你是@Transaction,然后当前类自己调用自己类下的方法, // 是不会生效的,因为是代理类,你可以先获取到代理类,然后用代理类去执行自己类的方法,)。你可以: // FrendService o = (FrendService)AopContext.currentProxy(); // System.out.println(o); // o.save(frend); frendMapper.save(frend); } }
package zheng.aop; import org.aspectj.lang.JoinPoint; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Before; import org.springframework.stereotype.Component; import zheng.annotation.WR; import zheng.config.DynamicDataSource; /** * @author: ztl * @date: 2024/02/21 22:30 * @desc: */ @Component @Aspect public class DynamicDataSourceAspect { /** * within 和 execute类似。within能制定到包,execute能制定到类中的方法。 * 如果不指定within的话,spring会把全部的bean都扫描一下,我们目前只需要扫描service, * 因为我们在service上加的注解,扫描其他bean没意义,白白浪费性能而已, * 所以指定了service,并且带这个wr注解的话,就set一下多数据源的数据库的链接, * @param point * @param wr */ @Before("within(zheng.service.impl.*) && @annotation(wr)") public void before(JoinPoint point, WR wr){ String name = wr.value(); DynamicDataSource.name.set(name); System.out.println("==============before:"+name); } }
DynamicDadaSourcePlugin
mybatis源码中,MybatisAutoConfiguration mybatis的自动配置类,会自动的将interceptors的数组给注入进来,所以我们只需要定义这个对象就行
因为执行增删改查的时候不是都要通过这个executor嘛,那我们对这个对象进行一个加强的操作,来达到我们切换数据源的目的。
spring中的bean只有一个无参构造函数的时候呢,spring就会自动调用这个无参构造函数,并把所有的参数都进行自动注入,所以我们要将interceptor自动注入,只需要创建一个这个类型的bean对象即可。(mybatis的自动配置类就会自动帮我们注入进来)
package com.zheng.plugin; import com.zheng.config.DynamicDataSource; import org.apache.ibatis.executor.Executor; import org.apache.ibatis.mapping.MappedStatement; import org.apache.ibatis.mapping.SqlCommandType; import org.apache.ibatis.plugin.*; import org.apache.ibatis.session.ResultHandler; import org.apache.ibatis.session.RowBounds; import java.util.Properties; /** * @author: ztl * @date: 2024/03/02 22:32 * @desc: */ @Intercepts({ @Signature(type = Executor.class,method = "update",args = {MappedStatement.class,Object.class}), @Signature(type = Executor.class,method = "query",args = {MappedStatement.class,Object.class, RowBounds.class, ResultHandler.class}) }) public class DynamicDadaSourcePlugin implements Interceptor { /** * 具体的方法 * @param invocation * @return * @throws Throwable */ @Override public Object intercept(Invocation invocation) throws Throwable { // 拿到当前方法的全部参数。(update的时候,就是update的参数,select的时候,就是select的参数) Object[] objects = invocation.getArgs(); // MappedStatement 封装了具体的sql、当前的操作类型(查询、update之类的) MappedStatement ms = (MappedStatement)objects[0]; // 读方法 if (ms.getSqlCommandType().equals(SqlCommandType.SELECT)){ System.out.println("111111+度方法"); DynamicDataSource.name.set("R"); }else { System.out.println("22222+写方法"); DynamicDataSource.name.set("W"); } // invocation.proceed() 这个是具体的调用 return invocation.proceed(); } @Override public Object plugin(Object target) { if (target instanceof Executor){ return Plugin.wrap(target,this); }else { return target; } } @Override public void setProperties(Properties properties) { } }
package com.zheng.config; import com.alibaba.druid.spring.boot.autoconfigure.DruidDataSourceBuilder; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import javax.sql.DataSource; /** * @author: ztl * @date: 2024/02/06 22:59 * @desc: */ @Configuration public class DataSourceConfig { @Bean @ConfigurationProperties(prefix = "spring.datasource.datasource1") public DataSource dataSource1() { // 底层会自动拿到spring.datasource中的配置, 创建一个DruidDataSource return DruidDataSourceBuilder.create().build(); } @Bean @ConfigurationProperties(prefix = "spring.datasource.datasource2") public DataSource dataSource2() { // 底层会自动拿到spring.datasource中的配置, 创建一个DruidDataSource return DruidDataSourceBuilder.create().build(); } }
package com.zheng.config; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Primary; import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource; import org.springframework.stereotype.Component; import javax.sql.DataSource; import java.util.HashMap; import java.util.Map; /** * @author: ztl * @date: 2024/02/08 22:29 * @desc: */ @Component @Primary // 将动态数据源作为核心返回。 // (datasource1、datasource2、DynamicDataSource都会被spring扫描出来,如果只返回datasource1、2就没法动态切换了) public class DynamicDataSource extends AbstractRoutingDataSource { // 当前使用的数据源 public static ThreadLocal<String > name = new ThreadLocal<>(); // 写 @Autowired DataSource dataSource1; // 读 @Autowired DataSource dataSource2; // 返回当前数据源的标识(此处是R/W) @Override protected Object determineCurrentLookupKey() { return name.get(); } @Override public void afterPropertiesSet() { // 拿到多数据源中,全部的数据源 Map<Object, Object> targetDataSources = new HashMap<>(); targetDataSources.put("W",dataSource1); targetDataSources.put("R",dataSource2); super.setTargetDataSources(targetDataSources); // 设置默认的数据源 super.setDefaultTargetDataSource(dataSource1); // 这个父类的方法还是需要的,不然spring没法把connection对象传递下去, super.afterPropertiesSet(); } }
多数据源,到这就分享完毕了。下次应该会给大家分享一下,shardingsphere的用法,以及在我们的项目中,所遇到的问题及解决方案。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。