前言
一般为了表述的简洁,我们在介绍 TIDB
分布式事务的时候,经常说 TIDB
会对 KEY
加锁,或者对 KEY
进行锁的检测,从而保障事务的逻辑性和隔离性。
但是这个 KEY
到底是什么,对想要了解分布式事务的同学来说,可能不够直观。
我们今天将会使用具体的 SQL,结合不同类型的主键与索引,来观察 TIKV
分布式事务中的锁到底是什么。
开始之前,需要参考文章:《说计算》来了解关系型数据库和 KV
键值之间的关联
TiDB 表中的一条数据,最后会对应多个 Key-Value 去存储。我们来一起回顾一下重点:这些 Key-Value 分为两类:
- RowID => Value : 主键信息,该类数据存储着当前对应行的所有信息。
- { Key:
tablePrefix{tableID}_recordPrefixSep{rowID
}, Value:[col1, col2, col3, col4]
}- Index Key => RowID: 索引信息,该类数据为索引到 RowID 的映射。表有多少的索引,每行数据就有多少个这样的 Key-Value。根据是否是唯一索引,分为如下两种类型
- { Key:
tablePrefix{tableID}_indexPrefixSep{indexID}_indexedColumnsValue
, Value:rowID
}- { Key:
tablePrefix{tableID}_indexPrefixSep{indexID}_indexedColumnsValue_rowID
, Value:null
}
理解了上面,其实分布式事务就是对这些 KEY
进行加锁: 一个是对 主键 进行加锁,一个是对 唯一索引 进行加锁。
对于普通的索引来说: (tablePrefix{tableID}_indexPrefixSep{indexID}_indexedColumnsValue_rowID
)
由于分布式事务会对 rowID
主键进行加锁,因此普通索引的 KEY
实际上受到了 rowID
这个主键上面锁的保护,不需要加锁。
测试过程中,建表语句如下:
CREATE TABLE `MANAGERS_UNIQUE` (
`MANAGER_ID` int(11) NOT NULL,
`FIRST_NAME` varchar(45) NOT NULL,
`LAST_NAME` varchar(45) NOT NULL,
`LEVER` int(11) DEFAULT NULL,
PRIMARY KEY (`MANAGER_ID`)
UNIQUE KEY `FIRST` (`FIRST_NAME`),
KEY `LEVEL` (`LEVEL`)
)
插入数据:
mysql> INSERT INTO MANAGERS_UNIQUE(MANAGER_ID,FIRST_NAME, LAST_NAME) VALUES (14273,'Brad7','Craven7',7);
mysql> INSERT INTO MANAGERS_UNIQUE(MANAGER_ID,FIRST_NAME, LAST_NAME) VALUES (14274,'Brad8','Craven8',8);
INSERT 语句
执行的 SQL
为:
mysql> BEGIN PESSIMISTIC;
Query OK, 0 rows affected (0.00 sec)
mysql> INSERT INTO MANAGERS_UNIQUE(MANAGER_ID,FIRST_NAME, LAST_NAME, LEVEL) VALUES ('14275','Brad9','Craven9',9);
Query OK, 1 row affected (0.01 sec)
此时扫描 TIKV
所有的 LOCK
记录:
./tikv-ctl --data-dir tikv-0/data raw-scan --cf lock
key: "zt\200\000\000\000\000\000\000\377j_i\200\000\000\000\000\377\000\000\002\001Brad\3779\000\000\000\374\000\000\000\374", value: "S:t\200\000\000\000\000\000\000j_i\200\000\000\000\000\000\000\002\001Brad9\000\000\000\374\201\200\240\337\201\206\353\233\006\357\246\002f\0067\2540\033\350\000\001c\0067\2540\033\350\000\002l\000\000\000\000\000\000\000\000\001"
key: "zt\200\000\000\000\000\000\000\377j_r\0011427\3775\000\000\000\374\000\000\000\374", value: "S:t\200\000\000\000\000\000\000j_i\200\000\000\000\000\000\000\002\001Brad9\000\000\000\374\201\200\240\337\201\206\353\233\006\365\246\002f\0067\2540\033\350\000\001c\0067\2540\033\350\000\002l\000\000\000\000\000\000\000\000\001"
Total scanned keys: 2
分别对两个 KEY 进行解析:
zt\200\000\000\000\000\000\000\377j_i\200\000\000\000\000\377\000\000\002\001Brad\3779\000\000\000\374\000\000\000\374
├─## decode rocksdb data key
│ └─"t\\200\\000\\000\\000\\000\\000\\000\\377j_i\\200\\000\\000\\000\\000\\377\\000\\000\\002\\001Brad\\3779\\000\\000\\000\\374\\000\\000\\000\\374"
│ ├─## table prefix
│ │ └─table: -2579946652266647504
│ └─## decode go literal key
│ └─"t\200\000\000\000\000\000\000\377j_i\200\000\000\000\000\377\000\000\002\001Brad\3779\000\000\000\374\000\000\000\374"
│ ├─## decode mvcc key
│ │ └─"t\200\000\000\000\000\000\000j_i\200\000\000\000\000\000\000\002\001Brad9\000\000\000\374"
│ │ ├─## table prefix
│ │ │ └─table: 106
│ │ └─## table index key
│ │ ├─table: 106
│ │ ├─index: 2
│ │ └─"\001Brad9\000\000\000\374"
│ │ └─## decode index values
│ │ └─kind: Bytes, value: Brad9
│ └─## table prefix
│ └─table: 255
"zt\200\000\000\000\000\000\000\377j_r\0011427\3775\000\000\000\374\000\000\000\374"
├─## decode rocksdb data key
│ └─"t\\200\\000\\000\\000\\000\\000\\000\\377j_r\\0011427\\3775\\000\\000\\000\\374\\000\\000\\000\\374"
│ ├─## table prefix
│ │ └─table: -2579946652266647504
│ └─## decode go literal key
│ └─"t\200\000\000\000\000\000\000\377j_r\0011427\3775\000\000\000\374\000\000\000\374"
│ ├─## decode mvcc key
│ │ └─"t\200\000\000\000\000\000\000j_r\00114275\000\000\000\374"
│ │ ├─## table prefix
│ │ │ └─table: 106
│ │ └─## table row key
│ │ ├─table: 106
│ │ └─"\00114275\000\000\000\374"
│ │ └─## decode index values
│ │ └─kind: Bytes, value: 14275
│ └─## table prefix
│ └─table: 255
INSERT
比较简单,分别对 RowID
和 唯一索引 进行加锁。
对唯一索引加锁是必要的,防止有其他数据并发的也在更新其唯一索引到相同的值。
DELETE
mysql> BEGIN PESSIMISTIC;
Query OK, 0 rows affected (0.00 sec)
mysql> DELETE FROM MANAGERS_UNIQUE WHERE MANAGER_ID=14274;
Query OK, 1 row affected (0.02 sec)
./tikv-ctl --data-dir tikv-0/data raw-scan --cf lock
key: "zt\200\000\000\000\000\000\000\377j_i\200\000\000\000\000\377\000\000\002\001Brad\3778\000\000\000\374\000\000\000\374", value: "S:t\200\000\000\000\000\000\000j_i\200\000\000\000\000\000\000\002\001Brad8\000\000\000\374\201\200\200\363\203\210\353\233\006\227\230\004f\0067\254@>`\000\001c\0067\254@>`\000\002l\0067\253\223\264<\000\002\001"
key: "zt\200\000\000\000\000\000\000\377j_r\0011427\3774\000\000\000\374\000\000\000\374", value: "S:t\200\000\000\000\000\000\000j_i\200\000\000\000\000\000\000\002\001Brad8\000\000\000\374\201\200\200\363\203\210\353\233\006\233\230\004f\0067\254@>`\000\001c\0067\254@>`\000\002l\0067\253\223\264<\000\002\001"
Total scanned keys: 2
DELETE
也比较简单,分别对 RowID
和 唯一索引 进行加锁。
DELETE
对唯一索引加锁好像是非必要的?目前本人想不到需要对 DELETE 的唯一索引加锁的场景。
SELECT FOR UPDATE
主键搜索场景
首先开启悲观事务,同时对主键进行 SELECT FOR UPDATE:
mysql> BEGIN PESSIMISTIC;
Query OK, 0 rows affected (0.00 sec)
mysql> select * from MANAGERS_UNIQUE where MANAGER_ID=14273 FOR UPDATE;
+------------+------------+-----------+
| MANAGER_ID | FIRST_NAME | LAST_NAME |
+------------+------------+-----------+
| 14273 | Brad7 | Craven7 |
+------------+------------+-----------+
1 row in set (0.01 sec)
此时扫描 TIKV 所有的 LOCK 记录:
➜ ./tikv-ctl --data-dir tikv-0/data raw-scan --cf lock
key: "zt\200\000\000\000\000\000\000\377j_r\0011427\3773\000\000\000\374\000\000\000\374",
value: "S*t\200\000\000\000\000\000\000j_r\00114273\000\000\000\374\201\200\260\246\322\243\352\233\006\262\307\025f\0067\251\035$\314\000\001c\0067\251\035$\314\000\002l\0067\244\235?\264\000\002\001"
Total scanned keys: 1
对 KEY 进行验证解析:
"74800000000000006A5F72013134323733000000FC"
└─## decode hex key
└─"t\200\000\000\000\000\000\000j_r\00114273\000\000\000\374"
├─## table prefix
│ └─table: 106
└─## table row key
├─table: 106
└─"\00114273\000\000\000\374"
└─## decode index values
└─kind: Bytes, value: 14273
可以得出结论,名字主键的场景下 SELECT FOR UPDATE 语句只会对 ROWID 进行加锁。
唯一键搜索场景
首先开启悲观事务,同时对唯一键进行 SELECT FOR UPDATE:
mysql> BEGIN PESSIMISTIC;
Query OK, 0 rows affected (0.00 sec)
mysql> select * from MANAGERS_UNIQUE where FIRST_NAME='Brad7' FOR UPDATE;
+------------+------------+-----------+
| MANAGER_ID | FIRST_NAME | LAST_NAME |
+------------+------------+-----------+
| 14273 | Brad7 | Craven7 |
+------------+------------+-----------+
1 row in set (0.00 sec)
此时扫描 TIKV 所有的 LOCK 记录:
./tikv-ctl --data-dir tikv-0/data raw-scan --cf lock
key: "zt\200\000\000\000\000\000\000\377j_i\200\000\000\000\000\377\000\000\002\001Brad\3777\000\000\000\374\000\000\000\374",
value: "S:t\200\000\000\000\000\000\000j_i\200\000\000\000\000\000\000\002\001Brad7\000\000\000\374\203\200\260\311\312\251\352\233\006\227\323\003f\0067\251L\251,\000\003c\0067\251L\251,\000\004l\0067\244\235?\264\000\002\001"
key: "zt\200\000\000\000\000\000\000\377j_r\0011427\3773\000\000\000\374\000\000\000\374",
value: "S:t\200\000\000\000\000\000\000j_i\200\000\000\000\000\000\000\002\001Brad7\000\000\000\374\203\200\260\311\312\251\352\233\006\343\204\003f\0067\251L\251,\000\003c\0067\251L\251,\000\004l\0067\244\235?\264\000\002\001"
Total scanned keys: 2
发现有两个 KEY,我们分别解析一下:
"t\\200\\000\\000\\000\\000\\000\\000\\377j_i\\200\\000\\000\\000\\000\\377\\000\\000\\002\\001Brad\\3777\\000\\000\\000\\374\\000\\000\\000\\374"
├─## table prefix
│ └─table: -2579946652266647504
└─## decode go literal key
└─"t\200\000\000\000\000\000\000\377j_i\200\000\000\000\000\377\000\000\002\001Brad\3777\000\000\000\374\000\000\000\374"
├─## decode mvcc key
│ └─"t\200\000\000\000\000\000\000j_i\200\000\000\000\000\000\000\002\001Brad7\000\000\000\374"
│ ├─## table prefix
│ │ └─table: 106
│ └─## table index key
│ ├─table: 106
│ ├─index: 2
│ └─"\001Brad7\000\000\000\374"
│ └─## decode index values
│ └─kind: Bytes, value: Brad7
└─## table prefix
└─table: 255
"zt\\200\\000\\000\\000\\000\\000\\000\\377j_r\\0011427\\3773\\000\\000\\000\\374\\000\\000\\000\\374"
├─## decode rocksdb data key
│ └─"t\\200\\000\\000\\000\\000\\000\\000\\377j_r\\0011427\\3773\\000\\000\\000\\374\\000\\000\\000\\374"
│ ├─## table prefix
│ │ └─table: -2579946652266647504
│ └─## decode go literal key
│ └─"t\200\000\000\000\000\000\000\377j_r\0011427\3773\000\000\000\374\000\000\000\374"
│ ├─## decode mvcc key
│ │ └─"t\200\000\000\000\000\000\000j_r\00114273\000\000\000\374"
│ │ ├─## table prefix
│ │ │ └─table: 106
│ │ └─## table row key
│ │ ├─table: 106
│ │ └─"\00114273\000\000\000\374"
│ │ └─## decode index values
│ │ └─kind: Bytes, value: 14273
│ └─## table prefix
│ └─table: 255
可以得出结论,以唯一索引为搜索条件的话,那么会加两个 KEY 的锁。
一个是 RowID,一个是 UniqueIndex
普通索引搜索场景
执行的 SQL 为:
mysql> BEGIN PESSIMISTIC;
Query OK, 0 rows affected (0.00 sec)
mysql> select * from MANAGERS_UNIQUE where level=7 for update;
+------------+------------+-----------+-------+
| MANAGER_ID | FIRST_NAME | LAST_NAME | LEVEL |
+------------+------------+-----------+-------+
| 14273 | Brad7 | Craven7 | 7 |
+------------+------------+-----------+-------+
1 row in set (0.02 sec)
此时扫描 TIKV 所有的 LOCK 记录:
./tikv-ctl --data-dir tikv-0/data raw-scan --cf lock
[2024/02/27 20:03:06.977 +08:00] [WARN] [config.rs:315] ["Create jemalloc nodump allocator for block cache failed: Not implemented: Not compiled with JEMALLOC, continue with default allocator"] [thread_id=0x4]
key: "zt\200\000\000\000\000\000\000\377j_r\0011427\3773\000\000\000\374\000\000\000\374", value: "S*t\200\000\000\000\000\000\000j_r\00114273\000\000\000\374\201\200\260\344\357\270\352\233\006\252\256\003f\0067\251\311 \370\000\001c\0067\251\311 \370\000\002l\0067\251\306\253L\000\002\001"
Total scanned keys: 1
普通索引命中的情况下,也只对 RowID 进行加锁。
无索引搜索场景
执行的 SQL 为:
mysql> BEGIN PESSIMISTIC;
Query OK, 0 rows affected (0.00 sec)
mysql> select * from MANAGERS_UNIQUE where LAST_NAME='Craven7' FOR UPDATE;
+------------+------------+-----------+
| MANAGER_ID | FIRST_NAME | LAST_NAME |
+------------+------------+-----------+
| 14273 | Brad7 | Craven7 |
+------------+------------+-----------+
1 row in set (0.01 sec)
此时扫描 TIKV 所有的 LOCK 记录:
./tikv-ctl --data-dir tikv-0/data raw-scan --cf lock
key: "zt\200\000\000\000\000\000\000\377j_r\0011427\3773\000\000\000\374\000\000\000\374", value: "S*t\200\000\000\000\000\000\000j_r\00114273\000\000\000\374\207\200\220\330\230\261\352\233\006\260\347\002f\0067\251\212\204<\000\001c\0067\251\212\204<\000\002l\0067\244\235?\264\000\002\001"
Total scanned keys: 1
无索引命中的情况下,也只对 RowID 进行加锁。
SELECT 未命中数据场景
RR 隔离级别
命中点查
mysql> BEGIN PESSIMISTIC;
Query OK, 0 rows affected (0.00 sec)
mysql> select * from MANAGERS_UNIQUE where MANAGER_ID=14275 FOR UPDATE;
Empty set (0.00 sec)
此时扫描 TIKV 所有的 LOCK 记录:
./tikv-ctl --data-dir tikv-0/data raw-scan --cf lock
key: "zt\200\000\000\000\000\000\000\377j_r\0011427\3775\000\000\000\374\000\000\000\374", value: "S*t\200\000\000\000\000\000\000j_r\00114275\000\000\000\374\201\200\360\306\231\335\362\233\006\333\267\003f\0067\312\351\230\334\000\001c\0067\312\351\230\334\000\002l\0067\301ja\344\000\002\001"
Total scanned keys: 1
RR 隔离级别下,命中点查,即使没有数据,也会对点查的唯一索引或者 RowID 进行加锁。
可以看做弱化的间隙锁。
未命中点查
mysql> BEGIN PESSIMISTIC;
Query OK, 0 rows affected (0.00 sec)
mysql> select * from MANAGERS_UNIQUE where LEVEL=9;
Empty set (0.01 sec)
此时扫描 TIKV 所有的 LOCK 记录:
./tikv-ctl --data-dir tikv-0/data raw-scan --cf lock
Total scanned keys: 0
未命中点查情况下,没有数据情况下,TIKV 是不加锁的。
RC 隔离级别
命中点查
mysql> SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
Query OK, 0 rows affected (0.00 sec)
mysql> BEGIN PESSIMISTIC;
Query OK, 0 rows affected (0.00 sec)
mysql> select * from MANAGERS_UNIQUE where MANAGER_ID=14275 FOR UPDATE;
Empty set (0.00 sec)
此时扫描 TIKV 所有的 LOCK 记录:
./tikv-ctl --data-dir tikv-0/data raw-scan --cf lock
Total scanned keys: 0
RR 隔离级别下,命中点查,即使没有数据,也会对点查的唯一索引或者 RowID 进行加锁。
可以看做弱化的间隙锁。
未命中点查
mysql> BEGIN PESSIMISTIC;
Query OK, 0 rows affected (0.00 sec)
mysql> select * from MANAGERS_UNIQUE where LEVEL=9;
Empty set (0.01 sec)
此时扫描 TIKV 所有的 LOCK 记录:
./tikv-ctl --data-dir tikv-0/data raw-scan --cf lock
Total scanned keys: 0
RR 级别下,无论是否命中点查,没有数据,TIKV 是不加锁的。
SELECT 命中多条数据场景
执行的 SQL 为:
mysql> BEGIN PESSIMISTIC;
Query OK, 0 rows affected (0.01 sec)
mysql> select * from MANAGERS_UNIQUE where MANAGER_ID > 0 for update;
+------------+------------+-----------+-------+
| MANAGER_ID | FIRST_NAME | LAST_NAME | LEVEL |
+------------+------------+-----------+-------+
| 14273 | Brad7 | Craven7 | 7 |
| 14274 | Brad8 | Craven8 | 8 |
+------------+------------+-----------+-------+
2 rows in set (0.02 sec)
此时扫描 TIKV 所有的 LOCK 记录:
./tikv-ctl --data-dir tikv-0/data raw-scan --cf lock
key: "zt\200\000\000\000\000\000\000\377j_r\0011427\3773\000\000\000\374\000\000\000\374",
value: "S*t\200\000\000\000\000\000\000j_r\00114273\000\000\000\374\201\200\200\355\321\363\352\233\006\264\256\001f\0067\253\235BX\000\001c\0067\253\235BX\000\002l\0067\251\306\253L\000\002\001"
key: "zt\200\000\000\000\000\000\000\377j_r\0011427\3774\000\000\000\374\000\000\000\374",
value: "S*t\200\000\000\000\000\000\000j_r\00114273\000\000\000\374\201\200\200\355\321\363\352\233\006\264\256\001f\0067\253\235BX\000\001c\0067\253\235BX\000\002l\0067\253\223\264<\000\002\001"
Total scanned keys: 2
命中多条数据的情况下,每条数据的 RowID 将会被加锁。
特别的,如果使用 SQL:
mysql> BEGIN PESSIMISTIC;
Query OK, 0 rows affected (0.00 sec)
mysql> select * from MANAGERS_UNIQUE where FIRST_NAME in ("Brad7","Brad8") for update;
+------------+------------+-----------+-------+
| MANAGER_ID | FIRST_NAME | LAST_NAME | LEVEL |
+------------+------------+-----------+-------+
| 14273 | Brad7 | Craven7 | 7 |
| 14274 | Brad8 | Craven8 | 8 |
+------------+------------+-----------+-------+
2 rows in set (0.01 sec)
也就是使用唯一索引的 WHERE IN 来搜索的话,此时扫描 TIKV 所有的 LOCK 记录:
./tikv-ctl --data-dir tikv-0/data raw-scan --cf lock
key: "zt\200\000\000\000\000\000\000\377j_i\200\000\000\000\000\377\000\000\002\001Brad\3777\000\000\000\374\000\000\000\374", value: "S:t\200\000\000\000\000\000\000j_i\200\000\000\000\000\000\000\002\001Brad7\000\000\000\374\202\200\240\252\265\367\352\233\006\310\302\002f\0067\253\273UH\000\002c\0067\253\273UH\000\003l\0067\244\235?\264\000\002\002"
key: "zt\200\000\000\000\000\000\000\377j_i\200\000\000\000\000\377\000\000\002\001Brad\3778\000\000\000\374\000\000\000\374", value: "S:t\200\000\000\000\000\000\000j_i\200\000\000\000\000\000\000\002\001Brad7\000\000\000\374\202\200\240\252\265\367\352\233\006\310\302\002f\0067\253\273UH\000\002c\0067\253\273UH\000\003l\0067\253\223\264<\000\002\001"
key: "zt\200\000\000\000\000\000\000\377j_r\0011427\3773\000\000\000\374\000\000\000\374", value: "S:t\200\000\000\000\000\000\000j_i\200\000\000\000\000\000\000\002\001Brad7\000\000\000\374\202\200\240\252\265\367\352\233\006\313\302\002f\0067\253\273UH\000\002c\0067\253\273UH\000\003l\0067\251\306\253L\000\002\001"
key: "zt\200\000\000\000\000\000\000\377j_r\0011427\3774\000\000\000\374\000\000\000\374", value: "S:t\200\000\000\000\000\000\000j_i\200\000\000\000\000\000\000\002\001Brad7\000\000\000\374\202\200\240\252\265\367\352\233\006\313\302\002f\0067\253\273UH\000\002c\0067\253\273UH\000\003l\0067\253\223\264<\000\002\001"
Total scanned keys: 4
虽然命中了 2 行数据,但是却对 4 个 KEY 进行了加锁。分别是每行数据的 RowID 和 唯一索引。
总结
上述场景总结如下:
Result 行数 | LOCK 数量 | RowID锁 | 唯一键锁 | SQL | |||
---|---|---|---|---|---|---|---|
主键搜索 | 1 | 1 | 1 | 0 | select * from MANAGERS_UNIQUE where MANAGER_ID=14274 for update; | ||
唯一索引搜索 | 1 | 2 | 1 | 1 | select * from MANAGERS_UNIQUE where FIRST_NAME='Brad7' for update; | ||
普通索引搜索 | 1 | 1 | 1 | 0 | select * from MANAGERS_UNIQUE where level=7 for update; | ||
无索引搜索 | 1 | 1 | 1 | 0 | select * from MANAGERS_UNIQUE where LAST_NAME='Craven7' for update; | ||
命中多条数据 | 2 | 2 | 2 | 0 | select * from MANAGERS_UNIQUE where MANAGER_ID IN (14273, 14274) for update; | ||
命中多条数据+唯一键搜索 | 2 | 4 | 2 | 2 | select * from MANAGERS_UNIQUE where FIRST_NAME IN ('Brad7' , 'Brad8' ) for update; |
如果 SELECT SQL 未能命中数据的话,情况就比较复杂了:
Result 行数 | LOCK 数量 | RowID锁 | 唯一键锁 | SQL | |
---|---|---|---|---|---|
RR + 点查 | 0 0 | 1 1 | 1 0 | 0 1 | select * from MANAGERS_UNIQUE where MANAGER_ID=999999 for update; select * from MANAGERS_UNIQUE where FIRST_NAME='Brad9999' for update; |
RR + 普通查 | 0 | 0 | 0 | 0 | select * from MANAGERS_UNIQUE where LAST_NAME='Craven9' for update; |
RC + 点查 | 0 | 0 | 0 | 0 | select * from MANAGERS_UNIQUE where MANAGER_ID=999999 for update; |
RC + 普通查 | 0 | 0 | 0 | 0 | select * from MANAGERS_UNIQUE where LAST_NAME='Craven9' for update; |
疑问
我们发现一个比较奇怪的现象,如果 SQL
语句的 WHERE
语句命中了唯一索引的点查,那么 TIDB
将会对唯一索引也加锁。其他情况下,不会对唯一索引进行加锁。(RC
级别下,未命中数据的话,还比较特殊, 唯一索引 / RowID
不加锁)
那么 SELECT FOR UPDATE
语句是否有必要对唯一索引加锁呢?
个人认为好像并不需要,因为 RowID 已经被加锁,所以被锁定的行数据不会被更新。
同时其他行数据是否可以更新唯一键呢?也不会。例如:
+------------+------------+-----------+-------+
| MANAGER_ID | FIRST_NAME | LAST_NAME | LEVEL |
+------------+------------+-----------+-------+
| 14273 | Brad7 | Craven7 | 7 |
| 14274 | Brad8 | Craven8 | 8 |
+------------+------------+-----------+-------+
事务一:
BEGIN PESSIMISTIC;
mysql> select * from MANAGERS_UNIQUE where MANAGER_ID=14273;
+------------+------------+-----------+-------+
| MANAGER_ID | FIRST_NAME | LAST_NAME | LEVEL |
+------------+------------+-----------+-------+
| 14273 | Brad7 | Craven7 | 7 |
+------------+------------+-----------+-------+
事务二:
BEGIN PESSIMISTIC;
update MANAGERS_UNIQUE SET FIRST_NAME ="Brad7" WHERE MANAGER_ID = 14274;
这种情况下,事务二会首先检查唯一索引 FIRST_NAME
的唯一性,发现已经有一个 "Brad7
" 的唯一索引后,会直接返回错误。因此好像并不需要对唯一键进行加锁就可以实现这个功能。(实际上,悲观锁的接口参数 should_not_exist
就是这个作用)
目前本人未能想出 SELECT FOR UPDATE
语句需要对唯一索引加锁的必要场景。
UPDATE 语句
更新普通 Value
执行的 SQL 为:
mysql> BEGIN PESSIMISTIC;
Query OK, 0 rows affected (0.00 sec)
mysql> UPDATE MANAGERS_UNIQUE SET LAST_NAME='Craven9' where LAST_NAME='Craven8';
Query OK, 1 row affected (0.02 sec)
Rows matched: 1 Changed: 1 Warnings: 0
此时扫描 TIKV
所有的 LOCK
记录:
./tikv-ctl --data-dir tikv-0/data raw-scan --cf lock
key: "zt\200\000\000\000\000\000\000\377j_i\200\000\000\000\000\377\000\000\002\001Brad\3778\000\000\000\374\000\000\000\374", value: "S*t\200\000\000\000\000\000\000j_r\00114274\000\000\000\374\201\200\360\331\241\217\353\233\006\320\254\004f\0067\254};<\000\001c\0067\254};<\000\002l\0067\254K-\230\000\002\001"
key: "zt\200\000\000\000\000\000\000\377j_r\0011427\3774\000\000\000\374\000\000\000\374", value: "S*t\200\000\000\000\000\000\000j_r\00114274\000\000\000\374\201\200\360\331\241\217\353\233\006\314\254\004f\0067\254};<\000\001c\0067\254};<\000\002l\0067\254K-\230\000\002\001"
Total scanned keys: 2
因此普通的 UPDATE
语句也会对 RowID
和 唯一索引 加锁。
同样的疑问:这个唯一索引 Value (Brad7
) 的加锁,是否是 必要 的呢?
更新主键 Value
执行的 SQL 为:
+------------+------------+-----------+-------+
| MANAGER_ID | FIRST_NAME | LAST_NAME | LEVEL |
+------------+------------+-----------+-------+
| 14273 | Brad7 | Craven7 | 7 |
| 14274 | Brad8 | Craven8 | 8 |
+------------+------------+-----------+-------+
mysql> BEGIN PESSIMISTIC;
Query OK, 0 rows affected (0.00 sec)
mysql> UPDATE MANAGERS_UNIQUE SET MANAGER_ID=14275 where MANAGER_ID=14273;
Query OK, 1 row affected (0.02 sec)
Rows matched: 1 Changed: 1 Warnings: 0
此时扫描 TIKV
所有的 LOCK
记录:
./tikv-ctl --data-dir tikv-0/data raw-scan --cf lock
key: "zt\200\000\000\000\000\000\000\377j_i\200\000\000\000\000\377\000\000\002\001Brad\3777\000\000\000\374\000\000\000\374", value: "S*t\200\000\000\000\000\000\000j_r\00114273\000\000\000\374\201\200\260\200\360\223\353\233\006\307\202\002f\0067\254\237\314\274\000\001c\0067\254\237\314\274\000\002l\0067\244\235?\264\000\002\002"
key: "zt\200\000\000\000\000\000\000\377j_r\0011427\3773\000\000\000\374\000\000\000\374", value: "S*t\200\000\000\000\000\000\000j_r\00114273\000\000\000\374\201\200\260\200\360\223\353\233\006\304\202\002f\0067\254\237\314\274\000\001c\0067\254\237\314\274\000\002l\0067\251\306\253L\000\002\001"
key: "zt\200\000\000\000\000\000\000\377j_r\0011427\3775\000\000\000\374\000\000\000\374", value: "S*t\200\000\000\000\000\000\000j_r\00114273\000\000\000\374\201\200\260\200\360\223\353\233\006\307\202\002f\0067\254\237\314\274\000\001c\0067\254\237\314\274\000\002l\000\000\000\000\000\000\000\000\001"
Total scanned keys: 3
如果更新的是主键,那么新主键 Value 和旧主键的 Value 都需要被加锁。
同时还要对唯一索引进行加锁。
更新唯一索引 Value
执行的 SQL 为:
BEGIN PESSIMISTIC;
Query OK, 0 rows affected (0.00 sec)
mysql> UPDATE MANAGERS_UNIQUE SET FIRST_NAME='Brad9' where FIRST_NAME='Brad7';
Query OK, 1 row affected (0.02 sec)
Rows matched: 1 Changed: 1 Warnings:
此时扫描 TIKV
所有的 LOCK
记录:
./tikv-ctl --data-dir tikv-0/data raw-scan --cf lock
key: "zt\200\000\000\000\000\000\000\377j_i\200\000\000\000\000\377\000\000\002\001Brad\3777\000\000\000\374\000\000\000\374", value: "S:t\200\000\000\000\000\000\000j_i\200\000\000\000\000\000\000\002\001Brad7\000\000\000\374\201\200\360\254\332\224\353\233\006\257\306\003f\0067\254\245\245\234\000\001c\0067\254\245\245\234\000\002l\0067\244\235?\264\000\002\002"
key: "zt\200\000\000\000\000\000\000\377j_i\200\000\000\000\000\377\000\000\002\001Brad\3779\000\000\000\374\000\000\000\374", value: "S:t\200\000\000\000\000\000\000j_i\200\000\000\000\000\000\000\002\001Brad7\000\000\000\374\201\200\360\254\332\224\353\233\006\270\306\003f\0067\254\245\245\234\000\001c\0067\254\245\245\234\000\002l\000\000\000\000\000\000\000\000\001"
key: "zt\200\000\000\000\000\000\000\377j_r\0011427\3773\000\000\000\374\000\000\000\374", value: "S:t\200\000\000\000\000\000\000j_i\200\000\000\000\000\000\000\002\001Brad7\000\000\000\374\201\200\360\254\332\224\353\233\006\264\306\003f\0067\254\245\245\234\000\001c\0067\254\245\245\234\000\002l\0067\251\306\253L\000\002\001"
Total scanned keys: 3
如果更新的是唯一索引,那么新唯一索引 Value 和旧唯一索引的 Value 都需要被加锁。
同时还需要对 RowID 进行加锁。
对新的唯一索引 Value
加锁这是必要的,例如如下场景:
+------------+------------+-----------+-------+
| MANAGER_ID | FIRST_NAME | LAST_NAME | LEVEL |
+------------+------------+-----------+-------+
| 14273 | Brad7 | Craven7 | 7 |
| 14274 | Brad8 | Craven8 | 8 |
+------------+------------+-----------+-------+
事务一:
mysql> BEGIN PESSIMISTIC;
mysql> UPDATE MANAGERS_UNIQUE SET FIRST_NAME='Brad9' where FIRST_NAME='Brad7';
+------------+------------+-----------+-------+
| MANAGER_ID | FIRST_NAME | LAST_NAME | LEVEL |
+------------+------------+-----------+-------+
| 14273 | Brad7 | Craven7 | 7 |
+------------+------------+-----------+-------+
事务二:
mysql> BEGIN PESSIMISTIC;
mysql> UPDATE MANAGERS_UNIQUE SET FIRST_NAME='Brad9' where FIRST_NAME='Brad8';
总结
对 RowID
主键进行加锁基本是每个 DML 语句必须操作。
如果数据表存在唯一索引的话,还需要根据各种场景对唯一索引进行加锁。