MySQL-lock(鎖)-v2.0

weixin_34337265發表於2018-10-18

概念

  • 對共享資源進行併發訪問

  • 保證資料的完整性和一致性

每個資料庫的鎖的實現是完全不同的。在MySQL中:

  • MyISAM是表鎖

  • InnoDB是行級鎖(和Oracle SQL Server的行鎖不一樣 (SQL Server有頁級別的鎖))

區別

要注意資料庫中的鎖(lock)與程式中的鎖(latch)的區別:

Lock Latch
物件 事務 執行緒
保護 資料庫內容 記憶體資料結構
持續時間 整個事務過程 臨界資源
模式 行鎖,表鎖,意向鎖 讀寫鎖,互斥量
死鎖 通過waits-for graph(等待圖), time out 等機制進行死鎖檢測和分析 無死鎖檢測與處理機制。僅通過應用程式加鎖的順序保證無死鎖的情況發生。
存在 Lock Manager的雜湊表中 每個資料結構的物件中

Tips:

  • 在應用程式中,如果使用互斥量mutex和讀寫鎖rw-lock,有個原則是它持有的時間越短越好,如果持有的時間太長,那說明程式不好
  • 進入臨界資源的時間應該越短越好,但資料庫中的鎖 鎖的是整個事務過程,鎖住的時間是根據事務時間而來的,資料庫中的鎖有 表鎖, 行鎖,意向鎖, 程式中的latch有讀寫鎖,互斥量, 應用程式中的latch沒有死鎖檢測。

MySQL中Latch的檢視:

mysql> show engine innodb mutex;
  +--------+------------------------+---------+
  | Type   | Name                   | Status  |
  +--------+------------------------+---------+
  | InnoDB | rwlock: log0log.cc:838 | waits=9 |
  +--------+------------------------+---------+
  1 row in set (0.10 sec)</pre>

Tips:通常來說,這些mutex沒有多大作用,主要是給核心DBA來看的。對於普通的DBA,要看的是資料庫中的鎖,Latch不是要關注的,只需要知道InnoDB也是有Latch的,因為一個程式肯定需要對共享資源進行併發 訪問,Latch是輕量級的鎖,持有的時間非常短,持有即釋放。

InnoDB中的鎖

  • S 行級共享鎖 表示可以併發進行訪問

  • X 行級排他鎖 有一個執行緒或會話佔用個這個行級別的鎖,那其他事務就不能去使用

  • IS意向S鎖 事務想要獲得一張表中某幾行的共享鎖

  • IX意向X鎖 事務想要獲得一張表中某幾行的排他鎖

  • AI(auto_increment)自增鎖 淘寶資料庫核心月報中關於這把鎖的介紹

Tips: 意向鎖揭示下一層級請求的鎖型別,即:下一層加一把什麼樣的鎖。 ​ InnoDB儲存引擎中的意向鎖都是表鎖

各種鎖的相容性
相容: o(無需等待) 互斥:x(需等待) s鎖 x鎖 ls鎖 lx鎖
s鎖 o x o x
x鎖 x x x x
ls鎖 o x o o
lx鎖 x x o o

意向鎖全部都是相容的(當前層是相容的,下一層再說),S和S鎖是相容的S和X鎖是不相容的。

如果表上加了一個X鎖,代表在表層級別進行加鎖,後面的執行緒都不能在下一層級別進行加鎖(IS,IX,S,X鎖都會發生等待)

資料庫加鎖過程是這樣的:

3401773-32d3e88e2a54748b.png
加鎖

如果想對row_id=1的記錄進行加鎖,就分別在庫,表,頁上加IS鎖,然後在記錄上加S鎖,加鎖時不是直接對記錄加鎖的,而是有一個樹的結構,對所有層次都進行加鎖,其他層次因為要加的不在它們上面,所以要加的不是S鎖,而是意向鎖,表示下一層級要加鎖的型別。

Q: 為什麼意向鎖之間是相互相容的
Q: 為什麼要設計意向鎖(為實現多粒度加鎖)
Q:為什麼不直接加鎖,而是有一個層級表示呢? 這是為了多層鎖的實現。

MySQL中沒有資料庫和頁級別的鎖,只有表鎖和記錄鎖,所以說,InnoDB中的意向鎖都是表鎖,加鎖都是從意向鎖開始加起的,不能直接加最低階別的鎖。

如果沒有意向鎖,現在想在表上加鎖,但是表鎖無法知道在下一層級記錄的情況.

如果沒有意向鎖機制,只有記錄鎖,那記錄鎖就是記錄鎖,如何實現表鎖呢?每一條記錄加鎖,但不能保證有其他事務在併發的使用這條記錄,不能實現表鎖。

鎖的資訊是儲存在記憶體中的。

innodb中的鎖都是表鎖,那給表加鎖時怎麼辦,直接加鎖嗎? 有些小小不一樣,mysql中的表鎖和innodb中的表鎖有些不一樣,通常來說,在innodb內部 通常不會在表級別加S,X鎖的. 加S鎖,在原始碼中有一個例子,加X鎖,在原始碼中沒有找到例子,什麼時候會在表級別加S鎖呢? 線上修改 (表鎖是在上層實現,不是在引擎層實現的)通常來說都是在表級別加意向鎖的,表級別是不會發生等待的,但是有種特別情況:alter table 並且要對錶加一個索引,add index建立索引的時候會對錶加一個S鎖,如果在加索引的過程中,有其他事務是IS鎖,要對一條記錄進行查詢,是可以執行的,但是如果有另外一個執行緒,執行要對記錄加X鎖的,S和IX在表層級互斥,就要發生等待 之前mysql加索引過程中,可以發現加索引對讀的操作是沒有影響的,但寫的事務就得等待了。這就是在innodb中,唯一在表級別加S鎖的情況:alter table add index . (因為索引要排序)

MySQL 如何加鎖

鎖的查詢:

> SHOW ENGINE INNODB STATUS;
 mysql> desc lock_test_1;
  +-------+---------+------+-----+---------+-------+
  | Field | Type    | Null | Key | Default | Extra |
  +-------+---------+------+-----+---------+-------+
  | a     | int(11) | NO   | PRI | NULL    |       |
  | b     | int(11) | YES  |     | NULL    |       |
  | c     | int(11) | YES  |     | NULL    |       |
  +-------+---------+------+-----+---------+-------+
  3 rows in set (0.00 sec)
  ​
  mysql> select * from lock_test_1;
  +---+------+------+
  | a | b    | c    |
  +---+------+------+
  | 1 |    4 |    3 |
  +---+------+------+
  1 row in set (0.00 sec)
  ​
  mysql> begin;
  Query OK, 0 rows affected (0.00 sec)
  ​
  mysql> select * from lock_test_1 where a = 1 for update;  #加排它鎖
  +---+------+------+
  | a | b    | c    |
  +---+------+------+
  | 1 |    4 |    3 |
  +---+------+------+
  1 row in set (0.01 sec)
  ​
  mysql> select * from lock_test_1 where a = 1 lock in share mode;  #加共享鎖
  ​
  mysql> pager less
  mysql> show engine innodb status\G\c
  ...
  ---TRANSACTION 3353, ACTIVE 3045 sec
  2 lock struct(s), heap size 1136, 1 row lock(s)
  MySQL thread id 9, OS thread handle 123145543925760, query id 133 localhost root
  TABLE LOCK table `test_mysql`.`lock_test_1` trx id 3353 lock mode IX
  RECORD LOCKS space id 46 page no 3 n bits 72 index PRIMARY of table `test_mysql`.`lock_test_1` trx id 3353 lock_mode X locks rec but not gap
  Record lock, heap no 2 PHYSICAL RECORD: n_fields 5; compact format; info bits 0
   0: len 4; hex 80000001; asc     ;;          #記錄
   1: len 6; hex 000000000d17; asc       ;;    #事務ID
   2: len 7; hex b4000001280110; asc     (  ;;    #回滾指標
   3: len 4; hex 80000004; asc     ;;          #記錄
   4: len 4; hex 80000003; asc     ;;          #記錄
  ...                                        
  ​
  mysql> set  global innodb_status_output_locks=1;
  mysql> show variables like "%wait%timeout%";
  +--------------------------+----------+
  | Variable_name            | Value    |
  +--------------------------+----------+
  | innodb_lock_wait_timeout | 50       |  # 鎖超時的設定變數(如果一個事務加不上鎖,會發生超時)
  +--------------------------+----------+
  3 rows in set (0.00 sec)

mysql5.6版本已經不會對錶加S鎖了,5.6出現online DDL功能的支援 對於很多DDL操作都是線上的了,讀寫都是可以的。

InnoDB有幾張源資料表(在information_schema中)儲存了事務和鎖的資訊

  • innodb_trx(檢視執行的事務),
mysql> select * from innodb_trx\G
  *************************** 1. row ***************************
                      trx_id: 3354
                   trx_state: RUNNING
                 trx_started: 2018-10-27 15:41:07
       trx_requested_lock_id: NULL
            trx_wait_started: NULL
                  trx_weight: 2
         trx_mysql_thread_id: 10
                   trx_query: NULL
         trx_operation_state: NULL
           trx_tables_in_use: 0
           trx_tables_locked: 1
            trx_lock_structs: 2
       trx_lock_memory_bytes: 1136
             trx_rows_locked: 1
           trx_rows_modified: 0
     trx_concurrency_tickets: 0
         trx_isolation_level: REPEATABLE READ
           trx_unique_checks: 1
      trx_foreign_key_checks: 1
  trx_last_foreign_key_error: NULL
   trx_adaptive_hash_latched: 0
   trx_adaptive_hash_timeout: 0
            trx_is_read_only: 0
  trx_autocommit_non_locking: 0
  1 row in set (0.00 sec)</pre>
  • innodb_locks(持有的鎖)
mysql> select * from innodb_locks\G
  *************************** 1. row ***************************
      lock_id: 3355:46:3:2
  lock_trx_id: 3355
    lock_mode: S
    lock_type: RECORD
   lock_table: `test_mysql`.`lock_test_1`
   lock_index: PRIMARY
   lock_space: 46
    lock_page: 3
     lock_rec: 2
    lock_data: 1
  *************************** 2. row ***************************
      lock_id: 3354:46:3:2
  lock_trx_id: 3354
    lock_mode: X
    lock_type: RECORD
   lock_table: `test_mysql`.`lock_test_1`
   lock_index: PRIMARY
   lock_space: 46
    lock_page: 3
     lock_rec: 2
    lock_data: 1
  2 rows in set, 1 warning (0.00 sec)</pre>
  • innodb_lock_waits(事務等待的事務)
mysql> melect * from innodb_lock_waits\G
  *************************** 1. row ***************************
  requesting_trx_id: 3355
  requested_lock_id: 3355:46:3:2
    blocking_trx_id: 3354
   blocking_lock_id: 3354:46:3:2
  1 row in set, 1 warning (0.00 sec)</pre>

建議用這三張表來檢視當前事務的鎖。(5.5 -5.6 版本中有的表)

  SELECT
    r.trx_id waiting_trx_id,
  r.trx_mysql_thread_id waiting_thread, 
    r.trx_query waiting_query,
  b.trx_id blocking_trx_id, 
  b.trx_mysql_thread_id blocking_thread, 
    b.trx_query blocking_query
  FROM information_schema.innodb_lock_waits w INNER JOIN information_schema.innodb_trx b ON b.trx_id = w.blocking_trx_id
  INNER JOIN information_schema.innodb_trx r ON r.trx_id = w.requesting_trx_id
  ​
  *************************** 1. row ***************************
   waiting_trx_id: 281479629835840
   waiting_thread: 11
    waiting_query: select * from lock_test_1 where a = 1 lock in share mode
  blocking_trx_id: 3356
  blocking_thread: 10
   blocking_query: NULL
  1 row in set, 1 warning (0.00 sec)
  ​
  # 缺點:看不到阻塞的SQL語句

MySQL 5.6開始還有個更牛逼的元資訊庫:

mysql> select * from innodb_lock_waits\G
  *************************** 1. row ***************************
                  wait_started: 2018-10-27 16:12:42
                      wait_age: 00:00:09
                 wait_age_secs: 9
                  locked_table: `test_mysql`.`lock_test_1`
                  locked_index: PRIMARY   # InnoDB 的鎖都是在索引上面
                   locked_type: RECORD
                waiting_trx_id: 281479629835840
           waiting_trx_started: 2018-10-27 16:12:42
               waiting_trx_age: 00:00:09
       waiting_trx_rows_locked: 1
     waiting_trx_rows_modified: 0
                   waiting_pid: 11
                 waiting_query: select * from lock_test_1 where a = 1 lock in share mode
               waiting_lock_id: 281479629835840:46:3:2
             waiting_lock_mode: S
               blocking_trx_id: 3356
                  blocking_pid: 10
                blocking_query: NULL
              blocking_lock_id: 3356:46:3:2
            blocking_lock_mode: X
          blocking_trx_started: 2018-10-27 16:08:16
              blocking_trx_age: 00:04:35
      blocking_trx_rows_locked: 1
    blocking_trx_rows_modified: 0
       sql_kill_blocking_query: KILL QUERY 10
  sql_kill_blocking_connection: KILL 10
  1 row in set, 3 warnings (0.01 sec)</pre>

遺憾的是在MySQL中,鎖的歷史資訊是看不到的。

鎖與事務隔離級別

先看四個概念:

  • locking

  • concurrency control

  • isolation

  • serializability

這四個概念在資料庫中是一個概念:鎖是用來實現併發控制的,併發控制用來實現隔離級別,同樣隔離級別是通過鎖來控制的,而鎖的目的是為了使得事務之間的執行時序列化的(序列化)。

併發控制準則:併發不能導致程式出錯, 不能導致吞吐率降低或者響應時間更快(Concurrent execution should not have lower throughput or much higher response times than serial execution.)

事務隔離級別

隔離性 : 多個事務並行執行,好似是序列執行的;一個事務所做的修改對其他事務是不可見的,好似是序列執行的。

事務隔離級別有四種:

  • READ UNCOMMITTED

  • READ COMMITTED

    • Oracle、DB2、SQL Server的預設事務隔離級別...
  • REPEATABLE READ

  • SERIALIZABLE

Oracle支援SERIALIZABLE和READ COMMITTED

SQL Server 和 MySQL四個級別都支援

但這樣沒有解決事務可能存在的問題,如:

在不同的事務隔離級別下,存在不同的問題:如:不可重複讀, 幻讀,髒讀.

髒讀 : 能讀到未提交事務的狀態.

不可重複讀:一個事務裡執行兩條相同的sql 對同一記錄執行結果不同

幻讀( phantom read) 連續執行兩次同樣的SQL語句可能導致不同的結果,第二次的SQL語句可能會返回之前不存在的行

在標準的事務隔離級別中:

  • READ UNCOMMITTED 三種問題都存在

  • READ COMMITTED 解決了髒讀問題

  • REPEATABLE READ 解決了髒讀和不可重複讀問題

  • SERIALIZABLE 是標準的隔離級別,解決了髒讀,幻讀,不可重複讀 問題。

MySQL的InnoDB在RR級別解決了幻讀問題(使用了next-key lock algorithm)。

舉例:

 # 首先檢視事務隔離級別
  mysql> select @@global.transaction_isolation;
  +-------------------------+
  | @@transaction_isolation |
  +-------------------------+
  | READ-UNCOMMITTED        |
  +-------------------------+
  1 row in set (0.00 sec)
  select @@global.tx_isolation, @@tx_isolation; </pre>
髒讀

在一個session中,顯示:

3401773-589c437ba02f6c1d.png
image.png

在另一個程式中,開啟一個事務:

3401773-1aa1b8f7c2a43e0e.png
image.png

未提交的資料,另一個session卻可以讀到:


3401773-e8ab5a108b4305e1.png
image.png

髒讀 : 能讀到未提交事務的狀態.

不可重複讀:

一個session中:

3401773-f1f4708a14187023.png
image.png

此時在另一個session中:

3401773-ca66d0055aa2e86c.png
image.png

此時在第一個session中:

3401773-9ff75c5701fd654d.png
image.png

不可重複讀:一個事務裡執行一條相同的sql對同一記錄執行結果不同

幻讀

一個事務中:連續執行兩次 select * from t where a <= 30 for update
會得到不同的結果

幻讀( phantom read) 連續執行兩次同樣的SQL語句可能導致不同的結果,第二次的SQL語句可能會返回之前不存在的行

比如:

事務1: SELECT*FROM tb_a WHERE A<=13; 對於READ COMMITTED,返回的是10,11,13 此時事務2 插入12,再執行sql語句,得到的是10,11,12,13,兩次返回的結果不同,12是之前沒有的,這就叫做幻讀,在InnnoDB中,鎖住的是(-∞,10],(10,11],(11,13]這三個範圍。 事務2要想插入,需要等待,這樣就解決了幻讀,第二次執行SELECT也不會有12這條記錄

但是這樣是有代價的:併發度低。12這條記錄不能插入了,要獲得更大的併發性,可以把隔離級別調成READ COMMITTED

記錄鎖的型別

  • Record Lock

    • 單個行記錄上的鎖
  • Gap Lock

    • 鎖定一個範圍,但不包含記錄本身
  • Next-key Lock

    • Gap Lock + Record Lock,鎖定一個範圍,並且鎖定記錄本身

舉例:

假設有 id 為 10 20 30 的幾條記錄

  • Record Lock:之鎖住10 20 30 這些記錄本身。
  • Gap Lock:當鎖10時,鎖住的是(-∞,10) 這樣的一個範圍,鎖20時,鎖的是 (10,20)以此類推
  • Next-key Lock:是前面兩個演算法的結合,當鎖10時,鎖住的是(-∞,10],鎖20時,鎖的是(10,20], 鎖住20本身,是不能對其進行update or delete 操作,而Gap 鎖不鎖定記錄本身,可以對其進行這些操作。
mysql> desc t;
  +-------+---------+------+-----+---------+-------+
  | Field | Type    | Null | Key | Default | Extra |
  +-------+---------+------+-----+---------+-------+
  | a     | int(11) | NO   | PRI | NULL    |       |
  +-------+---------+------+-----+---------+-------+
  1 row in set (0.00 sec)
  ​
  mysql> insert into t values (10), (20), (30), (40);
  ​
  mysql> begin;
  Query OK, 0 rows affected (0.00 sec)
  ​
  mysql> delete from t where a<= 30;
  Query OK, 3 rows affected (0.00 sec)
  ​
  ---TRANSACTION 3440, ACTIVE 35 sec
  2 lock struct(s), heap size 1136, 3 row lock(s), undo log entries 3
  MySQL thread id 13, OS thread handle 123145543925760, query id 569 localhost root
  TABLE LOCK table `test_mysql`.`t` trx id 3440 lock mode IX
  RECORD LOCKS space id 47 page no 3 n bits 72 index PRIMARY of table `test_mysql`.`t` trx id 3440 lock_mode X locks rec but not gap  # record lock
  Record lock, heap no 2 PHYSICAL RECORD: n_fields 3; compact format; info bits 32
   0: len 4; hex 8000000a; asc     ;;          # 10
   1: len 6; hex 000000000d70; asc      p;;
   2: len 7; hex 4f000001850110; asc O      ;;
  ​
  Record lock, heap no 3 PHYSICAL RECORD: n_fields 3; compact format; info bits 32
   0: len 4; hex 80000014; asc     ;;          # 20
   1: len 6; hex 000000000d70; asc      p;;
   2: len 7; hex 4f000001850132; asc O     2;;
  ​
  Record lock, heap no 4 PHYSICAL RECORD: n_fields 3; compact format; info bits 32
   0: len 4; hex 8000001e; asc     ;;          # 30
   1: len 6; hex 000000000d70; asc      p;;
   2: len 7; hex 4f000001850154; asc O     T;;</pre>
mysql> select @@global.tx_isolation, @@tx_isolation;
  +-----------------------+-----------------+
  | @@global.tx_isolation | @@tx_isolation  |
  +-----------------------+-----------------+
  | REPEATABLE-READ       | REPEATABLE-READ |
  +-----------------------+-----------------+
  ​
  mysql> select * from t;
  +----+
  | a  |
  +----+
  | 10 |
  | 20 |
  | 30 |
  | 40 |
  +----+
  4 rows in set (0.00 sec)
  ​
  mysql> begin;
  Query OK, 0 rows affected (0.00 sec)
  ​
  mysql> select * from t where a <= 30 for update;
  +----+
  | a  |
  +----+
  | 10 |
  | 20 |
  | 30 |
  +----+
  3 rows in set (0.00 sec)
  ​
  mysql> insert into t select 15;
  ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction
  ​
  insert into t select 15
  ------- TRX HAS BEEN WAITING 7 SEC FOR THIS LOCK TO BE GRANTED:
  RECORD LOCKS space id 47 page no 3 n bits 72 index PRIMARY of table `test_mysql`.`t` trx id 3485 lock_mode X locks **gap before rec insert intention waiting**
  Record lock, heap no 3 PHYSICAL RECORD: n_fields 3; compact format; info bits 0
   0: len 4; hex 80000014; asc     ;;
   1: len 6; hex 000000000d95; asc       ;;
   2: len 7; hex e40000018f011c; asc        ;;
  ------------------
  TABLE LOCK table `test_mysql`.`t` trx id 3483 lock mode IX RECORD LOCKS space id 47 page no 3 n bits 72 index PRIMAR Y of table `test_mysql`.`t` trx id 3483 lock_mode X
  Record lock, heap no 2 PHYSICAL RECORD: n_fields 3; compact format; info bits 0
   0: len 4; hex 8000000a; asc     ;;
   1: len 6; hex 000000000d95; asc       ;;
   2: len 7; hex e40000018f0110; asc        ;;
  ​
  Record lock, heap no 3 PHYSICAL RECORD: n_fields 3; compa
  ct format; info bits 0
   0: len 4; hex 80000014; asc     ;;
   1: len 6; hex 000000000d95; asc       ;;
   2: len 7; hex e40000018f011c; asc        ;;
  ​
  Record lock, heap no 4 PHYSICAL RECORD: n_fields 3; compa
  ct format; info bits 0
   0: len 4; hex 8000001e; asc     ;;
   1: len 6; hex 000000000d95; asc       ;;
   2: len 7; hex e40000018f0128; asc       (;;
  ​
  Record lock, heap no 5 PHYSICAL RECORD: n_fields 3; compa
  ct format; info bits 0
   0: len 4; hex 80000028; asc    (;;
   1: len 6; hex 000000000d95; asc       ;;
   2: len 7; hex e40000018f0134; asc       4;;
  ​
  # RR事務隔離級別:遊標掃記錄,直到第一條不符合要求的記錄都加鎖

MySQL預設的事務隔離級別(RR)用的是Next-key locking演算法

Next-key lock優化成record lock 的條件:

鎖定一條記錄,鎖定的這條記錄的索引包含唯一索引

Gap locking is not needed for statements that lock rows using a unique index to search for a unique row. (This does not include the case that the search condition includes only some columns of a multiple-column unique index; in that case, gap locking does occur.)

  RECORD LOCKS space id 47 page no 3 n bits 72 index PRIMAR
  Y of table `test_mysql`.`t` trx id 3487 lock_mode **X locks
   rec but not gap**
  Record lock, heap no 3 PHYSICAL RECORD: n_fields 3; compa
  ct format; info bits 0
   0: len 4; hex 80000014; asc     ;;
   1: len 6; hex 000000000d95; asc       ;;
   2: len 7; hex e40000018f011c; asc        ;;</pre>

Q:為什麼要有隔離性呢?
A: 如果沒有隔離性,一個事務del <= 7; 另一個事務在中間 INS 6;

事務 Tx1 Tx2
BEGIN
del <= 10
BEGIN
INS 5
COMMIT
COMMIT

這個表裡最後還剩5

但是在log裡記錄的其實是:

insert

delete

如果從機用這個日誌去備份,會有主從不一致

舉例

例子:

 對Z表插入了這些資料
  CREATE TABLE z ( a INT, b INT, PRIMARY KEY(a), KEY(b) );
  INSERT INTO z SELECT 1,1;
  INSERT INTO z SELECT 3,1;
  INSERT INTO z SELECT 5,3;
  INSERT INTO z SELECT 7,6;
  INSERT INTO z SELECT 10,8;
  ​
  一個事務執行:
  BEGIN; 
  SELECT * FROM z WHERE b=3 FOR UPDATE;
  ​
  Q: # 在RR隔離級別下,以下哪些能執行,哪些不能執行呢?
  ​
  a: 1  3  5  7  10
  b: 1  1  3  6  8
  ​
  a: 5  X record鎖
  b:  (1:3] ,(3,6)  X gap鎖
  SELECT * FROM z WHERE a = 5 LOCK IN SHARE MODE;  # 是給a加S鎖,前面加了X鎖,不行
  INSERT INTO z SELECT 4,2;                        # b=2,在(1,3]範圍之內,也不行
  INSERT INTO z SELECT 6,5;                        #不能插入  其實b還有一個GAP鎖,範圍是(3,6),如果鎖是二級索引,且非唯一,那麼就會有一個隱含的GAP鎖。
  INSERT INTO z SELECT 8,6;                    # OK 6不屬於(3,6)返回,會加到原來的記錄後面
  INSERT INTO z SELECT 2,0;                     # OK
  INSERT INTO z SELECT 6,7;                    # OK
  ​
  # 如果不鎖住(3,6),就可以插入(6,3)  如果再返回記錄b=3 ,那返回的就是(5,3),(6,3)

注意:這裡的中括號不是數學上的概念,如果新插入的是3,原來是(1,3] 還是能插入3的,是(3,3],它在原範圍之後的,不是之內,所以不鎖住這個範圍,就會有幻讀問題。

InnoDB預設使用Next-key lock algorithm,他鎖住的是前面記錄和當前記錄的範圍,若鎖住了13這條記錄,那12這條記錄是插入不進來的,這樣就解決了幻讀問題。

某些情況下,可以將Next-key lock 降解成Record Lock,從而獲得併發性。什麼情況呢? 當索引含有唯一約束 並且鎖定的是一條記錄,這時就可以只鎖住事務本身。 上面的例子:如果10,11,13,20是PK,意味著unique,如果SELECT PK=10,只鎖定一條記錄,那它鎖住的就是10本身,而不是(-∞,10)這樣的範圍,但是如果條件是<=10;那鎖定的還是 (-∞,10]這樣的範圍,記錄索引是唯一 一條而不是一個範圍)的話,鎖定1條記錄( point select),就不需要使用範圍鎖了。不管事務是序列還是並行執行的,得到的結果都是一致的。

隔離性越高,持有鎖的時間越長,併發性越差。

隔離級別與鎖的問題

RR -> next-key lock

RC -> record lock

對於沒有索引的表,選一個記錄進行刪除時。RR 時,鎖的是全部記錄,RC時,鎖的是一條記錄。

鎖重用

結果:鎖開銷進一步下降

BEGIN;
  SELECT * FROM t WHERE rowid = xxx FOR UPDATE; 
  SELECT * FROM t WHERE rowid = yyy FOR UPDATE; 
  ​
  BEGIN;
    SELECT * FROM t WHERE rowid = xxx FOR UPDATE;
  SELECT * FROM t WHERE rowid = xxx LOCK IN SHARE MODE; 

隱式鎖

開啟一個事務,當插入一條記錄時候,看不到鎖,但是當另一個事務想要對這條記錄加鎖時就會出現。

Q : 那如何知道這條記錄有沒有隱式鎖呢? 可以通過事務id來判斷

所以,在插入的過程中,多個事務可以並行執行。

鎖定讀:

select * from a where c = 1 for update/lock in share mode

但是一般我們用的是:

select * from a where c = 1

在事務隔離級別<= RR的情況下,這樣的讀是非鎖定讀(及時記錄被更新,也能被讀取,讀不會被阻塞,這是通過行多版本來實現的),這行資料是否被更新 可以通過事務id來確認(如果被更新,就讀之前的版本)。

死鎖

  • 資料庫中的死鎖有死鎖檢測機制,可以死鎖檢測把死鎖回滾掉

  • 兩個或兩個以上的事務在執行過程中 因爭奪鎖資源而造成的一種互相等待的現象

  • AB-BA

解決死鎖

  • 超時 • --innodb_lock_timeout

  • wait-for graph • 自動死鎖檢測

  • 當資料庫檢測到死鎖時,會自動回滾事務,釋放持有的鎖。

死鎖檢視: show engine innodb status\G LATEST DETECTED DEAFLOCK 會顯示最近的死鎖 5.6有一個引數: innodb_print_all_deadlocks=OFF 把這個引數設定為1,只要發生死鎖,就會把死鎖資訊寫到錯誤檔案裡面, 這時候,檢視錯誤日誌就能看到所有的死鎖的資訊。

InnoDB鎖的演算法:

3401773-bee5daaad52c5d2e.png
image.png

在InnoDB中,鎖是通過點陣圖來實現的,用點陣圖來儲存哪些記錄有鎖,儲存時不是以記錄為單位的,而是根據一個頁,一個頁中有多少條記錄,然後看看這些記錄哪些上鎖了,在記憶體中,有這樣一個區域,用點陣圖的方式記錄一個頁裡 哪些記錄有鎖。

相關文章