Github:https://github.com/yihonglei/daisy-framework/tree/master/daisy-springboot-framework(daisy工程)
多数据源主要解决高并发读写分离或多库在一个应用处理系统业务。
多数据源中心思想一样,但是根据自己业务情况有很多种写法实现多数据源,也有很多知名或不知名的
开源分布式 ORM 框架设计时候就按照多数据源设计的,使用时候天然就支持。
多数据源个人觉得要满足如下基本使用功能:
1、有多个业务库数据源时,需要能够明确切换到具体的操作业务数据库;
2、主从读写分离,默认写走主库,读走从库,同时可以指定读强制走主库,因为有时候为了避免主从延迟,
部分读操作能够支持指定强制走主库;
假如我有两个库,jpeony 和 order 库,其中 jpeony 业务用的一主两从,order 用的一主一从,具体配置格式如下。
数据源配置文件,未放置 druid 的其他属性,具体代码见 Github。
# DataSource
spring:
datasource:
type: com.alibaba.druid.pool.DruidDataSource
driverClassName: com.mysql.jdbc.Driver
druid:
jpeony:
master:
url: jdbc:mysql://127.0.0.1:3306/jpeony?characterEncoding=utf-8&useSSL=false
username: root
password: 123456
slave01:
url: jdbc:mysql://127.0.0.1:3306/jpeony?characterEncoding=utf-8&useSSL=false
username: root
password: 123456
slave02:
url: jdbc:mysql://127.0.0.1:3306/jpeony?characterEncoding=utf-8&useSSL=false
username: root
password: 123456
order:
master:
url: jdbc:mysql://127.0.0.1:3306/order?characterEncoding=utf-8&useSSL=false
username: root
password: 123456
slave:
url: jdbc:mysql://127.0.0.1:3306/order?characterEncoding=utf-8&useSSL=false
username: root
password: 123456
SpringBoot 工程读取 druid 的相关属性配置的值。
package com.jpeony.common.config.properties;
import com.alibaba.druid.pool.DruidDataSource;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Configuration;
/**
* druid 配置属性
*
* @author yihonglei
*/
@Configuration
public class DruidProperties {
@Value("${spring.datasource.druid.initialSize}")
private int initialSize;
@Value("${spring.datasource.druid.minIdle}")
private int minIdle;
@Value("${spring.datasource.druid.maxActive}")
private int maxActive;
@Value("${spring.datasource.druid.maxWait}")
private int maxWait;
@Value("${spring.datasource.druid.timeBetweenEvictionRunsMillis}")
private int timeBetweenEvictionRunsMillis;
@Value("${spring.datasource.druid.minEvictableIdleTimeMillis}")
private int minEvictableIdleTimeMillis;
@Value("${spring.datasource.druid.maxEvictableIdleTimeMillis}")
private int maxEvictableIdleTimeMillis;
@Value("${spring.datasource.druid.validationQuery}")
private String validationQuery;
@Value("${spring.datasource.druid.testWhileIdle}")
private boolean testWhileIdle;
@Value("${spring.datasource.druid.testOnBorrow}")
private boolean testOnBorrow;
@Value("${spring.datasource.druid.testOnReturn}")
private boolean testOnReturn;
public DruidDataSource dataSource(DruidDataSource datasource) {
/* 配置初始化大小、最小、最大 */
datasource.setInitialSize(initialSize);
datasource.setMaxActive(maxActive);
datasource.setMinIdle(minIdle);
/* 配置获取连接等待超时的时间 */
datasource.setMaxWait(maxWait);
/* 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒 */
datasource.setTimeBetweenEvictionRunsMillis(timeBetweenEvictionRunsMillis);
/* 配置一个连接在池中最小、最大生存的时间,单位是毫秒 */
datasource.setMinEvictableIdleTimeMillis(minEvictableIdleTimeMillis);
datasource.setMaxEvictableIdleTimeMillis(maxEvictableIdleTimeMillis);
/*
* 用来检测连接是否有效的sql,要求是一个查询语句,常用select 'x'。如果validationQuery为null,testOnBorrow、testOnReturn、testWhileIdle都不会起作用。
*/
datasource.setValidationQuery(validationQuery);
/* 建议配置为true,不影响性能,并且保证安全性。申请连接的时候检测,如果空闲时间大于timeBetweenEvictionRunsMillis,执行validationQuery检测连接是否有效。 */
datasource.setTestWhileIdle(testWhileIdle);
/* 申请连接时执行validationQuery检测连接是否有效,做了这个配置会降低性能。 */
datasource.setTestOnBorrow(testOnBorrow);
/* 归还连接时执行validationQuery检测连接是否有效,做了这个配置会降低性能。 */
datasource.setTestOnReturn(testOnReturn);
return datasource;
}
}
数据源枚举。
package com.jpeony.common.enums;
/**
* @author yihonglei
*/
public enum DataSourceTypeEnum {
JPEONY_MASTER,
JPEONY_SLAVE01,
JPEONY_SLAVE02,
ORDER_MASTER,
ORDER_SLAVE
}
线程持有的数据源设置。
package com.jpeony.common.datasource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* @author yihonglei
*/
public class MultipleDataSourceContextHolder {
public static final Logger logger = LoggerFactory.getLogger(MultipleDataSourceContextHolder.class);
private static final ThreadLocal<String> CONTEXT_HOLDER = new ThreadLocal<>();
public static void setDataSourceType(String dsType) {
CONTEXT_HOLDER.remove();
CONTEXT_HOLDER.set(dsType);
}
public static String getDataSourceType() {
return CONTEXT_HOLDER.get();
}
public static void clearDataSourceType() {
CONTEXT_HOLDER.remove();
}
}
多数据源路由器。
package com.jpeony.common.datasource;
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
/**
* @author yihonglei
*/
public class MultipleDataSource extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
return MultipleDataSourceContextHolder.getDataSourceType();
}
}
druid 多数据源配置,将配置文件中的数据源构建为 DataSource,最后放入 AbstractRoutingDataSource 路由中。
package com.jpeony.common.config;
import com.alibaba.druid.pool.DruidDataSource;
import com.alibaba.druid.spring.boot.autoconfigure.DruidDataSourceBuilder;
import com.alibaba.druid.spring.boot.autoconfigure.properties.DruidStatProperties;
import com.alibaba.druid.util.Utils;
import com.jpeony.common.config.properties.DruidProperties;
import com.jpeony.common.datasource.MultipleDataSource;
import com.jpeony.common.enums.DataSourceTypeEnum;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import javax.servlet.*;
import javax.sql.DataSource;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
/**
* @author yihonglei
*/
@Configuration
public class DruidDataSourceConfig {
@Bean
@ConfigurationProperties("spring.datasource.druid.jpeony.master")
public DataSource jpeonyMasterDataSource(DruidProperties druidProperties) {
DruidDataSource dataSource = DruidDataSourceBuilder.create().build();
return druidProperties.dataSource(dataSource);
}
@Bean
@ConfigurationProperties("spring.datasource.druid.jpeony.slave01")
public DataSource jpeonySlave01DataSource(DruidProperties druidProperties) {
DruidDataSource dataSource = DruidDataSourceBuilder.create().build();
return druidProperties.dataSource(dataSource);
}
@Bean
@ConfigurationProperties("spring.datasource.druid.jpeony.slave02")
public DataSource jpeonySlave02DataSource(DruidProperties druidProperties) {
DruidDataSource dataSource = DruidDataSourceBuilder.create().build();
return druidProperties.dataSource(dataSource);
}
@Bean
@ConfigurationProperties("spring.datasource.druid.order.master")
public DataSource orderMasterDataSource(DruidProperties druidProperties) {
DruidDataSource dataSource = DruidDataSourceBuilder.create().build();
return druidProperties.dataSource(dataSource);
}
@Bean
@ConfigurationProperties("spring.datasource.druid.order.slave")
public DataSource orderSlaveDataSource(DruidProperties druidProperties) {
DruidDataSource dataSource = DruidDataSourceBuilder.create().build();
return druidProperties.dataSource(dataSource);
}
@Bean(name = "multipleDataSource")
@Primary
public MultipleDataSource dataSource(DataSource jpeonyMasterDataSource, DataSource jpeonySlave01DataSource,
DataSource jpeonySlave02DataSource, DataSource orderMasterDataSource,
DataSource orderSlaveDataSource) {
// 数据源
Map<Object, Object> targetDataSources = new HashMap<>(16);
targetDataSources.put(DataSourceTypeEnum.JPEONY_MASTER, jpeonyMasterDataSource);
targetDataSources.put(DataSourceTypeEnum.JPEONY_SLAVE01, jpeonySlave01DataSource);
targetDataSources.put(DataSourceTypeEnum.JPEONY_SLAVE02, jpeonySlave02DataSource);
targetDataSources.put(DataSourceTypeEnum.ORDER_MASTER, orderMasterDataSource);
targetDataSources.put(DataSourceTypeEnum.ORDER_SLAVE, orderSlaveDataSource);
// 路由数据源
MultipleDataSource multipleDataSource = new MultipleDataSource();
multipleDataSource.setTargetDataSources(targetDataSources);
return multipleDataSource;
}
@SuppressWarnings({"rawtypes", "unchecked"})
@Bean
@ConditionalOnProperty(name = "spring.datasource.druid.statViewServlet.enabled", havingValue = "true")
public FilterRegistrationBean removeDruidFilterRegistrationBean(DruidStatProperties properties) {
// 获取web监控页面的参数
DruidStatProperties.StatViewServlet config = properties.getStatViewServlet();
// 提取common.js的配置路径
String pattern = config.getUrlPattern() != null ? config.getUrlPattern() : "/druid/*";
String commonJsPattern = pattern.replaceAll("\\*", "js/common.js");
final String filePath = "support/http/resources/js/common.js";
// 创建filter进行过滤
Filter filter = new Filter() {
@Override
public void init(javax.servlet.FilterConfig filterConfig) throws ServletException {
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
chain.doFilter(request, response);
// 重置缓冲区,响应头不会被重置
response.resetBuffer();
// 获取common.js
String text = Utils.readFromResource(filePath);
// 正则替换banner, 除去底部的广告信息
text = text.replaceAll("<a.*?banner\"></a><br/>", "");
text = text.replaceAll("powered.*?shrek.wang</a>", "");
response.getWriter().write(text);
}
@Override
public void destroy() {
}
};
FilterRegistrationBean registrationBean = new FilterRegistrationBean();
registrationBean.setFilter(filter);
registrationBean.addUrlPatterns(commonJsPattern);
return registrationBean;
}
}
在正式分析路由实现前,需要先明确自定义的注解。
这里自己定义了一个 DB 注解,用于在 Mapper 上面指定库,还要一个 UseMaster 注解,用于强制走主库指定。
package com.jpeony.common.annotation;
import java.lang.annotation.*;
/**
* 修饰 Mapper 接口,拦截指定数据源
*
* @author yihonglei
*/
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DB {
/**
* 数据源名称
*/
String name() default "";
}
package com.jpeony.common.annotation;
import java.lang.annotation.*;
/**
* 强制使用主库
*
* @author yihonglei
*/
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface UseMaster {
}
Mapper 上关于 DB 和 UseMaster 注解的使用。
package com.jpeony.core.mapper;
import com.jpeony.common.annotation.DB;
import com.jpeony.common.annotation.UseMaster;
import com.jpeony.common.constant.DBConstant;
import com.jpeony.core.pojo.domain.TestDO;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
import org.apache.ibatis.annotations.Update;
/**
* Mapper接口
*
* @author yihonglei
*/
@DB(name = DBConstant.JPEONY)
public interface TestMapper {
/**
* MyBatis 注解形式
*/
@Select("select * from test where id = #{id}")
TestDO queryTestById(@Param("id") int id);
@UseMaster
@Select("select * from test where id = #{id}")
TestDO queryTestByIdMaster(@Param("id") int id);
@Update("update test set test_name = #{testName} where id = #{id}")
int updateTestById(@Param("id") int id, @Param("testName") String testName);
/**
* MyBatis XML方式
*/
TestDO queryTestByIdXml(@Param("id") int id);
}
DBConstant 用于配置对应的数据库名,在 Mapper 上使用的时候,需要进行显示指定用的库,因为在是路由的依据。
package com.jpeony.common.constant;
/**
* 数据库名
*
* @author yihonglei
*/
public class DBConstant {
public final static String JPEONY = "jpeony";
public final static String ORDER = "order";
}
根据上面这些配置,通过 AOP 拦截 Mapper,然后路由切换数据源的逻辑。
1、aop 拦截 mapper 下的所有类方法;
2、通过反射,获取 Mapper 上 DB 指定的数据库名,用于匹配对应配置的主库或从库;
3、如果方法上使用了 UseMaster 强制走主或者 update、insert、delete 写操作,直接通过数据库名匹配找到对应的主库,
如果是走从库的,需要匹配出数据库对应的多个从库,然后随机选择一个从库,实现强制主或写操作走主库,其余默认走从库操作。
4、这里还做了一个兼容,当不配置主库的时候,默认选择一个从库做主库使用,
其实当没配置主库的时候,可以强制抛异常处理,也没有必要切换到从库连接,这个切换应该是由 数据库主从本身自己去做。
5、这个路由有一个弊端,当使用 MyBatis 的 xml 配置方式时,如果是写操作,需要显示的去指定走主库,要不然切不到主库去。
处理方式可以从方法名着手,比如 updateXXX,insertXXX,deleteXXX,editXXX,这样去命名方法名然后切主库,
就是写起代码来规范太多了,规范太多烦人。
package com.jpeony.common.datasource;
import com.jpeony.common.annotation.DB;
import com.jpeony.common.annotation.UseMaster;
import com.jpeony.common.enums.DataSourceTypeEnum;
import com.jpeony.common.enums.ErrorCodeEnum;
import com.jpeony.common.exception.DBException;
import com.jpeony.common.utils.MatchUtils;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.ibatis.annotations.Delete;
import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Update;
import org.aspectj.lang.ProceedingJoinPoint;
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.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Random;
/**
* @author yihonglei
*/
@Aspect
@Order(-1)// 在 Spring 事务生成代理对象之前指定数据源
@Component
public class DataSourceAop {
protected Logger logger = LoggerFactory.getLogger(getClass());
private final Random random = new Random();
@Pointcut("execution(* com.jpeony.core.mapper..*.*(..))")
public void dsPointCut() {
}
@Around("dsPointCut()")
public Object around(ProceedingJoinPoint point) throws Throwable {
String targetDataSource = getTargetDataSource(point);
MultipleDataSourceContextHolder.setDataSourceType(targetDataSource);
try {
return point.proceed();
} finally {
MultipleDataSourceContextHolder.clearDataSourceType();
}
}
private String getTargetDataSource(ProceedingJoinPoint point) {
String dbName = getDBName(point).toUpperCase();
boolean useMaster = getUseMaster(point);
String targetDataSource = StringUtils.EMPTY;
DataSourceTypeEnum[] values = DataSourceTypeEnum.values();
ArrayList<String> slaves = new ArrayList<>(values.length);
for (DataSourceTypeEnum dst : values) {
if (useMaster) {
boolean match = MatchUtils.matchDataSource(dbName + MatchUtils.PATTERN_MATCH_MASTER, dst.name());
if (match) {
targetDataSource = dst.name();
break;
}
} else {
// Match all slaves
boolean match = MatchUtils.matchDataSource(dbName + MatchUtils.PATTERN_MATCH_SLAVE, dst.name());
if (match) {
slaves.add(dst.name());
}
}
}
if (StringUtils.isNotBlank(targetDataSource)) {
return targetDataSource;
}
if (CollectionUtils.isEmpty(slaves)) {
throw new DBException(ErrorCodeEnum.DATA_SOURCE_ERROR);
}
return slaves.get(random.nextInt(slaves.size()));
}
private String getDBName(ProceedingJoinPoint point) {
Class<?> targetClass = point.getTarget().getClass();
DB dbName = AnnotationUtils.findAnnotation(targetClass, DB.class);
if (dbName == null) {
throw new DBException(targetClass.getName() + ", no specified database");
}
return dbName.name();
}
private boolean getUseMaster(ProceedingJoinPoint point) {
MethodSignature signature = (MethodSignature) point.getSignature();
Method method = signature.getMethod();
UseMaster useMaster = method.getAnnotation(UseMaster.class);
Update update = method.getAnnotation(Update.class);
Insert insert = method.getAnnotation(Insert.class);
Delete delete = method.getAnnotation(Delete.class);
return (useMaster != null || update != null || insert != null || delete != null);
}
}
package com.jpeony.test.mapper;
import com.jpeony.core.mapper.TestMapper;
import com.jpeony.core.pojo.domain.TestDO;
import com.jpeony.test.BaseServletTest;
import lombok.extern.slf4j.Slf4j;
import org.junit.Test;
import org.springframework.beans.factory.annotation.Autowired;
/**
* Mapper测试
*
* @author yihonglei
*/
@Slf4j
public class MapperTest extends BaseServletTest {
@Autowired
private TestMapper testMapper;
@Test
public void testAnnotation() {
TestDO testDO = testMapper.queryTestById(1);
log.info("testDO annotation={}", testDO);
}
@Test
public void testUseMaster() {
TestDO testDO = testMapper.queryTestByIdMaster(1);
log.info("testDO useMaster={}", testDO);
}
@Test
public void testUpdate() {
int i = testMapper.updateTestById(1, "oneone");
log.info("testUpdate={}", i);
}
@Test
public void testXml() {
TestDO testDO = testMapper.queryTestByIdXml(1);
log.info("testDO xml={}", testDO);
}
}
1、如果要新在配置一个数据源需要修改那些地方?
a. application.yml 按要求配置上你的数据源;
b. DBConstant 定义数据库名,比如 X;
c. DataSourceTypeEnum 加上你的数据源类型主从命名,X_MASTER,X_SLAVE01,X_SLAVE02 依次类推;
d. DruidDataSourceConfig 将新数据源构建 DataSource,加入到 数据源路由中;
e. Mapper 里面直接指定用就行了;
支持多个库数据源,支持一主多从,自动读写分离,也可强制走主,注意 Mapper XML 配置方式,
需要使用 UseMaster 强制指定走主,一般实际业务系统,单表处理业务多一些,用MyBatis注解方式会
更简洁方便些,像后台管理哪些关联大查询,用 MyBatis XML 好一些。