您好,登录后才能下订单哦!
导语
MySQL Binlog用于记录用户对数据库操作的结构化查询语言(Structured Query Language,SQL)语句信息。是MySQL数据库的二进制日志,可以使用mysqlbin命令查看二进制日志的内容。爱奇艺在会员订单系统使用到了 MySQL Binlog,用来实现订单事件驱动。在使用Binlog 后在简化系统设计的同时帮助系统提升了可用性和数据一致性。
本文将从实际应用角度出发理解 MySQL中的相关技术原理,从技术原理和工作实践相结合,帮助大家以及在相关设计中存在的潜在问题,希望能给大家有所帮助和启发,共同进步。
作者介绍:作者帆叔目前主要负责爱奇艺会员交易系统的技术和架构工作,专注异步编程、服务治理、代码重构等领域,热爱技术,乐于分享。
Binlog 是 MySQL 中一个很重要的日志,主要用于 MySQL 主从间的数据同步复制。正是因为 Binlog 的这项功用,它也被用于 MySQL 向其它类型数据库同步数据,以及业务流程的事件驱动设计。通过研究分析,我们发现使用 MySQL Binlog 实现事件驱动设计并没有想象中那么简单,所以接下来带大家了解 MySQL 的 Binlog、Redo Log、数据更新内部流程,并通过对这些技术原理的介绍,来分析对业务流程可能造成的问题,以及如何避免这些问题。希望通过本文的解析,能够帮助大家了解到 MySQL 的一些原理,从而帮助大家能够更顺利地使用 MySQL 这个流行的数据库技术。
基于 Binlog 的事件驱动
首先介绍一下会员订单系统的设计,订单系统直接向 MQ 发送消息,通过异步消息驱动后续业务流程,以实现消息驱动的设计。大致的业务流程示意图如下:
图1:直接发送消息的订单事件驱动
图2:基于 Binlog 的订单事件驱动
暗 藏 问 题
上文提到,虽然基于 Binlog 的订单事件驱动设计存在诸多优点,但后来发现其实暗藏问题。经过实验,我们发现偶尔会有订单履约延迟的现象。
在正常流程中,订单履约服务收到订单支付事件后,会检查订单状态,如果此时订单状态为已支付,则进行履约流程的处理。但对于有履约延迟的订单,订单履约服务收到此订单的支付事件后,查询数据库发现此订单并非支付状态。经过调查,我们排除了数据并发覆盖问题,并且订单状态查询是发生在主库上,也不存在主从同步延迟问题。
那究竟是什么原因导致业务系统收到根据 Binlog 生成的订单支付事件后,再查询主库得到的订单数据却是未支付状态的?
对于此问题的原因我们先放下不谈,先来看看 MySQL 在更新数据时的内部原理。
Redo Log | Binlog | |
日志类型 | 物理日志,即数据页中的真实二级制数据,恢复速度快 | 逻辑日志,SQL 语句 (statement) 或数据逻辑变化 (row),恢复速度慢 |
存储格式 | 基于 InnoDB 数据页格式进行存储 | SQL 语句或数据变化内容 |
用途 | 重做数据页 | 数据复制 |
层级 | InnoDB 存储引擎层 | MySQL Server 层 |
记录方式 | 循环写 | 追加写 |
图中描述了 update 语句执行过程中 MySQL 执行器、InnoDB,以及 Binlog、Redo Log 交互过程(图中深绿底色的是 MySQL 执行器负责的阶段,浅绿底色是 InnoDB 负责的阶段)
从上面对 MySQL 原理的介绍我们得知,写 Binlog 发生在事务提交阶段,但是 MySQL 因为在 Server 层和存储引擎层都引入了不同的日志结构,从而引入了两阶段提交。Binlog 的写入发生在存储引擎真正提交事务之前,这导致理论上通过 Binlog 同步数据的系统(MySQL 从库、其它数据库或业务系统)有可能早于 MySQL 主库使最新提交的数据生效。
所以上面提到的订单履约服务在收到基于 Binlog 的订单支付事件后却查到相应订单是未支付的,原因很可能是订单履约服务在查询数据时,订单支付数据更新操作在 MySQL 内部尚未彻底完成事务的提交。
我们通过开发验证程序重现了这一现象。验证程序接收到事务提交完成后的完整 Binlog 时会再次在 MySQL 主库上查询对应的记录,结果会有一定概览获得事务提交前的数据。
另外经过了解,也有同行反映遇到过从库早于主库看到数据提交的问题。
在了解问题背后的原因之后,我们需要思考如何解决此问题。目前解决此问题有两个方法:重试和直接使用 Binlog 数据。
重试这种做法简单粗暴,既然问题原因是 Binlog 早于事务提交,那等一下再重试查询自然就解决了。但在实践中,需要考虑重试的实现方法、以及是否会因为重试过多甚至无限重试导致服务异常。对于重试的实现,可使用的方法有线程 Sleep 大法和消息重投等方式。线程 Sleep 大法通常是不被推荐的,因为它会导致线程利用率降低,甚至导致服务无法响应。但考虑到本次问题出现概率较低,我们认为线程 Sleep 大法是可以使用的,并且此方式简单易行,可用于问题的快速修复。
第二种重试方式是消息重投,比如 RocketMQ 中 Consumer 返回 ConsumeConcurrentlyStatus.RECONSUME_LATER 即可触发消息重投。但这种重试方法成本较前一种方法高,另外重试间隔也相对较大,对时间敏感的业务影响也较大,因此是否采用此方法需从业务和技术两个角度综合考虑。
除了考虑用何种方式重试,还要考虑 ABA 问题,即状态变化按照 A->B->A 的方式进行。业务系统期待的状态是 B,但实际可能没办法再变成 B 了。因此在用重试解决此问题之前,需要先排除业务系统存在 ABA 问题的可能。对于状态 ABA 问题,可用状态机等方式解决,这里不再展开讨论。
除了重试,另一种方法就是直接使用 Binlog。因为 Binlog (row 格式) 直接反映了数据的变化情况,其中可以记录事务提交涉及到的完整数据,因此可直接用作业务处理。这样还可以降低数据库 QPS。如果是新设计的系统,我认为这样做法比较理想。但对于已有系统,这种方式改动可能较大,是否采用需权衡成本和收益。
招聘信息
爱奇艺会员开发团队诚招 Java 资深工程师/技术专家。会员业务是爱奇艺核心业务之一,我们致力于通过技术手段服务核心业务,研发通用化、高可用的业务系统,同时我们也需要擅长如数据库、服务治理、MQ 等技术的人才。欢迎感兴趣的同学发送简历至:luodi@qiyi.com(邮件标题请注明:会员开发)
免责声明:本站发布的内容(图片、视频和文字)以原创、转载和分享为主,文章观点不代表本网站立场,如果涉及侵权请联系站长邮箱:is@yisu.com进行举报,并提供相关证据,一经查实,将立刻删除涉嫌侵权内容。