DML 语句执行悲观事务过程中,唯一索引是否需要被加悲观锁

针对 SELECT FOR UPDATE 语句分别作了测试验证:

Result 行数 LOCK 数量 RowID锁 唯一键锁 SQL
主键搜索 1 1 1 0 SELECT * FROM T WHREE ID=1
唯一索引搜索 1 2 1 1 SELECT * FROM T WHREE UNIQUE_INDEX=1
普通索引搜索 1 1 1 0 SELECT * FROM T WHREE INDEX=1
无索引搜索 1 1 1 0 SELECT * FROM T WHREE COLUMN=1
未命中数据 0 0 0 0 SELECT * FROM T WHREE C1=0 AND C1=1
命中多条数据 2 2 2 0 SELECT * FROM T
命中多条数据+唯一键搜索 2 4 2 2 SELECT * FROM T WHREE UNIQUE_INDEX IN (1,2)

我们发现一个比较奇怪的现象,如果 SQL 语句的 WHERE 语句命中了唯一索引,那么 TIDB 将会对唯一索引也加锁。其他情况下,不会对唯一索引进行加锁。

那么 SELECT FOR UPDATE 语句是否有必要对唯一索引加锁呢?

个人认为好像并不需要,因为 RowID 已经被加锁,所以被锁定的行数据不会被更新。

同时其他行数据是否可以更新唯一键呢?也不会。例如:

CREATE TABLE `MANAGERS_UNIQUE` (
  `MANAGER_ID` int(11) NOT NULL, `FIRST_NAME` varchar(45) NOT NULL,`LAST_NAME` varchar(45) NOT NULL,`LEVEL` int(11) DEFAULT NULL,
  PRIMARY KEY (`MANAGER_ID`) /*T![clustered_index] CLUSTERED */,
  UNIQUE KEY `FIRST` (`FIRST_NAME`),
  KEY `level` (`LEVEL`)
)

+------------+------------+-----------+-------+
| 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” 的唯一索引后,会直接返回错误。因此好像并不需要对唯一键进行加锁就可以实现这个功能。(实际上 TIKVacquire_pessimistic_lock 接口的 should_not_exist 就是这个作用)

那么这个唯一索引是否有其他原因或者场景,必须加上锁才可以呢?

除了 Select for update 语句,Delete 语句也会对唯一索引加悲观锁,Update 也会对原有旧的唯一索引加悲观锁,详见:专栏 - TIKV 分布式事务--加锁的 KEY 是什么 | TiDB 社区

这些加锁操作内部核心的原因是什么呢?如果悲观锁的场景下,仅仅对 rowID 加锁,不对唯一索引加锁的话,是否可以减少一些系统的压力。

1 个赞

可以考虑内存悲观锁

高并发环境必须使用悲观锁

@dba远航 @WinterLiu
这个问题就是使用悲观锁的过程中,TIDB 的实现是不止对 rowID 进行了加锁,而且还对唯一索引加了锁。
这么做的核心原因是什么呢?
除了 INSERT 新的唯一索引 value 和 update 新的唯一索引以外,感觉并不需要对唯一索引加锁,只需要对 rowID 加锁即可实现悲观锁

个人理解还是确保数据库正确性

多加锁肯定正确性有提升,但是相应的系统压力也变大了
而且也确实想不到什么场景必须加才可以保证正确性

可能是想多一重保险吧