mysql读写分离_MySQL读写分离技术探究

1. 读写分离

当访问的用户越来越多的时候,后台的压力会越来越大,应用层往往是无状态的,所以应用层是很容易扩展,请求的压力最终都会落到数据库上,而数据库伸缩性很差,很难通过简单的增加服务器来达到提高数据库性能的目的。读写分离是提高数据库性能的方式之一。读写分离架构原理图如下所示

0b08df79e717d43dd4aac5f47898f054.png

大致的原理是分离数据库的读与写的职责,将一台服务器专门设置为写服务器,而其它数据库服务器专门设置为读服务器,读请求来的时候分配到读服务器,而写请求来的时候就分配到写服务器。读写分离的架构适用于读多写少的架构,当读请求继续增多的时候,只需要简单的增加读服务器就能实现整个应用的读性能的水平扩展。而写服务器和读服务器的数据同步依赖于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状态

1ba71fb29d28d5ec79f00dd20309e5f1.png

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代表利用本地的中继日志是否正常

dc0cd80054ca8a2735e820b56c52b207.png

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会强制走主库。

  1. Sharding-jdbc发现执行语句时DML和DDL语句的时候
  2. 用户指定当前查询走主库,对应上述的hintManager.setMasterRouteOnly()代码
  3. 用户首先执行了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压力也会非常大。如果正好处于用户访问高峰,这个问题会有比较大的安全隐患。解决这个问题的思路是尽量减少与写服务器交互的读服务器数量。可以采用下图这样的方案。

4ac3e01ae69d10aba591d1ce5e394a72.png

只保留一台读服务器和主服务器进行数据同步,其它的读服务器从读服务器1中同步数据。如此一来就能将写入服务器的数据同步压力负载到其它服务器上。

6.总结

读写分离的数据库架构适用于读多写少的应用场景,同时它存在读写服务器数据同步延迟造成的一致性问题,比如用户下了一个订单后,用户可能不能立即查询到该订单信息。读写分离解决的是数据库的读性能问题,现在也有很多的nosql查询性能很高,比如20万qps的redis,它就非常的适合用于读应用场景,我们可以订阅mysql的binlog,将mysql的数据同步到redis,mongodb,elasticsearch这些nosql中,用于海量请求高性能查询,笔者会在其它文章中介绍这些技术。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值