github :https://github.com/lanchengx/dynamic
MyBatis-Plus(简称 MP)是一个 MyBatis 的增强工具,在 MyBatis 的基础上只做增强不做改变,为简化开发、提高效率而生
dynamic-datasource-spring-boot-starter(简称 DS) 是一个基于springboot的快速集成多数据源的启动器。
1. Myabtis-Plus 配置
@Configuration
public class MybatisPlusConifg {
@Bean
public GlobalConfig globalConfig() {
GlobalConfig globalConfig = new GlobalConfig();
GlobalConfig.DbConfig dbConfig = new GlobalConfig.DbConfig();
dbConfig.setIdType(IdType.AUTO);
dbConfig.setSelectStrategy(FieldStrategy.NOT_EMPTY);
globalConfig.setDbConfig(dbConfig);
return globalConfig;
}
@Bean
public PaginationInterceptor masterPaginationInterceptor() {
PaginationInterceptor paginationInterceptor = new PaginationInterceptor();
// 设置请求的页面大于最大页后操作, true调回到首页,false 继续请求 默认false
// paginationInterceptor.setOverflow(false);
// 设置最大单页限制数量,默认 500 条,-1 不受限制
paginationInterceptor.setLimit(3);
// 开启 count 的 join 优化,只针对部分 left join
paginationInterceptor.setCountSqlParser(new JsqlParserCountOptimize(true));
List<ISqlParser> sqlParserList = new ArrayList<>();
// 攻击 SQL 阻断解析器、加入解析链
sqlParserList.add(getBlockAttackSqlParser());
paginationInterceptor.setSqlParserList(sqlParserList);
return paginationInterceptor;
}
private BlockAttackSqlParser getBlockAttackSqlParser(){
return new BlockAttackSqlParser() {
@Override
public void processDelete(Delete delete) {
// 如果你想自定义做点什么,可以重写父类方法像这样子
// if ("user".equals(delete.getTable().getName())) {
// 自定义跳过某个表,其他关联表可以调用 delete.getTables() 判断
return ;
}
super.processDelete(delete);
}
@Override
public void processUpdate(Update update) {
super.processUpdate(update);
}
};
}
}
2. dynamic-datasource-spring-boot-starter 配置
依赖
<dependency>
<groupId>com.baomidougroupId>
<artifactId>dynamic-datasource-spring-boot-starterartifactId>
<version>${version}version>
dependency>
配置文件
spring:
datasource:
dynamic:
primary: master #设置默认的数据源或者数据源组,默认值即为master
strict: false #设置严格模式,默认false不启动. 启动后在未匹配到指定数据源时候回抛出异常,不启动会使用默认数据源.
datasource:
master:
url: jdbc:mysql://xx.xx.xx.xx:3306/dynamic
username: root
password: 123456
driver-class-name: com.mysql.jdbc.Driver
slave_1:
url: jdbc:mysql://xx.xx.xx.xx:3307/dynamic
username: root
password: 123456
driver-class-name: com.mysql.jdbc.Driver
slave_2:
url: ENC(xxxxx) # 内置加密,使用请查看详细文档
username: ENC(xxxxx)
password: ENC(xxxxx)
driver-class-name: com.mysql.jdbc.Driver
schema: db/schema.sql # 配置则生效,自动初始化表结构
data: db/data.sql # 配置则生效,自动初始化数据
continue-on-error: true # 默认true,初始化失败是否继续
separator: ";" # sql默认分号分隔符
#......省略
#以上会配置一个默认库master,一个组slave下有两个子库slave_1,slave_2
使用 @DS 切换数据源。
@DS 可以注解在方法上和类上,同时存在方法注解优先于类上注解。
强烈建议只注解在service实现上。
注解 | 结果 |
---|---|
没有@DS | 默认数据源 |
@DS(“dsName”) | dsName可以为组名也可以为具体某个库的名称 |
实例
@Service
@DS("slave")
public class UserServiceImpl implements UserService {
@Autowired
private JdbcTemplate jdbcTemplate;
public List<Map<String, Object>> selectAll() {
return jdbcTemplate.queryForList("select * from user");
}
@Override
@DS("slave_1")
public List<Map<String, Object>> selectByCondition() {
return jdbcTemplate.queryForList("select * from user where age >10");
}
}
增加一个aop配置,通过方法名/类名 设置不同的数据源
/**
* @Author: lancx
* @Date: 2020/5/10 0010
*/
@Aspect
@Component
@Order(0)
@Lazy(false)
@Log
public class DataSourceAop {
private static final String MASTER = "master";
private static final String SLAVE = "slave";
@Pointcut(" execution(* com.example.dynamic.service..*.*(..)) " +
"|| execution(* com.baomidou.mybatisplus.extension.service..*.*(..)) " +
"|| @annotation(org.springframework.transaction.annotation.Transactional)")
public void pushDataSource() {
}
// 这里切到你的方法目录
@Before("pushDataSource()")
public void process(JoinPoint joinPoint) throws NoSuchMethodException, SecurityException {
String methodName = joinPoint.getSignature().getName();
Class clazz = joinPoint.getTarget().getClass();
if (clazz.isAnnotationPresent(DS.class)) {
//获取类上注解
return;
}
String targetName = clazz.getSimpleName();
Class[] parameterTypes =
((MethodSignature) joinPoint.getSignature()).getMethod().getParameterTypes();
Method methdo = clazz.getMethod(methodName, parameterTypes);
if (methdo.isAnnotationPresent(DS.class)) {
return;
}
if (methodName.startsWith("get")
|| methodName.startsWith("count")
|| methodName.startsWith("find")
|| methodName.startsWith("list")
|| methodName.startsWith("select")
|| methodName.startsWith("check")
|| methodName.startsWith("page")) {
log.info("当前执行的库:" + SLAVE);
DynamicDataSourceContextHolder.push(SLAVE);
} else {
log.info("当前执行的库:" + MASTER);
DynamicDataSourceContextHolder.push(MASTER);
}
}
@After("pushDataSource()")
public void afterAdvice() {
DynamicDataSourceContextHolder.clear();
}
}
部分源码分析
原理的是一个拦截器
DynamicDataSourceAnnotationInterceptor.class
@Override
public Object invoke(MethodInvocation invocation) throws Throwable {
try {
//这里把获取到的数据源标识如master存入本地线程
DynamicDataSourceContextHolder.push(determineDatasource(invocation));
return invocation.proceed();
} finally {
DynamicDataSourceContextHolder.poll();
}
private String determineDatasource(MethodInvocation invocation) throws Throwable {
//获得DS注解的方法
Method method = invocation.getMethod();
//获得方法上的DS注解。
Class<?> declaringClass = dynamicDataSourceClassResolver.targetClass(invocation);
DS ds = method.isAnnotationPresent(DS.class) ? method.getAnnotation(DS.class)
: AnnotationUtils.findAnnotation(declaringClass, DS.class);
//获得DS注解的内容
String key = ds.value();
//如果DS注解内容是以#开头解析动态最终值否则直接返回
return (!key.isEmpty() && key.startsWith(DYNAMIC_PREFIX)) ? dsProcessor.determineDatasource(invocation, key) : key;
}
DynamicRoutingDataSource.class
@Override
public DataSource determineDataSource() {
//从本地线程获取key解析最终真实的数据源
return getDataSource(DynamicDataSourceContextHolder.peek());
}
/**
* 获取数据源
*
* @param ds 数据源名称
* @return 数据源
*/
public DataSource getDataSource(String ds) {
if (StringUtils.isEmpty(ds)) {
return determinePrimaryDataSource();
} else if (!groupDataSources.isEmpty() && groupDataSources.containsKey(ds)) {
log.debug("dynamic-datasource switch to the datasource named [{}]", ds);
return groupDataSources.get(ds).determineDataSource();
} else if (dataSourceMap.containsKey(ds)) {
log.debug("dynamic-datasource switch to the datasource named [{}]", ds);
return dataSourceMap.get(ds);
}
if (strict) {
throw new RuntimeException("dynamic-datasource could not find a datasource named" + ds);
}
return determinePrimaryDataSource();
}
DynamicDataSourceContextHolder.class
public final class DynamicDataSourceContextHolder {
/**
* 为什么要用链表存储(准确的是栈)
*
* 为了支持嵌套切换,如ABC三个service都是不同的数据源
* 其中A的某个业务要调B的方法,B的方法需要调用C的方法。一级一级调用切换,形成了链。
* 传统的只设置当前线程的方式不能满足此业务需求,必须模拟栈,后进先出。
*
*/
@SuppressWarnings("unchecked")
private static final ThreadLocal<Deque<String>> LOOKUP_KEY_HOLDER = new ThreadLocal() {
@Override
protected Object initialValue() {
return new ArrayDeque();
}
};
private DynamicDataSourceContextHolder() {
}
/**
* 获得当前线程数据源
*
* @return 数据源名称
*/
public static String peek() {
return LOOKUP_KEY_HOLDER.get().peek();
}
/**
* 设置当前线程数据源
*
* 如非必要不要手动调用,调用后确保最终清除
*
*
* @param ds 数据源名称
*/
public static void push(String ds) {
LOOKUP_KEY_HOLDER.get().push(StringUtils.isEmpty(ds) ? "" : ds);
}
/**
* 清空当前线程数据源
*
* 如果当前线程是连续切换数据源 只会移除掉当前线程的数据源名称
*
*/
public static void poll() {
Deque<String> deque = LOOKUP_KEY_HOLDER.get();
deque.poll();
if (deque.isEmpty()) {
LOOKUP_KEY_HOLDER.remove();
}
}
/**
* 强制清空本地线程
*
* 防止内存泄漏,如手动调用了push可调用此方法确保清除
*
*/
public static void clear() {
LOOKUP_KEY_HOLDER.remove();
}
事务问题:
@Transactional
public void method1(){
service.getById(...);
...
doSomeThing();
}
@DS("master")
@Transactional
public void doSomeThing(){
service.save()
service.update()
}
在一个事务内先执行查询getById()方法(使用Slave数据源),后执行doSomeThing()方法,在执行doSomeThing()方法时会使用getById()所使用的数据源。
在org.springframework.jdbc.datasource.DataSourceTransactionManager#doBegin 这个类的源代码中
在开始一个事务前,如果当前上下文的连接对象为空,获取一个连接对象,然后保存起来,下次doBegin再调用时,就直接用这个连接了,根本不做任何切换(类似于缓存命中!)
doSomeThing()方法被调用前,加了一段select方法,相当于已经切换到了slave从库,然后再进入doBegin方法时,就直接拿这个从库的链接了,不再进行切换。
解决办法:
在doSomeThing() 之前手动切换数据库
@Transactional
public void method1(){
service.getById(...);
...
DBContext.setDBKey("master");//先切换到主库
doSomeThing();
}
在自定义切面中增加对@Transactional注解的判断,提前使用master库
if (methdo.isAnnotationPresent(Transactional.class)) {
log.info("当前执行的库:" + MASTER);
DynamicDataSourceContextHolder.push(MASTER);
return;
}
在doSomeThing()上使用@Transactional(propagation = Propagation.REQUIRES_NEW)
@DS("master")
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void doSomeThing(){
service.save()
service.update()
}
自定义切面时间测试
dynamic-datasource 强烈建议只注解在service实现上 , 故测试自定义aop在切换数据源时所消耗的时间。
测试过程:100 get操作的基础上使用不同的切换数据源方式,比较aop切换数据源的耗时情况
- | 使用默认数据源 | 使用DS注解 | 使用Aop |
---|---|---|---|
avg time(毫秒) | 8117.66 | 8108.3 | 8266.16 |