1. 读写分离
当访问的用户越来越多的时候,后台的压力会越来越大,应用层往往是无状态的,所以应用层是很容易扩展,请求的压力最终都会落到数据库上,而数据库伸缩性很差,很难通过简单的增加服务器来达到提高数据库性能的目的。读写分离是提高数据库性能的方式之一。读写分离架构原理图如下所示
大致的原理是分离数据库的读与写的职责,将一台服务器专门设置为写服务器,而其它数据库服务器专门设置为读服务器,读请求来的时候分配到读服务器,而写请求来的时候就分配到写服务器。读写分离的架构适用于读多写少的架构,当读请求继续增多的时候,只需要简单的增加读服务器就能实现整个应用的读性能的水平扩展。而写服务器和读服务器的数据同步依赖于mysql的binlog复制机制。
2. 环境搭建
2.1 写库配置
主库my.ini配置如下,binlog-do-db代表需要同步复制的数据库,而log-bin代表binlog文件的名字前缀
server_id=1
log-bin=master
binlog-do-db=book
binlog-do-db=test
完成修改配置文件后,重启mysql,查看mysql状态
2.2 读库配置
从库my.ini配置如下
server-id=100
read_only=1
因为从库只是提供给上层用户读取,所以必须要设置为只读模式,目的是避免上层用户错误修改了读库,导致写库和从库的数据不一致的问题。
在slave mysql命令行执行如下命令
change master to
master_host='这里填写库的ip',
master_port=3306,
master_user='root',
master_password='这里填用户密码',
master_log_file='master.000001'
,master_log_pos=0;
启动slave
start slave;
启动之后,我们查看一下slave同步状态,红框中的两个变量必须是Yes才能代表同步成功。Slave_IO_Running代表从写库拉取binlog的线程是否运行正常,而Slave_SQL_Running代表利用本地的中继日志是否正常
3. 代码实现读写分离
笔者使用了springboot和sharding-jdbc实现读写分离,数据源配置如下,master库为写库,slave1库为读库
database:
master:
url: jdbc:mysql://这里填写库的ip:3306/test?characterEncoding=utf8&useSSL=false
username: root
password: 这里填用户密码
driverClassName: com.mysql.jdbc.Driver
databaseName: master
slave1:
url: jdbc:mysql://这里填读库的ip:3306/test?characterEncoding=utf8&useSSL=false
username: root
password: 这里填用户密码
driverClassName: com.mysql.jdbc.Driver
databaseName: slave01
代码实现如下,所有的读库必须要放到slaveDataSourceMap,然后利用MasterSlaveDataSourceFactory构建一个读写分离的数据源
private DataSource buildDataSource() throws SQLException {
//设置从库数据源集合
Map<String, DataSource> slaveDataSourceMap = new HashMap<>();
slaveDataSourceMap.put(slave1Config.getDatabaseName(), slave1Config.createDataSource());
//获取数据源对象
return MasterSlaveDataSourceFactory.createDataSource("masterSlave", masterConfig.getDatabaseName()
, masterConfig.createDataSource(), slaveDataSourceMap, MasterSlaveLoadBalanceStrategyType.getDefaultStrategyType());
}
举一个添加商品库存的例子,代码如下所示。
public Product addProduct(Product product) {
DbUtil.useWriteDB();
final Product productTmp = this.productRepository.findByName(product.getName());
if (productTmp == null) {
this.productRepository.save(product);
} else {
product = null;
}
return product;
}
在添加商品的时候我们首先要检查是否存在了相同名字的商品,如果存在则不进行添加。程序在添加商品时候,首先读取数据源查看是否存在相同名字的商品,读取的时候sharding-jdbc会访问读库,但是读库的数据和写库存在同步延迟,读库数据可能不是最新的,所以我们需要显示的指定读取写库数据源,笔者使用了useWriteDB()指定写库数据源。在显示设置写库数据源后,随后访问的数据源都是写数据源
useWriteDB()代码如下所示
public static void useWriteDB(){
HintManager hintManager = HintManager.getInstance();
hintManager.setMasterRouteOnly();
}
查询所有商品时候,sharding-jdbc会自动使用读数据源
public List<Product> listAll() {
return this.productRepository.findAll();
}
数据源路由机制
我们有了写数据源,同时又有了读数据源,当执行用户请求时,如何区分用户的读写请求类型,将读请求分发到读数据源,将写请求分发到写数据源呢?笔者使用了Sharding-Jdbc,它的读写分离数据源路由机制具体原理如下。
Sharding-jdbc将查询类型分为3种:DQL,DML,DDL。DQL表示select查询语句,DML表示写入语句,比如插入、更新、删除,DDL表示对表对象的修改语句,比如create table,drop table,alter table。在执行请求的时候,如果是遇到如下三种情况,Sharding-jdbc会强制走主库。
- Sharding-jdbc发现执行语句时DML和DDL语句的时候
- 用户指定当前查询走主库,对应上述的hintManager.setMasterRouteOnly()代码
- 用户首先执行了DML语句,后续所有的读请求全都走写库
在访问读库的时候,如果读库有多个,那么sharding-jdbc就会使用负载均衡算法从多个读库中选择其中一个执行。
public NamedDataSource getDataSource(final SQLType sqlType) {
//判断是否是DML和DDL语句
//判断先前是否执行了DML语句
//判断用户是否是否进行了写库指定
if (isMasterRoute(sqlType)) {
DML_FLAG.set(true);
return new NamedDataSource(masterDataSourceName, masterDataSource);
}
//下面的代码都是选择一个读数据源
String selectedSourceName = masterSlaveLoadBalanceStrategy.getDataSource(name, masterDataSourceName, new ArrayList<>(slaveDataSources.keySet()));
DataSource selectedSource = selectedSourceName.equals(masterDataSourceName) ? masterDataSource : slaveDataSources.get(selectedSourceName);
Preconditions.checkNotNull(selectedSource, "");
return new NamedDataSource(selectedSourceName, selectedSource);
}
private static boolean isMasterRoute(final SQLType sqlType) {
return SQLType.DQL != sqlType || DML_FLAG.get() || HintManagerHolder.isMasterRouteOnly();
}
4. 读服务器的数据延迟问题
读服务器和写服务器的数据同步依赖于binlog复制。备库和主库发生数据同步延迟的原因比较复杂,笔者会在以后的文章中详细介绍读写服务器是如何同步数据的,以及同步延迟的处理方法。
5. 写服务器的IO瓶颈问题
写服务器既需要处理用户的写入请求,同时还需要处理读服务器的数据同步,如果读服务器比较多,那么写服务器的IO压力就会比较大。或者说某台读服务器宕机,过了较长一段时间后恢复运行,宕机的服务器需要和写服务器同步大量的落后数据,此时写入服务器IO压力也会非常大。如果正好处于用户访问高峰,这个问题会有比较大的安全隐患。解决这个问题的思路是尽量减少与写服务器交互的读服务器数量。可以采用下图这样的方案。
只保留一台读服务器和主服务器进行数据同步,其它的读服务器从读服务器1中同步数据。如此一来就能将写入服务器的数据同步压力负载到其它服务器上。
6.总结
读写分离的数据库架构适用于读多写少的应用场景,同时它存在读写服务器数据同步延迟造成的一致性问题,比如用户下了一个订单后,用户可能不能立即查询到该订单信息。读写分离解决的是数据库的读性能问题,现在也有很多的nosql查询性能很高,比如20万qps的redis,它就非常的适合用于读应用场景,我们可以订阅mysql的binlog,将mysql的数据同步到redis,mongodb,elasticsearch这些nosql中,用于海量请求高性能查询,笔者会在其它文章中介绍这些技术。