湖南中小企业建站价格,最新购物平台,网站设计公司无锡,藤县建设局网站最近在做业务需求时#xff0c;需要从不同的数据库中获取数据然后写入到当前数据库中#xff0c;因此涉及到切换数据源问题。本来想着使用Mybatis-plus中提供的动态数据源SpringBoot的starter#xff1a;dynamic-datasource-spring-boot-starter来实现。 结果引入后发现由于…最近在做业务需求时需要从不同的数据库中获取数据然后写入到当前数据库中因此涉及到切换数据源问题。本来想着使用Mybatis-plus中提供的动态数据源SpringBoot的starterdynamic-datasource-spring-boot-starter来实现。结果引入后发现由于之前项目环境问题导致无法使用。然后研究了下数据源切换代码决定自己采用ThreadLocalAbstractRoutingDataSource来模拟实现dynamic-datasource-spring-boot-starter中线程数据源切换。1 简介上述提到了ThreadLocal和AbstractRoutingDataSource我们来对其进行简单介绍下。ThreadLocal想必大家必不会陌生全称thread local variable。主要是为解决多线程时由于并发而产生数据不一致问题。ThreadLocal为每个线程提供变量副本确保每个线程在某一时间访问到的不是同一个对象这样做到了隔离性增加了内存但大大减少了线程同步时的性能消耗减少了线程并发控制的复杂程度。ThreadLocal作用在一个线程中共享不同线程间隔离ThreadLocal原理ThreadLocal存入值时会获取当前线程实例作为key存入当前线程对象中的Map中。AbstractRoutingDataSource根据用户定义的规则选择当前的数据源作用在执行查询之前设置使用的数据源实现动态路由的数据源在每次数据库查询操作前执行它的抽象方法determineCurrentLookupKey()决定使用哪个数据源。2 代码实现程序环境SpringBoot2.4.8Mybatis-plus3.2.0Druid1.2.6lombok1.18.20commons-lang3 3.102.1 实现ThreadLocal创建一个类用于实现ThreadLocal主要是通过getsetremove方法来获取、设置、删除当前线程对应的数据源。/** * author: jiangjs * description: * date: 2023/7/27 11:21 **/ public class DataSourceContextHolder { //此类提供线程局部变量。这些变量不同于它们的正常对应关系是每个线程访问一个线程(通过get、set方法),有自己的独立初始化变量的副本。 private static final ThreadLocalString DATASOURCE_HOLDER new ThreadLocal(); /** * 设置数据源 * param dataSourceName 数据源名称 */ public static void setDataSource(String dataSourceName){ DATASOURCE_HOLDER.set(dataSourceName); } /** * 获取当前线程的数据源 * return 数据源名称 */ public static String getDataSource(){ return DATASOURCE_HOLDER.get(); } /** * 删除当前数据源 */ public static void removeDataSource(){ DATASOURCE_HOLDER.remove(); } }2.2 实现AbstractRoutingDataSource定义一个动态数据源类实现AbstractRoutingDataSource通过determineCurrentLookupKey方法与上述实现的ThreadLocal类中的get方法进行关联实现动态切换数据源。/** * author: jiangjs * description: 实现动态数据源根据AbstractRoutingDataSource路由到不同数据源中 * date: 2023/7/27 11:18 **/ public class DynamicDataSource extends AbstractRoutingDataSource { public DynamicDataSource(DataSource defaultDataSource,MapObject, Object targetDataSources){ super.setDefaultTargetDataSource(defaultDataSource); super.setTargetDataSources(targetDataSources); } Override protected Object determineCurrentLookupKey() { return DataSourceContextHolder.getDataSource(); } }上述代码中还实现了一个动态数据源类的构造方法主要是为了设置默认数据源以及以Map保存的各种目标数据源。其中Map的key是设置的数据源名称value则是对应的数据源DataSource。2.3 配置数据库application.yml中配置数据库信息#设置数据源 spring: datasource: type: com.alibaba.druid.pool.DruidDataSource druid: master: url: jdbc:mysql://xxxxxx:3306/test1?characterEncodingutf-8allowMultiQueriestruezeroDateTimeBehaviorconvertToNulluseSSLfalse username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver slave: url: jdbc:mysql://xxxxx:3306/test2?characterEncodingutf-8allowMultiQueriestruezeroDateTimeBehaviorconvertToNulluseSSLfalse username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver initial-size: 15 min-idle: 15 max-active: 200 max-wait: 60000 time-between-eviction-runs-millis: 60000 min-evictable-idle-time-millis: 300000 validation-query: test-while-idle: true test-on-borrow: false test-on-return: false pool-prepared-statements: false connection-properties: false /** * author: jiangjs * description: 设置数据源 * date: 2023/7/27 11:34 **/ Configuration public class DateSourceConfig { Bean ConfigurationProperties(spring.datasource.druid.master) public DataSource masterDataSource(){ return DruidDataSourceBuilder.create().build(); } Bean ConfigurationProperties(spring.datasource.druid.slave) public DataSource slaveDataSource(){ return DruidDataSourceBuilder.create().build(); } Bean(name dynamicDataSource) Primary public DynamicDataSource createDynamicDataSource(){ MapObject,Object dataSourceMap new HashMap(); DataSource defaultDataSource masterDataSource(); dataSourceMap.put(master,defaultDataSource); dataSourceMap.put(slave,slaveDataSource()); return new DynamicDataSource(defaultDataSource,dataSourceMap); } }通过配置类将配置文件中的配置的数据库信息转换成datasource并添加到DynamicDataSource中同时通过Bean将DynamicDataSource注入Spring中进行管理后期在进行动态数据源添加时会用到。2.4 测试在主从两个测试库中分别添加一张表test_user里面只有一个字段user_name。create table test_user( user_name varchar(255) not null comment 用户名 )在主库添加信息insert into test_user (user_name) value (master);从库中添加信息insert into test_user (user_name) value (slave);我们创建一个getData的方法参数就是需要查询数据的数据源名称。GetMapping(/getData.do/{datasourceName}) public String getMasterData(PathVariable(datasourceName) String datasourceName){ DataSourceContextHolder.setDataSource(datasourceName); TestUser testUser testUserMapper.selectOne(null); DataSourceContextHolder.removeDataSource(); return testUser.getUserName(); }其他的Mapper和实体类大家自行实现。执行结果1、传递master时图片2、传递slave时图片通过执行结果我们看到传递不同的数据源名称查询对应的数据库是不一样的返回结果也不一样。在上述代码中我们看到DataSourceContextHolder.setDataSource(datasourceName);来设置了当前线程需要查询的数据库通过DataSourceContextHolder.removeDataSource();来移除当前线程已设置的数据源。使用过Mybatis-plus动态数据源的小伙伴应该还记得我们在使用切换数据源时会使用到DynamicDataSourceContextHolder.push(String ds);和DynamicDataSourceContextHolder.poll();这两个方法翻看源码我们会发现其实就是在使用ThreadLocal时使用了栈这样的好处就是能使用多数据源嵌套这里就不带大家实现了有兴趣的小伙伴可以看看Mybatis-plus中动态数据源的源码。注启动程序时小伙伴不要忘记将SpringBoot自动添加数据源进行排除哦否则会报循环依赖问题。SpringBootApplication(exclude DataSourceAutoConfiguration.class)2.5 优化调整2.5.1 注解切换数据源在上述中虽然已经实现了动态切换数据源但是我们会发现如果涉及到多个业务进行切换数据源的话我们就需要在每一个实现类中添加这一段代码。说到这有小伙伴应该就会想到使用注解来进行优化接下来我们来实现一下。2.5.1.1 定义注解我们就用mybatis动态数据源切换的注解DS代码如下/** * author: jiangjs * description: * date: 2023/7/27 14:39 **/ Target({ElementType.METHOD,ElementType.TYPE}) Retention(RetentionPolicy.RUNTIME) Documented Inherited public interface DS { String value() default master; }2.5.1.2 实现aopAspect Component Slf4j public class DSAspect { Pointcut(annotation(com.jiashn.dynamic_datasource.dynamic.aop.DS)) public void dynamicDataSource(){} Around(dynamicDataSource()) public Object datasourceAround(ProceedingJoinPoint point) throws Throwable { MethodSignature signature (MethodSignature)point.getSignature(); Method method signature.getMethod(); DS ds method.getAnnotation(DS.class); if (Objects.nonNull(ds)){ DataSourceContextHolder.setDataSource(ds.value()); } try { return point.proceed(); } finally { DataSourceContextHolder.removeDataSource(); } } }代码使用了Around通过ProceedingJoinPoint获取注解信息拿到注解传递值然后设置当前线程的数据源。对aop不了解的小伙伴可以自行google或百度。2.5.1.3 测试添加两个测试方法GetMapping(/getMasterData.do) public String getMasterData(){ TestUser testUser testUserMapper.selectOne(null); return testUser.getUserName(); } GetMapping(/getSlaveData.do) DS(slave) public String getSlaveData(){ TestUser testUser testUserMapper.selectOne(null); return testUser.getUserName(); }由于DS中设置的默认值是master因此在调用主数据源时可以不用进行添加。执行结果1、调用getMasterData.do方法图片2、调用getSlaveData.do方法图片通过执行结果我们通过DS也进行了数据源的切换实现了Mybatis-plus动态切换数据源中的通过注解切换数据源的方式。2.5.2 动态添加数据源业务场景有时候我们的业务会要求我们从保存有其他数据源的数据库表中添加这些数据源然后再根据不同的情况切换这些数据源。因此我们需要改造下DynamicDataSource来实现动态加载数据源。2.5.2.1 数据源实体/** * author: jiangjs * description: 数据源实体 * date: 2023/7/27 15:55 **/ Data Accessors(chain true) public class DataSourceEntity { /** * 数据库地址 */ private String url; /** * 数据库用户名 */ private String userName; /** * 密码 */ private String passWord; /** * 数据库驱动 */ private String driverClassName; /** * 数据库key即保存Map中的key */ private String key; }实体中定义数据源的一般信息同时定义一个key用于作为DynamicDataSource中Map中的key。2.5.2.2 修改DynamicDataSource/** * author: jiangjs * description: 实现动态数据源根据AbstractRoutingDataSource路由到不同数据源中 * date: 2023/7/27 11:18 **/ Slf4j public class DynamicDataSource extends AbstractRoutingDataSource { private final MapObject,Object targetDataSourceMap; public DynamicDataSource(DataSource defaultDataSource,MapObject, Object targetDataSources){ super.setDefaultTargetDataSource(defaultDataSource); super.setTargetDataSources(targetDataSources); this.targetDataSourceMap targetDataSources; } Override protected Object determineCurrentLookupKey() { return DataSourceContextHolder.getDataSource(); } /** * 添加数据源信息 * param dataSources 数据源实体集合 * return 返回添加结果 */ public void createDataSource(ListDataSourceEntity dataSources){ try { if (CollectionUtils.isNotEmpty(dataSources)){ for (DataSourceEntity ds : dataSources) { //校验数据库是否可以连接 Class.forName(ds.getDriverClassName()); DriverManager.getConnection(ds.getUrl(),ds.getUserName(),ds.getPassWord()); //定义数据源 DruidDataSource dataSource new DruidDataSource(); BeanUtils.copyProperties(ds,dataSource); //申请连接时执行validationQuery检测连接是否有效这里建议配置为TRUE防止取到的连接不可用 dataSource.setTestOnBorrow(true); //建议配置为true不影响性能并且保证安全性。 //申请连接的时候检测如果空闲时间大于timeBetweenEvictionRunsMillis执行validationQuery检测连接是否有效。 dataSource.setTestWhileIdle(true); //用来检测连接是否有效的sql要求是一个查询语句。 dataSource.setValidationQuery(select 1 ); dataSource.init(); this.targetDataSourceMap.put(ds.getKey(),dataSource); } super.setTargetDataSources(this.targetDataSourceMap); // 将TargetDataSources中的连接信息放入resolvedDataSources管理 super.afterPropertiesSet(); return Boolean.TRUE; } }catch (ClassNotFoundException | SQLException e) { log.error(---程序报错---:{}, e.getMessage()); } return Boolean.FALSE; } /** * 校验数据源是否存在 * param key 数据源保存的key * return 返回结果true存在false不存在 */ public boolean existsDataSource(String key){ return Objects.nonNull(this.targetDataSourceMap.get(key)); } }在改造后的DynamicDataSource中我们添加可以一个private final MapObject,Object targetDataSourceMap这个map会在添加数据源的配置文件时将创建的Map数据源信息通过DynamicDataSource构造方法进行初始赋值即DateSourceConfig类中的createDynamicDataSource()方法中。同时我们在该类中添加了一个createDataSource方法进行数据源的创建并添加到map中再通过super.setTargetDataSources(this.targetDataSourceMap);进行目标数据源的重新赋值。2.5.2.3 动态添加数据源上述代码已经实现了添加数据源的方法那么我们来模拟通过从数据库表中添加数据源然后我们通过调用加载数据源的方法将数据源添加进数据源Map中。在主数据库中定义一个数据库表用于保存数据库信息。create table test_db_info( id int auto_increment primary key not null comment 主键Id, url varchar(255) not null comment 数据库URL, username varchar(255) not null comment 用户名, password varchar(255) not null comment 密码, driver_class_name varchar(255) not null comment 数据库驱动 name varchar(255) not null comment 数据库名称 )为了方便我们将之前的从库录入到数据库中修改数据库名称。insert into test_db_info(url, username, password,driver_class_name, name) value (jdbc:mysql://xxxxx:3306/test2?characterEncodingutf-8allowMultiQueriestruezeroDateTimeBehaviorconvertToNulluseSSLfalse, root,123456,com.mysql.cj.jdbc.Driver,add_slave)数据库表对应的实体、mapper小伙伴们自行添加。启动SpringBoot时添加数据源/** * author: jiangjs * description: * date: 2023/7/27 16:56 **/ Component public class LoadDataSourceRunner implements CommandLineRunner { Resource private DynamicDataSource dynamicDataSource; Resource private TestDbInfoMapper testDbInfoMapper; Override public void run(String... args) throws Exception { ListTestDbInfo testDbInfos testDbInfoMapper.selectList(null); if (CollectionUtils.isNotEmpty(testDbInfos)) { ListDataSourceEntity ds new ArrayList(); for (TestDbInfo testDbInfo : testDbInfos) { DataSourceEntity sourceEntity new DataSourceEntity(); BeanUtils.copyProperties(testDbInfo,sourceEntity); sourceEntity.setKey(testDbInfo.getName()); ds.add(sourceEntity); } dynamicDataSource.createDataSource(ds); } } }经过上述SpringBoot启动后已经将数据库表中的数据添加到动态数据源中我们调用之前的测试方法将数据源名称作为参数传入看看执行结果。2.5.2.4 测试通过测试我们发现数据库表中的数据库被动态加入了数据源中小伙伴可以愉快地随意添加数据源了。