[精选] MySQL主从同步延迟问题,数据一致性问题,你如何去解决
学习与交流:
数据库为什么要读写分离
1 现在很多大型互联网业务,往往读多写少,那么数据库的读会首先成为数据库的瓶颈,我们希望提升数据库的读性能。
消除读写锁冲突从而提升数据库的写性能,那么读写分离架构。主从只负责各自的写和读,极大程度的缓解X锁和S锁争用。
解释:排它锁(X锁)和共享锁(S锁)。
X锁,是事务T对数据A加上X锁时,只允许事务T读取和修改数据A。
S锁,是事务T对数据A加上S锁时,其他事务只能再对数据A加S锁,而不能加X锁,直到T释放A上的S锁。
若事务T对数据对象A加了S锁,则T就可以对A进行读取,但不能进行更新(S锁因此又称为读锁),在T释放A上的S锁以前,其他事务可以再对A加S锁,但不能加X锁,从而可以读取A,但不能更新A。
2 假如我们的从库是多台的,可以分摊读取。假如原来每分钟150条读数据,分摊给3台服务器,每台服务器也就处理50条,同时增加冗余,提高可用性,当一台数据库服务器宕机后能通过调整另外一台从库来以最快的速度恢复服务,保证业务正常运行。
怎么做到主从复制
master配置:
配置my.cnf文件
#在[mysqld]中添加:
server-id=1 #server-id 服务器唯一标识
log_bin=master-bin #启动MySQL二进制日志,即数据同步语句。
log_bin_index=master-bin.index
binlog_do_db=hxg #指定记录二进制日志的数据库,即需要复制的数据库名
创建从服务器的用户和权限
mysql> grant replication slave on *.* to masterbackup@'你的从主机地址' identified by '从库连接密码';
#如果有多个用正则匹配 地址。
配置完后,重启一下。可以用show master status查看一下。
slave配置
#在[mysqld]中添加:
server-id=2 #server-id 服务器唯一标识,如果有多个从服务器,每个服务器的server-id不能重复,跟IP一样是唯一标识
relay-log=slave-relay-bin #relay-log 启动MySQL二进制日志,可以用来做数据备份和崩溃恢复,或主服务器挂掉了,将此从服务器作为其他从服务器的主服务器。
relay-log-index=slave-relay-bin.index
#replicate-do-db=hxg #replicate-do-db 指定同步的数据库,如果复制多个数据库,重复设置这个选项即可。若在master端不指定binlog-do-db,则在slave端可用replication-do-db来过滤。
同样需要重新启动。
连接master主服务器
mysql> change master to master_host='主机IP',
master_port=3306,master_user='masterbackup',
master_password='连接密码',master_log_file='File',
master_log_pos=Position;
#master_log_file、master_log_pos分别对应show master status显示的File列和Position列。
怎么读写分离呢
基于中间代理层实现:代理一般位于客户端和服务器之间,代理服务器接到客户段的请求通过判断后转发到后端数据库。
使用Mysql-proxy实现mysql的读写分离,Mysql-proxy实际上是作为后端mysql主从服务器的代理,它直接接受客户端的请求,对SQL语句进行分析,判断出是读操作还是写操作,然后分发至对应的Mysql服务器上。
Mysql-proxy是官方提供的Mysql中间件产品可以实现负载平衡,读写分离,failover等
MySQL Proxy就是这么一个中间层代理,简单的说,MySQL Proxy就是一个连接池,负责将前台应用的连接请求转发给后台的数据库,并且通过使用lua脚本,可以实现复杂的连接控制和过滤
从而实现读写分离和负载平衡。对于应用来说,MySQL Proxy是完全透明的,应用则只需要连接到MySQL Proxy的监听端口即可。
当然,这样Proxy机器可能成为单点失效,但完全可以使用多个proxy机器做为冗余,在应用服务器的连接池配置中配置到多 个proxy的连接参数即可。
从上图很清晰的看到,主库提供写,多个从库提供读。
主从同步的详细步骤
mysql主从复制需要三个线程,master(binlog dump thread)、slave(I/O thread 、SQL thread)。
master:
binlog dump线程:当主库中有数据更新时,那么主库就会根据按照设置的binlog格式,将此次更新的事件类型写入到主库的binlog文件中。
此时主库会创建log dump线程通知slave有数据更新,当I/O线程请求日志内容时,会将此时的binlog名称和当前更新的位置同时传给slave的I/O线程。
slave:
1)I/O线程:该线程会连接到master,向log dump线程请求一份指定binlog文件位置的副本,并将请求回来的binlog存到本地的relay log(中继日志)中,relay log和binlog日志一样也是记录了数据更新的事件,它也是按照递增后缀名的方式,产生多个relay log( host_name-relay-bin.000001)文件,slave会使用一个index文件( host_name-relay-bin.index)来追踪当前正在使用的relay log文件。
2)SQL线程:该线程检测到relay log有更新后,会读取并在本地做redo操作,将发生在主库的事件在本地重新执行一遍,来保证主从数据同步。此外,如果一个relay log文件中的全部事件都执行完毕,那么SQL线程会自动将该relay log 文件删除掉
同步方式
异步复制
1 MySQL默认的复制即是异步的,主库在执行完客户端提交的事务后会立即将结果返给给客户端,并不关心从库是否已经接收并处理,这样就会有一个问题,主如果crash掉了,此时主上已经提交的事务可能并没有传到从库上,如果此时,强行将从提升为主,可能导致新主上的数据不完整。
2 主库将事务 Binlog 事件写入到 Binlog 文件中,此时主库只会通知一下 Dump 线程发送这些新的 Binlog,然后主库就会继续处理提交操作,而此时不会保证这些 Binlog 传到任何一个从库节点上。
全局同步复制
1 指当主库执行完一个事务,所有的从库都执行了该事务才返回给客户端。因为需要等待所有从库执行完该事务才能返回,所以全同步复制的性能必然会收到严重的影响。
2 当主库提交事务之后,所有的从库节点必须收到、APPLY并且提交这些事务,然后主库线程才能继续做后续操作。但缺点是,主库完成一个事务的时间会被拉长,性能降低。
半同步复制
1 是介于全同步复制与全异步复制之间的一种,主库只需要等待至少一个从库节点收到并且 Flush Binlog 到 Relay Log 文件即可,主库不需要等待所有从库给主库反馈。
同时,这里只是一个收到的反馈,而不是已经完全完成并且提交的反馈,如此,节省了很多时间。
2 介于异步复制和全同步复制之间,主库在执行完客户端提交的事务后不是立刻返回给客户端,而是等待至少一个从库接收到并写到relay log中才返回给客户端。
相对于异步复制,半同步复制提高了数据的安全性,同时它也造成了一定程度的延迟,这个延迟最少是一个TCP/IP往返的时间。所以,半同步复制最好在低延时的网络中使用。
主从同步延迟问题
mysql的主从复制都是单线程的操作,主库对所有DDL和DML产生binlog,binlog是顺序写,所以效率很高,slave的I/O线程到主库取日志,效率也比较高,但是,slave的SQL线程将主库的DDL和DML操作在slave实施。
DML和DDL的IO操作是随即的,不是顺序的,成本高很多,还可能存在slave上的其他查询产生lock争用的情况,由于SQL也是单线程的,所以一个DDL卡住了,需要执行很长一段事件,后续的DDL线程会等待这个DDL执行完毕之后才执行,这就导致了延时。
当主库的TPS(每秒事务量:(Com_commit + Com_rollback)/Uptime)并发较高时,产生的DDL数量超过slave一个sql线程所能承受的范围,延时就产生了,除此之外,还有可能与slave的大型query语句产生了锁等待导致。
解决方案
1 业务的持久化层的实现采用分库架构,mysql服务可平行扩展,分散压力。
2 服务的基础架构在业务和mysql之间加入memcache或者Redis的cache层。降低mysql的读压力。
3 使用比主库更好的硬件设备作为slave。
怎么保证数据一致性?
一致性指分布式服务化系统之间的弱一致性,包括应用系统一致性和数据一致性。
无论是水平拆分还是垂直拆分,都解决了特定场景下的特定问题,凡事有好的一面,都会有坏的一面,拆分后的系统或者服务化的系统最大的问题就是一致性问题,这么多个具有元功能的模块,或者同一个功能池中的多个节点之间,如何保证他们的信息是一致的、工作步伐是一致的、状态是一致的、互相协调有序的工作呢?
以下经典案例:
转账:扣除自己成功,别人增加失败,那么你就损失这笔钱。扣除自己失败,增加别人成功,银行就损失这笔钱。
下订单和扣库存: 下订单和库存,先下单,后扣库存,那么会导致超卖。如果下单成功,扣库存成功,那么会导致少卖。
同步超时:服务化的系统间调用常常因为网络问题导致系统间调用超时,系统A同步调用系统B超时,系统A可以明确得到超时反馈,但是无法确定系统B是否已经完成了预定的功能或者没有完成预定的功能。于是,系统A就迷茫了,不知道应该继续做什么,如何反馈给使用方。
异步回调超时:统A同步调用系统B发起指令,系统B采用受理模式,受理后则返回受理成功,然后系统B异步通知系统A。
在这个过程中,如果系统A由于某种原因迟迟没有收到回调结果,那么两个系统间的状态就不一致,互相认知不同会导致系统间发生错误,严重情况下会影响核心事务,甚至会导致资金损失。
掉单:分布式系统中,两个系统协作处理一个流程,分别为对方的上下游,如果一个系统中存在一个请求,通常指订单,另外一个系统不存在,则导致掉单,掉单的后果很严重,有时候也会导致资金损失。
系统间状态不一致:两个系统间都存在请求,但是请求的状态不一致。
缓存和数据库不一致:一些特殊的场景对读的性能要求极高,服务于交易的数据库难以抗住大规模的读流量,通常需要在数据库前垫缓存,那么缓存和数据库之间的数据如何保持一致性?是要保持强一致呢还是弱一致性呢?
缓存数据结构不一致:某系统需要种某一数据结构的缓存,这一数据结构有多个数据元素组成,其中,某个数据元素都需要从数据库中或者服务中获取,如果一部分数据元素获取失败,由于程序处理不正确,仍然将不完全的数据结构存入缓存,那么缓存的消费者消费的时候很有可能因为没有合理处理异常情况而出错。
本地缓存节点间不一致:一个服务池上的多个节点为了满足较高的性能需求,需要使用本地缓存,使用了本地缓存,每个节点都会有一份缓存数据的拷贝,如果这些数据是静态的、不变的,那永远都不会有问题。
但是如果这些数据是半静态的或者常被更新的,当被更新的时候,各个节点更新是有先后顺序的,在更新的瞬间,各个节点的数据是不一致的,如果这些数据是为某一个开关服务的。
想象一下重复的请求走进了不同的节点。一个请求走了开关打开的逻辑,同时另外一个请求走了开关关闭的逻辑,这导致请求被处理两次,最坏的情况下会导致灾难性的后果,就是资金损失。
酸碱平衡理论(ACID在英文中的意思是“酸”)
ACID
A: Atomicity 原子性
C: Consistency 一致性
I: Isolation 隔离性
D: Durability 持久性
具有ACID的特性的数据库支持强一致性,强一致性代表数据库本身不会出现不一致,每个事务是原子的,或者成功或者失败,事物间是隔离的,互相完全不影响,而且最终状态是持久落盘的。
CAP(帽子理论)
C:Consistency,一致性, 数据一致更新,所有数据变动都是同步的
A:Availability,可用性, 好的响应性能,完全的可用性指的是在任何故障模型下,服务都会在有限的时间处理响应
P:Partition tolerance,分区容错性,可靠性
任何分布式系统只可同时满足二点,没法三者兼顾。关系型数据库由于关系型数据库是单节点的,因此,不具有分区容错性,但是具有一致性和可用性。
BASE理论(碱):
BASE理论解决CAP理论提出了分布式系统的一致性和可用性不能兼得的问题。满足CAP理论,通过牺牲强一致性,获得可用性,一般应用在服务化系统的应用层或者大数据处理系统,通过达到最终一致性
BA:Basically Available,基本可用
S:Soft State,软状态,状态可以有一段时间不同步
E:Eventually Consistent,最终一致,最终数据是一致的就可以了,而不是时时保持强一致
以转账为例:
我们把用户A给用户B转账分成四个阶段
第一个阶段用户A准备转账
第二个阶段从用户A账户扣减余额
第三个阶段对用户B增加余额
第四个阶段完成转账。
系统需要记录操作过程中每一步骤的状态,一旦系统出现故障,系统能够自动发现没有完成的任务,然后,根据任务所处的状态,继续执行任务,最终完成任务,达到一致的最终状态。
上面过程是通过持久化执行任务的状态和环境信息,一旦出现问题,定时任务会捞取未执行完的任务,继续未执行完的任务,直到执行完成为止,或者取消已经完成的部分操作回到原始状态。
下面介绍一下保持数据一致性的方案:
一、 semi-sync(半同步复制)
之所以会读取到旧数据,关键在于主从同步需要一个时间段,而读取请求可能刚好就发生在同步阶段。为了读取到最新的数据,需要等主从同步完成之后,主库上的写请求再返回 。
1)系统先对DB-master进行了一个写操作,写主库;
2)等主从同步完成,写主库的请求才返回;
3)读从库,读到最新的数据(如果读请求先完成,写请求后完成,读取到的是“当时”最新的数据)。
显然带来的后果就是主库的写请求时延会增加,吞吐量会降低。
二、 利用中间件
借助中间件的路由作用,对服务层的读写请求进行分发,从而避免出现不一致问题。
1)所有的读写都走数据库中间件,通常情况下,写请求路由到主库,读请求路由到从库。
2)记录所有路由到主库的key,在经验主从同步时间窗口内(假设是500ms),如果有读请求访问中间件,此时有可能从库还是旧数据,就把这个key上的读请求路由到主库。
3)经验主从同步时间过完后,对应key的读请求继续路由到从库。
中间件带来的好处就是能保证数据的绝对一致性,但同时也带来成本上升的问题。
三、利用缓存
1)将某个库上的某个key要发生写操作,记录在cache里,并设置“经验主从同步时间”的cache超时时间。
2)修改数据库。
而读取请求发生的时候:
从图中可以看出:
1)先到cache里查看,对应库的对应key有没有相关数据;
2)如果cache hit,有相关数据,说明这个key上刚发生过写操作,此时需要将请求路由到主库读最新的数据;
3)如果cache miss,说明这个key上近期没有发生过写操作,此时将请求路由到从库,继续读写分离。
显然,利用缓存,减少了中间件带来的成本问题,但多了一个Cache组件,并且读写数据库多了一步Cache操作,操作相对其他稍较繁琐。
其实用缓存策略,也存在很多问题。
缓存和数据库一致性
1 先更新数据库,再更新缓存
存在线程安全问题,同时如果数据更新频繁,读行为还没到,缓存更新频繁存在性能浪费。同时如果写高并发场景,压力则直接压到了DB上。
同时有请求A和请求B进行更新操作,那么会出现
1)线程A更新了数据库
2)线程B更新了数据库
3)线程B更新了缓存
4)线程A更新了缓存
2 先删缓存,再更新数据库
1)请求A进行写操作,删除缓存
2)请求B查询发现缓存不存在
3)请求B去数据库查询得到旧值
4)请求B将旧值写入缓存
5)请求A将新值写入数据库
解决方案:采用延时双删策略。删两次。
1)先淘汰缓存
2)再写数据库(这两步和原来一样)
3)休眠1秒,再次淘汰缓存
这么做,可以将1秒内所造成的缓存脏数据,再次删除。
3 先更新数据库,再删缓存
1)缓存刚好失效
2)请求A查询数据库,得一个旧值
3)请求B将新值写入数据库
4)请求B删除缓存
5)请求A将查到的旧值写入缓存
发生上述情况有一个先天性条件,就是步骤(3)的写数据库操作比步骤(2)的读数据库操作耗时更短,才有可能使得步骤(4)先于步骤(5)。
可是,大家想想,数据库的读操作的速度远快于写操作的(不然做读写分离干嘛,做读写分离的意义就是因为读操作比较快,耗资源少),因此步骤(3)耗时比步骤(2)更短,这一情形很难出现。
首先,给缓存设有效时间是一种方案。其次,采用策略(2)里给出的异步延时删除策略,保证读请求完成以后,再进行删除操作。
还有如果,删除缓存失败了怎么办?也会导致数据不一致。
那么我们需要一个重试机制
方案一:
1)更新数据库数据
2)缓存因为种种问题删除失败
3)将需要删除的key发送至消息队列
4)自己消费消息,获得需要删除的key
5)继续重试删除操作,直到成功
你会发现,,该方案有一个缺点,对业务线代码造成大量的侵入。于是有了方案二,在方案二中,启动一个订阅程序去订阅数据库的binlog,获得需要操作的数据。在应用程序中,另起一段程序,获得这个订阅程序传来的信息,进行删除缓存操作。
方案二:
1)更新数据库数据
2)数据库会将操作信息写入binlog日志当中
3)订阅程序提取出所需要的数据以及key
4)另起一段非业务代码,获得该信息
5)尝试删除缓存操作,发现删除失败
6)将这些信息发送至消息队列
7)重新从消息队列中获得该数据,重试操作。
上述的订阅binlog程序在mysql中有现成的中间件叫canal。
文章参考:https://blog.csdn.net/weixin_43885417/article/details/101676610
以上是本文的全部内容,希望对你的学习有帮助,也感谢你对PHP自学中心的支持
让学习成为一种习惯
长按二维码关注我
经验 | 方法 | 面试 | 文章