数据库读写分离架构(主从架构)

实现原理

image.png

  1. 数据库服务器搭建主从集群,一主一从,或一主多从
  2. 数据库主机负责写操作,从机负责读操作
  3. 数据库主机通过复制将数据同步到从机,每台数据库服务器都存储了所有的业务数据
  4. 业务服务器将读写操作发给数据库主机,将读操作发给数据库从机

如何判断应该使用主从架构而不是主备架构?

  1. 业务量持续增加
    绝大部分系统,业务量持续增加的时候,读请求的增加速度肯定是超过写请求的,此时就需要从原来单台的数据库服务器的架构或者是主备的架构演进到主从的架构
  2. 先优化,再重构
    并不是一增加业务量就直接上主从架构,可以先优化已有的系统,例如优化索引、加缓存、历史数据从当前的数据库剥离到一个独立的库来降低数据库的压力。当这些方法都不行的时候,可以考虑上主从架构,因为主从架构尽管带来了读性能上面的提升,但是也会带来一些新的复杂度。

复杂度分析

image.png

  1. 复制延迟
    从主机到从机,复制是有延迟的,几百毫秒到几分钟(数据库压力大或者网络不好)都有可能。
    最典型的影响:业务服务器在主机上写了新数据,如果读的是从机而不是主机就会导致读的数据仍然是旧数据。
  2. 任务分解
    业务服务器需要对任务进行分解,需要判断哪些是读操作,哪些是写操作,写操作发给主机,读操作发给从机。

复杂度应对方法

image.png

  1. 复制延迟的应对方法

    • 读写绑定
      写操作后的读操作指定发到主机上。
      用代码判断此处写入数据之后立刻到主机去读。
      缺点:代码写死了,业务侵入很大,容易留坑
    • 二次读取
      读从机失败后再读一次主机
      insert数据后发现从机没读取到,就从主机中进行二次读取。
      缺点:而过有很多二次读取,会增加主机的读操作的压力,会导致从机没有起到降低主机压力的作用。
      大批量插入数据,或黑客攻击产生垃圾数据的时候会导致从机读取失败。
    • 业务分级
      关键业务读写操作全部指向主机,非关键业务采取读写分离。
      关键业务:读写操作对数据的实时性要求比较高
      缺点:编码人员容易全部采取读写主机。

    实际落地时采用业务分级的方案比较多
    对代码编写的要求不高
    缺点就是容易偷懒,读写全部使用主机,需要代码评审时来发现。
    是一个综合效果比较好的落地方案

  2. 任务分解的应对方法
    怎样去做任务分解,怎么样识别读操作和写操作?

    • 程序代码封装模式image.png

      Sharding-JDBC的实现:在我们的应用程序中嵌入一个SDK,SDK自己实现了数据库读写操作的一个识别和分发。
      实现简单,基于JDBC封装
      维护简单,无需部署,无需考虑高性能高可用。
      缺点:每个语言都需要实现一遍。

    • 中间件封装模式image.png

      Sharding-Proxy:是一个代理,将自己模拟成一个数据库服务器。由它来完成sql解析,数据聚合,以及连接到后面的数据库服务器这些操作。
      实现复杂,独立服务器,需要实现连接管理。
      维护复杂,需要独立部署,并且要考虑集群部署支持高性能高可用。
      优点:跨语言

      读写绑定、业务分级和Sharding-JDBC相关示例代码,参考读写分离实现
      MySQL主从实现方法参考MySQL主从分离实现

数据库分库分表架构

为什么要使用分库分表架构?主从架构模式中主机接收所有的写请求,它的写性能是有瓶颈的。另外一个方面,每个节点都需要存储所有的数据,单个节点的存储容量也是有瓶颈的。为了应对主从架构中的性能瓶颈和存储瓶颈,就需要考虑数据库分库分表。

基本原理

分库分表核心就是叠加法则(加机器)

数据库分库

image.png

复杂度分析

Join问题

原本在同一个数据库的表分散到不同的数据库中,导致无法使用SQL的join查询。
解法

  1. 小表冗余
    将一些小表在每个库中冗余一份,例如字典表。
  2. 代码Join
    在代码里面实现Join功能
  3. 字段冗余
    例如:订单表中直接记录商品类型,一旦生成订单,商品类型就不会再变了。但是有些字段会不断变化的话,就不适合用字段冗余了。

事务问题

表分散到不同的数据库中,无法通过事务统一修改。

解法

  • 分布式事务
    案例
    先扣商品库存,扣成功后生成订单,如果因为订单数据库异常导致生成订单失败,业务程序又需要将商品库存加上。而如果因为业务程序自己异常导致生成订单失败,则商品库存就无法自动恢复,需要人工通过日志等方式来手工修复库存异常。

数据库分表

垂直拆分和水平拆分都是针对单个表来说的

image.png

垂直拆分

按列拆分,优化单机处理性能,常见于ToB领域超多列的表拆分。

水平拆分

image.png

按行拆分,提升系统处理性能,常见于ToC领域超多行的表拆分。

例如:将一个1000万的大表拆分成10个100万的表,然后将这样10个表分布到不同的数据库服务器来提升数据库的读写性能。
多大的表需要拆分(Mysql)?

  1. B+Tree的层数为3层,大概2000万条数据的时候。
  2. Innodb buffer pool:2000万条数据,每条数据100字节,单表就2G了;一旦数据库的存储超过Innodb buffer pool的配置后,Mysql的性能会急剧下降。
  3. 数据量持续增长的表;

水平分表复杂度

  1. 路由问题
    路由算法进行计算数据归属
  2. Count计算
    如何计算Count
  3. Join操作
    如何执行与其他表Join
  4. Order By
    如何执行Order By

核心问题:原来一张表的数据分散到多张表上了,你要如何去做?

应对方法

一般不建议自己写代码去解决这些问题。建议直接用第三方的中间件比如说Sharding-JDBC。

水平分表伸缩瓶颈

能否通过无限数据库水平拆分不断提高系统的性能?

image.png

  1. 每个应用都要连接所有的分片,当应用数量增多后,数据库连接会逐渐成为瓶颈。以Mysql为例,默认100连接,实测50~100连接性能最高,超过200后悔显著下降。
  2. 单个Sharding-JDBC的聚合操作会有性能瓶颈

数据库分布式事务

分库分表都会带来分布式事务的问题,如何解决?

2PC

image.png

简单来说分为2个阶段。第一个Prepare阶段:事务协调者向所有的参与者(数据库)发起一个Prepare的消息说大家都准备好了吗。当所有的库都回复说准备好了之后进行第二个阶段Commit阶段:全部参与者都来提交事务。

  • 优点
    实现简单
  • 缺点
    阶段1:单个参与者故障会导致整体事务失败。
    阶段2:事务协调者故障会导致整体状态不一致,参与者一直等待事务协调者指令,可能需要人工修复。

3PC

为了解决2PC的问题,提出了3PC的算法

image.png

区别在于3PC会有一个超时机制,当参与者收到preCommit后,一段时候后仍然没有收到doCommit也会执行commit动作。

  1. Yes丢了会导致协调者取消事务。
  2. 部分preCommit丢失会导致部分参与者超时取消事务,而其他参与者收到了preCommit消息,然后超时提交事务,因此造成脑裂。
  3. Ack丢了会导致参与者超时提交事务。
  4. doCommit丢了会导致参与者超时提交事务。

2PC和3PC哪个更好,应用更广?

在实际应用中2PC算法反而应用更广,3PC过于复杂,且可能导致脑裂,也就是参与者事务提交不一致的情况,这种情况处理起来更麻烦。

数据库的分布式事务,例如Mysql XA

package study.xa;

import com.mysql.jdbc.jdbc2.optional.MysqlXAConnection;
import com.mysql.jdbc.jdbc2.optional.MysqlXid;

import javax.sql.XAConnection;
import javax.transaction.xa.XAException;
import javax.transaction.xa.XAResource;
import javax.transaction.xa.Xid;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.SQLException;

/***
 * @Description mysql分布式事务XAConnection模拟
 * @author denny
 * @date 2019/4/3 上午9:15
 */
public class MysqlXaConnectionTest {

    public static void main(String[] args) throws SQLException {
        //true表示打印XA语句,,用于调试
        boolean logXaCommands = true;
        // 获得资源管理器操作接口实例 RM1
        Connection conn1 = DriverManager.getConnection("jdbc:mysql://localhost:3306/test", "root", "12345");
        XAConnection xaConn1 = new MysqlXAConnection((com.mysql.jdbc.Connection)conn1, logXaCommands);
        XAResource rm1 = xaConn1.getXAResource();

        // 获得资源管理器操作接口实例 RM2
        Connection conn2 = DriverManager.getConnection("jdbc:mysql://localhost:3306/test2", "root", "12345");
        XAConnection xaConn2 = new MysqlXAConnection((com.mysql.jdbc.Connection)conn2, logXaCommands);
        XAResource rm2 = xaConn2.getXAResource();
        // AP请求TM执行一个分布式事务,TM生成全局事务id
        byte[] gtrid = "g12345".getBytes();
        int formatId = 1;
        try {
            // ==============分别执行RM1和RM2上的事务分支====================
            // TM生成rm1上的事务分支id
            byte[] bqual1 = "b00001".getBytes();
            Xid xid1 = new MysqlXid(gtrid, bqual1, formatId);
            // 执行rm1上的事务分支 One of TMNOFLAGS, TMJOIN, or TMRESUME.
            rm1.start(xid1, XAResource.TMNOFLAGS);
            // 业务1:插入user表
            PreparedStatement ps1 = conn1.prepareStatement("INSERT into user VALUES ('99', 'user99')");
            ps1.execute();
            rm1.end(xid1, XAResource.TMSUCCESS);

            // TM生成rm2上的事务分支id
            byte[] bqual2 = "b00002".getBytes();
            Xid xid2 = new MysqlXid(gtrid, bqual2, formatId);
            // 执行rm2上的事务分支
            rm2.start(xid2, XAResource.TMNOFLAGS);
            // 业务2:插入user_msg表
            PreparedStatement ps2 = conn2.prepareStatement("INSERT into user_msg VALUES ('88', '99', 'user99的备注')");
            ps2.execute();
            rm2.end(xid2, XAResource.TMSUCCESS);

            // ===================两阶段提交================================
            // phase1:询问所有的RM 准备提交事务分支
            int rm1Prepare = rm1.prepare(xid1);
            int rm2Prepare = rm2.prepare(xid2);
            // phase2:提交所有事务分支
            boolean onePhase = false;
            //TM判断有2个事务分支,所以不能优化为一阶段提交
            if (rm1Prepare == XAResource.XA_OK
                && rm2Prepare == XAResource.XA_OK
                ) {
                //所有事务分支都prepare成功,提交所有事务分支
                rm1.commit(xid1, onePhase);
                rm2.commit(xid2, onePhase);
            } else {
                //如果有事务分支没有成功,则回滚
                rm1.rollback(xid1);
                rm1.rollback(xid2);
            }
        } catch (XAException e) {
            // 如果出现异常,也要进行回滚
            e.printStackTrace();
        }
    }
}
  • 外部XA
    跨多MySql实例的分布式事务,由应用程序代码作为2PC的事务协调者。
  • 内部XA
    跨同一实例多存储引擎的事务,由Binlog作为2PC的事务协调者

思维导图

image.png

关联文章:
https://www.jianshu.com/p/6f5bcbb0d0f7
https://zhuanlan.zhihu.com/p/113165245
https://www.jianshu.com/p/549d88222528


标题:系统架构之数据库存储架构
作者:xingzhegu
地址:https://www.fxg.life/articles/2021/12/20/1640008094791.html