优点
- 降低数据库读取压力,尤其是有些需要大量计算的实时报表类应用
- 增强数据安全性,读写分离有个好处就是数据近乎实时备份,一旦某台服务器硬盘发生了损坏,从库的数据可以无限接近主库
- 可以实现高可用,当然只是配置了读写分离并不能实现搞可用,最多就是在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的替代方案,
Apache ShardingSphere
- ShardingSphere-JDBC 轻量级Java架构,在Java的JDBC层提供的额外服务,就是jdbc驱动二次包装jar,实现读写分离,复杂的分库分表逻辑 ,性能损耗7%
- ShardingSphere-Proxy 类似与MyCAT,中间件软件实现读写分离,复杂分库分表。性能损耗20%
总结
从MySQL读写分离理解,由于能分担主库的压力,很多情况会考虑读写分离,但是在使用时,就应该考虑到延迟是否敏感。存在延迟则把读请求放到主库,没延迟就读从库。
中间件方面:目前社区活跃度,关注度来说,Shardingjdbc发展趋势非常好。轻量级方面MySQL Router目前社区活跃度也非常好。其他中间件可以学习他们的思想和技术。