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

背景

在司机数据库中,有一张用于存储司机车型的表,暂且称之为表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'; 

9cf444f8a2beb4cb641b686b346cc4cf.png

发现问题

由于司机车型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检索到全部日志,根据不同事件发生的时间点,还原两次编辑发生的完整历程。因为问题主要是渠道重复,因此主要将目光集中在渠道相关的几次操作,时间线如下:

67de94de66abb8829700c6c9a8dfadcc.png

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

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

(1)建表、初始化数据

466a762164613425f9209b610e1755bc.png

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

5818f6b77ab09afbc0a03487ce2f2506.png

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

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

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

ca5891dec01101f6aa0e0e33d60c8f53.png

重新梳理流程图

ce55b5b9696f33d13bdfb9150be5e2cf.png

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

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

66891fd26133d1b3d1ad0ad2da8fd16b.png

可以看到第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数据行上的隐藏列)

78bb22246d112b5b931a12fd54bb3c78.png

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

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

  • 将trx_id设置为2

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

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

fc0a2d76bd33ab5f664038e72c8f445a.png

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

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

68b57aef4a9016a29bb6cee0f78b1334.png

【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操作不对它们做操作)

c991e37c6240e5a5eba45328139c26c1.png

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

54f80e6e3d60d4a7a083704147e22c79.png

【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语句,强制进行一致性读,暂时可以解决这个问题(对于去除长事务是一个稍微大点的改造,暂时没有做)。不知道各位小伙伴有没有更好的办法,欢迎在评论区留言,文章中的错误提前感谢各位指正。

其他:长事物拆分、异步

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/558095.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

mysql 可以用多个索引_mysql索引合并:一条sql可以使用多个索引

前言mysql的索引合并并不是什么新特性。早在mysql5.0版本就已经实现。之所以还写这篇博文&#xff0c;是因为好多人还一直保留着一条sql语句只能使用一个索引的错误观念。本文会通过一些示例来说明如何使用索引合并。什么是索引合并下面我们看下mysql文档中对索引合并的说明&am…

mysql三种引擎_MySQL常见的三种存储引擎

原文连接&#xff1a;https://www.cnblogs.com/yuxiuyan/p/6511837.htmlhtml简单来讲&#xff0c;存储引擎就是指表的类型以及表在计算机上的存储方式。数据库存储引擎的概念是MySQL的特色&#xff0c;Oracle中没有专门的存储引擎的概念&#xff0c;Oracle有OLTP和OLAP模式的区…

django win下安装mysql_python测试开发django-8.windows系统安装mysql8教程

前言MySQL 是最流行的关系型数据库管理系统&#xff0c;可以在本地搭建一个mysql的环境&#xff0c;便于学习。windows7/windows10mysql-8.0.11-winx64下载安装包也可以在此页面【https://dev.mysql.com/downloads/file/?id476233】&#xff0c;进入页面后可以不登录。后点击底…

php mysql ajax 注册验证 实例_Ajax小实例   用户注册异步验证

简介51cto的程序员应该都有sina微博吧&#xff01;你会发现当你更改新浪会员名称&#xff0c;输入用户名Tab后&#xff0c;光标焦点移动到密码输出框时&#xff0c;用户名输出框的后面&#xff0c;就已经显示出了验证。验证信息是&#xff1a;你的用户名是否唯一&#xff0c;因…

同一事务中未提交的写能读到吗_03、MySQL事务的隔离性分析

事务可以用来保证数据库的完整性&#xff1a;要么都做&#xff0c;要么不做。在 MySQL 中&#xff0c;事务支持是在引擎层实现的。你现在知道&#xff0c;MySQL 是一个支持多引擎的系统&#xff0c;但并不是所有的引擎都支持事务。比如 MySQL 原生的 MyISAM 引擎就不支持事务&a…

python软件管理系统_conda:基于python的软件管理系统

python语言在发展的过程中&#xff0c;经历了python2到python3的迁移&#xff0c;对应的包也出现了多个版本。如何在一台服务器上同时安装不同版本的python,不同版本的package, 而且不互相干扰&#xff0c;是一个令人头痛的问题。为了解决这个问题&#xff0c;首先是virtualenv…

vscode远程Mysql数据库_windows系统vscode远程调试mysql

准备工作本文目标是可以从 WindowsVSCode 环境远程访问 Linux 的 mysql源码, 以及执行 GDB 远程调试首先准备以下软件在本地机器上:安装VS Code目前最新版本支持 Remote - Development 插件安装 VSCode 扩展 “Remote - Development”, 方法是左下角管理(⚙) ->扩展, 直接搜…

mysql免安装出现1067_mysql,免安装,1067错误

设置密码关闭正在运行的MySQL。2.打开DOS窗口&#xff0c;转到mysql\bin目录。3.输入mysqld --skip-grant-tables回车。如果没有出现提示信息&#xff0c;那就对了。4.再开一个DOS窗口(因为刚才那个DOS窗口已经不能动了)&#xff0c;转到mysql\bin目录。5.输入mysql回车&#x…

python外汇兑换代码_python爬取人民币汇率中间价

python爬取人民币汇率中间价&#xff0c;从最权威的网站中国外汇交易中心。首先找到相关网页&#xff0c;解析链接&#xff0c;这中间需要经验和耐心&#xff0c;在此不多说。以人民币兑美元的汇率为例(CNY/USD)&#xff0c;脚本详情如下&#xff1a;windows&#xff1a;# -*- …

python捕捉线程错误_Pythonrequests多线程抓取出现HTTPConnectionPoolMaxretiresexceeded异常...

问题&#xff1a; Python requests 多线程抓取 出现HTTPConnectionPool Max retires exceeded异常描述:主要代码如下&#xff1a;import threadingimport requestsdef get_info():try:res requests.get(http://www.xxx.com/test/json)if res.status_code 200 and res.text ! …

java io 文件路径_【IO流】java中文件路径(相对路径、绝对路径)相关类及方法...

1. URL菜鸟教程&#xff1a;Java URL处理 通常推荐对http等协议进行使用&#xff0c;若操作(file:)部分属性将无意义&#xff0c;建议getFile()。相关&#xff1a;URLConnections 类 可以打开连接进行IO 通过getResource方法可以获得相对于classPath的文件的URL2. FileTestvoid…

java classpath顺序_JVM中类加载顺序及classpath简介

昨天&#xff0c;看一个build Standalone中databrusher的一个脚本&#xff0c;发现一个Java类似乎没有在classpath中&#xff0c;好像也可一直运行了。很疑惑&#xff0c;问了对应的开发同学&#xff0c;然后自己好好看了下它的代码&#xff0c;才知道了原理。命令是&#xff1…

java 过滤脚本_【快学SpringBoot】过滤XSS脚本攻击(包括json格式)

XSS攻击是什么XSS攻击全称跨站脚本攻击&#xff0c;是为不和层叠样式表(Cascading Style Sheets, CSS)的缩写混淆&#xff0c;故将跨站脚本攻击缩写为XSS&#xff0c;XSS是一种在web应用中的计算机安全漏洞&#xff0c;它允许恶意web用户将代码植入到提供给其它用户使用的页面中…

java高级教程_Java高级教程02

1.Java线程1.1. 多线程和多进程多进程&#xff1a;操作系统能够同时进行多个任务&#xff1a; 每个app(word,播放器&#xff0c;浏览器)可以同时运行多线程&#xff1a;同一应用程序中哟多个顺序流同时执行线程是进程中的一部分1.2. 线程的执行过程&#xff1a;主要过程:多线程…

java 带宽控制_如何使用Java netty正确限制带宽使用?

对于我使用netty nio lib在Java中开发的下载客户端,我还实现了带宽限制功能.从技术上讲,我是通过GlobalTrafficShapingHandler对象完成的.基于这个类’JavaDoc我初始化nio客户端管道如下&#xff1a;...trafficHandler new GlobalTrafficShapingHandler(new HashedWheelTimer(…

【LeetCode刷题笔记】动态规划(二)

647. 回文子串 解题思路: 1. 暴力穷举 , i 遍历 [0, N) , j 遍历 [i+1, N] ,判断每一个子串 s[i, j) 是否是回文串,判断是否是回文串可以采用 对撞指针 的方法。如果是回文串就计数 +1

mysql数据库集群备份策略_mysql高可用方案之集群(cluster)

1.实验环境我用三台服务器搭建mysql cluster环境,sql节点和数据节点在同一服务器上,管理节点单独一台.cluster node1:192.168.1.102 data node1:192.168.1.102cluster node2:192.168.1.104 data node2:192.168.1.104manager node:192.168.1.19OS:centos 6.4 64位…

数据结构 排序 java_Java数据结构之排序---选择排序

简单选择排序的介绍&#xff1a;从给定的序列中&#xff0c;按照指定的规则选出某一个元素&#xff0c;再根据规定交换位置后达到有序的目的。简单选择排序的基本思想&#xff1a;假定我们的数组为int [] arr new int[n]&#xff0c;第一次我们从arr[0]~arr[n-1]中选择出最小的…

php和java 2017_Php与java的区别

Php与java的区别1.java和php技术层面对比:java是纯面向对象开发,功能强大,分支众多,没有java不能做的软件,PHP有他独特的领域,那就是WEB在这方面没有可以和他相比较,其与java相比较之下在这一方面基本上完胜java因其专注的领域不同所以没有太大可比性,PHP适合于快速开发,中小型…

python百度aip移动目标监控系统_python利用百度云接口实现车牌识别

一个小需求---实现车牌识别。目前有两个想法调云在线的接口或者使用SDK做开发(配置环境和编译第三方库很麻烦&#xff0c;当然使用python可以避免这些问题)自己实现车牌识别算法(复杂)&#xff01;一开始准备使用百度云文字识别C SDK来做&#xff0c;发现需要准备curl、jsoncpp…