公司內部分享之mysql邏輯框架

mysql的邏輯架構

首先咱們來看一下mysql的邏輯架構圖mysql

server層包括鏈接器,查詢緩存,分析器,優化器,執行器等,內置函數(例如時間函數,數學函數等)和存儲過程,觸發器,事務等都在這一層實現。redis

引擎層負責數據的存儲和提取,包括不少咱們經常使用的引擎如Innodb,MyISAM,Memory等。sql

鏈接器

咱們使用下面命令來鏈接數據庫:數據庫

mysql -uxxxx -pxxxx
複製代碼

鏈接器接收到命令後,完成以下操做:json

  • 判斷用戶名和密碼是否正確,若是錯誤,會報 "Access denied for user"的錯誤,而後結束本次會話。
  • 用戶名密碼正確,會去權限表中查看權限,而後把權限寫入這次鏈接的進程中。這意味在這鏈接成功後,即便你用管理員帳號對這個用戶作了修改,也不會影響這次鏈接的權限。

一個鏈接默認保存時間是8個小時,由參數wait_timeout決定。也就是說若是不操做,過了8小時鏈接器會自動斷開鏈接,再次操做,則會報 "LOST CONNECTION"的錯誤。數組

緩存

mysql拿到一個請求後會先看緩存中是否有,若是有則直接返回,若是沒有,再往下執行,執行完成後,把結果放入緩存,若是是複雜的查詢,效率會很是的高。 但mysql的緩存有一個很是大的問題: 只要對一個表更新,這個表上的全部緩存都會失效。 因此緩存的命中率很是低。在大多數狀況下,不建議使用緩存。緩存

mysql8.0以上版本直接將查詢緩存的整快功能都去掉了。性能優化

分析器

分析器主要是對sql語句進行解析,分析出關鍵字,讓mysql知道你要作什麼,若是sql語句有語法錯誤,會拋錯。bash

優化器

優化器主要目的是生成執行計劃。好比語句:服務器

SELECT a,b FROM t WHERE a=1 AND b=1
複製代碼
  • 能夠根據a索引,找到全部a=1的數據,而後再判斷b是否等於1
  • 也能夠根據b索引,找到全部b=1的數據,而後再判斷a是否等於1
  • 甚至能夠全表掃描,找出全部a=1 而且 b=1的數據

至於具體使用哪一種執行計劃,就是優化器根據效率最高來作判斷的了。

執行器

執行期在拿到執行計劃後,會先作一個權限的判斷,看用戶是否對錶有操做權限,若是沒有權限,會拋錯。若是有權限,就打開表調用引擎接口獲取數據。

小練習

mysql當中已經有了緩存,爲何咱們還要用redis,memcache等第三方的緩存呢?

定位分析sql語句

在工做中,咱們是否是有時會遇到查詢返回很是慢的狀況,那麼這種狀況如何定位慢sql,而且優化呢?

定位慢sql

定位慢sql有如下兩種方案:

  • 經過慢查詢日誌肯定慢查詢
  • 經過show processlist查看正在執行的查詢

慢查詢日誌

mysql慢查詢日誌是記錄執行時間超過設置的閥值的SQL語句,可使用以下命令來查看是否開啓

show variables like '%slow_query_log%';
複製代碼

默認慢日誌是未開啓狀態。

慢查詢日誌有四個比較關鍵的參數:

  • slow_query_log:是否開啓慢查詢日誌。
  • long_query_time:慢查詢日誌設置的時間閥值,超過這個閥值會被記錄到日誌中。
  • show_query_log_file: 慢查詢日誌記錄的文件
  • log_queries_not_using_indexes: 是否把沒有走索引的sql語句也記錄到日誌中。

在開啓慢查詢日誌以前,咱們先在表裏插入一下數據

drop table if exists `slow_log`;  
CREATE TABLE `slow_log` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `a` int(11) DEFAULT NULL,
  `b` int(11) DEFAULT NULL,
  PRIMARY KEY (`id`),
  KEY `a` (`a`)
) ENGINE=InnoDB;

drop procedure if exists slow_log_insert;
delimiter ;;
create procedure slow_log_insert()
begin
  declare i int;                    
  set i=1;                          
  while(i<=10000)do                 
    insert into slow_log(a,b) values(i,i);  
    set i=i+1; 
  end while;
end;;
delimiter ;
call slow_log_insert(); 

複製代碼

咱們來開啓一下慢查詢:

set global slow_query_log=1;  -- 在本會話中打開慢日誌
set global long_query_time=0.005; -- 在本會話中設置閥值時間爲5ms
set global slow_query_log_file='/tmp/mysql_slow.log'; -- 設置慢日誌的文件路徑
複製代碼

而後執行sql語句

select * from slow_log;
select * from slow_log where a = 5;
複製代碼

發現只有前一條sql語句記錄到了慢查詢日誌裏面

日誌中比較重要的參數以下:

  • Query_time: 執行時間
  • Lock_time: 等待表鎖時間
  • Rows_sent: 語句返回行數
  • Rows_examined:語句執行期間從存儲引擎讀取行數

固然在生成上,慢日誌中的內容會不少,咱們可使用mysqldumpslow 來對慢日誌進行分析和彙總。

查看正在查詢的慢查詢

有時候,慢查詢還在進行,但數據庫負載已經偏高了,這時候能夠用 show processlist 來找出慢查詢。 若是有PROCESS權限,能夠看到在執行的語句。若是沒有則只能看到本次會話中的執行語句。

咱們開啓兩個會話,而後執行下面語句

會話1 會話2
SELECT SLEEP(100)
SHOW PROCESSLIST

會話2的顯示結果以下:

這裏對幾個重要參數解釋一下:

  • id: 會話的id
  • Command: 如今會話的狀態
  • Time: 已執行的時間
  • Info:執行的語句

咱們可使用 "kill [query] id" 命令來終止執行

kill 27
或
kill query 27
複製代碼

"kill 27 和 kill query 27" 的區別在於"kill 27"是結束id爲27的會話。"kill query 27"表示結束會話27的本次操做,而保留會話27。

分析SQL語句

經過上面的兩個步驟咱們已經找到了慢sql語句,如今咱們要進行進行分析了,那麼如何分析SQL語句呢?咱們可使用如下三種工具進行分析:

  • explain 獲取mysql的執行計劃
  • show profile 獲取每一個環節mysql的執行時間
  • trace 查看優化器的執行計劃,獲取每種可能性所須要的代價

explain 獲取mysql的執行計劃

EXPLAIN SELECT * FROM slow_log WHERE a=1
複製代碼

執行結果以下:

咱們重點看如下以下幾個參數:

  • select_type 查詢類型
  • type 本次查詢表的鏈接類型
  • key 所用到的索引
  • rows 掃描的行數
  • Extra 其餘附加信息

當key爲空的時候表示沒有用到索引,能夠考慮優化了。 當Extra出現以下幾個狀況,也能夠考慮優化。

解釋 sql例子
Using filesort 是用外部排序,而非索引排序 EXPLAIN select b from slow_log order by b
Using temporary 建立了臨時表 EXPLAIN SELECT b FROM slow_log group by b order by null
Using join buffer (flat, BNL join) 關聯查詢中,被驅動表字段沒有索引 EXPLAIN SELECT * FROM slow_log AS s1 INNER JOIN slow_log AS s2 ON s1.b=s2.b

show profile 獲取每一個環節mysql的執行時間

有的時候,咱們須要確認究竟是哪一個環節出問題了,此時explain就不是那麼好用了。咱們須要使用show profile。

show profile使用步驟以下:

  1. 查看是否支持 profile:
SHOW VARIABLES LIKE '%profiling%';
複製代碼

  • have_profiling 表示支持 profile
  • profiling 表示暫未開啓 profile
  1. 咱們來開啓profile
SET profiling=1
複製代碼

上面的命令只是在本次會話中開啓,若是須要全局開的,能夠在命令中加上"GLOBAL"

SET GLOBAL profiling=1
複製代碼
  1. 執行sql語句
SELECT a FROM slow_log WHERE a=1
複製代碼
  1. 肯定sql的query id
SHOW PROFILES;
複製代碼

  1. 查看sql執行詳情
SHOW PROFILE FOR QUERY 2;
複製代碼

trace 查看優化器的執行計劃,獲取每種可能性所須要的代價

咱們使用explain能夠看到執行計劃,可是explain並不能告訴咱們爲何選擇了A方案而不是B方案。 咱們可使用trace來知道執行方案的細節。

ps:

  • trace只支持mysql5.6及以上版本。
  • 開啓trace會影響到服務器的性能,因此咱們通常只是在須要調試時開啓。

trace 使用步驟以下:

  • 打開trace
  • 執行sql語句
  • 獲取sql語句的執行計劃明細
  • 關閉trace

接下來咱們用一個例子來講明一下trace是怎麼使用的。 有下面一條sql語句:

SELECT a,b FROM slow_log WHERE a>8000; 
複製代碼

由於a上面有索引,按照大部分人的常識,應該會走a索引,可是咱們用explain工具查看,發現這條語句,居然使用了全表掃描。

那麼接下來咱們用trace來分析一下這條sql的細節。

1.打開trace,而且以json格式輸出

SET SESSION optimizer_trace="enabled=on",end_markers_in_json=on;    
複製代碼

2.執行sql語句:

SELECT a,b FROM slow_log WHERE a>8000; 
複製代碼

3.獲取結果:

SELECT * FROM information_schema.OPTIMIZER_TRACE
複製代碼

返回的結果以下:

{
  "steps": [
    {
      "join_preparation": {
        "select#": 1,
        "steps": [
          {
            "expanded_query": "/* select#1 */ select `slow_log`.`a` AS `a`,`slow_log`.`b` AS `b` from `slow_log` where (`slow_log`.`a` > 8000)"
          }
        ] /* steps */
      } /* join_preparation */
    },
    {
      "join_optimization": {
        "select#": 1,
        "steps": [
          {
            "condition_processing": {
              "condition": "WHERE",
              "original_condition": "(`slow_log`.`a` > 8000)",
              "steps": [
                {
                  "transformation": "equality_propagation",
                  "resulting_condition": "(`slow_log`.`a` > 8000)"
                },
                {
                  "transformation": "constant_propagation",
                  "resulting_condition": "(`slow_log`.`a` > 8000)"
                },
                {
                  "transformation": "trivial_condition_removal",
                  "resulting_condition": "(`slow_log`.`a` > 8000)"
                }
              ] /* steps */
            } /* condition_processing */
          },
          {
            "substitute_generated_columns": {
            } /* substitute_generated_columns */
          },
          {
            "table_dependencies": [
              {
                "table": "`slow_log`",
                "row_may_be_null": false,
                "map_bit": 0,
                "depends_on_map_bits": [
                ] /* depends_on_map_bits */
              }
            ] /* table_dependencies */
          },
          {
            "ref_optimizer_key_uses": [
            ] /* ref_optimizer_key_uses */
          },
          {
            "rows_estimation": [
              {
                "table": "`slow_log`",
                "range_analysis": {
                  "table_scan": {
                    "rows": 10337,
                    "cost": 2092.5
                  } /* table_scan */,
                  "potential_range_indexes": [
                    {
                      "index": "PRIMARY",
                      "usable": false,
                      "cause": "not_applicable"
                    },
                    {
                      "index": "a",
                      "usable": true,
                      "key_parts": [
                        "a",
                        "id"
                      ] /* key_parts */
                    }
                  ] /* potential_range_indexes */,
                  "setup_range_conditions": [
                  ] /* setup_range_conditions */,
                  "group_index_range": {
                    "chosen": false,
                    "cause": "not_group_by_or_distinct"
                  } /* group_index_range */,
                  "analyzing_range_alternatives": {
                    "range_scan_alternatives": [
                      {
                        "index": "a",
                        "ranges": [
                          "8000 < a"
                        ] /* ranges */,
                        "index_dives_for_eq_ranges": true,
                        "rowid_ordered": false,
                        "using_mrr": false,
                        "index_only": false,
                        "rows": 2000,
                        "cost": 2401,
                        "chosen": false,
                        "cause": "cost"
                      }
                    ] /* range_scan_alternatives */,
                    "analyzing_roworder_intersect": {
                      "usable": false,
                      "cause": "too_few_roworder_scans"
                    } /* analyzing_roworder_intersect */
                  } /* analyzing_range_alternatives */
                } /* range_analysis */
              }
            ] /* rows_estimation */
          },
          {
            "considered_execution_plans": [
              {
                "plan_prefix": [
                ] /* plan_prefix */,
                "table": "`slow_log`",
                "best_access_path": {
                  "considered_access_paths": [
                    {
                      "rows_to_scan": 10337,
                      "access_type": "scan",
                      "resulting_rows": 10337,
                      "cost": 2090.4,
                      "chosen": true
                    }
                  ] /* considered_access_paths */
                } /* best_access_path */,
                "condition_filtering_pct": 100,
                "rows_for_plan": 10337,
                "cost_for_plan": 2090.4,
                "chosen": true
              }
            ] /* considered_execution_plans */
          },
          {
            "attaching_conditions_to_tables": {
              "original_condition": "(`slow_log`.`a` > 8000)",
              "attached_conditions_computation": [
              ] /* attached_conditions_computation */,
              "attached_conditions_summary": [
                {
                  "table": "`slow_log`",
                  "attached": "(`slow_log`.`a` > 8000)"
                }
              ] /* attached_conditions_summary */
            } /* attaching_conditions_to_tables */
          },
          {
            "refine_plan": [
              {
                "table": "`slow_log`"
              }
            ] /* refine_plan */
          }
        ] /* steps */
      } /* join_optimization */
    },
    {
      "join_execution": {
        "select#": 1,
        "steps": [
        ] /* steps */
      } /* join_execution */
    }
  ] /* steps */
}
複製代碼

這個結果主要分爲三個部分:

  • join_preparation:準備階段,對應邏輯圖中的分析器
  • join_optimization:優化階段, 對應邏輯圖中的優化器
  • join_execution:執行階段,對應邏輯圖中的執行器

這裏咱們重點來看一下join_optimization的內容:

上面的表格中,rows和cost最爲重要

  • rows 預計掃描行數
  • costs 消耗的資料

因此咱們能夠得出

類型 掃描行數 消耗資源
全表掃描 10337 2092.5
使用"a"索引 2000 2401

咱們發現全表掃描比使用"a"索引消耗的資源少,因此mysql使用了全表掃描的方案。

4.關閉trace

SET SESSION optimizer_trace="enabled=off"
複製代碼

mysql索引

B+樹索引

你們都知道,在mysql中使用的最多索引就是B+樹索引,那麼今天咱們就來了解一下B+樹索引他的數據結構究竟是什麼樣子的。在介紹B+樹以前咱們先來聊一聊其餘咱們經常使用的索引。 好比咱們如今有數據[1,2,3,5,6,7,9]。

順序表

咱們先把這個數據放在順序表中。獲得以下結構圖:

接下來咱們要查找是否存在數字6, 可使用二分法查找。

他的時間複雜度爲O{logn}。查詢效率很是高。可是插入的效率就不是特別好了,每次插入都須要把後面的元素向後移動一位,而刪除則是把後面的元素向前一位,修改能夠看做是刪除和插入的合集操做。 插入數字4的邏輯結構圖以下:

刪除數字5的邏輯結構圖以下:

固然通常咱們遇到刪除操做,會作作邏輯刪除,把要刪除的數據設置爲null,內存先不釋放,等進行n此操做後再進行重建順序表。

因此由於順序表他的插入效率過低,最好是用來存儲那些一次插入不常變的或只作增量遞增的數據。

二叉樹

二叉樹特色:左節點的值小於根節點,右節點的值大於根節點,而且每一個節點共有兩課樹 咱們根據二叉樹的邏輯結構創建以下結構的二叉樹:

二叉樹的插入不像順序表那麼複雜,他只須要找到插入數字在樹中的位置,修改根節點和自身的索引便可。 舉個栗子: 咱們要插入數字8,我找到值爲7的節點,把7的右節點指向8,8的右節點指向9。

二叉樹的刪除也比較簡單,只須要把刪除節點左子樹最大節點或右子樹最小節點移上來代替本身便可。 舉個栗子: 咱們要刪除數字數字5,咱們能夠把3移動上來,或用6移動上來。

聊完了二叉樹的插入與刪除,咱們再來聊一下二叉樹的查詢,和層級相關,上圖中二叉樹的層級爲3,因此他查詢一個數字,最多隻要三次。可是二叉樹並不僅只有這一種創建方法,他只要知足「左節點的值小於根節點,右節點的值大於根節點,而且每一個節點共有兩課樹」就能夠,咱們來看一個比較極端的二叉樹結構圖。他的層級爲7,並且沒辦法使用二分法,只能逐個查找,效率就很是低了。

平衡二叉樹

上一節咱們知道,二叉樹的搜索和自身的層級有很大的關係,層級越少,檢索效率越高。咱們這裏引出了平衡二叉樹: 平衡二叉樹是一種二叉樹,其中每個節點的左子樹和右子樹的高度差之多等於1。而左子樹深度減去右子樹的深度的值稱爲平衡因子BF。 BF只能夠是(-1,0,1),若是不在這三個值的範圍內,則須要翻轉。 咱們來看一個平衡二叉樹的栗子,如今要構建用平衡二叉樹構建 [3,2,1,4,5]的數組:

剛開始插入「3」和「2」的時候咱們很正常的構建,到插入「1」後,發現3節點的平衡因子變成了2 須要調整,因而向右旋轉。再插入「4」,沒有發生變化,插入「5」時,「3」節點爲「-2」右不平衡了,因而向左旋轉。使樹繼續達到平衡。

上面的栗子是徹底平衡二叉樹(AVL),但平衡二叉樹維護樹平衡的效率太高,因此不少系統中採用紅黑樹,紅黑樹是AVL樹的改進版本。具體實現方法,這裏就不介紹了。

B樹

咱們前面討論的各類數據結構,處理樹都是在內存中,所以考慮的都是內存中的運算時間複雜度。但對於mysql而言,大部分數據是存放在硬盤上的,因此硬盤的讀取次數是影響性能的關鍵因素。對於通一個檢索,咱們讀取硬盤幾百次和讀取硬盤幾回是有本質差異的。 咱們以前所說的樹都只存放一個元素。當數據很是多的時候,樹一定會很是大,並且深度很是的深,使得讀硬盤的次數很是多,這是很是影響檢索效率的。因此這使咱們不得不打破一個節點只存一個元素的限制,這就是B樹的由來。 咱們用B樹來構建[1,2,3,5,6,7,9]:

PS:mysql存儲引擎每個節點默認大小是16k,是讀取磁盤一次的數據大小。這裏只是爲了說明數據結構而作了簡化。

B+樹

雖然咱們上面說了B樹的不少優勢,但B樹仍是有不少優化的空間,這裏咱們拿B+樹說明一下。

咱們來用B+樹來構建數組[1,2,3,5,6,7,9]。

咱們來分析一下B+樹相對於B樹有什麼不一樣點,以及有什麼優點:

  • B+樹全部的葉子節點存數據,非葉子節點只存key。使得非葉子節點能存儲更多的數據,使樹的層級更淺了,增長了查詢的效率
  • 各葉子節點用指針相連,提升了遍歷和範圍查詢的效率。

Innodb的索引類別

咱們先來建立一張表,而且插入一些數據:

drop table if exists t8; 
CREATE TABLE `t8` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`a` int(11) NOT NULL,
`b` char(2) NOT NULL,
PRIMARY KEY (`id`),
KEY `idx_a` (`a`)
) ENGINE=InnoDB  DEFAULT CHARSET=utf8mb4;

insert into t8(a,b) values (1,'a'),(2,'b'),(3,'c'),(5,'e'),(6,'f'),(7,'g'),(9,'i');
複製代碼

而後使用查詢語句,獲得以下結果:

SELECT * FROM t8
複製代碼

聚簇索引

「聚簇索引」又稱爲「主鍵索引」,主要是以下結構:

能夠看到,聚簇索引有兩個特色:

  • 根據主鍵按照B+樹的結構構建
  • 每一個葉子節點包含爭行數據

二級索引

「二級索引」也稱爲「普通索引」,主要結構以下:

能夠看到,二級索引有如下兩個特色:

  • 根據a建立B+樹結構
  • 葉子節點每一個字段保存本身和主鍵ID

回表

咱們用下面sql語句查詢數據:

SELECT a,b FROM t8 WHERE a=1
複製代碼

這條語句的執行順序以下:

  • 在二級索引中經過a=1查找出id=1
  • 而後再用id=1在聚簇索引中查找出(a=1,b=a)的數據 咱們把 回到主鍵索引樹搜索的過程,稱之爲回表

覆蓋索引

咱們把t8表的二級索引a,改爲(a,b)索引,二級索引結構以下:

alter table t8 drop index idx_a;
create index idx_ab on t8(a,b);
複製代碼

咱們再用下面的sql語句查詢數據:

SELECT a,b FROM t8 WHERE a=1
複製代碼

這條語句查詢順序以下

  • 再二級索引中經過a=1 找出(a=1,b=a)

咱們看,這裏只查詢了二級索引,沒有回表。 咱們把沒有回表的查找稱爲「覆蓋索引」。 由於覆蓋索引能夠減小查詢硬盤的次數,顯著提高性能,因此覆蓋索引是一個經常使用的性能優化手段。

小練習

在show_log表中,咱們執行sql語句:

SELECT a,b FROM slow_log WHERE a>8000; 
複製代碼

爲何全表掃描相比於"a"索引掃描的行數多,但消耗的資源反而少?如何優化?

快照讀在四種隔離級別中的區別

說明

mysql有兩種讀的方式,快照讀和當前讀。

  • 快照讀通俗講就是讀某一個時刻的數據,通常爲簡單的select操做(不包括 select ... lock in share mode, select ... for update)
  • 當前讀通俗將就是讀最新時刻的數據,操做包括select ... lock in share mode,select ... for update,insert,update,delete

mysql有四種隔離級別:

  • read uncommitted(讀未提交)
  • read committed(讀提交)
  • repeatable read(可重複讀)
  • serializable(串行化)

在這一章,咱們不考慮當前讀,重點分析一下快照讀在四種隔離級別的應用。 快照讀在四個隔離級別中應用,會存在三種讀的問題:

類型 說明
髒讀 事務A讀取了事務B未提交的數據。
不可重複讀 事務 A 屢次讀取同一數據,事務 B 在事務A屢次讀取的過程當中,對數據做了更新並提交,致使事務A屢次讀取同一數據時,結果 不一致。
幻讀 事務A首先根據條件索引獲得N條數據,而後事務B增添了M條符合A搜索條件的數據,致使事務A再次搜索發現有N+M條數據

接下來咱們就來分析一下這四種隔離級別和這三個讀問題的關係: 咱們先建立一張表:

DROP TABLE if exists `tran`;  
CREATE TABLE `tran` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `a` int(11) DEFAULT NULL,
  `b` int(11) DEFAULT NULL,  
  PRIMARY KEY (`id`)
) ENGINE=InnoDB;

INSERT INTO `tran`(a,b) VALUES(1,1);
複製代碼

而後咱們開啓兩個事務,分別執行以下sql語句:

事務A 事務B
設置隔離級別 設置隔離級別
BEGIN; BEGIN
SELECT b FROM `tran` WHERE a=1;
UPDATE `tran` SET b=2 WHERE a=1;
INSERT INTO `tran`(a,b) VALUES(1,3);
SELECT b FROM `tran` WHERE a=1; // 記爲VAL1
COMMIT;
SELECT b FROM `tran` WHERE a=1; // 記爲VAL2
COMMIT;

當VAL1中包含 b=2時,表示讀到了未提交的數據,有「髒讀」的問題。 當VAL2中包含 b=2時,表示兩次讀取的數據不一致,有「不可重複讀」的問題。 當VAL2中包含 b=3時,表示讀到了新插入的行,有「幻讀」的問題。

咱們使用下面語句來設置隔離級別

set session transaction isolation level [隔離級別];
// read uncommitted,read committed,repeatable read, serializable
複製代碼

在執行結束以後咱們可使用下面的sql語句來初始化表的狀態

TRUNCATE TABLE `tran`;
INSERT INTO `tran`(a,b) VALUES(1,1);
複製代碼

咱們分別設置四種不一樣的隔離級別,執行上面的sql,獲得以下的結果:

隔離級別 VAL1 VAL2 髒讀 不可重複讀 幻讀
read uncommitted 2,3 2,3 Y Y Y
read committed 1 2,3 N Y Y
repeatable read 1 1 N N N
serializable 1 1 N N N

PS:

  • 1.在mysql中 「repeatable read」的隔離級別是沒有幻讀的(網上有不少文章在這個知識點上有錯誤)
  • 2.在「read uncommitted(讀未提交)」,讀的始終是最新的數據,因此快照讀和當前讀是同樣的。
  • 3.在「serializable(串行)」隔離級別下,事務B執行到 「UPDATE」 語句時會阻塞住,必需要等到事務A 「COMMIT」 以後才能繼續執行。說明在「serializable」隔離級別下,事務會 在「SELECT」語句中加上了鎖,因此在此隔離級別中,不存在快照讀,全部的讀都是當前讀。

快照讀的實現原理

咱們在上一節瞭解到,mysql四個隔離級別中,只有RC和RR用到了快照讀。這一節咱們就來分析一下他們是怎麼實現的。

術語說明

在系統中,每一個事務都有惟一的事務id,叫作"transaction id",在事務開始的時候,是向系統申請的,是嚴格遞增的。

每一行數據,也會用多個版本。每次更新一個事務都會產生一個新的版本,而且把transaction id 賦值給當前數據,叫作"row tx_id"。固然舊的版本會保留。

這裏咱們使用上一節的"tran",裏面有一條數據(id,a,b)= (1,1,1),這裏的"row tx_id" = 10,存儲的邏輯圖以下:

如今咱們執行下面的修改語句:

UPDATE `tran` SET b=2 WHERE a=1; // "transaction id"=20
複製代碼

存儲的邏輯圖以下:

PS:方框裏面的就是undo log(回滾日誌),當事務失敗時,咱們作逆向操做,把undo log中的數據回填就去就能夠實現事務的回滾了。

在瞭解了innerdb的存儲邏輯以後,咱們來分析「RR」隔離級別。在開啓一個新事務的時候,事務會生成一個一致性視圖(Read-View)。 裏面主要包含三個四個參數:

  • transaction id: 本次事務的id
  • trx_list: 系統中活躍的事務數組
  • up_limit_id: trx_list中的最小事務數組id
  • low_limit_id: 當前系統已經建立過的最大事務+1

每一行的"row tx_id"在一致性視圖(Read_View)大概能夠分爲三種狀況

    1. "row tx_id" < up_limit_id: 落在綠色區間,表示已提交事務或當前本身的事務,這個數據是可見的。
    1. "row tx_id" >= low_limit_id:落在紅色區間,表示這個版本是未來事務生成的,不可見
    1. up_limit_id<="row tx_id"<low_limit_id: 落在黃色區間,此處分爲兩種狀況
    • 3.1. "row tx_id" 在 trx_list 中表示事務還未提交,不可見
    • 3.2. "row tx_id" 不在 trx_list 中表示事務已提交,可見

咱們仍是使用上一節裏的「tran」表,裏面有一條數據(id,a,b)= (1,1,1),這裏的"row tx_id" = 10,系統目前的事務id爲20,咱們執行以下語句:

事務A 事務B 事務C
Start transaction with consistent snapshot
Start transaction with consistent snapshot
UPDATE `tran` SET b=2 WHERE a=1;
SELECT b FROM `tran` WHERE a=1; // VAL1
COMMIT;
SELECT b FROM `tran` WHERE a=1; // VAL2
Start transaction with consistent snapshot
UPDATE `tran` SET b=3 WHERE a=1;
SELECT b FROM `tran` WHERE a=1; // VAL3
COMMIT;
SELECT b FROM `tran` WHERE a=1; // VAL4

咱們把上面的sql語句轉換成邏輯圖以下:

在圖中咱們能夠知道Read-View是在事務開始的時候生成的。 咱們能夠獲得Read-View以下:

  • transaction id: 21
  • trx_list: [20, 21]
  • up_limit_id: 20
  • low_limit_id: 22

邏輯分析以下:

  • 最初的數據,"row tx_id" = 10,小於 up_limit_id,表示已提交事務,走了「1」邏輯,可見
  • 事務A的事務id爲20,「row tx_id」也爲20,等於up_limit_id,而小於low_limit_id,但在trx_list中存在,表示事務未提交,因此走的是「3.1」邏輯,不可見
  • 事務C的事務id爲22,「row tx_id」也爲22,等於low_limit_id,表示是將來事務,因此走的是「2」邏輯,不可見

因此在RR隔離級別下面,VAL1,VAL2,VAL3, VAL4的值都爲「1」,

咱們再來分析一下RC隔離級別,RC隔離級別和RR隔離級別判斷邏輯是一致的,惟一區別是,RR隔離級別在事務開始的時候生成"Read-View", 而RC隔離級別是在每次「SELECT」語句時生成"Read-View"。

RC隔離級別邏輯圖以下:

WX20190926-201003.png

在獲取VAL1時 Read-View以下:

  • transaction id: 21
  • trx_list: [20, 21]
  • up_limit_id: 20
  • low_limit_id: 22

分析:

  • "row tx_id" = 20,在up_limit_id<="row tx_id" < low_limit_id,而且 在trx_list中,因此爲未提交事務走邏輯「3.1」,不可見
  • "row tx_id" = 10,"row tx_id"<up_limit_id,爲已提交事務,走邏輯「1」,可見。

獲取VAL2時 Read-View以下:

  • transaction id: 21
  • trx_list: [21]
  • up_limit_id: 21
  • low_limit_id: 22

分析:

  • "row tx_id" = 20,"row tx_id"<up_limit_id,爲已提交事務,走邏輯「1」,可見。

獲取VAL3時 Read-View以下:

  • transaction id: 21
  • trx_list: [21,22]
  • up_limit_id: 21
  • low_limit_id: 23

分析:

  • "row tx_id" = 22,在up_limit_id<="row tx_id" < low_limit_id,而且 在trx_list中,因此爲未提交事務走邏輯「3.1」,不可見
  • "row tx_id" = 20,"row tx_id"<up_limit_id,爲已提交事務,走邏輯「1」,可見。

獲取VAL4時 Read-View以下:

  • transaction id: 21
  • trx_list: [21]
  • up_limit_id: 21
  • low_limit_id: 23

分析:

  • "row tx_id" = 22,在up_limit_id<="row tx_id" < low_limit_id,而且 不在trx_list中,因此爲未提交事務走邏輯「3.2」,可見

因此在RC隔離級別下,咱們能夠得出結論: VAL1=1,VAL2=2,VAL3=2,VAL4=3。

參考文檔

慕課網 《一線數據庫工程師帶你深刻理解 MySQL》 s.imooc.com/W2749EM

極客時間 《MYSQL實戰45講》time.geekbang.org/column/intr…

《大話數據結構》

小錯誤修正

在公司作分享後,小夥伴raywang對於RR隔離級別下是存在幻讀,而且也給出了本身的例子,咱們仍是根據那張tran表,初始值仍是(a=1,b=1)

事務A 事務B
BEGIN;
BEGIN;
SELECT b FROM tran WHERE a=1;
INSERT INTO tran(a,b) VALUES(1,2);
COMMIT;
update tran set b=b+10 where a=1;
SELECT b FROM tran WHERE a=1; // VAL
COMMIT;

咱們能夠看到最終VAL結果有2個值 「11和12」,產生了幻讀

那麼爲何會幻讀?咱們來分析畫張邏輯圖分析一下

這裏的關鍵是update語句,把兩行數據都改了,使他們的tx_id爲事務A的id。 因此知足以前說的條件1(小於up_limit_id或爲本身自己時可見),因此能夠被查出來。

固然在RR隔離級別下面也一樣會出現不可重複讀的狀況。咱們依然根據那張tran表,初始值仍是(a=1,b=1),來看下面的例子:

事務A 事務B
BEGIN;
BEGIN;
SELECT a,b FROM tran WHERE id=1;
update tran set a=2 where id=1;
COMMIT;
update tran set b=2 where id=1;
SELECT a,b FROM tran WHERE id=1; // VAL
COMMIT;

咱們在事務A裏面只修改了b=2的值,並無修改a的值,但卻把B事務修改的a讀出來了。至於具體緣由和上面的同樣,這裏可交給各位讀者自行分析。 因此咱們能夠得出結論: 在RR隔離級別下,當一個事務修改了別的事務修改/新增過的數據時,可能會出現不可重複讀和幻讀。

最後,感謝 raywang 給出的很是寶貴的建議,使得此次分享更加的圓滿。

相關文章
相關標籤/搜索