基于Spring多数据源实现数据库读写分离的最佳实践

757 阅读2分钟

前言

随着业务的发展,系统用户访问量会不断的增加,为了使系统能够承受的住大量用户的并发访问,需要对系统进行优化。优化系统的方法有很多,数据库的读写分离是其中一种。

需求分析

为避免开发过程中不当的使用读写分离,导致程序异常或其他未知错误,需要满足以下要求:

  1. 通过注解方式基于Spring的AOP动态切换读写库
  2. 避免读数据源方法中操作写数据程序异常,如下:
@ReadDataSource
public Object read(Object record){
   updateOne(Object record);
}
  1. 数据源读写切换不能影响事务注解的使用,即开启事务后切换数据源不会引起其他问题,伪代码如下:
@ReadDataSource
public Object read(Object record);

@Transactional
public void test(Object record){
   mapper.read(record);
   
   updateOther(record);
}

代码设计

1. 定义自定义注解

@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface ReadDataSource {

}

2. 定义读写分离上下文标识(线程安全)

public class DataSourceContextHolder {

   // 线程本地环境
   private static final ThreadLocal<String> local = new ThreadLocal<String>();

   /**
    * 读库
    */
   public static void setRead() {
      local.set("read");
   }

   /**
    * 写库
    */
   public static void setWrite() {
      local.set("write");
   }

   public static String getReadOrWrite() {
      return local.get();
   }

   public static void clear() {
      local.remove();
   }
}

3. 配置数据源(读数据源、写数据源、动态路由数据源)

@Bean
@Primary
public DataSource writeDataSource() throws Exception {
   return new DruidDataSource();
}

@Bean
public DataSource readDataSource() throws Exception {
   return new DruidDataSource();
}

@Bean
public SqlSessionFactory sqlSessionFactorys(DataSource writeDataSource,DataSource readDataSource) throws Exception {

        Map<Object, Object> targetDataSources = new HashMap<Object, Object>();
        targetDataSources.put("write", writeDataSource);
        targetDataSources.put("read", readDataSource);
        AbstractRoutingDataSource proxy = new AbstractRoutingDataSource() {
                @Override
                protected Object determineCurrentLookupKey() {
                        String typeKey = DataSourceContextHolder.getReadOrWrite();
                        if (typeKey == null) {
                                return "write";
                        }
                        return typeKey;
                }
        };

        proxy.setDefaultTargetDataSource(writeDataSource);// 默认库
        proxy.setTargetDataSources(targetDataSources);

        SqlSessionFactoryBean sessionFactoryBean = new  SqlSessionFactoryBean();
                sessionFactoryBean.setDataSource(proxy);
        return sessionFactoryBean.getObject();
}

4. 对读写数据源切换做AOP

1)切换读数据源放在mybatis的Mapper层,Mapper层是Interface,方法内无法编写内容。同时在且读数据源之前判断当前线程是否已经开启事务,已经开启事务则放弃切换读数据源,不会破坏当前事务。开启数据读数据源后方法执行结束手动切回写数据源,防止并发情况下当前线程被分配到执行写操作的代码,产生程序异常。

@Around("execution(* com.test.mapper..*.*(..))")
public Object setReadDataSourceType(ProceedingJoinPoint point) throws Throwable {
   ReadDataSource annotation = null;
   try {
      //获取方法注解
      MethodSignature methodSignature = (MethodSignature) point.getSignature();
      annotation = methodSignature.getMethod().getAnnotation(ReadDataSource.class);
      if(annotation != null){
         try {
            //获取当前事务
            TransactionAspectSupport.currentTransactionStatus();
         } catch (NoTransactionException e) {
            //抛异常说明当前没有事务,可以切读库
            DataSourceContextHolder.setRead();
         }
      }
      return point.proceed();
   } finally {
      //当前为读库时,用完需要清除,使用默认写库
      if(annotation != null
            && DataSourceContextHolder.getReadOrWrite() != null
            && DataSourceContextHolder.getReadOrWrite().equals("read")){
         DataSourceContextHolder.clear();
      }
   }
}