使用场景
在实际开发中,可能遇到多数据源的场景。
- 业务复杂(数据量大)
数据分布在不同的数据库中,对业务数据进行垂直拆分。
可以拆分为微服务架构,依赖的业务可以通过远程调用的方式来是实现,那么这种方式是不存在多数据源的情况。但是对于性能要求不高,以实现功能为目的,则可以使用多数据源。
- 读写分离
为了解决数据库读性能瓶颈(读比写的性能要高,但是写锁会影响阻塞,从而影响读性能)
主从架构:一台主数据库对外提供增删改业务,其他从数据库进行读操作。
- 这里还有读取上传的业务。
自定义多数据源实现类
关键类解析
DynamicDataSource 需要编写这个类并继承 AbstractRoutingDataSource
/*** 继承抽象路由数据源类AbstractRoutingDataSource*/
public class DynamicDataSource extends AbstractRoutingDataSource {/*** 每次查询数据库都会执行该方法* @return*/@Overrideprotected Object determineCurrentLookupKey() {// 返回AbstractRoutingDataSource类targetDataSources集合的key。return null;}
}
AbstractRoutingDataSource解析
public abstract class AbstractRoutingDataSource extends AbstractDataSource implements InitializingBean {@Nullableprivate Map<Object, Object> targetDataSources;@Nullableprivate Object defaultTargetDataSource;private boolean lenientFallback = true;private DataSourceLookup dataSourceLookup = new JndiDataSourceLookup();@Nullableprivate Map<Object, DataSource> resolvedDataSources;@Nullableprivate DataSource resolvedDefaultDataSource;// 这个类中有四个需要传入的非空属性。其中我们只需要传入上面两个即可,下面的两个会通过调用默认的afterPropertiesSet()方法来赋值。public void afterPropertiesSet() {if (this.targetDataSources == null) {throw new IllegalArgumentException("Property 'targetDataSources' is required");} else {this.resolvedDataSources = new HashMap(this.targetDataSources.size());this.targetDataSources.forEach((key, value) -> {Object lookupKey = this.resolveSpecifiedLookupKey(key);DataSource dataSource = this.resolveSpecifiedDataSource(value);this.resolvedDataSources.put(lookupKey, dataSource);});if (this.defaultTargetDataSource != null) {this.resolvedDefaultDataSource = this.resolveSpecifiedDataSource(this.defaultTargetDataSource);}}// 这里省略
}
示例:
- 创建SpringBoot程序,并编写配置文件。配置好两个数据源。这里我的叫db1,db2
server:port: 8888
spring:application:name: MultiDatasourcedatasource:driver-class-name: com.mysql.cj.jdbc.Drivername: defaultDataSourceurl: jdbc:mysql://localhost:3306/blue?serverTimezone=UTCusername: 'root'password: '123456'type: com.zaxxer.hikari.HikariDataSourcedb1:url: jdbc:mysql://localhost:3306/blue?serverTimezone=UTCusername: 'root'password: '123456'db2:url: jdbc:mysql://localhost:3306/blue?serverTimezone=UTCusername: 'root'password: '123456'mybatis:mapper-locations: classpath:mappers/*xmltype-aliases-package: com.hx.multiDB.entityconfiguration.map-underscore-to-camel-case: true
- 新建数据源配置类 DataSourceConfig.java(包含了数据源配置)
package com.hx.multiDB.config;import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.jdbc.DataSourceBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;import javax.sql.DataSource;
import java.util.HashMap;
import java.util.Map;@Configuration
public class DataSourceConfig {@Bean(name = "db1")@ConfigurationProperties("spring.datasource.db1")public DataSource db1DataSource() {return DataSourceBuilder.create().build();}@Bean@ConfigurationProperties("spring.datasource.db2")// 配置当存在enable属性值,且为true时才注入容器@ConditionalOnProperty(prefix = "spring.datasource.db2",name = "enable",havingValue = "true")public DataSource db2() {return DataSourceBuilder.create().build();}/*** 通过AOP在不同的数据库之间进行切换。** @param db1* @return*/@Primary //告诉spring这个是主数据源@Bean// 注意这个类名不可以public DynamicDataSource dynamicDB(@Qualifier("db1") DataSource db1) {DynamicDataSource dynamicDataSource = new DynamicDataSource();Map<Object, Object> targetDataSources = new HashMap<>();// 这里有两种方法把上面的配置放入map集合中,1.以参数传入,2.调用方法targetDataSources.put("db1", db1);targetDataSources.put("db2", db2());dynamicDataSource.setDefaultTargetDataSource(db1);dynamicDataSource.setTargetDataSources(targetDataSources);dynamicDataSource.afterPropertiesSet();return dynamicDataSource;}
}
- 新建动态数据源类 DynamicDataSource.java(继承AbstractRoutingDataSource)
package com.hx.multiDB.config;import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
import org.springframework.stereotype.Component;import java.util.Map;/*** 继承抽象路由数据源类AbstractRoutingDataSource* 这里使用的是AbstractRoutingDataSource来切换配置,* 我们也可以使用mybatis中的SqlSessionTemplate,* 继承他的getSqlSessionFactory来切换数据源*/
public class DynamicDataSource extends AbstractRoutingDataSource {/*** 每次查询数据库都会执行该方法** @return*/@Overrideprotected Object determineCurrentLookupKey() {return DynamicDataSourceHandler.getDB();}
}
- 创建动态数据源处理类(包括保存数据源的容器,设置、获取、清空数据源方法)
package com.hx.multiDB.config;import lombok.extern.slf4j.Slf4j;/*** 动态数据源处理器*/
@Slf4j
public class DynamicDataSourceHandler {// 必须要线程变量的集合。不能用线程不安全的,否则在查询的时候并发切换,会导致错误。private static final ThreadLocal<String> DATASOURCE_HOLDER = new ThreadLocal<>();public static void setDB(String dbName){log.info("切换数据源为:{}",dbName);DATASOURCE_HOLDER.set(dbName);}public static String getDB(){return DATASOURCE_HOLDER.get();}// 每次使用完毕都要移除,否则会导致内存泄露public static void clear(){DATASOURCE_HOLDER.remove();}
}
利用Aop和注解来自动切换数据源
- 新建数据源注解 DataSource.java
package com.hx.multiDB.config;import java.lang.annotation.*;@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DataSource {String value();
}
- 创建数据源AOP DataSourceAop.java
package com.hx.multiDB.config;import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.aopalliance.intercept.Joinpoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;import java.lang.annotation.Annotation;
import java.lang.reflect.Method;@Aspect
@Component
@Slf4j
public class DataSourceAop {// 通过参数来拿到注解
// @Around("@annotation(dataSource)")
// public Object around( DataSource dataSource){
// dataSource.value();
// }// 通过切点对象来获取注解
// @Around("@annotation(com.hx.multiDB.config.DataSource)")// 切dao中的所有方法。@Around("execution(* com.hx.multiDB.mapper.*Mapper*.*(..))")public Object around(ProceedingJoinPoint joinpoint) {log.info("进入环绕通知...");MethodSignature signature = (MethodSignature) joinpoint.getSignature();Method method = signature.getMethod();// 获取方法上的注解DataSource annotation = method.getAnnotation(DataSource.class);if(annotation == null){// 通过签名拿到类Class type = signature.getDeclaringType();// 拿到类上的注解Annotation methodAnno = type.getAnnotation(DataSource.class);if(methodAnno instanceof DataSource){annotation = (DataSource) methodAnno;}}if (annotation != null) {String value = annotation.value();// 设置数据源DynamicDataSourceHandler.setDB(value);}try {return joinpoint.proceed();} catch (Throwable throwable) {throwable.printStackTrace();return null;}finally {// 清空,避免内存泄露DynamicDataSourceHandler.clear();log.info("清空数据源Holder...");}}
}
测试
- 编写TestMapper1.java和TestMapper2.java
/*** 实际开发可能是读写分离等场景。*/
@Mapper
@Repository
public interface TestMapper1 {@Select(" SELECT * FROM TEST")List<Test> getAll();
}
/***** 以下是TestMapper2.java *****/
@Mapper
@Repository
public interface TestMapper2 {@Select(" SELECT * FROM TEST2 ")List<Test> getAll();
}
- 新建测试 TestController.java
package com.hx.multiDB.controller;import com.hx.multiDB.config.DataSource;
import com.hx.multiDB.mapper.TestMapper1;
import com.hx.multiDB.mapper.TestMapper2;
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.RequestMethod;
import org.springframework.web.bind.annotation.RestController;@RestController
@RequestMapping(path="/test",method={RequestMethod.GET,RequestMethod.POST})
public class TestController {@Autowiredprivate TestMapper1 mapper1;@Autowiredprivate TestMapper2 mapper2;@GetMapping("/select1")public Object select1(){return mapper1.getAll();}@GetMapping("/select2")public Object select2(){return mapper2.getAll();}
}
TreadLocal原理
官方描述: ThreadLocal类用来提供线程内部的局部变量,这种变量在多线程环境下访问(通过get和set方法访问)时,能保证各个线程的变量相对独立于其他线程内的变量。ThreadLocal实例通常是private static类型的,用于关联线程和线程的上下文。
作用: 提供线程内的局部变量,不同的线程之间不会相互干扰,这种变量在线程的生命周期内起作用,减少一个线程内多个函数或组件之间一些公共变量传递的复杂度。
总结:
线程并发
:在多线程并发的场景下使用
传递数据
:我们可以通过ThreadLocal在同一线程,不同组件中传递公共变量
线程隔离
:每个线程的变量都是独立的,不会互相影响
synchronized | ThreadLocal | |
---|---|---|
原理 | 同步机制采用以时间换空间的方式,只提供了一份变量,让不同的线程排队访问 | ThreadLocal采用以空间换时间的方式,为每一个线程都提供了一份变量的副本,从而实现同时访问而相互不干扰 |
侧重点 | 多个线程之间访问资源的同步 | 多线程中让每个线程之间的数据相互隔离 |
ThreadLocal设计对比JDK8前后
// JDK8的ThreadLocal下的Entry类通过继承WeakReference采用弱引用。
static class Entry extends WeakReference<ThreadLocal<?>> {
}
Memory overflow: 内存溢出,没有足够的内存提供申请者使用
Memory leak: 内存泄漏,是指程序中已动态分配的堆内存由于某种原因程序未释放或无法释放,造成系统内存的浪费,导致程序运行速度缓慢甚至系统前遗等严重后果。内存泄漏的堆积终将导致内存溢出
java中的引用有4种类型:强、软、弱、虚。当前这个问题主要涉及到强引用和弱引用
StrongReference: 强引用,就是我们最常见的普通对象引用,只要还有强引用指向一个对象,就能证明对象还"活若”,垃圾回收器就不会回收这种对象,
WeakReference: 弱引用,垃圾回收器一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。