写点什么

TIKV 分布式事务 -- 加锁的 KEY 是什么

  • 2024-03-01
    北京
  • 本文字数:12677 字

    阅读完需:约 42 分钟

作者: 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 语句必须操作。


如果数据表存在唯一索引的话,还需要根据各种场景对唯一索引进行加锁。


发布于: 刚刚阅读数: 2
用户头像

TiDB 社区官网:https://tidb.net/ 2021-12-15 加入

TiDB 社区干货传送门是由 TiDB 社区中布道师组委会自发组织的 TiDB 社区优质内容对外宣布的栏目,旨在加深 TiDBer 之间的交流和学习。一起构建有爱、互助、共创共建的 TiDB 社区 https://tidb.net/

评论

发布
暂无评论
TIKV 分布式事务--加锁的 KEY 是什么_TiDB 底层架构_TiDB 社区干货传送门_InfoQ写作社区