作者: ylldty 原文来源:https://tidb.net/blog/df90582f
前言
一般为了表述的简洁,我们在介绍 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 lockkey: "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 lockkey: "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 未能命中数据的话,情况就比较复杂了:
疑问
我们发现一个比较奇怪的现象,如果 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 语句必须操作。
如果数据表存在唯一索引的话,还需要根据各种场景对唯一索引进行加锁。
评论