InnoDB存储引擎提供了两个特点
1.事务
2.行锁
关于事务的四种隔离级别,在上篇已经说过了,关于可重复读隔离策略能够防止“幻读”问题,需要根据InnoDB的加锁策略来分析。
InnoDB支持的行锁是指通过索引实现对结果记录集合加锁,可以理解为对索引加锁,来保证根据索引搜索的范围内,存在“范围锁”,之所以这么说,是因为如果表中不存在索引,则可能加锁的范围为整张表,即升级为“表锁”。
锁模式
锁的类型可以分为两种
1.共享锁
2.排他锁
对记录集上加共享锁目的很明显,就是防止其他事务对相同记录集上加排他锁;排他锁的用意更明显,就是防止其他事务对相同记录集加锁,无论是共享锁还是排他锁。
加锁的方式
共享锁加锁:select ... lock in share mode
排他锁加锁:select ... for update,其实insert、delete和update自动加排他锁,不过演示效果起见,使用for update方式较好,至于select则不加任何锁,所以无论什么隔离级别,或者已经占据什么锁,对select都没有影响。(你可以阻止它读到数据,但是不能阻止它的执行)
比方说java自身的synchronized关键字,当存在代码块:
synchronized(obj){
//action
}
只是说明 obj 对象上的锁被占用,但是并不能阻止对 obj 对象的访问。
可重复读隔离级别
回顾下,可重复读隔离级别事务可以避免“不可重复读”问题
建表 t 无主键,索引为 id1,插入数据,确定隔离级别
mysql> create table t(
-> id1 int,
-> id2 int,
-> index index_id1(id1))
-> engine=innodb;
Query OK, 0 rows affected (0.22 sec)
mysql> insert into t values(1,1),(2,2);
Query OK, 2 rows affected (0.04 sec)
Records: 2 Duplicates: 0 Warnings: 0
mysql> select @@session.tx_isolation;
+------------------------+
| @@session.tx_isolation |
+------------------------+
| REPEATABLE-READ |
+------------------------+
1 row in set (0.00 sec)
session 1 开启事务,读取数据
mysql> start transaction;
Query OK, 0 rows affected (0.00 sec)
mysql> select * from t;
+------+------+
| id1 | id2 |
+------+------+
| 1 | 1 |
| 2 | 2 |
+------+------+
2 rows in set (0.00 sec)
session 2开启事务,修改数据并提交事务
mysql> start transaction;
Query OK, 0 rows affected (0.00 sec)
mysql> update t set id2=3 where id1=1;
Query OK, 1 row affected (0.00 sec)
Rows matched: 1 Changed: 1 Warnings: 0
mysql> commit;
Query OK, 0 rows affected (0.03 sec)
然后再查询session 1 中的表数据
mysql> select * from t;
+------+------+
| id1 | id2 |
+------+------+
| 1 | 1 |
| 2 | 2 |
+------+------+
2 rows in set (0.00 sec)
数据没有变化,即不存在“不可重复读”,可重复读。(如果session 1 中没有对表 t的修改,则数据显示为session 2 中对表 t的修改)
幻读问题
分析幻读问题,主要查看记录加锁的范围。
1.对包含索引的表进行加锁检索
session 1 开启事务,加锁记录
mysql> start transaction;
Query OK, 0 rows affected (0.00 sec)
mysql> select * from t where id1=1 for update;
+------+------+
| id1 | id2 |
+------+------+
| 1 | 1 |
+------+------+
1 row in set (0.00 sec)
session 2 开启事务,修改非锁定记录,并执行对session 1 锁定记录的修改
mysql> start transaction;
Query OK, 0 rows affected (0.00 sec)
mysql> update t set id2=3 where id1=2;
Query OK, 1 row affected (0.00 sec)
Rows matched: 1 Changed: 1 Warnings: 0
mysql> update t set id2=3 where id1=1;
阻塞直到session 1 提交事务然后完成执行或者超时
2.对非索引表加锁检索
session 1 开启事务锁定记录
mysql> alter table t drop index index_id1;
Query OK, 0 rows affected (0.11 sec)
Records: 0 Duplicates: 0 Warnings: 0
mysql> start transaction;
Query OK, 0 rows affected (0.00 sec)
mysql> select * from t where id1=1 for update;
+------+------+
| id1 | id2 |
+------+------+
| 1 | 1 |
+------+------+
1 row in set (0.00 sec)
session 2 仍然执行修改非锁定记录
mysql> start transaction;
Query OK, 0 rows affected (0.00 sec)
mysql> update t set id2=3 where id1=2;
修改被阻塞,因为session 1 已经锁定了整张表
举例
在其他网页上看到有描述的这样一种情况:
个人持不同观点:
事务的隔离级别的产生,究其原因是为了协调并发事务的高效性和共享索引的安全性,也就是在并发和串行这两个对立概念之间找个折中。隔离作为维护串行性的概念是阻碍并发的产生的,造成的影响就是索引使用效率的降低,响应时间增长,但是给其做出的弥补则是增强了安全性。
但是上图中所示,事务的隔离只发挥了其“隔离”的作用,在模拟事务操作中,并没有对其中的访问记录加锁保护,结果就是,session B中索引的更新因为事务的“隔离性”,对session A不可见,而在session A执行加锁操作之后,才显示出记录的修改。如下:
建表后,session A 开启事务,查询记录
mysql> create table t(
-> id int,
-> age int,
-> primary key (id))
-> engine=innodb;
Query OK, 0 rows affected (0.14 sec)
mysql> start transaction;
Query OK, 0 rows affected (0.00 sec)
mysql> select * from t;
Empty set (0.00 sec)
session B 开启事务,更新索引并提交事务
mysql> start transaction;
Query OK, 0 rows affected (0.00 sec)
mysql> insert into t values(1,1);
Query OK, 1 row affected (0.02 sec)
mysql> commit;
Query OK, 0 rows affected (0.02 sec)
在session A 对加锁范围进行更新之后,才会显示出更新记录
mysql> select * from t;
Empty set (0.00 sec)
mysql> select * from t for update;
+----+------+
| id | age |
+----+------+
| 1 | 1 |
+----+------+
1 row in set (0.00 sec)
也就是上图片中提到的session A 执行的insert、update或delete等自带加锁性质的操作之后,才会对记录集加锁
举例:
session 1 开启事务,限定加锁范围
mysql> select * from t;
+----+------+
| id | age |
+----+------+
| 1 | 1 |
+----+------+
1 row in set (0.00 sec)
mysql> start transaction;
Query OK, 0 rows affected (0.00 sec)
mysql> explain update t set age=2 where id=1;
</span><span style="font-family:FangSong_GB2312;font-size:12px;">+----+-------------+-------+------------+-------+---------------+---------+---------+-------+------+----------+-------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+-------+------------+-------+---------------+---------+---------+-------+------+----------+-------------+
| 1 | UPDATE | t | NULL | range | PRIMARY | PRIMARY | 4 | const | 1 | 100.00 | Using where |
+----+-------------+-------+------------+-------+---------------+---------+---------+-------+------+----------+-------------+
</span><span style="font-family:FangSong_GB2312;font-size:18px;">1 row in set (0.00 sec)
mysql> update t set age=2 where id=1;
Query OK, 1 row affected (0.00 sec)
Rows matched: 1 Changed: 1 Warnings: 0
mysql> select * from t;
+----+------+
| id | age |
+----+------+
| 1 | 2 |
+----+------+
1 row in set (0.00 sec)
执行更新操作,观察explain结果,加锁范围type:range(该值会受MySQL影响,比如表数据较少时可能不会用指定的索引,而改为全局遍历,毕竟一次IO就可以把所有数据读出来(局部性原理),就没必要先在索引中查询再取数据,执行两次IO操作)
session 2 插入数据并提交事务
mysql> start transaction;
Query OK, 0 rows affected (0.00 sec)
mysql> insert into t values(2,2);
Query OK, 1 row affected (0.02 sec)
mysql> commit;
Query OK, 0 rows affected (0.02 sec)
更新session 1 的加锁范围即可显示出新数据mysql> select * from t;
+----+------+
| id | age |
+----+------+
| 1 | 2 |
+----+------+
1 row in set (0.00 sec)
mysql> explain select * from t for update;
</span><span style="font-family:FangSong_GB2312;font-size:14px;">+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+-------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+-------+
| 1 | SIMPLE | t | NULL | ALL | NULL | NULL | NULL | NULL | 2 | 100.00 | NULL |
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+-------+
</span><span style="font-family:FangSong_GB2312;font-size:18px;">1 row in set, 1 warning (0.00 sec)
mysql> select * from t for update;
+----+------+
| id | age |
+----+------+
| 1 | 2 |
| 2 | 2 |
+----+------+
2 rows in set (0.00 sec)
所以使用next-key机制用来保证一定范围内,即满足加锁条件的当前表项以及待添加表项,避免“幻读”问题总结
InnoDB在一定条件可重复读隔离级别下可以实现避免“幻读”问题。至于for update加锁的范围可以通过explain进行查看,例如上面对包含索引的表的加锁,type:ref,锁定相关索引表示的某些列,无索引表的加锁,type:all,还有range等表示范围的加锁。