目录

MySQL 读写分离

优点

  • 降低数据库读取压力,尤其是有些需要大量计算的实时报表类应用
  • 增强数据安全性,读写分离有个好处就是数据近乎实时备份,一旦某台服务器硬盘发生了损坏,从库的数据可以无限接近主库
  • 可以实现高可用,当然只是配置了读写分离并不能实现搞可用,最多就是在Master(主库)宕机了还能进行查询操作,具体高可用还需要其他操作

缺点

  • 增大成本,一台数据库服务器和多台数据库的成本肯定是不一样的
  • 增大代码复杂度,不过这点还比较轻微吧,但是也的确会一定程度上加重
  • 增大写入成本,虽然降低了读取成本,但是写入成本却是一点也没有降低,毕竟还有从库一直在向主库请求数据

读写分离最基本原则:

  • 对于延迟敏感业务必须在主库读取,或采取主从验证机制,可在从库读取
  • 报表,统计类,查询可以通过从库读取

多个数据源

读写分离如果撇开框架无非就是实现多个数据源,主库用写的数据源,从库用读的数据源。

实现方式:

  • 将读写的DAO分离 或 service 层决定调用主库还是从库
  • Spring 提供 AbstractRoutingDataSource

AbstractRoutingDataSource

Spring内置了一个AbstractRoutingDataSource,它可以把多个数据源配置成一个Map,然后,根据不同的key返回不同的数据源。因为AbstractRoutingDataSource也是一个DataSource接口,因此,应用程序可以先设置好key, 访问数据库的代码就可以从AbstractRoutingDataSource拿到对应的一个真实的数据源,从而访问指定的数据库。

在开发环境下,没有必要配置主从数据库。只需要给数据库设置两个用户,一个rw具有读写权限,一个ro只有SELECT权限,这样就模拟了生产环境下对主从数据库的读写分离。

使用AbstractRoutingDataSource+aop+annotation

在dao层决定数据源

 缺点:不支持事务。因为事务在service层开启时,就必须拿到数据源了。

service层决定数据源,可以支持事务.

缺点:类内部方法通过this.xx()方式相互调用时,aop不会进行拦截,需进行特殊处理。

多数据源的配置application.yml

配置数据源

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
@Configuration
@Slf4j
public class DataSourceConfiguration {
/**
     * Master data source.
     */
    @Bean("masterDataSource")
    @ConfigurationProperties(prefix = "mysql.datasource.master")
    DataSource masterDataSource() {
       	log.info("create master datasource...");
        return DataSourceBuilder.create().build();
    }

    /**
     * Slave (read only) data source.
     */
    @Bean("slaveDataSource1")
    @ConfigurationProperties(prefix = "mysql.datasource.slave1")
    DataSource slaveDataSource1() {
        log.info("create slave datasource...");
        return DataSourceBuilder.create().build();
    }
    
    
}

实现 AbstractRoutingDataSource

1
2
3
4
5
6
7
8
public class RoutingDataSource extends AbstractRoutingDataSource {
    // 返回的是生效的数据源名称
	@Override
    protected Object determineCurrentLookupKey() {
        // 读库在此可以做简单的负载均衡
        return DataSourceContextHolder.get();
	}
}

通过 ThreadLocal<DataSourceType> contextHolder 切换数据源

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public class DataSourceContextHolder {
    public enum DataSourceType {
    	MASTER,
    	SLAVE
  }
    private static final ThreadLocal<DataSourceType> contextHolder = new ThreadLocal<DataSourceType>();

    /**
     * 读可能是多个库
     */
    public static void read() {
        contextHolder.set(DataSourceType.SLAVE);
    }

    /**
     * 写只有一个库
     */
    public static void write() {
        contextHolder.set(DataSourceType.MASTER);
    }

    public static DataSourceType get() {
        return contextHolder.get();
    }
    public static void clear() {
    	contextHolder.remove();
  	}
}

实例化 AbstractRoutingDataSource 的实现类

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
@Configuration
@AutoConfigureAfter(DataSourceConfiguration.class)
@EnableTransactionManagement(order = 10)
@Slf4j
public class DatasourceAgentConfig {
	@Autowired
    @Qualifier("writeDataSource")
    private DataSource writeDataSource;
    @Autowired
    @Qualifier("readDataSource01")
    private DataSource readDataSource01;
//    @Autowired
//    @Qualifier("readDataSource02")
//    private DataSource readDataSource02;

    
    @Bean
    public RoutingDataSource routingDataSource() {
        Map<Object, Object> targetDataSources = new HashMap<Object, Object>();
        targetDataSources.put(DataSourceContextHolder.DatabaseType.Master, writeDataSource);
        targetDataSources.put(DataSourceContextHolder.DatabaseType.Slave, readDataSource01);

        RoutingDataSource dataSource = new DynamRoutingDataSourcecDataSource();
        dataSource.setTargetDataSources(targetDataSources);
        dataSource.setDefaultTargetDataSource(writeDataSource);
        return dataSource;
    }

读写数据源的注解

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface ReadOnlyConnection {
}

@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface DataSource {
    DataSourceType value();
}


// 利用AspectJ实现一个Around拦截
@Aspect
@Component
public class ReadOnlyConnectionInterceptor implements Ordered {
 
  private static final Logger logger = LoggerFactory.getLogger(ReadOnlyConnectionInterceptor.class);
 
  @Around("@annotation(readOnlyConnection)")
  public Object proceed(ProceedingJoinPoint proceedingJoinPoint, ReadOnlyConnection readOnlyConnection) throws Throwable {
    try {
      logger.info("set database connection to read only");
      DbContextHolder.read();
      Object result = proceedingJoinPoint.proceed();
      return result;
    } finally {
      DbContextHolder.clearDbType();     
    }
  }
 
  @Override
  public int getOrder() {
    return 0;
  }
}

// sevice 调用
@ReadOnlyConnection
public List<User> getUsers(Integer page, Integer limit) {
  return repository.findAll(new PageRequest(page, limit));
}

使用限制

如果是service, 必须实现Ordered,并且优先级优于事务的开启。

受Servlet线程模型的局限,动态数据源不能在一个请求内设定后再修改,也就是@RoutingWith不能嵌套。此外,@RoutingWith@Transactional混用时,要设定AOP的优先级。

本文代码需要SpringBoot支持,JDK 1.8编译并打开-parameters编译参数。

中间件

MySQL Router

MySQL官方推荐的读写分离中间件,MySQL Router是MySQL Proxy的替代方案,

  • 读写分离
  • 无法动态更改配置。
  • MGR中充当代理

Apache ShardingSphere

  • ShardingSphere-JDBC 轻量级Java架构,在Java的JDBC层提供的额外服务,就是jdbc驱动二次包装jar,实现读写分离,复杂的分库分表逻辑 ,性能损耗7%
  • ShardingSphere-Proxy 类似与MyCAT,中间件软件实现读写分离,复杂分库分表。性能损耗20%

总结

从MySQL读写分离理解,由于能分担主库的压力,很多情况会考虑读写分离,但是在使用时,就应该考虑到延迟是否敏感。存在延迟则把读请求放到主库,没延迟就读从库。

中间件方面:目前社区活跃度,关注度来说,Shardingjdbc发展趋势非常好。轻量级方面MySQL Router目前社区活跃度也非常好。其他中间件可以学习他们的思想和技术。