1. INNODB 默认情况下的加锁情况
在默认情况下(不去修改事务的隔离级别为 serializable),所有的 select … …. from 这样的SQL语句都是快照读,读取的是数据库的一个快照。是不会加任何锁的 serializable下,凡是通过beginl;start transaction 这样的语句开启了事务,就算是普通的select 语句,也会被隐式转换成 SELECT … LOCK IN SHARE MODE,所以还是加上了共享锁 而针对于写的操作,update、insert into、delete 这样的SQL,会自动加上排他锁(行级别的),为这些行加上了行级别的排他锁,所以也是避免了数据的不安全性。
2. 什么是快照读?什么是当前读?
快照读(snapshot read)
- 语句:
- 简单的select语句(不包括)SELECT … LOCK IN SHARE MODE 和 SELECT …FOR UPDATE
- 实现:
- 基于MVVC 和 UNDO LOG 来实现
- 而MySQL中 MVVC的实现又是依靠 ReadView(事务视图)来实现的。多个ReadView组成 回滚日志。每个普通的 select … from 查询的都是最新的 ReadView 的该条数据的值。
ReadView:ReadView是针对同一条数据生成的视图
- 读已提交:
- 事务中每个sql语句执行都会生成一个ReadView,所以一个事务中的多条sql语句也就生成了多个ReadView,而每条sql执行时,都会查询最新的那个ReadView的值。
- 事务A有2个sql语句,第一个查询的sql生成了一个 id = n 的 ReadView。如果事务B对该数据进行了操作,那么就会生成 id = n+1 的 readview ,下一次查询就会选择 id = n+1 的 readview。
- 可重复读:
- 读已提交:
语句:
- select … lock in share mode
- select … for update
- insert
- update
- delete
- 实现:
- 是基于 临键锁(行锁 + 间歇锁)来实现的。
- 更新数据时,都是先读后写,而这个读就是当前读。说白了,当前读就是携带锁的读,目的就是为了防止读数据时,其他事务对数据进行了修改,就是为了保证数据的安全性。
3. MySql InnoDB 中的三种行锁(记录锁、间隙锁、临键锁)
参考 行锁在InnoDB中通过索引实现,所以一旦某个加锁的操作没有使用索引,那么就会由行锁升级成为表锁。
记录锁(Record Locks)
就是给某行的记录加锁,它是基于一行或者说是表中的一条记录,那么就一定需要依赖主键或者唯一索引,来进行精准匹配, 如:
SELECT * FROM `STUDENT` WHERE `ID` = 1 FOR UPDATE;
注意ID这个字段必须为主键或者是唯一索引,否则加的锁就会变成临键锁。而且查询语句必须为精准匹配(=),像是>、<、like等也会退化成临键锁。
我们都知道 INNODB 在写的情况下会自动加上排他锁来保证数据的安全性。为了确保这个锁是仅仅针对于某一行记录的锁,我们通常这样来写
-- id 是主键或者唯一索引
UPDATE SET AGE = 50 WHERE ID = 1;
间隙锁(Gap Locks)
间隙锁基于非唯一索引,它锁定的是一段范围性的索引记录。间隙锁基于 Next-Key Locking 算法,使用间隙锁锁住的是一个区间,而不仅仅是这个区间,而不仅仅是这个区间中的每一条数据。如:
SELECT * FROM table WHERE ID BETWEEN 1 AND 10 FOR UPDATE;
所有在(1, 10)区间内的记录行都会被锁住。除了像是这样手动加上一个间隙锁。在执行完某些 SQL 后,InnoDB 也会自动加间隙锁
临键锁(Next-Key Locks)
临键锁可以理解为一种特殊的间隙锁,它是一种特殊的算法。通过临键锁可以解决幻读的问题。没个数据行上的非唯一索引列上都会存在一把临键锁。当某个事务持有该数据行的临键锁时,会锁住一段左开右闭区间的数据。
也就是(a, b]. InnoDB中行级锁是基于索引实现的,临建锁只与非唯一索引列有关,所以在唯一索引列上(包括主键)不存在临建锁
-- 开启一个cmd
mysql> use cook;
Database changed
mysql> select version();
+-----------+
| version() |
+-----------+
| 8.0.12 |
+-----------+
1 row in set (0.00 sec)
mysql> select * from person;
+----+-----+-------+
| id | age | name |
+----+-----+-------+
| 1 | 10 | test1 |
| 2 | 24 | test2 |
| 3 | 32 | test3 |
| 4 | 45 | test4 |
+----+-----+-------+
-- id 是主键,age 和name是普通字段
-- 现在分析age可能存在的临键锁区间
(-∞, 10],
(10, 24],
(24, 32],
(32, 45],
(45, +∞],
-- 现在开启事务A
mysql> begin;
mysql> update person set name = 'xs' where age = 24;
Query OK, 1 row affected (0.00 sec)
-- 然后开启事务B
mysql> begin;
Query OK, 0 rows affected (0.01 sec)
mysql> insert into person (id, age, name) values(5, 11, 'test5');
ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction
-- 我们可以看到,事务B的这条sql被阻塞了。
-- 因为事务A在执行上述sql 后会获得一个(10, 32) 的临届主键。
-- 哪怕age的范围也不行,因为... ...
insert into person (id, age, name) VALUES (10, 44, 'fly');
小结
- InnoDB 中的行锁的实现依赖于索引,一旦某个加锁操作没有使用到索引,那么该锁就会退化为表锁。
- 记录锁存在于包括主键索引在内的唯一索引中,锁定单条索引记录。
- 间隙锁存在于非唯一索引中,锁定开区间范围内的一段间隔,它是基于临键锁实现的。
- 临键锁存在于非唯一索引中,该类型的每条记录的索引上都存在这种锁,它是一种特殊的间隙锁,锁定一段左开右闭的索引区间。