背景

在司机数据库中,有一张用于存储司机车型的表,暂且称之为表t。该表结构如下所示:

MySQL

[comp_epower]> show create table t \G; *************************** 1. row *************************** Table:

Create Table:

CREATE TABLE `t` (

`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键',

`full_station_id` varchar(64) COLLATE utf8_bin NOT NULL DEFAULT '' COMMENT '司机唯一Id',

`platform` int(4) NOT NULL DEFAULT '-1' COMMENT '车型id',

`create_time` datetime NOT NULL DEFAULT '1970-01-01 00:00:00' COMMENT '创建时间', `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '时间',

PRIMARY KEY (`id`) USING BTREE,

KEY `idx_full_station_id` (`full_station_id`) USING BTREE )

ENGINE=InnoDB AUTO_INCREMENT=145612 DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='桩站上线渠道'

这张表的业务场景:以full_station_id为'test'来举例(full_station_id为的唯一Id),如果在平板(车型号1)、小面(2)、金杯(3),则在该表中会有如下三条记录:

MySQL [comp_epower]> select * from t where full_station_id = 'test';

发现问题

由于司机车型C端列表页的检索需求,DB中车型相关的信息需要同步到ES中,这其中就包括车型信息。由于历史原因,“同步”这个动作发生在业务流程中,即在B端管理平台上编辑司机车型信息时,后端服务器更新完DB需要检索出车型完整信息发送到MQ,再清洗数据到ES。数据同步到ES中后,每条车型信息中都存在一个platform的List,存储所有的渠道号用于C端检索。

以上述full_station_id为'test'的车型为例,ES中就可能有如下的一条数据:

{    // ... 省略其他字段    "full_station_id": "test",

"platform": [1, 2, 3]    // ... 省略其他字段 }

近期偶然发现,ES中platform列表中存在重复的渠道号。即DB中的数据是[1, 2, 3]三条记录,在es中变成了[1, 2, 3, 1, 2, 3]

定位问题

虽然初看起来好像并不影响检索,但是作为一个具有技术洁癖的程序猿来讲,错误一旦发现就必须解决。随后在日志检索平台上搜索该站最近一次的变更记录,成功定位到在16日该充车型在B端有编辑记录,而且是连续两次提交。查看源码后发现,这个B端的更新流程被包裹在一个长事务中,在此次事务中有多次RPC请求。这里简单介绍下编辑车型的流程:

开启事务

  • 做一些车型相关的其他DB操作

  • 删除该车型的全部已上线渠道记录,再将B端上传的渠道列表保存到库中

  • 一堆RPC操作

  • 从DB检索车型的已上线渠道记录,再结合其他一些车型的信息组成宽表发送给ES

  • 提交事务

前面提到这次异常更新有两次连续的长事务,因此在机器上通过trace检索到全部日志,根据不同事件发生的时间点,还原两次编辑发生的完整历程。因为问题主要是渠道重复,因此主要将目光集中在渠道相关的几次操作,时间线如下:

通过日志还看出,在这两次事务开始之前,库中full_station_id为'test'的车型已经有[1, 2, 3]三条渠道记录。

那么开始在本地的mysql服务器上还原两次事务流程(本地mysql版本5.7.31-log,隔离级别可重复读):

(1)建表、初始化数据

(2)开始按照上述两个事务的执行过程,还原现场

如上图所示。但是奇怪,上图中第9步按照当时事发现场日志来看应该是查询出来6条记录才对!

看到这里的小伙伴可以停下来想想上面的还原步骤哪里出了差错(在编辑车型流程里有线索)

回过头来仔细看了下编辑车型的流程以及日志的输出时间线,发现在对渠道进行DB操作之前,还进行了其他一些DB操作,而且在事务一提交之前,事务二已经完成了部分DB操作。回想到innodb事务开启的时机,我在本地还原的时候,右边事务二其实是在7这个位置才开启的(innodb事务不是在begin处开启,而是在第一次真正的db操作时开启),也就是左边事务一在时刻6提交之后开启的。而实际上因为该长事务在渠道相关DB操作之前,已经做了其他的DB操作,所以这里梳理的流程图缺少了及其重要的一步:在事务一commit之前,将事务二开启起来。

重新梳理流程图

流程图V2中,事务二在t2时刻使用begin语句声明事务的开始,并在t3时刻事务一提交之前,完成了一次查询(随便什么),开启了事务。那么此时事务二就是在事务一未提交之前开启的。

再次在本地mysql上按照时间线还原现场

可以看到第9步检索出了6条数据,成功还原!

真相只有一个

简单介绍下MVCC的原理

每个事务开启时,都会被分配到一个全局唯一且递增的事务id,即trx_id,当每次事务对某些数据行进行修改时,都会将事务自身的trx_id记录在数据行的隐藏列上。在事务开启的那一刻,MVCC机制会为事务生成一个当前mysql服务器上所有事务的快照。这个快照是按照如下方式实现的:

  • 将当前服务端活跃的全部事务id记录在set中

  • 当前活跃的事务最小id记为min_trx_id

  • 当前活跃的事务最大id记为max_trx_id

  • 当进行一次普通查询的时候,根据数据行上的trx_id进行判断。

    trx_id < min_trx_id,该行是在当前事务开启前就提交的,对当前事务可见。

    trx_id > max_trx_id,该行是在当前事务开启后开启的,对当前事务不可以。

    (但是trx_id > max_trx_id,并且max_trx_id比trx_id先提交,也是对当前事务可见的。事务对数据做修改时会发起一致性读,即强制读取最新的记录)

  • min_trx_id <= trx_id <= max_trx_id,该行处在最大最小事务id中间,则判断是否为set中的活跃事务的修改。若是,则不可见;若不是,则说明当前事务开启时已经提交,则可见。

如何理解?

那既然 trx_id 这行数据,在快照最大最小事物中间了。那就肯定是最大最小中间的事物去修改的吧。会存在【若不是】的情况吗

就是在最大最小中间的某个事物,可能在拍照的时候,就已经提交了。这种就可以理解为在最小之前开启的,所以可见。

换个角度看,其实就是看这行数据是在拍照之前提交的,就可见。拍照之后(还未提交、就还在set中)就不可见(对前两条的补充)

对于隔离级别为可重复读而言,这个快照是在事务开启时生成的;对于读已提交,是在每次进行快照读的时刻生成的。

为行文方便,再次将上述流程梳理如下:

  • 【1】[事务一]:begin;

  • 【2】[事务一]:delete from t where full_station_id = 'test';

  • 【3】[事务一]:insert into t(full_station_id, platform) values('test', 1), ('test', 2), ('test', 3);

  • 【4】[事务一]:select * from t where full_station_id = 'test'

  • 【5】[事务二]:begin;

  • 【6】[事务二]:select * from t where full_station_id = 'test'

  • 【7】[事务一]:commit;

  • 【8】[事务二]:delete from t where full_station_id = 'test';

  • 【9】[事务二]:insert into t(full_station_id, platform) values('test', 1), ('test', 2), ('test', 3);

  • 【10】[事务二]:select * from t where full_station_id = 'test'

  • 【11】[事务二]:commit

【1】首先,在两次事务开始之前,表里fullStationId为'test'的渠道记录有三条,如下所示:(这里的trx_id用来记录最后一次更新该列的事务Id,delete_mask用作删除标记,这两列都是innodb数据行上的隐藏列)

【2】事务一开启,此时事务一的活跃事务集合为【2】,即只有自身。进行删除操作,此时针对【1】中的三条数据会做如下几条操作:(省略无关操作)

  • 将delete_mask设置为1(标记删除)

  • 将trx_id设置为2

  • 生成undo log,内容为将delete_mask改回0,trx_id改回1

此时这三条记录状态如下图所示:

这里的示意图可能与实际情况有所偏差,具体实现情况查看mysql相关文档。

【3】事务一插入三条记录。(注意后续示意图中没有画insert相关的undolog,只要清楚新insert数据的undo就是该行数据不存在即可)

【4】事务一查询fullStationId为'test'的记录,将六条数据分为上下两组,流程如下:

  • 上面三条数据trx_id为2,为自身删除,因此不可见。

  • 下面三条数据trx_id为2,为自身插入,可见。放入结果集中返回。

因此本次查询看到的是下面三条记录。

【5】事务二声明begin;

【6】事务二做了一次select操作,此时事务二真正开启,MVCC机制开始工作,因为事务一此刻还没提交,所以事务一的修改对事务二是不可见的。

假设事务二的trx_id为3,则在事务二开启一刻生成的活跃事务集合为【2,3】

那么事务二在进行普通的select查询时,实际上是做了一次快照读。将表里当前存在的数据分为两组,上面三条和下面三条。

  • 上面三条的trx_id为2,在活跃事务集合里,不可见。沿着undo链表往前回溯到上一个trx_id为1的版本。trx_id=1

  • 下面三条的trx_id为2,在活跃事务集合里,不可见。回溯undolog,发现是insert类型undolog,停止回溯,结束查询。

所以此时事务二进行快照读,只读到了上面三条记录。

【7】事务一提交

【8】事务二执行delete操作。此时删除的是上面三条,还是下面三条?

答案是下面三条,原因是一致性读。对于delete而言,首先肯定要在表里检索到符合条件的记录,那么在可重复读级别下,事务对数据做修改时会发起一致性读,即强制读取最新的记录,不管MVCC。所以该次操作实际上删除的是事务一插入的三条记录,流程与步骤【2】相仿,之后数据表状态如下图所示:(一致性读时对于上面三条记录而言,已经是被已提交事务删除了,因此事务二本次delete操作不对它们做操作)

【9】事务二插入三条记录,之后数据表状态如下:

【10】事务二进行一次快照读,此时MVCC机制产生作用。将九条数据分为上中下三组,则流程如下:

  • 上面三组trx_id为2,在活跃事务集合中,因此不可见。沿着undo链表回溯到trx_id为1的记录,可见,放入结果集。

问:trx_id为2,在活跃事务集合中?如何判断是否在活跃事物中?

答:事物二开启的时候,快照了set

  • 中间三组trx_id为3,是当前事务的trx_id。因为delete_mask为1,表明当前事务做了删除,不可见。

  • 下面三组trx_id为3,是当前事务的trx_id,因此可见,放入结果集。

所以本次快照读结果为6条,分别为事务一和二开启前的三条数据,加上事务二插入的三条数据。

【11】事务二提交。

后记

分析流程到这里就结束了,感谢在排查问题过程中跟我一起讨论问题的同学们,能遇到这种mysql实践的场景也不多,所以也感谢写出这个长事务的朋友:)。其实对于这个case每步操作的加锁情况也值得深入分析,包括实际上undolog是使用数据行上的roll_pointer指针来引用的等等这些原理,碍于篇幅都没有过多提及。

最终我也是在代码中将最后一个对于渠道的select查询加上了for update语句,强制进行一致性读,暂时可以解决这个问题(对于去除长事务是一个稍微大点的改造,暂时没有做)。不知道各位小伙伴有没有更好的办法,欢迎在评论区留言,文章中的错误提前感谢各位指正。

其他:长事物拆分、异步

mysql id还原_一次线上DB问题排查(MySQL、事务、MVCC)相关推荐

  1. 线上基础问题排查常用手册

    线上基础问题排查常用手册 问题分类 业务问题 日志排查 代码逻辑排查 配置排查 性能问题 接口问题 JVM问题 Redis问题 MySQL问题 系统问题 实施手段 日志排查 阿里云 参考: https ...

  2. Alibaba Cloud Toolkit 中SLS插件助力线上服务问题排查

    简介:Alibaba Cloud Toolkit 是一款非常优秀的插件,新增SLS日志服务的功能,针对软件开发者日常工作中常见的问题排查场景,将日志服务平台的功能集成到ide当中,省去了不同窗口之间来 ...

  3. 线上应用故障排查之二:高内存占用

    为什么80%的码农都做不了架构师?>>>    搞Java开发的,经常会碰到下面两种异常: 1.java.lang.OutOfMemoryError: PermGen space 2 ...

  4. Java线上应用故障排查之二:高内存占用

    前一篇介绍了线上应用故障排查之一:高CPU占用,这篇主要分析高内存占用故障的排查. 搞Java开发的,经常会碰到下面两种异常: 1.java.lang.OutOfMemoryError: PermGe ...

  5. Java线上应用故障排查之一:高内存占用

    Java线上应用故障排查之一:高内存占用 转载地址:http://www.blogjava.net/hankchen 搞Java开发的,经常会碰到下面两种异常: 1.java.lang.OutOfMe ...

  6. mysql连接池泄露_一次线上故障:数据库连接池泄露后的思考

    作者:陈朗,普兰金融科技能效工程部开发工程师 一:初步排查 早上作为能效平台系统的使用高峰期,系统负载通常比其它时间段更大一些,某个时间段会有大量用户登录.当天系统开始有用户报障,发布系统线上无法构建 ...

  7. mysql死锁的排查方法_MySQL死锁系列-线上死锁问题排查思路

    前言 MySQL 死锁异常是我们经常会遇到的线上异常类别,一旦线上业务日间复杂,各种业务操作之间往往会产生锁冲突,有些会导致死锁异常.这种死锁异常一般要在特定时间特定数据和特定业务操作才会复现,并且分 ...

  8. mysql mydump还原_用mydump对所有数据库进行备份,还原具体案例

    现有的测试数据: 对所有数据库做备份: C:\Users\Administrator>mysqldump -u root -p--all-databases> D:\mysql_bak\2 ...

  9. java商城并发_一次线上商城系统高并发优化,涨姿势了~

    对于线上系统调优,它本身是个技术活,不仅需要很强的技术实战能力,很强的问题定位,问题识别,问题排查能力,还需要很丰富的调优能力. 本篇文章从实战角度,从问题识别,问题定位,问题分析,提出解决方案,实施 ...

最新文章

  1. 中国陶瓷辊棒市场全景调查及供需格局预测报告2022-2028年版
  2. how to change logo in ae template
  3. Go image: unknown format 错误解决
  4. DecimalFormat 类
  5. jzoj3189-解密【字符串hash】
  6. 蓝桥杯基础模块8_2:串口进阶
  7. 付费社群聊天小程序V1.4.5+前端
  8. 使用Maven命令安装jar包到repo中
  9. Oracle学习之DATAGUARD(八) Switchover与failover
  10. python求圆周率马青公式_计算圆周率的马青公式
  11. dell R740secure boot_凯诺 10月11日 DELL 电脑报价
  12. GPS精确授时方法研究-基于ublox GPS
  13. 远程服务器下载百度网盘中的内容
  14. 网站漏洞测试分析查找问题攻防演练
  15. OpenCV C++案例实战五《答题卡识别》
  16. linux加载的驱动无法卸载,linux驱动加载后不能再卸载
  17. RK3229 android9.0 按刷机按键进入loader
  18. Linux系统ISO镜像文件下载地址
  19. R语言ggplot2可视化:使用patchwork包(直接使用加号+)将两个ggplot2可视化结果横向组合、接着再和第三个图像横向组合起来(三幅图各占比例为50%、25%、25%)
  20. Win10怎么优化网络?网络优化设置

热门文章

  1. ruby推送示例_Ruby直到示例循环
  2. java的equals方法_Java Vector equals()方法与示例
  3. 算法图解:如何找出栈中的最小值?
  4. Android 模拟器调试的缺点
  5. Python实现GCS bucket断点续传功能,分块上传文件
  6. 链表的基本操作 java_Java_实现单链表-基本操作
  7. 读芯术python教程答案_攻略Python的免费书单:走进编程,从这五本书开始
  8. 权限申请_Android 开发工程师必须掌握的动态权限申请,三步轻松搞定!
  9. python 输出纯音频_Python如何录制系统音频(扬声器的输出)?
  10. left join 重复数据_Python数据分析整理小节