1. 程式人生 > 實用技巧 >MySQL 面試問題分析總結 - (事務)

MySQL 面試問題分析總結 - (事務)

背景介紹

最近面試了比較多,發現很多公司喜歡問如下問題:

  • MySQL 的InnoDB與MyIsam的區別
  • MySQL 如何與redis同步快取
  • MySQL 索引命中分析
  • 分庫分表
  • MySQL鎖的原理
  • 為什麼使用B+ 樹,而不雜湊索引
  • B+樹原理

準備工作

構建一個測試資料庫,並且設計一個測試表,需要了解如下一些基本的工具

use db;
CREATE TABLE `user_with_innodb` (
                            `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '使用者表id',
                            `username` varchar(50) NOT NULL COMMENT '使用者名稱',
                            `password` varchar(50) NOT NULL COMMENT '使用者密碼,MD5加密',
                            `email` varchar(50) DEFAULT NULL,
                            `phone` varchar(20) DEFAULT NULL,
                            `role` int(4) NOT NULL COMMENT '角色0-管理員,1-普通使用者',
                            `create_time` datetime NOT NULL DEFAULT current_timestamp() COMMENT '建立時間',
                            `update_time` datetime NOT NULL DEFAULT current_timestamp() COMMENT '最後一次更新時間',
                            PRIMARY KEY (`id`),
                            UNIQUE KEY `user_name_unique` (`username`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=22 DEFAULT CHARSET=utf8;

我們需要使用到的一些隨機生成資料的小工具,分別給出了demo如下:

select uuid();
select SUBSTRING_INDEX(uuid(), "-", 1);
select concat("name_", SUBSTRING_INDEX(uuid(), "-", 1));
select concat(SUBSTRING_INDEX(uuid(), "-", 1), "@example.com");
select rand() * 10> 5 as role;
select CONVERT("13766880000", unsigned int) as phone_digit;

如何使用我們上面的小工具插入資料到表中呢?可以參考如下方式

INSERT INTO `user_with_innodb` (
                                username,
                                password,
                                email,
                                phone,
                                role)
                                select concat("name_", SUBSTRING_INDEX(uuid(), "-", 1)),
                                       uuid(),
                                       concat(SUBSTRING_INDEX(uuid(), "-", 1), "@example.com"),
                                       CONVERT("13766880000", unsigned int)  + LAST_INSERT_ID(),
                                       rand() * 10> 5;

使用工具函式,當然不是為了插入一條資料而已,因此我們接下來是使用迴圈不斷的插入大量的資料到表中,這樣才會方便我們接下來的測試


delimiter #
create procedure load_foo_test_data()
begin
declare v_max int unsigned default 1000;
declare v_counter int unsigned default 0;

start transaction;
while v_counter < v_max do
    select "test loop";
end while;
commit;
end #

call load_foo_test_data();

批量隨機插入大量測試資料


delimiter #
create procedure load_foo_test_data()
begin
declare v_max int unsigned default 1000;
declare v_counter int unsigned default 0;

start transaction;
while v_counter < v_max do
      INSERT INTO `user_with_innodb` (
                                username,
                                password,
                                email,
                                phone,
                                role)
                                select concat("name_", SUBSTRING_INDEX(uuid(), "-", 1)),
                                       uuid(),
                                       concat(SUBSTRING_INDEX(uuid(), "-", 1), "@example.com"),
                                       CONVERT("13766880000", unsigned int)  + LAST_INSERT_ID(),
                                       rand() * 10> 5;
end while;
commit;
end #

call load_foo_test_data();

直接允許如上迴圈會一直阻塞,並且我們開啟另外的一個視窗也無法查詢到任何一條資料被插入,分析
原因可能是我們沒有將v_counter 自增,導致無法跳出迴圈。通過 show full processlist 檢視當前執行中的事物,然後通過 kill <ID>就可以殺死當前阻塞的事物了。

mysql> show full processlist\G;
*************************** 1. row ***************************
     Id: 5
   User: event_scheduler
   Host: localhost
     db: NULL
Command: Daemon
   Time: 757796
  State: Waiting on empty queue
   Info: NULL

如上可以看到我們的id是第一行。
修復後的程式碼如下:

delimiter #
create procedure load_user_test_data()
begin
    declare v_max int unsigned default 1000;
    declare v_counter int unsigned default 0;

    start transaction;
    while v_counter < v_max do
            set v_counter = 1 + v_counter;
            INSERT INTO `user_with_innodb` (
                username,
                password,
                email,
                phone,
                role)
            select concat("name_", SUBSTRING_INDEX(uuid(), "-", 1)),
                   uuid(),
                   concat(SUBSTRING_INDEX(uuid(), "-", 1), "@example.com"),
                   CONVERT("13766880000", unsigned int)  + LAST_INSERT_ID(),
                   rand() * 10> 5;
        end while;
    commit;
end #

call load_user_test_data();

InnoDB 與MyIsam 對比

先看看網上比較優秀的答案來自 SO

InnoDB has row-level locking, MyISAM can only do full table-level locking.
InnoDB has better crash recovery.
MyISAM has FULLTEXT search indexes, InnoDB did not until MySQL 5.6 (Feb 2013).
InnoDB implements transactions, foreign keys and relationship constraints, MyISAM does not.

對應的中文意思大概是:

  1. InnoDB支援行鎖,而MyISAM不支援
  2. InnoDB有非常好的宕機恢復資料能力
  3. MyISAM一直都支援全文搜尋,而InnoDB在5.6版本以後才支援
    4.InnoDB實現事物、外來鍵、關係等

瞭解了這些區別以後,我們可以試著去一一驗證了,在第一個terminal中打終端執行如下命令

mysql> begin;
Query OK, 0 rows affected (0.00 sec)

mysql> start transaction;
Query OK, 0 rows affected (0.00 sec)

mysql> select * from user_with_innodb where username="name_1be71e3a" for update;
+----------+---------------+--------------------------------------+----------------------+-------------+------+---------------------+---------------------+
| id       | username      | password                             | email                | phone       | role | create_time         | update_time         |
+----------+---------------+--------------------------------------+----------------------+-------------+------+---------------------+---------------------+
| 25471368 | name_1be71e3a | 1be71e44-3abe-11eb-9a64-9184e1dca9bb | [email protected] | 13792351367 |    0 | 2020-12-10 16:02:55 | 2020-12-10 16:02:55 |
+----------+---------------+--------------------------------------+----------------------+-------------+------+---------------------+---------------------+
1 row in set (0.00 sec)

開啟第二個終端執行如下命令

 select * from user_with_innodb where username="name_1be71e3a" for update;

都卡在如圖:

在第一個終端執行commit 才能看到第二個查詢被釋放。

mysql> commit;

現在,我們查詢其他row的資料,看看會不會阻塞我們第二個terminal。

mysql> begin;
Query OK, 0 rows affected (0.00 sec)

mysql> start transaction;
Query OK, 0 rows affected (0.00 sec)

mysql> select * from user_with_innodb where username="name_1be71c00" for update;
+----------+---------------+--------------------------------------+----------------------+-------------+------+---------------------+---------------------+
| id       | username      | password                             | email                | phone       | role | create_time         | update_time         |
+----------+---------------+--------------------------------------+----------------------+-------------+------+---------------------+---------------------+
| 25471367 | name_1be71c00 | 1be71c0a-3abe-11eb-9a64-9184e1dca9bb | [email protected] | 13792351366 |    0 | 2020-12-10 16:02:55 | 2020-12-10 16:02:55 |
+----------+---------------+--------------------------------------+----------------------+-------------+------+---------------------+---------------------+
1 row in set (0.00 sec)

現在去看第二個terminal發現,並不會阻塞等待第一個terminal的commit

mysql> select * from user_with_innodb where username="name_1be71e3a" for update;
+----------+---------------+--------------------------------------+----------------------+-------------+------+---------------------+---------------------+
| id       | username      | password                             | email                | phone       | role | create_time         | update_time         |
+----------+---------------+--------------------------------------+----------------------+-------------+------+---------------------+---------------------+
| 25471368 | name_1be71e3a | 1be71e44-3abe-11eb-9a64-9184e1dca9bb | [email protected] | 13792351367 |    0 | 2020-12-10 16:02:55 | 2020-12-10 16:02:55 |
+----------+---------------+--------------------------------------+----------------------+-------------+------+---------------------+---------------------+
1 row in set (0.00 sec)

這就驗證了我們的鎖是行鎖,不會造成全面的阻塞等待。當然即便是行鎖我們也可以使用 LOCK IN SHARE MODE 模式。