SpringBoot 集成 MP、DS实现读写分离 事务分析

SpringBoot 集成 Mybatis-Plus、Dynamic-DataSource实现读写分离 事务分析

github :https://github.com/lanchengx/dynamic


MyBatis-Plus(简称 MP)是一个 MyBatis 的增强工具,在 MyBatis 的基础上只做增强不做改变,为简化开发、提高效率而生

SpringBoot 集成 MP、DS实现读写分离 事务分析_第1张图片



dynamic-datasource-spring-boot-starter(简称 DS) 是一个基于springboot的快速集成多数据源的启动器。

SpringBoot 集成 MP、DS实现读写分离 事务分析_第2张图片


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()所使用的数据源。

原因分析:
SpringBoot 集成 MP、DS实现读写分离 事务分析_第3张图片

在org.springframework.jdbc.datasource.DataSourceTransactionManager#doBegin 这个类的源代码中

在开始一个事务前,如果当前上下文的连接对象为空,获取一个连接对象,然后保存起来,下次doBegin再调用时,就直接用这个连接了,根本不做任何切换(类似于缓存命中!)
doSomeThing()方法被调用前,加了一段select方法,相当于已经切换到了slave从库,然后再进入doBegin方法时,就直接拿这个从库的链接了,不再进行切换。

解决办法:

  1. 在doSomeThing() 之前手动切换数据库

    @Transactional
    public void method1(){
        service.getById(...);
        ...
        DBContext.setDBKey("master");//先切换到主库
        doSomeThing();
    }
    
  2. 在自定义切面中增加对@Transactional注解的判断,提前使用master库

        if (methdo.isAnnotationPresent(Transactional.class)) {
            log.info("当前执行的库:" + MASTER);
            DynamicDataSourceContextHolder.push(MASTER);
            return;
        }
    
  3. 在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

个人学习笔记,如有错漏之处欢迎指正!!!

你可能感兴趣的:(Mybatis,相关)