[TOC]
# 丟失數據
旺財是數據庫村的一個程序, 小強也是。
數據庫村有個特點, 很多數據支持共享操作,多個程序可以同時讀寫,他們倆經常會為了讀寫同一個數據, 爭奪的不可開交。
這一天,當旺財和小強對同一個銀行賬戶A進行寫操作時候, 出現了這么一個錯誤:

看看, 本來旺財要加上的20元就丟掉了。
同樣的事情發生的多了, 他倆給這種情況起了一個名字,叫“丟失修改”, 其實說白了就是倆人都去寫一個數據, 一個人的數據把另外一個給覆蓋了。
村里的MySQL老頭兒說: “你們兩個小家伙,寫數據的時候連加鎖都不做,肯定會出大亂子!"
旺財說:“加什么鎖?”
“來來來, 我教你們一個排他鎖(Exclusive Lock) , 簡稱X鎖, 旺財你要寫數據了, 就把它用X鎖鎖住, 鎖住后,除非你釋放, 否則小強無法獲得X鎖。 這不就解決你們的問題了? ”
小強想了想, 就把上面的操作過程用X鎖改了一下:

旺財說:“果然不錯, 確實可以解決兩個人同時修改導致的問題。”
# 臟數據
小強說:“旺財, 我們約定,寫數據的時候都用X鎖吧?”
旺財說: “這沒問題, 可是X鎖只在寫數據的時候用, 我們讀數據是不用加鎖的, 我想起了一種情況, 你看看怎么辦?”

小強在旺財執行的途中讀了A的值, 但是旺財把對A的修改給回滾(Rollback)了, 這下小強尷尬了, 他讀到了臟數據。
“要不我們在讀取數據的時候也加個X鎖 ? ” 小強說。
“那樣太嚴格了, 就是讀一個數據啊, 值得嗎?”
“這樣吧, 我們再搞一個新的鎖出來, 專門用于共享數據的讀取, 就叫共享鎖(Share lock) ,簡稱S鎖, 這個鎖和之前的排他鎖X鎖有區別, 主要用于讀取數據, 如果一個數據加了X鎖, 就沒法加S鎖, 同樣加了S鎖, 就沒法加X鎖” 小強想出了一個點子。
“那如果我加了S鎖, 你還能加S鎖嗎? ” 旺財問。
“應該可以吧, 咱們倆都是讀數據, 互不影響啊。 還有為了防止長時間的鎖住, 我們可以約定一下,不管我們要做的事情有多少, 讀一個數據之前加S鎖, 讀完之后立刻釋放該S鎖 ! ”

果然,這樣一來“臟數據”的問題就解決了 !
# 沒法重復讀?
旺財和小強兩個程序相安無事了很久, 但是S鎖在讀完數據后立刻釋放的約定, 導致出了一個新問題。
旺財在一次數據處理中, 先讀取了A和B的值, 相加得到了150 , 然后小強把B改成了30
旺財再次讀取A和B, 發現求和以后是130 , 剛才的不一樣了!
假定旺財的處理是在一個事務當中

旺財說: “小強, 我在讀取數據的時候你不能改啊 , 要不然我這里會出現不一致, 你看剛開始是A+B是 150, 現在變成130了”
小強說: “我們之前的約定是讀數據時加S鎖, 讀完立馬釋放, 問題就出現在這里了。”
“看來在讀數據的時候, 也需要一直鎖定了, 直到事務提交。”

# 幻覺出現
旺財和小強現在已經能靈活的使用X鎖和S鎖了。
他們倆總結了一下, 分為了這么幾種情況:
1. 寫數據時加上X鎖,直到事務結束, 讀的時候不加鎖。
雖然能夠避免丟失數據, 但是可以讀到沒有提交或者回滾的內容 (臟數據), 這其實就是數據庫最低的事務隔離級別 --- Read uncommitted
2. 寫數據的時候加上X鎖, 直到事務結束, 讀的時候加上S鎖, 讀完數據立刻釋放。
這能避免“丟失數據”和“臟數據”, 但是會出現“不可重復讀”的問題 , 這是第二級的事務隔離級別 -- Read committed
3. 寫數據的時候加上X鎖, 直到事務結束, 讀數據的時候加S鎖, 也是直到事務結束。
這能避免“丟失數據”和“臟數據”, “不可重復讀”三個問題 , 這是數據庫常用的隔離級別 --
Repeatable read
整個世界似乎清凈了。
有一次旺財對一個“學生表”進行操作,選取了年齡是18歲的所有行, 用X鎖鎖住, 并且做了修改。
改完以后旺財再次選擇所有年齡是18歲的行, 想做一個確認, 沒想到有一行竟然沒有修改!
這是怎么回事? 出了幻覺嗎?
原來就在旺財查詢并修改的的時候, 小強也對學生表進行操作, 他插入了一個新的行,其中的年齡也是18歲! 雖然兩個人的修改都沒有問題, 互不影響, 但從最終效果看, 還是出了事。
正是小強的操作, 讓旺財出現了“幻讀”
旺財說: “沒轍了, 我們倆非得串行執行不可, 你必須得等我執行完。 ”
這就是數據庫事務隔離級別的終極大招:**Serializable (串行化)**
最后, 為了方便記憶, 他們倆倒騰了半天, 整出了一張表, 用于記錄各種情況:

兩個人看著這張表, 感慨的說:“唉, 這數據庫村的事務隔離級別可真是不容易啊!”
# MVCC
旺財和小強使用了一段時間的“串行化”隔離級別,雖然不會出錯,但是效率實在太低了。數據庫村的人都笑話他倆干活太慢, 于是倆人商量著退到“可重復讀”,雖然會出現幻讀,但是也能忍受。
“可重復讀”用了一段時間,他們又不滿意了。
旺財唉聲嘆氣地說:“為了實現可重復讀, 我們需要在事務中對讀操作加鎖,并且得持續到整個事務結束,這實在是不爽啊!”
小強說:“是啊,我修改數據的時候,還得等待你讀完成,效率就太低嘍。”
許久不見的MySQL聽到他倆的抱怨,插嘴道:“看來你們兩個已經開始思考了啊,我有一個辦法, 可以在讀的時候不用加鎖,也能實現可重復讀。”
“你就吹吧!這怎么可能?” 旺財和小強根本不相信。
MySQL老頭兒說: “你們兩個太孤陋寡聞了,這個方法叫做MVCC(多版本并發控制)。”
頓了一下, MySQL老頭兒故意激他們:“可是有點難啊,你們倆不一定能弄明白。”
旺財和小強很不服氣:“說來聽聽!”
“假設啊,數據庫中有一個叫做users的表,里邊有這么一行數據:” MySQL老頭兒開始畫圖:

“現在,我要給他加兩個隱藏的字段:”

“事務ID? 是不是每次開始事務的時候分配的? ”
“沒錯,這個事務ID就表明這一行數據是哪個事務操作的,注意啊,事務ID是一個遞增的數字,每次開始新事務,這個數字就會增加。”
“這有什么用?”
“別急,馬上就會講到,” MySQL老頭兒地說:“ 旺財,小強,假設你們倆的事務中SQL的執行次序如下: ”

在標號為 (1) 的地方,數據是這樣的:

與此同時,需要建立一個叫做Read View的數據結構,它有三個部分:
(1) 當前活躍的事務列表 ,即`[101,102]`
(2) Tmin ,就是活躍事務的最小值, 在這里 Tmin = 101
(3) Tmax, 是系統中最大事務ID(不管事務是否提交)加上1。 在這里例子中,Tmax = 103
(注: 在可重復讀的隔離級別下,當第一個Read操作發生的時候,Read view就會建立。 在Read Committed隔離級別下,每次發出Read操作,都會建立新的Read view。)
旺財和小強還不知道有什么用處,只是死記硬背,生怕跟不上老頭兒的思路。
MySQL老頭兒接著說道: “在標號為(2)的地方,小強做了修改,數據是這樣的:”

“看到回滾指針沒有? 它指向了上一條記錄。”
“怪不得叫做多版本并發控制,你這里維護了數據的多個版本啊。” 小強感慨道。
“按照可重復讀的要求,我開始了一個事務,無論我讀多少次,我總是能讀到age=20的那行記錄,即使小強修改了age,我也不受影響。你這個結構該怎么實現啊? ” 旺財問道。
“關鍵部分要到了,我這里有個算法,用來判斷這些數據版本記錄中哪些對你來說是可見的(可讀的)。 ”

旺財只覺得覺得自己的頭嗡地一下就大了:“這....怎么這么麻煩!”
MySQL老頭說:“這還麻煩? 已經很簡單的算法了,就是幾個if else ,加上幾個循環而已! 連這個都整不明白,別在我們數據庫村混了! 對于上面的例子,ReadView 中事務列表是`[101,102]`, Tmin= 101, Tmax = 103,你們想想,第一次讀和第二次讀是什么樣子。”
只聽到小強嘴里嘟囔著:“ 當旺財第一次讀的時候,只有一條記錄, tid = 100 ,小于Tmin,所以是可以讀的。 然后我做了修改, 當旺財第二次讀的時候,tid=102,程序走到了‘tid是否在Read View中這一分支,由于102確實在Read View的活動事務列表中,那就順著回滾指針找到下一行記錄,即tid為100那一行,再次判斷,這就和第一次讀一樣了。”
MySQL老頭兒得意地說:“對嘍,這不就實現了可保證可重復讀嘛! 旺財你想想,你在讀數據的時候,需不需要加鎖操作?”
旺財搖頭:“不用加鎖, 我只要找到正確的版本就可以了。 ”
(注: 但是在寫數據的時候,MySQL還是要加鎖的,防止寫-寫沖突)
“這就是MVCC的好處啊,讀寫不互相等待,能極大地提高數據庫的并發能力啊。”
旺財還是有點不放心,覺得這種方式太復雜了,但是轉念一想,讀的時候不用加鎖,這個誘惑實在太大, 他說:“這樣吧,我和小強再合計合計。”
MySQL老頭兒自信地說:“沒問題,你們來再想想,有問題再找我吧。”
(注:本文講解了可重復讀的情況,對于Read Committed 這個可以適用同樣的算法,只是每次讀操作的時候,都要建立新的Read view,朋友們可自行分析下。)
- SQL
- 名詞
- mysql
- 初識mysql
- 備份和恢復
- 存儲引擎
- 數據表損壞和修復
- mysql工具
- 數據庫操作
- 增
- 刪
- 改
- 查
- 數據類型
- 整數類型
- 小數類型
- 日期時間類型
- 字符和文本型
- enum類型
- set類型
- 時間類型
- null與not null和null與空值''的區別
- 數據表操作
- 創建
- 索引
- 約束
- 表選項列表
- 表的其他語句
- 視圖
- sql增刪改查
- sql增
- sql刪
- sql改
- sql查
- sql語句練習
- 連接查詢和更新
- 常用sql語句集錦
- 函數
- 字符函數
- 數值運算符
- 比較運算符與函數
- 日期時間函數
- 信息函數
- 聚合函數
- 加密函數
- null函數
- 用戶權限管理
- 用戶管理
- 權限管理
- pdo
- 與pdo相關的幾個類
- 連接數據庫
- 使用
- pdo的錯誤處理
- pdo結果集對象
- pdo結果集對象常用方法
- pdo預處理
- 常用屬性
- mysql編程
- 事務
- 語句塊
- mysql中的變量
- 存儲函數
- 存儲過程
- 觸發器
- mysql優化
- 存儲引擎
- 字段類型
- 三范式和逆范式
- 索引
- 查詢緩存
- limit分頁優化
- 分區
- 介紹
- 分區算法
- list分區
- range范圍
- Hash哈希
- key鍵值
- 分區管理
- 特別注意
- 分表
- 數據碎片與維護
- innodb表壓縮
- 慢查詢
- explain執行計劃
- count和max,groupby優化
- 子查詢優化
- mysql鎖機制
- 介紹
- 演示
- 總結
- 樂觀鎖和悲觀鎖
- 扛得住的mysql
- 實例和故事
- 系統參數優化
- mysql體系結構
- mysql基準測試
- 索引
- mysql的復制
- win配置MySQL主從
- mysql5.7新特性
- 常見問題
- general log
- 忘記密碼
- uodo log與redo log
- 事務隔離級別
- mysql8密碼登錄
- explain
- 高效的Tree表
- on delete cascade 總結
- mongod
- 簡介
- 集合文檔操作語句
- 增刪改查
- 索引
- 數據導入和導出
- 主從復制
- php7操作mongod
- 權限管理
- redis
- redis簡介
- 3.2版本配置文件
- 3.0版本配置文件
- 2.8版本配置文件
- 配置文件總結
- 外網連接
- 持久化
- RDB備份方式保存數據
- AOF備份方式保存數據
- 總結
- win安裝redis和sentinel部署
- 事務
- Sentinel模式配置
- 分布式鎖
- 管道
- php中redis代碼
- 發布訂閱
- slowlog
- Redis4.0
- scan和keys
- elasticsearch
- 配置說明
- 啟動
- kibana
- kibana下載
- kibana配置文件
- kibana常用功能
- 常用術語
- Beats
- Beats簡介
- Filebeat
- Packetbeat
- Logstash
- 配置
- elasticsearch架構
- es1.7
- head和bigdesk插件
- 插件大全
- 倒排索引
- 單模式下API增刪改查
- mget獲取多個文檔
- 批量操作bulk
- 版本控制
- Mapping映射
- 基本查詢
- Filter過濾
- 組合查詢
- es配置文件
- es集群優化和管理
- logstash
- kibana
- es5.2
- 安裝
- 沖突處理
- 數據備份
- 缺陷不足
- 集群管理api
- 分布式事務
- CAP理論
- BASE模型
- 兩階段提交(2PC)
- TCC (Try-Confirm-Cancle)
- 異步確保型
- 最大努力通知型
- 總結