国产精品电影_久久视频免费_欧美日韩国产激情_成年人视频免费在线播放_日本久久亚洲电影_久久都是精品_66av99_九色精品美女在线_蜜臀a∨国产成人精品_冲田杏梨av在线_欧美精品在线一区二区三区_麻豆mv在线看

MySQL 更新消失案

數據庫 MySQL
在一個平常的夜晚,收到業務反饋某個檢測單流程卡住,不能繼續往下走。接到反饋后,連忙上線排查。發現問題點在一個詭異的現象 —— 數據庫更新消失:?在事務中,先更新一行記錄,后讀取該行,讀取到竟為更新前的舊值!

1. 問題背景

2. 有什么場景可能會出現這樣的問題?

2.1 主從同步延遲?

2.2 事務沒有生效?

2.3 快照讀?

3. Update執行流程

3.1 源碼分析

3.2 啟發

4. 場景復現

4.1 表結構和數據

4.2 步驟

5. 總結

注: 本文使用的MySQL版本 5.7.44, 事務隔離級別為 REPEATABLE-READ。

1. 問題背景

在一個平常的夜晚,收到業務反饋某個檢測單流程卡住,不能繼續往下走。接到反饋后,連忙上線排查。發現問題點在一個詭異的現象 —— 數據庫更新消失: 在事務中,先更新一行記錄,后讀取該行,讀取到竟為更新前的舊值!

接口對應的業務邏輯大致有以下幾步:

  • 用戶選擇一批初始狀態為10的檢測單ID發起流轉;
  • 后端服務獲取檢測單對應的異常信息,將沒有異常的檢測單的狀態修改為 20。如果有異常的,則不修改狀態
  • 最后根據檢測單狀態和其他信息執行不同的業務邏輯。

以上步驟均在一個事務內執行。

假設第一步獲取到的檢測單id列表為[1001,1002],第二、第三步簡化流程如下:

# T0 select獲取檢測單的異常記錄
select id, exception_type, qc_order_id 
from qc_order_exception
where qc_order_id in (1001, 1002)


# T1 執行如下SQL,ID為1001, 1002更新檢測單記錄的狀態。  注: 更新前 status = 10
update qc_order
set status = 20
where id in (1001, 1002);        


# T2 執行如下SQL,通過ID獲取檢測單信息。 注: T1更新為satus=20的記錄,可能讀取出來的 status = 10
select id, status, 
from qc_order 
where id in (1001, 1002)


# T3 根據檢測單狀態,執行不同的業務邏輯。 
if (qcOrder.getStatus() == 20) { // 檢測單無異常
 log.info("邏輯1");         // 正常情況:1001、1002都執行這里邏輯。打印日志: "邏輯1"
} else {                  
 log.info("邏輯2");        // 偶發異常現象:1002走上面邏輯 1001走了這里的邏輯。 打印日志: "邏輯2"。但分明T1時刻ID = 1001這條記錄的status應該已經被更新為了20
}

接到業務反饋后,我們在 打印日志: "邏輯2" 的分支中,加了監控告警,觸發告警后,檢測單沒有后續的操作。這時去查看數據庫中檢測單的狀態,沒有異常的檢測單的狀態均為 status = 20(檢測單最后狀態是對的,但是在處理業務邏輯時,可能讀取出舊值 status=10)。

我們觀測日志發現,status被更新為20(即沒有異常)的一批檢測單,大部分情況,該批檢測單內所有檢測單執行打印日志: "邏輯1";小部分情況,比如上面舉例的[1001,1002],這批檢測單中,ID=1001的檢測單執行:打印日志: "邏輯2"。——出現了奇怪的“更新消失”現象!

遇到這種偶發性"更新消失"的場景,此時我們表情如下:

2. 有什么場景可能會出現這樣的問題?

2.1 主從同步延遲?

  • 如果 T2時刻,獲取數據庫記錄時是通過數據庫從節點讀取,可能因為主從同步延遲的原因,導致讀取到數據庫舊值
  • 為什么不是這個原因?

T2時刻,通過斷點com.alibaba.druid.pool.DruidPooledConnection(項目使用Druid連接池)的方法,確認T1和T2時刻執行SQL是用同一個數據源。所以不可能是主從同步延遲的原因。

2.2 事務沒有生效?

  • 如果事務沒有生效時,存在 A 事務修改記錄的status = 20,事務B又修改status = 10。如果沒有事務,事務A可以看到事務B的修改。
  • 為什么不可能是這個原因?

業務邏輯方法上有使用聲明式事務@Transctional,斷點后,確認線程棧中包含org.springframework.transaction.interceptor.TransactionInterceptor.invoke,可以判定所有操作均在同一個事務中執行。

在測試環境復現場景時,在事務未提交時,記錄修改,在其他事務不可見。

2.3 快照讀?

ReadView

事務級別為 REPEATABLE-READ 時,則事務第一次執行 SELECT 時會生成一個 ReadView ,后續所有讀操作復用該 ReadView。ReadView 的內容為:

  • m_ids:  在創建 Read View 時,當前數據庫中「活躍事務」的事務 id 列表,注意是一個列表,“活躍事務"指的是啟動了但還沒提交的事務。
  • min_trx_id: 在創建 Read View 時,當前數據庫中「活躍事務」中事務 id 最小的事務,也就是 m_ids 的最小值。
  • max_trx_id: 這個并不是 m ids 的最大值,而是創建 Read View 時當前數據庫中應該給下一個事務的id 值,也就是全局事務中最大的事務 id 值 +1。
  • creator_trx_id: 創建該 Read View 的事務的事務 id
快照讀流程

當需要讀取某行記錄流程大致如下:

  1. InnoDB 會先會定位到該行的最新版本(即當前數據頁中的最新記錄)。
  2. 遍歷 Undo Log 版本鏈找到一個當前 ReadView 可見的版本記錄: 從最新版本開始,沿著行記錄的DB_ROLL_PTR指針(指向舊版本數據的指針)遍歷 Undo Log 中的歷史版本鏈,通過行記錄的 DB_TRX_ID(最后修改該行記錄的事務ID)判斷是否滿足可見條件,條件如下:
  • 如果版本 DB_TRX_ID == creator_trx_id: 該版本是本事務修改的,可見。
  • 如果版本 DB_TRX_ID< min_trx_id: 該版本在 Read View 創建時已提交,可見。
  • 如果版本 DB_TRX_ID >= max_trx_id: 該版本在 Read View 創建后啟動,不可見。遍歷記錄的版本鏈,繼續查找更舊版本。
  • 如果版本 DB_TRX_ID 在 m_ids 列表中: 該版本對應的事務在 Read View 創建時仍活躍(未提交),不可見,繼續查找更舊版本。
  1. 如果找到滿足可見的版本,返回該版本數據。如果不滿足,則不返回數據。

如果在事務中執行了 Update 語句,則更新的行記錄的 DB_TRX_ID 應該更新為當前事務ID 。根據快照讀的流程,在更新后通過 Select 讀取的這條行記錄時,當前事務ID == DB_TRX_ID,這條行記錄應該被讀取出來,不應通過 DB_ROLL_PTR 讀取歷史版本數據。

但事故中,更新后偶發性地讀取到歷史版本的數據,是什么原因造成的呢?

苦思冥想后沒有頭緒,最終決定 Show code —— 嘗試從 MySQL 源碼中找到答案。

3. Update執行流程

3.1 源碼分析

閱讀MySQL源碼的Update SQL執行邏輯,從 mysql_update() 函數中看出端倪,整理后更新的簡化流程如下:

圖片?? MySQL 在更新記錄前,會通過 compare_records() 函數判斷行記錄更新前后是否發生了變化,如果行記錄沒有發生變化,則跳過更新。

compare_records() 怎么比對行記錄更新前后是否有變化的?

圖片圖片

  • 如果存儲引擎支持部分列讀取,則只比較需要更新的列的值是否相等,如果列可為NULL,需要額外比較NULL值是否相等。
  • 對于只支持完整讀取的存儲引擎:

判斷是否是固定長度的列?如果是,則直接通過 memcmp 字節比較是否相同。否則執行第2步。

判斷更新前后的數據,所有可為NULL的列的NULL值是否相同?如果不相同,直接返回數據不相同。否則執行第3步。

遍歷所有需要更新的列,比較更新前后值是否相同。

3.2 啟發

從流程圖中可以看出:

  • 如果行記錄更新前后沒有變化, 就不會調用存儲引擎執行實際更新行記錄邏輯, Update 語句可以正常執行沒有報錯。
  • 批更新記錄時,MySQL是逐行對比后,再調用存儲引擎執行更新。

那又為什么沒有數據變化呢?

  • 我們檢查了業務邏輯代碼,發現更新檢測單狀態時,并沒有對記錄加鎖,那很大可能是存在并發執行的其他流程修改了檢測單狀態,且兩個流程修改的檢測單狀態相同,導致數據執行前后沒有變化。

我們在測試環境構造下面場景來復現。

4. 場景復現

4.1 表結構和數據

create table qc_order_exception
(
    id             int      notnull primary key auto_increment comment'id',
    qc_order_id    int      notnullcomment'檢測單id',
    exception_type tinyintcomment'異常類型',
    create_time    datetime notnulldefaultcurrent_timestampcomment'創建時間',
    update_time    datetime notnulldefaultcurrent_timestamponupdatecurrent_timestampcomment'更新時間'
) engine = innodbcomment'檢測單異常記錄';


createtable qc_order
(
    id          int      notnull primary key auto_increment comment'id',
    status      int      notnullcomment'狀態',
    create_time datetime notnulldefaultcurrent_timestampcomment'創建時間',
    update_time datetime notnulldefaultcurrent_timestamponupdatecurrent_timestampcomment'更新時間'
) engine = innodbcomment'檢測單表';


insertinto qc_order(id, status)
values (1001, 10),
       (1002, 10);

4.2 步驟

記錄初始狀態:

select id, status, create_time, update_time
from qc_order;

圖片圖片

T0時刻(事務A): 開啟事務 &&  創建ReadView

begin;


select id, exception_type, qc_order_id
from qc_order_exception
where qc_order_id in (1001, 1002);
 -- 返回數據為空。

select trx_id, trx_state, trx_state, trx_mysql_thread_id, trx_query from information_schema.INNODB_TRX;

執行結果:

圖片圖片

  • 事務A的線程ID: 7,trx_id:  421164174274160。這是臨時的事務ID。

T1時刻(事務B): 開啟事務 &&  更新數據 id=1001 && 獲取事務信息 && 提交

begin;

update qc_order
set status = 20
where id = 1001;

select trx_id, trx_state, trx_state, trx_mysql_thread_id, trx_query from information_schema.INNODB_TRX;

commit;

執行結果:

圖片圖片

事務B中:

  • Update 返回: 匹配行數: 1, 更新行行數: 1 。 表示更新成功了。
  • select ...  from information_schema 返回的線程ID: 8,trx_id:4364

T2時刻(事務A): 更新數據 id in (1001,1002),設置 status = 20

update qc_order
set status = 20
where id in (1001, 1002);

select trx_id, trx_state, trx_state, trx_mysql_thread_id, trx_query from information_schema.INNODB_TRX;

執行結果:

圖片圖片

  • Update返回: 匹配行數: 2, 更新行行數: 1 。表示找到2條數據,但只更新一條,有的數據實際沒有執行更新。
  • 事務A的 trx_id 發生變更,trx_id:  4366。因為這里執行了DML操作,需要分配實際的 trx_id。

T3時刻(事務A): 查詢數據 id=1001

select id, status, create_time, update_time
from qc_order
where id in (1001, 1002);

執行結果:

圖片圖片

返回的 id=1001 數據, status = 10。

??注意:

  • 事務A的trx_id:4366
  • 事務B的trx_id:4364

使用 undrop-for-innodb 工具分析ibd文件,觀測qc_order表記錄的 DB_TRX_ID 。驗證是否事務A沒有更新記錄id=1001

undrop-for-innodb 是一款專為 InnoDB 存儲引擎設計的開源數據恢復工具,支持從文件級別恢復誤刪除、損壞或丟失的數據。也可以用來從ibd文件中解析出表的數據,其兼容 MySQL 5.7 版本。

執行如下步驟:

# 1. 關閉 MySQL,復制 qc_order的ibd文件到 ./data 目錄。需要確保MySQL參數, innodb_file_per_table=on
[root@localhost undrop-for-innodb]# ls -lh ./data/
total 96K
-rw-r-----. 1 root root 96K Aug 17 21:49 qc_order.ibd

# 2. 使用 stream_parser 進行拆頁
[root@localhost undrop-for-innodb]# ./undrop-for-innodb/stream_parser -f ./data/qc_order.ibd 
Opening file: ./data/qc_order.ibd
File information:

ID of device containing file:        64768
inode number:                     70389401
protection:                         100640 (regular file)
number of hard links:                    1
user ID of owner:                        0
group ID of owner:                       0
device ID (if special file):             0
blocksize for filesystem I/O:         4096
number of blocks allocated:            192
time of last access:            1755438550 Sun Aug 17 21:49:10 2025
time of last modification:      1755438550 Sun Aug 17 21:49:10 2025
time of last status change:     1755438825 Sun Aug 17 21:53:45 2025
total size, in bytes:                98304 (96.000 kiB)

Size to process:                     98304 (96.000 kiB)
All workers finished in 0 sec

# 會生成目錄:pages-qc_order.ibd/,其中 FIL_PAGE_INDEX/ 里是索引頁

# 3. 確定葉子頁文件名
[root@localhost undrop-for-innodb]# ls -lh ./pages-qc_order.ibd/FIL_PAGE_INDEX/
total 16K
-rw-r--r--. 1 root root 16K Aug 17 21:57 0000000000000049.page

# 4. 準備qc_order的DDL。
# undrop-for-innodb 的 c_parser 在解析 CREATE TABLE 時有個限制 —— 它的 SQL parser 只接受 簡化版的 DDL,不能有多余的引號或不支持的選項。

[root@localhost undrop-for-innodb]# cat ./qc_order.sql 
CREATE TABLE qc_order (
  id INT NOT NULL,
  status INT NOT NULL,
  create_time DATETIME NOT NULL,
  update_time DATETIME NOT NULL,
  PRIMARY KEY (id)
) ENGINE=InnoDB DEFAULT CHARSET=latin1 ROW_FORMAT=Dynamic;

# 5. 用 c_parser 解析葉子頁。數據輸出到 dump.tsv
[root@localhost undrop-for-innodb]# ./undrop-for-innodb/c_parser -6f ./pages-qc_order.ibd/FIL_PAGE_INDEX/0000000000000049.page -t ./qc_order.sql > ./dump.tsv 
SET FOREIGN_KEY_CHECKS=0;
LOAD DATA LOCAL INFILE '/root/undrop-for-innodb/dumps/default/qc_order' REPLACE INTO TABLE `qc_order` CHARACTER SET UTF8 FIELDS TERMINATED BY '\t' OPTIONALLY ENCLOSED BY '"' LINES STARTING BY 'qc_order\t' (`id`, `status`, `create_time`, `update_time`);
-- STATUS {"records_expected": 2, "records_dumped": 2, "records_lost": false} STATUS END

# 6. 查看 dump.tsv 內容
[root@localhost undrop-for-innodb]# cat dump.tsv 
-- Page id: 3, Format: COMPACT, Records list: Valid, Expected records: (2 2)
00000000110C	2A0000013C0323	qc_order	1001	20	"2025-08-17 17:04:35.0"	"2025-08-17 17:06:57.0"
00000000110E	2B0000013D0403	qc_order	1002	20	"2025-08-17 17:04:35.0"	"2025-08-17 17:07:42.0"
-- Page id: 3, Found records: 2, Lost records: NO, Leaf page: YES

# 第一列為記錄的 DB_TRX_ID,以十六進制輸出,也就是我們需要找的數據了。轉換為 10進制:
00000000110C ->  4364    (id=1001)
00000000110E ->  4366    (id=1002)

DB_TRX_ID = 4364,就對應事務B的 trx_id。DB_TRX_ID = 4366,就對應事務A的 trx_id。可以表明事務B 更新了id=1001,事務A更新了 id=1002。

5. 總結

  • 事務中執行 Update 后為什么有時候不能讀到更新后的數據,只能讀到歷史數據?

如果在事務A創建了 ReadView 后,Update語句執行前,存在其他流程并發的執行相同行記錄的更新并提交了事務,且兩個事務更新后的數據一致。當事務A執行 Update 語句時,MySQL會因行記錄更新前后數據沒有發生變化,跳過這條記錄的更新。

因為 MySQL 跳過了行記錄的更新,則行記錄的 DB_TRX_ID 不會更新為當前事務 A 的事務ID。Update 更新后,通過Select 讀取更新記錄,根據快照讀流程,這條記錄的最新版本的數據對當前事務A 是不可見的,那只能通過記錄的歷史版本鏈,讀取出創建ReadView開始前的數據返回。

  • 怎么避免 這種 "更新消失" 場景?

在事務開始前,使用 select ... for update 將待更新的記錄加鎖。避免事務期間,其他事務更新。

對于先更新后讀取的場景,可以將更新的內容填充到后讀取的記錄中。

采用樂觀鎖機制,記錄增加 version 字段,每次更新的時候:version = version + 1。

參考:

關于作者

曾明,轉轉履約中臺研發工程師,主要負責質檢業務。

責任編輯:武曉燕 來源: 轉轉技術
相關推薦

2018-05-03 08:53:47

Windows 功能系統

2010-06-01 16:26:43

MySQL無法遠程

2021-05-31 09:47:03

Windows10操作系統微軟

2010-05-13 10:47:44

MySQL數據庫查詢

2010-05-21 13:48:36

MySQL數據庫

2010-05-18 16:58:31

MySQL數據庫

2010-11-22 16:12:54

MySQL多表更新

2023-10-12 07:43:45

2025-11-10 08:36:00

2010-06-17 09:15:02

MySQL數據庫查詢

2010-06-11 12:32:57

MySQL數據庫查詢

2010-06-11 09:50:30

MySQL 服務器

2010-05-27 18:36:13

配置MySQL

2010-06-01 17:14:28

2013-08-20 09:48:59

2011-09-09 09:24:27

OracleMySQL

2023-08-28 06:52:38

主板北橋芯片

2020-12-09 07:45:12

Windows10AdobeFlash

2019-04-10 21:33:34

2023-01-11 07:20:27

編程能力人工智能
點贊
收藏

51CTO技術棧公眾號

亚洲欧美经典视频| 一区二区三视频| 欧美日韩爆操| 一区二区高清视频| 一区二区三区视频免费观看| 国产精品一区电影| 亚洲视频综合| www.com毛片| 欧美日韩国产首页| 精东粉嫩av免费一区二区三区| 中文字幕av一区二区三区人| 美州a亚洲一视本频v色道| 久久精品最新地址| 日韩理论在线观看| 九色porny自拍视频在线观看| 7777免费精品视频| 每日更新成人在线视频| 天天色综合天天色| 色www精品视频在线观看| 咪咪网在线视频| 91社区国产高清| 豆国产96在线|亚洲| 午夜在线观看91| 久久亚洲影音av资源网 | 依依成人综合视频| 国产精品秘入口| 久久精品在线视频| 蜜桃av一区二区| 日本新janpanese乱熟| 国产剧情在线观看一区二区| 日韩视频在线视频| 欧美激情1区| 中文字幕一区二区三区四区五区六区 | 久久青青视频| 国产成人精品日本亚洲专区61 | 国产精品一区二| 国产亚洲成年网址在线观看| 亚洲摸摸操操av| 国产亚洲美女久久| 欧美视频网站| 五月婷婷导航| 在线播放日韩欧美| 国产精品国码视频| 亚洲一本二本| 欧美激情视频网| 久久国产乱子精品免费女| 中文字幕高清在线观看| 性日韩欧美在线视频| 国产激情视频一区二区三区欧美| 日本一区视频| 91chinesevideo永久地址| www.日韩av| 久草在线视频资源| 91入口在线观看| 性欧美疯狂xxxxbbbb| 国产精品巨作av| 国产资源在线视频| 国产一区不卡在线| 中文字幕在线中文字幕日亚韩一区 | 2欧美一区二区三区在线观看视频| 日韩国产精品大片| 免费成人小视频| 欧美日韩国产一级二级| 97免费资源站| 精品嫩模一区二区三区| 国产福利电影在线观看| 色综合天天色| 激情视频极品美女日韩| heyzo久久| 国产一卡不卡| 国产日产精品一区二区三区四区的观看方式| 国产成人免费9x9x人网站视频| 中文在线视频| 婷婷六月激情| 91精品国产吴梦梦| 激情五月综合色婷婷一区二区| 午夜精品一区二区三区在线视| 亚洲成人三级在线| 动漫精品一区二区| 亚洲精品成a人| 欧美日在线观看| 欧美午夜激情小视频| 五月天一区二区三区| 亚洲人精选亚洲人成在线| 亚洲最色的网站| 国产精品一区二区在线播放 | 久久精品国产99国产精品| 亚洲女同另类| 91香蕉视频污在线| 久久九九影视网| 欧美一区二区三区日韩视频| 欧美午夜影院一区| 亚洲黄色尤物视频| 国产精品久久久久一区二区三区 | 久久久无码精品亚洲日韩按摩| 久久男人中文字幕资源站| 国产成人免费在线视频| 岛国av在线不卡| 中文字幕免费精品一区高清| 97视频在线观看免费高清完整版在线观看 | 一卡二卡欧美日韩| 日韩电影免费网址| 精品亚洲免a| av成人在线看| 99热国产在线中文| 国产黄大片在线观看画质优化| 天天操天天爽天天射| 一道精品一区二区三区| 精品一区久久| 久久久久久久有限公司| 亚洲伊人第一页| 国产精品乱码一区二区三区| 亚洲自拍av在线| 国产99午夜精品一区二区三区| 成人乱色短篇合集| 成人一区二区三区四区| 国产伦精品一区二区三区| 国产欧美一区二区三区不卡高清| 91九色视频导航| 国产精品吴梦梦| 国产精品区一区二区三含羞草| 久久亚洲精品欧美| 欧美一区二区视频在线播放| 4444亚洲人成无码网在线观看| 黄色www网站| 天天色综合社区| 偷拍自拍在线| 呦呦在线视频| 九九色在线视频| 免费欧美视频| 视频一区国产视频| 成人性色生活片| 黄色一区二区三区| 亚洲欧美制服中文字幕| 成人激情在线播放| xx欧美撒尿嘘撒尿xx| 韩日精品一区二区| 成人免费毛片嘿嘿连载视频…| 日韩欧美电影| 国产99久久久国产精品免费看| 日本一区二区免费在线| 日韩午夜中文字幕| 亚洲自拍小视频免费观看| 成人亚洲精品777777大片| 97人人爽人人澡人人精品| 午夜精品在线免费观看| 精品视频一二区| 国产丝袜一区| 国产成人精品三级| 精品无人国产偷自产在线| 91香蕉亚洲精品| 极品粉嫩饱满一线天在线| 中文字幕av一区二区三区人| 久久久噜噜噜久久中文字幕色伊伊| 在线视频你懂得一区| 国产精品露脸自拍| 免费男女羞羞的视频网站中文版| 青青草国产一区二区三区| 免费日韩av片| 欧美v亚洲v综合ⅴ国产v| 国产青春久久久国产毛片| 香蕉97视频观看在线观看| 95精品视频| 国模大尺度一区二区三区| 亚洲成色www8888| 五月婷婷综合色| 日韩成人综合网站| 不卡一二三区首页| 久久精品人人爽| 精品中文字幕av| 久久aimee| 亚洲第一主播视频| 成人免费xxxxx在线观看| 在线播放的av| 亚洲欧美日韩精品一区二区 | 涩涩涩在线视频| 成人黄色国产精品网站大全在线免费观看 | 国产在线一二三区| 美女在线观看视频一区二区| 欧美巨大黑人极品精男| 中文字幕第38页| 红桃视频国产精品| 亚洲三级黄色在线观看| 日韩中文字幕a| 亚洲激情不卡| 久久精品中文字幕电影| 992tv成人国产福利在线| 免费人成精品欧美精品| 欧美激情视频网址| 欧美r片在线| 秋霞国产午夜精品免费视频| 九九精品视频在线| 毛片在线视频| 亚洲天堂a在线| 六月婷婷久久| 日韩系列在线| 亚洲激情 国产| 国产一二三区在线视频| 国产日韩欧美综合一区| 日韩三级电影免费观看|