你好!在大多数项目中,数据库的读写走的都是一个库,这在并发量小的时候,完全没问题,但是一旦并发上来,数据库的读写压力会变的很大,在实际业务中,读写分离是有必要的,特别是在读多写少的业务场景中;
spring:
datasource:
dynamic-datasource:
enable: true
slave:
type: com.zaxxer.hikari.HikariDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
jdbc-url: jdbc:mysql://localhost:3306/my_db2
username: root
password: 123456
# Hikari 连接池配置
hikari:
# 最小空闲连接数量
minimum-idle: 1
# 空闲连接存活最大时间,默认600000(10分钟)
idle-timeout: 180000
# 连接池最大连接数,默认是10
maximum-pool-size: 4
# 此属性控制从池返回的连接的默认自动提交行为,默认值:true
auto-commit: true
# 连接池名称
pool-name: ${spring.application.name}
# 此属性控制池中连接的最长生命周期,值0表示无限生命周期,默认1800000即30分钟
max-lifetime: 1800000
# 数据库连接超时时间,默认30秒,即30000
connection-timeout: 30000
master:
type: com.zaxxer.hikari.HikariDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
jdbc-url: jdbc:mysql://localhost:3306/my_db
username: root
password: 123456
# Hikari 连接池配置
hikari:
# 最小空闲连接数量
minimum-idle: 1
# 空闲连接存活最大时间,默认600000(10分钟)
idle-timeout: 180000
# 连接池最大连接数,默认是10
maximum-pool-size: 4
# 此属性控制从池返回的连接的默认自动提交行为,默认值:true
auto-commit: true
# 连接池名称
pool-name: ${spring.application.name}
# 此属性控制池中连接的最长生命周期,值0表示无限生命周期,默认1800000即30分钟
max-lifetime: 1800000
# 数据库连接超时时间,默认30秒,即30000
connection-timeout: 30000
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD, ElementType.TYPE})
public @interface DataSourceSelector {
DataSourceType type() default DataSourceType.MASTER;
}
public enum DataSourceType {
MASTER, SLAVE
}
@Configuration(proxyBeanMethods = false)
@AutoConfigureBefore({DataSourceAutoConfiguration.class})
@EnableConfigurationProperties({DataSourceProperties.class,DynamicDataSourceProperties.class})
@RequiredArgsConstructor
public class DynamicDataSourceAutoConfiguration {
@Bean(name = "masterDataSource")
@ConfigurationProperties(prefix = "spring.datasource.master")
public DataSource masterDataSource() {
return DataSourceBuilder.create().build();
}
@Bean(name = "salveDataSource")
@ConfigurationProperties(prefix = "spring.datasource.slave")
public DataSource salveDataSource() {
return DataSourceBuilder.create().build();
}
@Primary
@Bean(name = "dynamicDataSource")
public DataSource dynamicDataSource(@Qualifier("masterDataSource") DataSource master, @Qualifier("salveDataSource") DataSource salve) {
DynamicDataSource dynamicDataSource = new DynamicDataSource();
Map<Object, Object> targetDataSources = new HashMap<>();
targetDataSources.put(DataSourceType.MASTER, master);
targetDataSources.put(DataSourceType.SLAVE, salve);
dynamicDataSource.setTargetDataSources(targetDataSources);
dynamicDataSource.setDefaultTargetDataSource(master);
return dynamicDataSource;
}
}
public class DynamicDataSource extends AbstractRoutingDataSource {
private static final Logger log = LoggerFactory.getLogger(DynamicDataSource.class);
@Override
protected Object determineCurrentLookupKey() {
DataSourceType dataSourceType = DataSourceContextHolder.getDataSourceType();
log.info("use datasource: {}", dataSourceType);
return dataSourceType;
}
}
IS_DATA_SOURCE_SELECTED_HOLDER
用于异步场景public class DataSourceContextHolder {
private DataSourceContextHolder() {
}
/**
* 线程本地环境
*/
private static final ThreadLocal<DataSourceType> CONTEXT_TYPE_HOLDER = ThreadLocal.withInitial(() -> DataSourceType.MASTER);
private static final ThreadLocal<Boolean> IS_DATA_SOURCE_SELECTED_HOLDER = ThreadLocal.withInitial(() -> Boolean.FALSE);
/**
* 设置数据源类型:枚举式
*/
public static void setDataSourceType(DataSourceType dbType) {
Assert.notNull(dbType, "DataSourceType cannot be null");
CONTEXT_TYPE_HOLDER.set(dbType);
}
/**
* 当前线程已选择数据源
*/
public static void inDataSourceSelected() {
IS_DATA_SOURCE_SELECTED_HOLDER.set(Boolean.TRUE);
}
/**
* 获取数据源类型:枚举式
*/
public static DataSourceType getDataSourceType() {
return CONTEXT_TYPE_HOLDER.get();
}
public static boolean isDataSourceSelected() {
return IS_DATA_SOURCE_SELECTED_HOLDER.get();
}
/**
* 清除数据源类型
*/
public static void clearDataSourceType() {
CONTEXT_TYPE_HOLDER.remove();
}
public static void clearIsDataSourceSelected() {
IS_DATA_SOURCE_SELECTED_HOLDER.remove();
}
}
DataSourceSelector
注解,如果有对类生成代理对象;AbstractAutoProxyCreator
进行注解的拦截处理,和传统的AOP写法不太一样,这种写法更适合于组件。public class DynamicDataSourceCreator extends AbstractAutoProxyCreator {
private static final Set<String> PROXY_SET = new HashSet<>();
@Override
protected Object[] getAdvicesAndAdvisorsForBean(Class<?> aClass, String s, TargetSource targetSource) throws BeansException {
return new Object[]{new DynamicDataSourceMethodInterceptor()};
}
@SneakyThrows
@Override
protected Object wrapIfNecessary(Object bean, String beanName, Object cacheKey) {
synchronized (PROXY_SET) {
if (PROXY_SET.contains(beanName)) {
return bean;
}
Class<?> serviceInterface = SpringProxyUtils.findTargetClass(bean);
Class<?>[] interfacesIfJdk = SpringProxyUtils.findInterfaces(bean);
if (!CommUtils.existsAnnotation(new Class[]{serviceInterface}) && !CommUtils.existsAnnotation(interfacesIfJdk)) {
return bean;
}
if (!AopUtils.isAopProxy(bean)) {
bean = super.wrapIfNecessary(bean, beanName, cacheKey);
} else {
AdvisedSupport advised = SpringProxyUtils.getAdvisedSupport(bean);
Advisor[] advisor = buildAdvisors(beanName, getAdvicesAndAdvisorsForBean(null, null, null));
for (Advisor avr : advisor) {
advised.addAdvisor(0, avr);
}
}
PROXY_SET.add(beanName);
return bean;
}
}
}
public class DynamicDataSourceMethodInterceptor implements MethodInterceptor {
@Override
@SneakyThrows
public Object invoke(MethodInvocation methodInvocation) {
if (DataSourceContextHolder.isDataSourceSelected()) {
return methodInvocation.proceed();
}
try {
Class<?> targetClass = methodInvocation.getThis() != null ? AopUtils.getTargetClass(methodInvocation.getThis()) : null;
Method specificMethod = ClassUtils.getMostSpecificMethod(methodInvocation.getMethod(), targetClass);
DataSourceSelector methodAnnotation = getAnnotation(specificMethod, targetClass, DataSourceSelector.class);
if (Objects.nonNull(methodAnnotation)) {
CommUtils.setDataSourceType(methodAnnotation.type());
} else {
CommUtils.setDataSourceType(DataSourceType.MASTER);
}
return methodInvocation.proceed();
} finally {
CommUtils.clearContext();
}
}
private <T extends Annotation> T getAnnotation(Method method, Class<?> targetClass, Class<T> annotationClass) {
return Optional.ofNullable(method).map(m -> m.getAnnotation(annotationClass))
.orElse(Optional.ofNullable(targetClass).map(t -> t.getAnnotation(annotationClass)).orElse(null));
}
}
@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})})
@RequiredArgsConstructor
public class DynamicDataSourceInterceptor implements Interceptor {
private static final String REGEX = ".*insert\\u0020.*|.*delete\\u0020.*|.*update\\u0020.*";
private static final Set<String> cacheSet = new ConcurrentSkipListSet<>();
@Override
public Object intercept(Invocation invocation) throws Throwable {
if (Objects.equals(DataSourceType.SLAVE, DataSourceContextHolder.getDataSourceType())) {
Object[] objects = invocation.getArgs();
MappedStatement ms = (MappedStatement) objects[0];
BoundSql boundSql = ms.getSqlSource().getBoundSql(objects[1]);
boolean actualTransactionActive = TransactionSynchronizationManager
.isActualTransactionActive();
String sql = boundSql.getSql().toLowerCase(Locale.CHINA)
.replaceAll("[\\t\\n\\r]", " ");
if (cacheSet.contains(ms.getId())) {
throw new IllegalStateException(String.format("The current sql:[%s] should use the master database", boundSql.getSql().toLowerCase(Locale.CHINA)));
}
boolean isMatch = sql.matches(REGEX);
if (isMatch || actualTransactionActive) {
if (isMatch) {
cacheSet.add(ms.getId());
}
throw new IllegalStateException(String.format("The current sql:[%s] should use the master database", boundSql.getSql().toLowerCase(Locale.CHINA)));
}
}
return invocation.proceed();
}
@Override
public Object plugin(Object target) {
if (target instanceof Executor) {
return Plugin.wrap(target, this);
} else {
return target;
}
}
}