文章目錄
MySql BTree和Hash索引的比較,爲什麼InnoDB不使用Hash索引
MySql BTree和Hash索引的比較,爲什麼InnoDB不使用Hash索引
-1 爲啥要用B+Tree而不用btree?
btree所有的節點都存儲數據,而b+tree只有葉子節點才存儲數據
而內存每次加載數據的大小是有限的,而有數據的時候,加載的量就會很小,如果都是索引,就會加載更多的索引值
索引覆蓋:MySQL官網,類似的說法出現在explain查詢計劃優化章節,即explain的輸出結果Extra字段爲Using index時,能夠觸發索引覆蓋。 其實簡單些:只需要在一棵索引樹上就能獲取SQL所需的所有列數據,無需回表,速度更快。
- 主鍵索引當然不需要索引覆蓋,因爲本身能查詢到所有的數據
- 非主鍵索引分爲幾個情況:
1. 比如name是索引,id是主鍵,
則select id,name from XXX where name ='a'使用到了索引覆蓋
2. 聯合索引情況,id主鍵,name version 是聯合索引 ,
則select id,name,version from XXX where name ='a' and version='B' 使用到了索引覆蓋
回表是指:在非主鍵索引的情況下,先查詢到了主鍵,再通過主鍵查詢數據
索引下推ICP:官網介紹
0. B+Tree能存多少數據
mysql> show global status like 'Innodb_page_size';
+------------------+-------+
| Variable_name | Value |
+------------------+-------+
| Innodb_page_size | 16384 |
+------------------+-------+
1 row in set
- innodb的所有數據文件.idb大小都是16K即16384的整數倍
- 我們的InnoDB頁的大小默認是16k?爲啥是16K?因爲16K的話,高度爲3的樹就可以存儲千萬級別的數據,如下有說明
- 假設一行數據的大小是1k,那麼一個頁可以存放16行這樣的數據。
- 我們假設主鍵ID爲bigint類型,長度爲8字節,而指針大小在InnoDB源碼中設置爲6字節,這樣一共14字節
- 我們一個頁中能存放多少這樣的單元,其實就代表有多少指針,即16384/14=1170。
- 那麼可以算出一棵高度爲2的B+樹,即存在一個根節點和若干個葉子節點,那麼這棵B+樹的存放總記錄數爲:根節點指針數單個葉子節點記錄行數。能存放117016=18720條這樣的數據記錄。
- 根據同樣的原理我們可以算出一個高度爲3的B+樹可以存放:1170117016=21902400條這樣的記錄。
所以在InnoDB中B+樹高度一般爲1-3層,它就能滿足千萬級的數據存儲。
在查找數據時一次頁的查找代表一次IO,所以通過主鍵索引查詢通常只需要1-3次IO操作即可查找到數據。
1. 統計數據庫索引佔用的空間大小
如果想知道MySQL數據庫中每個表佔用的空間、表記錄的行數的話,可以打開MySQL的 information_schema 數據庫。在該庫中有一個 TABLES 表,這個表主要字段分別是:
TABLE_SCHEMA : 數據庫名
TABLE_NAME:表名
ENGINE:所使用的存儲引擎
TABLES_ROWS:記錄數
DATA_LENGTH:數據大小
INDEX_LENGTH:索引大小
其他字段請參考MySQL的手冊,我們只需要瞭解這幾個就足夠了。
- 首先查看某一實例下的所有佔用磁盤空間
(表數據+索引數據,得到的結果爲B,這裏做了數據處理轉成M):
select concat(round((sum(DATA_LENGTH)+sum(INDEX_LENGTH))/1024/1024,2),'M')
from information_schema.tables where table_schema='數據庫名稱';
- 上面是查詢所有的表計的累計量,下面是是查詢單個表計的的SQL(按照實例名查詢):
select table_name,
DATA_LENGTH/1024/1024 as tablesData,
INDEX_LENGTH/1024/1024 as indexData
from information_schema.tables
where table_schema='數據庫名稱'
ORDER BY tablesData desc;
2.查看是否有表鎖
1、查詢正在使用哪個表
show OPEN TABLES where In_use > 0;
2、查詢進程
show processlist
查詢到相對應的進程===然後 kill id
補充:
查看正在鎖的事務
SELECT * FROM INFORMATION_SCHEMA.INNODB_LOCKS;
查看等待鎖的事務
SELECT * FROM INFORMATION_SCHEMA.INNODB_LOCK_WAITS;
# 查詢是否有表鎖
SELECT * from information_schema.INNODB_TRX
kill trx_mysql_thread_id
從 information_schema.innodb_trx 表中查看當前未提交的事務
select trx_state, trx_started, trx_mysql_thread_id, trx_query from information_schema.innodb_trx;
字段意義:
trx_state: 事務狀態,一般爲RUNNING
trx_started: 事務執行的起始時間,若時間較長,則要分析該事務是否合理
trx_mysql_thread_id: MySQL的線程ID,用於kill
trx_query: 事務中的sql
一般只要kill掉這些線程,DDL操作就不會Waiting for table metadata lock。
2. 調整鎖超時閾值
lock_wait_timeout 表示獲取metadata lock的超時(單位秒),允許的值範圍爲1到31536000(1年)。 默認值爲31536000 (1年)。官方解釋:https://dev.mysql.com/doc/refman/5.6/en/server-system-variables.html#sysvar_lock_wait_timeout 。
修改等待時間:
set session lock_wait_timeout = 1800;
set global lock_wait_timeout = 1800;
3. MySQL8.0 查看默認事務的隔離級別,並測試
如下:MySQL默認是:可重複讀
mysql> select @@global.transaction_isolation,@@transaction_isolation;
+--------------------------------+-------------------------+
| @@global.transaction_isolation | @@transaction_isolation |
+--------------------------------+-------------------------+
| REPEATABLE-READ | REPEATABLE-READ |
+--------------------------------+-------------------------+
1 row in set (0.00 sec)
3.1 更改隔離級別爲:未提交讀測試
兩個命令行客戶端分別爲A,B;不斷改變A的隔離級別,在B端修改數據。
但是不改變B的隔離級別,只改變A的,B還是默認的隔離級別:可重複讀
A:
mysql> set session transaction isolation level read uncommitted;
Query OK, 0 rows affected (0.00 sec)
## 查看
mysql> select @@global.transaction_isolation,@@transaction_isolation;
+--------------------------------+-------------------------+
| @@global.transaction_isolation | @@transaction_isolation |
+--------------------------------+-------------------------+
| REPEATABLE-READ | READ-UNCOMMITTED |
+--------------------------------+-------------------------+
1 row in set (0.00 sec)
- 準一張表,測試事務
CREATE TABLE `test` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`num` int(11) NOT NULL DEFAULT '0',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
## 插入3條數據
INSERT INTO `trans`.`test` (`id`, `num`) VALUES ('1', '1');
INSERT INTO `trans`.`test` (`id`, `num`) VALUES ('2', '2');
INSERT INTO `trans`.`test` (`id`, `num`) VALUES ('3', '3');
用兩個窗口分別打開MySQL
mysql -uroot -p123456
- 窗口1: 啓動事務A,數據庫3條記錄是初始狀態
mysql> start transaction;
Query OK, 0 rows affected (0.00 sec)
mysql> select * from test;
+----+-----+
| id | num |
+----+-----+
| 1 | 1 |
| 2 | 2 |
| 3 | 3 |
+----+-----+
3 rows in set (0.00 sec)
- 窗口2. 啓動事務B ,修改數據,但是不提交事務B
mysql> start transaction;
Query OK, 0 rows affected (0.00 sec)
mysql> update test set num =33 where id =3;
Query OK, 1 row affected (0.00 sec)
Rows matched: 1 Changed: 1 Warnings: 0
## 在事務B內已經修改了
mysql> select * from test ;
+----+-----+
| id | num |
+----+-----+
| 1 | 1 |
| 2 | 2 |
| 3 | 33 |
+----+-----+
3 rows in set (0.00 sec)
- 窗口1 事務A 查看數據是否已經被修改
發現已經被修改,這就是所謂的“髒讀”
mysql> select * from test;
+----+-----+
| id | num |
+----+-----+
| 1 | 1 |
| 2 | 2 |
| 3 | 33 |
+----+-----+
3 rows in set (0.00 sec)
- 窗口2 回滾事務B
mysql> rollback ;
Query OK, 0 rows affected (0.04 sec)
- 窗口1 事務A查看數據是否回滾
發現已經回滾
mysql> select * from test;
+----+-----+
| id | num |
+----+-----+
| 1 | 1 |
| 2 | 2 |
| 3 | 3 |
+----+-----+
3 rows in set (0.00 sec)
- 經過上面的實驗可以得出結論,事務B更新了一條記錄,但是沒有提交,此時事務A可以查詢出未提交記錄。造成髒讀現象。未提交讀是最低的隔離級別
3.2 測試隔離級別爲:提交讀
把窗口A的隔離級別換爲:提交讀,窗口B的不換還是默認的
mysql> set session transaction isolation level read committed ;
Query OK, 0 rows affected (0.00 sec)
mysql> select @@global.transaction_isolation,@@transaction_isolation;
+--------------------------------+-------------------------+
| @@global.transaction_isolation | @@transaction_isolation |
+--------------------------------+-------------------------+
| REPEATABLE-READ | READ-COMMITTED |
+--------------------------------+-------------------------+
1 row in set (0.00 sec)
- 窗口A開啓事務,查詢
mysql> start transaction;
Query OK, 0 rows affected (0.00 sec)
mysql> select * from test ;
+----+-----+
| id | num |
+----+-----+
| 1 | 1 |
| 2 | 2 |
| 3 | 3 |
+----+-----+
3 rows in set (0.00 sec)
- 窗口2開啓事務,修改數據,並查看
mysql> start transaction;
Query OK, 0 rows affected (0.00 sec)
mysql> update test set num =22 where id =2;
Query OK, 1 row affected (0.00 sec)
Rows matched: 1 Changed: 1 Warnings: 0
mysql> select * from test;
+----+-----+
| id | num |
+----+-----+
| 1 | 1 |
| 2 | 22 |
| 3 | 3 |
+----+-----+
3 rows in set (0.00 sec)
- 窗口A再次查看數據有沒有被更新
發現沒有,隔離級別生效
mysql> select * from test;
+----+-----+
| id | num |
+----+-----+
| 1 | 1 |
| 2 | 2 |
| 3 | 3 |
+----+-----+
3 rows in set (0.00 sec)
- 窗口B 提交事務
mysql> commit ;
Query OK, 0 rows affected (0.08 sec)
- 窗口A 再次查詢數據
發現數據被修改了,符合預期
mysql> select * from test;
+----+-----+
| id | num |
+----+-----+
| 1 | 1 |
| 2 | 22 |
| 3 | 3 |
+----+-----+
3 rows in set (0.00 sec
看是正常的代碼,但是這個時候有什麼問題?
有:在事務A中,兩次讀取的數據不一致,即不可重複讀
- 經過上面的實驗可以得出結論,已提交讀隔離級別解決了髒讀的問題,但是出現了不可重複讀的問題,即事務A在兩次查詢的數據不一致,因爲在兩次查詢之間事務B更新了一條數據。已提交讀只允許讀取已提交的記錄,但不要求可重複讀。
3.3 測試可重複讀
- 事務A 設置可重讀,並開啓事務查看
mysql> set session transaction isolation level repeatable read ;
Query OK, 0 rows affected (0.00 sec)
mysql> select @@global.transaction_isolation,@@transaction_isolation;
+--------------------------------+-------------------------+
| @@global.transaction_isolation | @@transaction_isolation |
+--------------------------------+-------------------------+
| REPEATABLE-READ | REPEATABLE-READ |
+--------------------------------+-------------------------+
1 row in set (0.00 sec)
## 查看數據
mysql> start transaction;
Query OK, 0 rows affected (0.00 sec)
mysql> select * from test ;
+----+-----+
| id | num |
+----+-----+
| 1 | 1 |
| 2 | 22 |
| 3 | 3 |
+----+-----+
3 rows in set (0.00 sec)
- 事務B查看事務,默認的即可
mysql> select @@global.transaction_isolation,@@transaction_isolation;
+--------------------------------+-------------------------+
| @@global.transaction_isolation | @@transaction_isolation |
+--------------------------------+-------------------------+
| REPEATABLE-READ | REPEATABLE-READ |
+--------------------------------+-------------------------+
1 row in set (0.00 sec)
## 事務B開啓事務並修改id=1的數據
mysql> start transaction;
Query OK, 0 rows affected (0.00 sec)
mysql> select * from test ;
+----+-----+
| id | num |
+----+-----+
| 1 | 1 |
| 2 | 22 |
| 3 | 3 |
+----+-----+
3 rows in set (0.00 sec)
mysql> update test set num =11 where id =1;
Query OK, 1 row affected (0.00 sec)
Rows matched: 1 Changed: 1 Warnings: 0
mysql> select * from test ;
+----+-----+
| id | num |
+----+-----+
| 1 | 11 |
| 2 | 22 |
| 3 | 3 |
+----+-----+
3 rows in set (0.00 sec)
- 窗口A 查看發現沒有被修改,符合預期
mysql> select * from test ;
+----+-----+
| id | num |
+----+-----+
| 1 | 1 |
| 2 | 22 |
| 3 | 3 |
+----+-----+
3 rows in set (0.00 sec)
- 窗口B提交事務並查看,發現數據已經被更新了
mysql> commit ;
Query OK, 0 rows affected (0.08 sec)
mysql> select * from test ;
+----+-----+
| id | num |
+----+-----+
| 1 | 11 |
| 2 | 22 |
| 3 | 3 |
+----+-----+
3 rows in set (0.00 sec)
- 窗口A查看數據
mysql> select * from test ;
+----+-----+
| id | num |
+----+-----+
| 1 | 1 |
| 2 | 22 |
| 3 | 3 |
+----+-----+
3 rows in set (0.00 sec)
發現數據依然沒有被修改,符合可重複讀的預期,即在一個事務內讀取一個值的數據始終應該保持一致
但是!!!
有什麼問題沒有!!!——————》》數據不是最新的數據
- 如果我 再次開啓事務並提交事務在窗口B插入一條記錄呢?
下面在B窗口中,但是不開啓事務
mysql> start transaction;
mysql> insert into test (num) value(4);
Query OK, 1 row affected (0.07 sec)
mysql> commit;
Query OK, 0 rows affected (0.00 sec)
mysql> select * from test;
+----+-----+
| id | num |
+----+-----+
| 1 | 11 |
| 2 | 22 |
| 3 | 3 |
| 4 | 4 |
+----+-----+
4 rows in set (0.00 sec)
- 窗口A 再次查看,發現還是沒有獲取到最新數據
mysql> select * from test ;
+----+-----+
| id | num |
+----+-----+
| 1 | 1 |
| 2 | 22 |
| 3 | 3 |
+----+-----+
3 rows in set (0.00 sec
- 把事務A提交了,獲取數據,才發現是最新的
mysql> commit;
Query OK, 0 rows affected (0.00 sec)
mysql> select * from test ;
+----+-----+
| id | num |
+----+-----+
| 1 | 11 |
| 2 | 22 |
| 3 | 3 |
| 4 | 4 |
+----+-----+
4 rows in set (0.00 sec
由以上的實驗可以得出結論,可重複讀隔離級別只允許讀取已提交記錄,而且在一個事務A兩次讀取一個記錄期間,其他事務B如果更新該記錄,事務A是看不到的。但該事務不要求與其他事務可串行化。例如,當一個事務可以找到由一個已提交事務更新的記錄,但是可能產生幻讀問題(注意是可能,因爲數據庫對隔離級別的實現有所差別)。像以上的實驗,就沒有出現數據幻讀的問題。
3.3.1 模擬幻讀
上述的實驗沒有出現幻讀的現象,下面我們來模擬一下什麼是幻讀?
- 保證兩個窗口AB的隔離級別:都是可重複讀
select @@global.transaction_isolation,@@transaction_isolation;
+--------------------------------+-------------------------+
| @@global.transaction_isolation | @@transaction_isolation |
+--------------------------------+-------------------------+
| REPEATABLE-READ | REPEATABLE-READ |
+--------------------------------+-------------------------+
## 假設現在已經有了6條記錄分別是
mysql> select * from test;
+----+-----+
| id | num |
+----+-----+
| 1 | 11 |
| 2 | 22 |
| 3 | 3 |
| 4 | 4 |
| 5 | 55 |
| 6 | 55 |
+----+-----+
6 rows in set (0.00 sec)
- 在A窗口開啓事務,查詢
mysql> start transaction ;
Query OK, 0 rows affected (0.00 sec)
mysql> select * from test;
+----+-----+
| id | num |
+----+-----+
| 1 | 11 |
| 2 | 22 |
| 3 | 3 |
| 4 | 4 |
| 5 | 55 |
| 6 | 55 |
+----+-----+
6 rows in set (0.00 sec)
- 在B窗口不開啓事務直接插入一條記錄,num=7
mysql> insert into test (num) values (7);
Query OK, 1 row affected (0.10 sec)
- 在A窗口查詢,是否有num=7的記錄
mysql> select * from test;
+----+-----+
| id | num |
+----+-----+
| 1 | 11 |
| 2 | 22 |
| 3 | 3 |
| 4 | 4 |
| 5 | 55 |
| 6 | 55 |
+----+-----+
6 rows in set (0.00 sec)
發現沒有Num=7的記錄,驗證了可重複讀的特性,符合預期,但是我們怎麼模擬幻讀呢?
- 在窗口A中添加一條記錄,num也等於7,並查詢
mysql> insert into test (num) values (7);
Query OK, 1 row affected (0.00 sec)
mysql> select * from test;
+----+-----+
| id | num |
+----+-----+
| 1 | 11 |
| 2 | 22 |
| 3 | 3 |
| 4 | 4 |
| 5 | 55 |
| 6 | 55 |
| 8 | 7 |
+----+-----+
7 rows in set (0.00 sec)
發現直接跳過了id=7的數據,也說明了肯定有其它的線程在操作,試想我們如果更新num=7的記錄會更新幾條呢?如果更新了1條就不是幻讀,如果更新了2條就產生了幻讀。下面驗證一下
- 在窗口A中更新num=7的記錄,並再次查詢
mysql> update test set num =777 where num =7;
Query OK, 2 rows affected (0.00 sec)
Rows matched: 2 Changed: 2 Warnings: 0
mysql> select * from test;
+----+-----+
| id | num |
+----+-----+
| 1 | 11 |
| 2 | 22 |
| 3 | 3 |
| 4 | 4 |
| 5 | 55 |
| 6 | 55 |
| 7 | 777 |
| 8 | 777 |
+----+-----+
8 rows in set (0.00 sec)
哇!!! 結果匹配了兩條,而且查詢出來多了一條記錄,這難道是幻讀????YES
,這就是幻讀
模擬成功,類似刪除也有類似效果,我們不再模擬
大致思路是:在A窗口開啓事務,此時查詢到兩條777的記錄,在B窗口新添加一條記錄num =777,再加上原來兩條777的記錄公有三條,此時在A窗口執行刪除語句
mysql> delete from test where num =777;
Query OK, 3 rows affected (0.00 sec)
發現刪除了3條,但是預期是2條,所以出現了幻讀
3.4 可串行化就不測試了,就是順序的問題,誰的事務先開啓誰進行,其它的都要等待
3.5 MVCC
3.5.0 MVCC
(Multi-Version Concurrency Control多版本併發控制):
爲了提高併發度,InnoDb提供了 「非鎖定讀」,即不需要等待訪問行上的鎖釋放,讀取行的一個快照即可。 既然是多版本讀,那麼肯定讀不到隔壁事務的新插入數據了,所以解決了幻讀。
3.5.1 MVCC與隔離級別
Read Uncommitted每次都讀取記錄的最新版本,會出現髒讀,未實現MVCC
Serializable對所有讀操作都加鎖,讀寫發生衝突,不會使用MVCC
SELECT
(RR REPEATABLE-READ 可重複讀級別)InnoDb檢查每行數據,確保它們符合兩個標準:
- 只查找創建時間早於當前事務id的記錄,這確保當前事務讀取的行都是事務之前已經存在的,
- 或者是由當前事務創建或修改的行
- 行的DELETE BIT爲1時,查找刪除時間晚於當前事務id的記錄,確定了當前事務開始之前,行沒有被刪除
(RC級別)每次重新計算read view,read view的範圍爲InnoDb中最大的事務id,爲避免髒讀讀取的是DB_ROLL_PT指向的記錄
就這麼簡單嗎? 其實幻讀有很多種出現形式,簡單的SELECT不加條件的查詢在RR下肯定是讀不到隔壁事務提交的數據的。但是仍然可能在執行INSERT/UPDATE時遇到幻讀現象。因爲SELECT 不加鎖的快照讀行爲是無法限制其他事務對新增重合範圍的數據的插入的。
所以還要引入第二個機制。
3.5.2 Next-Key Lock
其實更多的幻讀現象是通過寫操作來發現的,如SELECT了3條數據,UPDATE的時候可能返回了4個成功結果,或者INSERT某條不在的數據時忽然報錯說唯一索引衝突等。
首先來了解一下InnoDb的鎖機制,InnoDB有三種行鎖:
Record Lock:單個行記錄上的鎖
Gap Lock:間隙鎖,鎖定一個範圍,但不包括記錄本身。GAP鎖的目的,是爲了防止同一事務的兩次當前讀,出現幻讀的情況
Next-Key Lock:前兩個鎖的加和,鎖定一個範圍,並且鎖定記錄本身。對於行的查詢,都是採用該方法,主要目的是解決幻讀的問題
如果是帶排他鎖操作(除了INSERT/UPDATE/DELETE這種,還包括SELECT FOR UPDATE/LOCK IN SHARE MODE等),它們默認都在操作的記錄上加了Next-Key Lock。只有使用了這裏的操作後纔會在相應的記錄周圍和記錄本身加鎖,即Record Lock + Gap Lock,所以會導致有衝突操作的事務阻塞進而超時失敗。