高频面试:如何解决MySQL主从复制延时问题

MySQL 主从一直是面试常客,里面的知识点虽然基础,但是能回答全的同学不多。

比如我之前面试小米,就被问到过主从复制的原理,以及主从延迟的解决方案,你之前面试,有遇到过哪些 MySQL 主从的问题呢?

不 BB,上文章目录。

1. MySQL 主从

1.1 什么是 MySQL 主从 ?

所谓 MySQL 主从,就是建立两个完全一样的数据库,一个是主库,一个是从库,主库对外提供读写的操作,从库对外提供读的操作

1.2 为什么使用 MySQL 主从 ?

对于数据库单机部署,在 4 核 8G 的机器上运行 MySQL 5.7 时,大概可以支撑 500 的 TPS 和 10000 的 QPS,当遇到一些活动时,查询流量骤然,就需要进行主从分离。

大部分系统的访问模型是读多写少,读写请求量的差距可能达到几个数量级,所以我们可以通过一主多从的方式,主库只负责写入和部分核心逻辑的查询,多个从库只负责查询,提升查询性能,降低主库压力。

当主库宕机时,从库可以切成主库,保证服务的高可用,然后主库也可以做数据的容灾备份,整体场景总结如下:

  • 读写分离:从库提供查询,减少主库压力,提升性能;
  • 高可用:故障时可切换从库,保证服务高可用;
  • 数据备份:数据备份到从库,防止服务器宕机导致数据丢失。

2. 主从复制

2.1 主从复制原理

MySQL 的主从复制是依赖于 binlog,也就是记录 MySQL 上的所有变化并以二进制形式保存在磁盘上二进制日志文件。

主从复制就是将 binlog 中的数据从主库传输到从库上,一般这个过程是异步的,即主库上的操作不会等待 binlog 同步地完成。

详细流程如下:

  1. 主库写 binlog:主库的更新 SQL(update、insert、delete) 被写到 binlog;
  2. 主库发送 binlog:主库创建一个 log dump 线程来发送 binlog 给从库;
  3. 从库写 relay log:从库在连接到主节点时会创建一个 IO 线程,以请求主库更新的 binlog,并且把接收到的 binlog 信息写入一个叫做 relay log 的日志文件;
  4. 从库回放:从库还会创建一个 SQL 线程读取 relay log 中的内容,并且在从库中做回放,最终实现主从的一致性。

2.2 如何保证主从一致

当主库和从库数据同步时,突然中断怎么办?因为主库与从库之间维持了一个长链接,主库内部有一个线程,专门服务于从库的这个长链接。

对于下面的情况,假如主库执行如下 SQL,其中 a 和 create_time 都是索引:

delete from t where a > '666' and create_time<'2022-03-01' limit 1;
复制代码

我们知道,数据选择了 a 索引和选择 create_time 索引,最后 limit 1 出来的数据一般是不一样的。

所以就会存在这种情况:在 binlog = statement 格式时,主库在执行这条 SQL 时,使用的是索引 a,而从库在执行这条 SQL 时,使用了索引 create_time,最后主从数据不一致了。

那么我们该如何解决呢?

可以把 binlog 格式修改为 row,row 格式的 binlog 日志记录的不是 SQL 原文,而是两个 event:Table_map 和 Delete_rows。

Table_map event 说明要操作的表,Delete_rows event用于定义要删除的行为,记录删除的具体行数。row 格式的 binlog 记录的就是要删除的主键 ID 信息,因此不会出现主从不一致的问题。

但是如果 SQL 删除 10 万行数据,使用 row 格式就会很占空间,10 万条数据都在 binlog 里面,写 binlog 的时候也很耗 IO。但是 statement 格式的 binlog 可能会导致数据不一致。

设计 MySQL 的大叔想了一个折中的方案,mixed 格式的 binlog,其实就是 row 和 statement 格式混合使用,当 MySQL 判断可能数据不一致时,就用 row 格式,否则使用就用 statement 格式。

3. 主从延迟

有时候我们遇到从数据库中获取不到信息的诡异问题时,会纠结于代码中是否有一些逻辑会把之前写入的内容删除,但是你又会发现,过了一段时间再去查询时又可以读到数据了,这基本上就是主从延迟在作怪。

主从延迟,其实就是“从库回放” 完成的时间,与 “主库写 binlog” 完成时间的差值,会导致从库查询的数据,和主库的不一致

3.1 主从延迟原理

谈到 MySQL 数据库主从同步延迟原理,得从 MySQL 的主从复制原理说起:

  • MySQL 的主从复制都是单线程的操作,主库对所有 DDL 和 DML 产生 binlog,binlog 是顺序写,所以效率很高;
  • Slave 的 Slave_IO_Running 线程会到主库取日志,放入 relay log,效率会比较高;
  • Slave 的 Slave_SQL_Running 线程将主库的 DDL 和 DML 操作都在 Slave 实施,DML 和 DDL 的 IO 操作是随机的,不是顺序的,因此成本会很高,还可能是 Slave 上的其他查询产生 lock 争用,由于 Slave_SQL_Running 也是单线程的,所以一个 DDL 卡住了,需要执行 10 分钟,那么所有之后的 DDL 会等待这个 DDL 执行完才会继续执行,这就导致了延时。

总结一下主从延迟的主要原因:主从延迟主要是出现在 “relay log 回放” 这一步,当主库的 TPS 并发较高,产生的 DDL 数量超过从库一个 SQL 线程所能承受的范围,那么延时就产生了,当然还有就是可能与从库的大型 query 语句产生了锁等待。

3.2 主从延迟情况

  • 从库机器性能:从库机器比主库的机器性能差,只需选择主从库一样规格的机器就好。
  • 从库压力大:可以搞了一主多从的架构,还可以把 binlog 接入到 Hadoop 这类系统,让它们提供查询的能力。
  • 从库过多:要避免复制的从节点数量过多,从库数据一般以3-5个为宜。
  • 大事务:如果一个事务执行就要 10 分钟,那么主库执行完后,给到从库执行,最后这个事务可能就会导致从库延迟 10 分钟啦。日常开发中,不要一次性 delete 太多 SQL,需要分批进行,另外大表的 DDL 语句,也会导致大事务。
  • 网络延迟:优化网络,比如带宽 20M 升级到 100M。
  • MySQL 版本低:低版本的 MySQL 只支持单线程复制,如果主库并发高,来不及传送到从库,就会导致延迟,可以换用更高版本的 MySQL,支持多线程复制。

3.3 主从延迟解决方案

我们一般会把从库落后的时间作为一个重点的数据库指标做监控和报警,正常的时间是在毫秒级别,一旦落后的时间达到了秒级别就需要告警了。

解决该问题的方法,除了缩短主从延迟的时间,还有一些其它的方法,基本原理都是尽量不查询从库,具体解决方案如下:

  • 使用缓存:我们在同步写数据库的同时,也把数据写到缓存,查询数据时,会先查询缓存,不过这种情况会带来 MySQL 和 Redis 数据一致性问题。
  • 查询主库:直接查询主库,这种情况会给主库太大压力,不建议这种方式。
  • 数据冗余:对于一些异步处理的场景,如果只扔数据 ID,消费数据时,需要查询从库,我们可以把数据全部都扔给消息队列,这样消费者就无需再查询从库。(这种情况应该不太能出现,数据转了一圈,MySQL 主从还没有同步好,直接去撕 DBA 吧)

在实际应用场景中,对于一些非常核心的场景,比如库存,支付订单等,需要直接查询主库,其它非核心场景,就不要去查主库了。

4. 主从切换

4.1 一主一从

两台机器 A 和 B,A 为主库,负责读写,B 为从库,负责读数据。

如果 A 库发生故障,B 库成为主库负责读写,修复故障后,A 成为从库,主库 B 同步数据到从库 A。

  • 优点: 从库支持读,分担了主库的压力,提升了并发度,且一个机器故障了可以自动切换,操作比较简单,公司从库还可以充当数据备份的角色;
  • 缺点: 一台从库,并发支持还是不够,并且一共两台机器,还是存在同时故障的机率,不够高可用。

对于一主一从的模式,一般小公司会这么用,不过该模式下,主从分离的意义其实并不大,因为小公司的流量不高,更多是为了数据库的可用性,以及数据备份。

4.2 一主多从

一台主库多台从库,A 为主库,负责读写,B、C、D为从库,负责读数据。

如果 A 库发生故障,B 库成为主库负责读写,C、D 负责读,修复故障后,A 也成为从库,主库 B 同步数据到从库 A。

  • 优点: 多个从库支持读,分担了主库的压力,明显提升了读的并发度。
  • 缺点: 只有一台主机写,因此写的并发度不高。

基本上大公司,比如百度、滴滴,都是这种一主多从的模式,因为查询流量太高,一定需要进行读写分离,同时也需要支持服务的高可用、数据容灾。

<think> 我们被要求使用延时双删策略解决MySQL和Redis双写一致性问题。根据引用[1][2][3][4],延时双删是一种常用的策略,用于减少缓存和数据之间的数据一致。下面我将详细解释延时双删的步骤、原理、适用场景以及优缺点。 ### 延时双删策略详解 延时双删的基本思想是:在更新数据前后各删除一次缓存,并在第二次删除之前加入一个短暂的延迟,以确保在数据主从同步完成或并发读操作完成后,再次清除可能脏的缓存。 #### 步骤 1. **第一次删除缓存**:在更新数据之前,先删除Redis中的相关缓存。 2. **更新数据**:执行数据更新操作(如MySQL的UPDATE)。 3. **延时等待**:等待一段时间(通常为几百毫秒),这个时间主要考虑: - 主从数据同步的延迟(如果使用了主从架构)。 - 确保在更新数据期间可能发生的并发读操作已经完成(这些读操作可能会将旧数据重新加载到缓存)。 4. **第二次删除缓存**:再次删除Redis中的同一缓存项,以清除在等待期间可能被重新加载的旧数据。 #### 伪代码示例 ```python def update_data(key, new_value): # 1. 第一次删除缓存 redis.delete(key) # 2. 更新数据 mysql.update(key, new_value) # 3. 延时等待(例如500毫秒) sleep(0.5) # 4. 第二次删除缓存 redis.delete(key) ``` #### 为什么需要延时? 在更新数据后,如果有其他线程在数据更新完成之后、第二次删除缓存之前读取了数据(此时数据已经是新值),那么这些线程会将旧值(因为此时可能主从同步未完成,或者事务未提交,导致读到旧值)或新值写入缓存。如果是旧值,那么第二次删除可以清除这个旧值。延时等待的主要目的是为了确保在主从架构中,主的更新已经同步到从(从而避免读操作从从读到旧值并写入缓存),同时也让在更新期间发生的并发读操作完成(这些读操作可能将旧数据写入缓存)[^3]。 #### 适用场景 - 对数据一致性要求较高,但可以接受短暂延迟(毫秒级)的业务场景。 - 读多写少的场景,因为写操作需要两次删除和一次延时等待,写操作的开销较大。 - 能接受强一致性(即要求数据和缓存实时一致)的场景,因为延时双删只能保证最终一致性。 #### 缺点 1. **延迟问题**:延时等待的时间需要根据业务场景预估,设置过长会影响系统性能,设置过短则可能无法覆盖主从延迟或并发读操作,导致数据一致[^2]。 2. **性能开销**:两次删除操作加上延时等待,增加了写操作的耗时。 3. **完全保证一致性**:在极端情况下(如第二次删除失败),仍然可能出现数据一致。 4. **适用于频繁写操作**:例如秒杀场景,频繁的写操作会导致大量延时等待,降低系统吞吐量[^2]。 ### 如何优化延时双删? 1. **异步延时删除**:第二次删除可以异步进行,避免阻塞主线程。例如,将第二次删除操作放入消息队列或者线程池中执行。 2. **设置合理的延时时间**:需要根据系统的主从延迟时间和业务并发量来设定延时时间。可以通过监控系统获取主从同步的平均延迟时间,以此为基础设置延时时间。 3. **重试机制**:如果第二次删除失败,加入重试机制(如通过消息队列重试或定时任务重试)确保删除成功。 4. **结合其他策略**:如使用消息队列监听数据变更(如通过binlog),再删除缓存,这样可以更精确地控制缓存删除的时机。 ### 与其他策略对比 - **先更新数据再删除缓存(Cache-Aside)**:可能出现删除缓存失败导致数据一致,而延时双删通过两次删除减少了这种情况的发生。 - **先删除缓存再更新数据**:在删除缓存后、更新数据前,如果有读操作,会读取旧数据写入缓存,导致缓存中一直是旧数据延时双删通过第二次删除解决了这个问题。 ### 总结 延时双删是一种以最终一致性为目标的策略,通过两次删除缓存和中间的延时等待,减少MySQL和Redis之间的数据一致。它适用于对一致性要求较高但能容忍短暂延迟的场景。在实际应用中,需要根据业务特点调整延时时间,并结合异步操作和重试机制来提高可靠性[^1][^2][^3][^4]。 §§相关问题§§ 1. 如何确定延时双删中的等待时间? 2. 如果第二次删除缓存失败,有什么补救措施? 3. 延时双删策略在分布式系统中有哪些挑战? </think> ### 延时双删策略解决MySQL和Redis双写一致性问题 延时双删策略是一种通过两次删除缓存并结合延迟机制来解决MySQL和Redis数据一致的方法。以下是详细实现步骤和原理分析: #### 一、核心步骤(分四步操作) 1. **第一次删除缓存** 在执行MySQL更新**前**,先删除Redis中对应的缓存数据: ```redis DEL user:1001_cache ``` *目的*:确保后续读请求会命中旧缓存。 2. **更新MySQL数据** 执行SQL更新操作: ```sql UPDATE users SET balance=150 WHERE id=1001; ``` 3. **设置延迟等待** 引入指定时长的延迟(通常200-500ms): ```python import time time.sleep(0.3) # 300ms延迟 ``` *关键作用*: - 等待MySQL主从同步完成(若使用读写分离) - 允许并发读操作将MySQL数据加载到Redis 4. **第二次删除缓存** 再次删除相同缓存项: ```redis DEL user:1001_cache ``` *清理目标*:清除延迟期间可能被旧数据污染的缓存[^2][^3] #### 二、解决一致问题的原理 通过双时序图解释并发冲突场景: ```mermaid sequenceDiagram participant 线程A participant Redis participant MySQL participant 线程B 线程A->>Redis: 1. DEL缓存 线程A->>MySQL: 2. UPDATE数据 Note right of 线程B: 并发读请求 线程B->>Redis: 3. 读缓存(未命中) 线程B->>MySQL: 4. 读旧数据(未更新完成) 线程B->>Redis: 5. 写回旧数据 线程A->>+Redis: 6. 延迟后二次DEL Note left of 线程A: 关键步骤:清除脏缓存 线程B-->>Redis: 7. 下次读触发缓存重建 ``` #### 三、技术实现要点 1. **延迟时间设定** 计算公式: $$ t_{delay} = t_{主从延迟} + t_{业务处理} + 100ms $$ 建议基准值: - 单机MySQL:100-200ms - 主从架构:300-500ms - 需通过压测校准[^2] 2. **异步优化方案** 避免阻塞主线程: ```python def async_double_delete(key): redis.delete(key) # 同步第一次删除 thread = threading.Thread( target=delayed_delete, args=(key, 300) # 300ms后异步执行 ) thread.start() def delayed_delete(key, delay_ms): time.sleep(delay_ms/1000) redis.delete(key) # 第二次删除 ``` 3. **补偿机制设计** 应对极端情况(如二次删除失败): - 记录删除日志到消息队列(Kafka/RabbitMQ) - 后台任务重试失败操作 - 添加监控告警(如Redis key存活检测) #### 四、适用场景与限制 | 场景类型 | 适用性 | 说明 | |----------------|--------|-------------------------| | 读多写少 | ✅ 推荐 | 低频写减少性能损耗 | | 秒杀等高并发写 | ❌ 适用 | 延迟会严重降低吞吐量[^2] | | 强一致性要求 | ❌ 适用 | 只能保证最终一致性 | | 主从架构 | ⚠️ 需调优 | 需准确评估主从延迟 | > 最佳实践:电商商品详情页更新(每天少量更新+海量读取)[^5] #### 五、与其他方案对比 | 方案 | 一致性强度 | 复杂度 | 性能影响 | |---------------------|------------|--------|----------| | 延时双删 | 最终一致 | 中 | 写操作延迟 | | 分布式锁 | 强一致 | 高 | 高并发瓶颈 | | 订阅binlog同步 | 最终一致 | 高 | 低延迟 | | 先更新DB后删缓存 | 弱一致 | 低 | 缓存穿透风险 | **总结**:延时双删通过两次删除缓存+延迟等待的简单设计,有效降低了MySQL和Redis的数据一致概率,尤其适用于能接受毫秒级延迟的读多写少场景。但需注意其适用于强一致要求或高频写场景[^1][^3]。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值