专栏首页Coder Sam的专栏当 MySQL 连接池遇上事务(二):消失的记录
原创

当 MySQL 连接池遇上事务(二):消失的记录

1. 往事回顾

MySQL连接池是一个很好的设计,通过将大量短连接转化为少量的长连接,从而提高整个系统的吞吐率。但是当跟事务一起使用时,如果使用方式不恰当时,就会发生一些奇怪的事。之前写过一篇文章专门讲述了遇到的一件奇怪的事情,详见《当MySQL连接池遇上事务(一):神秘的幽灵锁》

简单地说,《神秘的幽灵锁》一文,问题出在上层业务使用MySQL公共库时没意识到底层的连接池,导致使用方式不当。在上层看来是:

开启事务->执行SQL->commit

而实际底层实现是:

获取一个连接->开启事务->扔回连接池->获取一个连接->执行SQL->扔回连接池->获取一个连接->commit->扔回连接池。

这个过程无法保证每次拿到的都是同一个连接,也就存在了很大的隐患。当业务接口异常退出时,由于没有执行commit或rollback的连接已经被放回连接池,导致该带状态的连接没有被释放,并且进一步影响到该连接后续操作过的表。

解决方案是修改所有使用事务的接口,在事务结束之前不能将连接放回连接池。但是由于改动量较大,在全部接口修改完成之前,先对可能导致接口退出的异常进行处理,避免异常情况的发生。这样也正常稳定地运行了一段时间,没有再发生之前的问题。

直到……

2. 非阻塞HTTP也来搞怪

2.1 非阻塞HTTP的背景

之前说过,项目组使用OpenResty作为API Server,当需要执行HTTP调用时,早期很自然地选择了成熟的luasocket库。luasocket库是lua的一个开源库,对于常用的HTTP功能都能很好的满足,包括直接调用、代理转发、超时时间设置等。但是lua的库大多数阻塞调用的,对于OpenResty这样一个100%异步非阻塞的高性能服务器来说,阻塞的HTTP调用对对整体性能造成致命的伤害。

因此,近期正在使用非阻塞的resty.http库来替换luasocket库。resty.http是OpenResty的一个第三方开源HTTP调用的实现,采用了与OpenResty一致的风格,支持直接调用、反向代理、超时时间设置等特性,最重要的,它是非阻塞的。

2.2 HTTP调用方式居然影响到MySQL

按理说,替换HTTP库跟MySQL不应该有任何关系。阻塞与非阻塞强调的是调用方,只要保证替换HTTP库前后,对于同一个HTTP调用,被调用方收到的请求参数和请求方法完全一致即可。被调接口不应该也不能感知调用方使用的是阻塞还是非阻塞调用。

但是,奇怪的事情又双叒叕发生了……

替换luasocket库为非阻塞的resty.http之后,在页面配置时必现错误(后续定位是MySQL引起的)。奇怪的是,使用resty.http时错误必现,而恢复luasocket后则不会发生!!

2.3 消失的记录

为了定位,在平台接口内加了很多日志。定位的结果居然是,平台接口往异步任务表X插入一条记录,插入成功并且获取到一个自增长的任务ID N,但是当sleep之后再次查询该任务状态时,发现任务ID为N的记录并不存在。并且,之后再往表X插入记录,自增长的任务ID居然跳过了N,直接是N+1。

接口的日志和N+1的任务ID,都证明了任务ID为N的记录曾经存在过,但是从数据库中却找不到这条记录的任何踪迹。我把这叫做“消失的记录”。

3. 事物是普遍联系滴

奇怪的事情屡次发生,我又开始了艰辛的探索之路。这一次,我需要把两个看起来不相关的东西(HTTP调用方式和MySQL)联系起来。这很艰难,我还是根据现有的线索一步步往前推,看看究竟能走到哪。接下来还是以“提问-解答”的方式进行。

1) 记录会不会是被删除了?

遇到消失的记录,首先的怀疑是,记录会不会被删除了?

于是对该接口代码进行审阅(该接口是其他同事开发的)。审阅的结果令人失望,所有代码都是那么的正常,连让人怀疑的地方都没有。于是又把所有代码都搜索了一遍,居然没找到有删除任务表X的地方。至于第三方脚本删除,从时间上和删除记录的选择性上看,应该是不可能的。

当然,为了验证我的判断,解析了binlog,发现任务ID为N的记录压根就没有插入过,更没有被删除过。

记录被删除的可能,排除!

2) 记录是不是插入失败了?

既然从binlog看,记录没有被插入过,那么接口日志为什么显示获得了自增长的任务ID N呢?一个合理的怀疑是,在获得自增长ID之后,因为某个未知的原因导致插入失败了。查看MySQL文档,确实在插入失败的情况下,仍然可能会占用一个自增长ID。

那么是否是插入失败了呢?因为接口日志显示的是插入成功并且没有发生任何错误,怀疑插入失败就是怀疑resty.mysql库有问题。。没事,咱有怀疑精神,确认就是了。于是又开始阅读resty.mysql库的源码了。源码并不复杂,确认了只有MySQL APi返回正常时,resty.mysql库才会返回正常。MySQL API我还是信得过的,嘿嘿嘿。

也就是说,记录确实是插入成功了!

3) 插入成功的记录为什么没有binlog?

有了上一次《神秘的幽灵锁》的经验,这一次我很快意识到可能是因为事务!在事务内,接口认为插入成功了,但是后面事务rollback了,所以导致没有写入binlog。那么,这一切就解释的通了。

因为平台接口没有使用事务,只有业务接口使用了,所以只能是跨接口影响。于是,我赶紧搜索OpenResty的错误日志,希望找到上次一样的“lua entry thread abort”异常。但是很遗憾,这次所有接口都没有异常退出。

这条路到这里走到了尽头。。

4) HTTP调用方式为什么会跟事务扯上关系?

既然从MySQL本身出发的路走不下去,那就从HTTP调用方式思考。

替换luasocket为resty.http,从HTTP请求的功能上看是完全等价的,唯一的不同在于调用方式从阻塞变成了非阻塞。也就是说,非阻塞调用导致了MySQL连接的混用,平台接口拿到了业务接口开启了事务的连接。

为了验证这个猜想,我再次查看resty.mysql的文档,找到了一个函数get_reused_times(),该函数返回MySQL连接被使用的次数。通过在业务接口和平台接口加上日志打印get_reused_times()的结果,确认了我的猜想:业务接口调用了平台接口,当使用luasocket时,平台接口第一次get_reused_times()的结果是0,说明是新创建的连接;而使用resty.http时,平台接口第一次get_reused_times()的结果是业务接口调用平台接口前get_reused_times()的结果加1,说明平台接口拿到了业务接口的同一个连接。

那么,非阻塞在这个过程中究竟起到什么作用呢?我百思不得其解,直到我看到了这么一句话:

You can specify the max idle timeout (in ms) when the connection is in the pool and the maximal size of the pool every nginx worker process.

重点是“every nginx worker process”!也就是说,resty.mysql的连接池是worker级别的!!

使用luasocket时,因为阻塞,所以新的请求不会被分配到业务请求相同的worker上,也就是说,开启了事务的连接,不会被其他请求使用,因为根本就没有其他请求会使用这个worker的连接池!

但是换成非阻塞的resty.http之后,业务接口发起HTTP请求后,该worker仍然可以接受新的请求,并且非阻塞内部接口调用类似于子查询,在OpenResty看来就是同一个请求,所以必然分配到同一个worker。被调用的平台接口很自然的拿到了开启事务的连接,并往任务表X成功插入了一条记录(任务ID为N)。而在平台接口sleep之后,因为该连接超过了keepalive时间已经被释放,事务没有被提交,再次获取连接查询时,就查不到刚才插入的记录了,从而造成“消失的记录”。

进一步推想,如果sleep时间没超过keepalive时间,那么也是会有问题的。这次不会出现消失的记录,异步任务记录插入成功,但是由于该连接已开启了事务,会导致任务表被加上行锁和间隙锁,从而导致任务处理svr等锁而无法处理任务,任务最终还是会超时失败。

4. 那些年,我们那一如既往的幸运

“消失的记录”问题总算搞清楚了,现在再回顾一下,在《神秘的幽灵锁》一文,我说过我们使用“连接池+事务”的方式一直是错误的,但是却很幸运地没发生过问题,其根本原因就在于我们使用了阻塞的HTTP请求库。阻塞的方式导致我们的连接池同一时刻只有一个请求在使用,也就避免了接口间相互影响的可能。而如今,非阻塞的resty.http,把我们的运气用完了,所以到了需要处理这个问题的时候了。

问题的处理方式之前已经说过,就是修改事务接口用连接池的方式,在事务结束之前不能将连接放回连接池。但这个改动量较大,在全部修改完成之前,resty.http只怕是不能上线了。

原创声明,本文系作者授权云+社区发表,未经许可,不得转载。

如有侵权,请联系 yunjia_community@tencent.com 删除。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • 当 MySQL 连接池遇上事务(一):神秘的幽灵锁

    MySQL连接池是一个很好的设计,通过将大量短连接转化为少量的长连接,从而提高整个系统的吞吐率。一般各个团队都会对连接池进行封装,只提供简洁的接口供上层使用。但...

    Coder Sam
  • Java面试高频知识点汇总 数据库专题

    MySQL的默认存储引擎是InnoDB,并且在5.7版本的所有存储引擎中只有InnoDB是事务性存储引擎,也就是说只有InnoDB支持事务。

    Steve Wang
  • 初探Mysql架构和InnoDB存储引擎

    4.更新操作为什么不直接更新磁盘反而设计这样⼀个复杂的InnoDB存储引擎来完成?

    HUC思梦
  • 在 MySQL 5.7下排查内存泄露和 OOM 问题全过程

    我的个人网站后台使用的是 MySQL 5.7 版本,前段时间经常被oom-kill,借助5.7的新特性,经过一番排查,终于抓到这只鬼。

    叶金荣
  • Mysql专栏 - mysql、innodb存储引擎、binlog的工作流程

    这次新开了一个个人的mysql专栏,专门用于总结mysql的一些细节以及相关的案例总结,同时也包括了一些mysql的底层实现,在后续的篇章则是根据《mysql技...

    阿东
  • Canal+Otter - 前日篇(2)

    MySQL体系前端接受连接,并提供多种API,连接池化可重用。这里连接可以理解为线程,来处理来自客户端的请求。后台存储引擎负责控制IO策略,内存缓冲和线程...

    干货满满张哈希
  • 从零开始带你成为MySQL实战优化高手学习笔记(一)

    很多新入职的小朋友可能和现在的我一样,对数据库的了解仅仅停留在建库建表增删改查这些操作,日常工作也都是用封装好的代码,别说底层原理了,数据库和系统之间是如何工作...

    chenchenchen
  • Linux系统上安装MySQL与远程访问配置

    首先说明一下环境,用的是VMware虚拟机搭载CentOS6.5的Linux系统,并用CRT远程访问控制,所用MySQL版本为mysql-5.7.23。

    Java阿呆
  • 快速学习-Mycat 前世今生

    如果我有一个 32 核心的服务器,我就可以实现 1 个亿的数据分片,我有 32 核心的服务器么?没有,所以我至今无法实现 1 个亿的数据分片。——Mycat’s...

    cwl_java
  • 字节三面:详解一条 SQL 的执行过程

    天天和数据库打交道,一天能写上几十条 SQL 语句,但你知道我们的系统是如何和数据库交互的吗?MySQL 如何帮我们存储数据、又是如何帮我们管理事务?....是...

    Java程序猿
  • MYSQL数据优化常用配置参数

    #指定MySQL可能的连接数量。当MySQL主线程在很短的时间内得到非常多的连接请求,该参数就起作用,之后主线程花些时间(尽管很短)检查连接并且启动一个新线程。...

    TrueDei
  • mysql事务的实现原理

    此篇文章算是对mysql事务的一个总结,在了解这些之前我们先对mysql在执行的过程中 有一个整体的认识,如下图

    程序员小饭
  • 头条二面: 详解一条 SQL 的执行过程|文末送书

    天天和数据库打交道,一天能写上几十条 SQL 语句,但你知道我们的系统是如何和数据库交互的吗?MySQL 如何帮我们存储数据、又是如何帮我们管理事务?....是...

    kunge
  • 一位Java工程师的阶段性工作总结

    1.1.1、通常的模块分布:一般如果你要实现一个web应用,你从后台将数据展示到前端页面,在一个比较大的公司,你少不了跟其他项目有交集(你调用他的接口,他依赖你...

    lyb-geek
  • JAVA三年面试总结,金九银十,你准备好了吗?

    Mshu
  • Java学习笔记-全栈-Java基础-13-JavaWeb基础

    RESTful是一种前后端交互的规范API。可以理解为“一种约定成俗的编程习惯”。

    devi
  • MySQL亿级数据快速导出

    嘉美伯爵
  • 【技术干货】数据蜂巢架构演进之路

    京东技术
  • 超详细的mysql数据库参数优化,都总结在这里了

    最近在对各个系统的mysql做一些参数上的优化,也开了慢查询,准备后面针对特定sql再进一步优化。下面主要介绍一下一些优化的参数。

    lyb-geek

扫码关注云+社区

领取腾讯云代金券