MySQL Binlog 技术原理和业务应用案例分析( 二 )


那究竟是什么原因导致业务系统收到根据 Binlog 生成的订单支付事件后 , 再查询主库得到的订单数据却是未支付状态的?
对于此问题的原因我们先放下不谈 , 先来看看 MySQL 在更新数据时的内部原理 。
MySQL 数据更新相关原理本节将向大家介绍 MySQL 数据更新相关原理 , 以及在这一过程中最重要的两种日志:Redo Log 和 Binlog 。
Redo Log 和 Binlog先来介绍 Redo Log 和 Binary Log(Binlog):

  • Redo Log :Redo Log 是 InnoDB 存储引擎提供的一种物理日志结构 , 用来描述对底层数据页操作的具体内容 , 主要用于实现 crash-safe , 并提升磁盘操作效率 。
  • Binlog :Binlog 是 MySQL 本身提供的一种逻辑日志 , 和具体存储引擎无关 , 描述的是数据库所执行的 SQL 语句或数据变更情况 , 主要用于数据复制 。
InnoDB 引入 Redo Log 的目的在于实现 crash-safe 和提升数据更新效率 。如果 InnoDB 每次数据写操作都要直接持久化到磁盘上的数据页中 , 那样会大量增加磁盘随机 IO 次数 。引入 Redo Log 后 , 在对数据写操作时 , 会将部分随机 IO 写变为顺序写 。因为磁盘的顺序 IO 效率远高于随机 IO , 因此引入 Redo Log 机制有助于提升更新数据时的性能(如何实现 crash-safe 将在下一节介绍) 。
下面的表格说明了两种日志的作用和它们的不同:
Redo LogBinlog日志类型物理日志 , 即数据页中的真实二级制数据 , 恢复速度快逻辑日志 , SQL 语句 (statement) 或数据逻辑变化 (row) , 恢复速度慢存储格式基于 InnoDB 数据页格式进行存储SQL 语句或数据变化内容用途重做数据页数据复制层级InnoDB 存储引擎层MySQL Server 层记录方式循环写追加写
这时问题来了 , 现在 MySQL 中存在了两种日志结构:Redo Log 和 Binlog 。虽然它们的结构和功能有所不同 , 但却记录着相同的数据 。如何保证这两种日志数据的一致性 , 以及如何实现 crash-safe 呢?这就引出了两阶段提交设计 。
两阶段提交两阶段提交不是 Redo Log 或 InnoDB 中的设计 , 而是 MySQL 服务器的设计(但通常说到两阶段提交时都和 Redo Log 放在一起) 。因为 MySQL 采用插件化的存储引擎设计 , 事务提交时 , 服务器本身和存储引擎都需要提交数据 。所以从 MySQL 服务器角度看 , 其本身就面临着分布式事务问题 。
为解决此问题 , MySQL 引入了两阶段提交 。在两阶段提交过程中 , Redo Log 会有两次操作:Prepare 和 Commit 。而 Binlog 写操作则夹在 Redo Log 的 Prepare 和 Commit 操作之间 。我们可以设想一下不同失败场景下两阶段提交的设计是如何保证数据一致的:
  1. Redo Log Prepare 成功 , 在写 Binlog 前崩溃:在故障恢复后事务就会回滚 。这样 Redo Log 和 Binlog 的内容还是一致的 。这种情况比较简单 , 比较复杂的是下一种情况 , 即在写 Binlog 和 Redo Log Commit 中间崩溃时 , MySQL 是如何处理的?
  2. 在写 Binlog 之后 , 但 Redo Log 还没有 Commit 之前崩溃
  • 如果 Redo Log 有 Commit 标识 , 说明 Redo Log 其实已经 Commit 成功 。这时直接提交事务;
  • 如果 Redo Log 没有 Commit 标识 , 则使用 XID(事务 ID)查询 Binlog 相应日志 , 并检查日志的完整 。如果 Binlog 是完整的 , 则提交事务 , 否则回滚;
如何判断 Binlog 是否完整?简单来说 Statement 格式的 Binlog 最后有 Commit , 或 Row 格式的 Binlog 有 XID Event , 那 Binlog 就是完整的 。
MySQL 数据更新流程接下来看一下 MySQL 执行器和 InnoDB 存储引擎在执行简单 update 语句 update t set n = n + 1 where id = 2 时的流程(因为此例只执行单条更新语句 , 所以其自身就是一个事务) 。
  1. 执行器先找引擎取 ID=2 这一行 。ID 是主键 , 引擎直接用树搜索找到这一行 。如果 ID=2 这一行所在的数据页本来就在内存中 , 就直接返回给执行器;否则 , 需要先从磁盘读入内存 , 然后再返回 。
  2. 执行器拿到引擎给的行数据 , 把这个值加上 1 , 比如原来是 N , 现在就是 N+1 , 得到新的一行数据 , 再调用引擎接口写入这行新数据 。
  3. 引擎将这行新数据更新到内存中 。然后将对内存数据页的更新内容记录在 Redo Log Buffer 中(这里不详细介绍 Redo Log Buffer 。只需知道对 Redo Log 的操作并不会直接写在文件上 , 而是先记录在内存中 , 然后在特定时刻才会写入磁盘) 。此时完成了数据更新操作 。


    推荐阅读