事务的四大特性ACID,原子性、一致性、隔离性、持久性。
其中隔离性的实现是通过隔离级别的设置。
那么要说到隔离级别,就涉及到没有隔离级别时,会有什么问题?
并发事务之间操作相同数据,如果没有隔离性级别。就会导致脏读、幻读、不可重复读问题。
而不同的隔离级别也就对应不同的场景。
1.读未提交,解决不了任何并发事务问题,数据库也都不采用此隔离级别。
2.读已提交,解决了脏读的问题。
3.可重复读,解决了脏读、不可重复读、部分幻读问题,但是没有彻底解决幻读问题。
4.串行化,解决了脏读、幻读、不可重复读问题。
而隔离级别从低到高,性能也随之下降。
串行化隔离级别最高,并发事务场景下数据安全性最高,但是采用的+锁机制,因此性能也最低。
读已提交和可重复读则是基于MVCC多版本并发机制实现的。
读已提交级别下,单个事务每一次查询都会生成一个数据的快照读,并且快照读依赖可见性规则。
可重复读级别下,单个事务只有第一次查询生成一个数据的快照读,后续查询都是查询的第一次生成的快照读。
快照读里有4个重要属性:
1.未提交事务集合,指的是操作当前数据的所有未提交的事务id集合。事务id生成时是从小到大自增的。
2.未提交事务集合里的最小事务id,也就是最早操作该数据的事务。
3.创建当前快照读的事务id
4.创建当前快照读的事务id+1,也就是创建当前快照读的事务的下一个事务id。
快照读可见性规则依赖于上面的部分属性。
规则1:未提交事务id中包含当前事务id,且当前事务id等于创建快照读的事务id,那该数据对于当前事务可见。
规则2:未提交事务id中包含当前事务id,但是当前事务id并不等于创建快照读的事务id,那说明当前事务不是该数据快照读的创建者,并且当前事务也没提交,那该数据对于当前事务不可见。
规则3:未提交事务id中不包含当前事务id,那说明当前事务已经提交了,该数据对于已经提交了的事务是可见的。
除了快照读,还有当前读。当前读会读取最新数据。
在读提交和可重复读两种事务隔离级别下,普通的select操作使用“快照读”,不会对数据加锁,也不会被事务阻塞。
当前事务的select操作,如果后面没有加 lock in share mode 或者 for update 关键字,生成的就是普通的快照读。
当前读的语句:
select * from table where ? lock in share mode;
select * from table where ? for update;
insert into table values (…);
update table set ? where ?;
delete from table where ?;
当前读的实现方式
当前读使用next-key锁(行记录锁+Gap间隙锁)实现
间隙锁
:只有在Read Repeatable、Serializable隔离级别才有,就是锁定范围空间的数据。
假设id有3,4,5,锁定id>3的数据,是指的4,5及后面的数字都会被锁定,因为此时如果不锁定没有的数据,例如当加入了新的数据id=6,就会出现幻读,间隙锁避免了幻读。
对主键或唯一索引,如果当前读时,where条件全部精确命中(=或者in),这种场景本身就不会出现幻读,所以只会加行记录锁。
没有索引的列,当前读操作时,会加全表gap锁,生产环境要注意。
非唯一索引列,如果where条件部分命中(>、<、like等)或者全未命中,则会加附近Gap间隙锁。例如,某表数据如下,非唯一索引2,6,9,9,11,15。如下语句要操作非唯一索引列9的数据,gap锁将会锁定的列是(6,11],该区间内无法插入数据。
下面就着重讲一下为什么说可重复读不能彻底解决幻读问题!!!
首先解释一下什么是幻读?(以下引用自百度百科)
事务A读取与搜索条件相匹配的若干行。事务B以插入或删除行等方式来修改事务A的结果集,然后再提交。
幻读是指当事务不是独立执行时发生的一种现象,例如第一个事务对一个表中的数据进行了修改,比如这种修改涉及到表中的“全部数据行”。同时,第二个事务也修改这个表中的数据,这种修改是向表中插入“一行新数据”。那么,以后就会发生操作第一个事务的用户发现表中还存在没有修改的数据行,就好象发生了幻觉一样.一般解决幻读的方法是增加范围锁RangeS,锁定检索范围为只读,这样就避免了幻读。
也就是说幻读指的是,多个事务都发生了写的操作,但是写的是同一个范围内的数据集,导致一个事务的写操作的结果影响到了另一个事务中的写操作后的数据集。
可重复读的隔离级别下,使用的还是MVCC的快照读机制,并没有加锁,因此数据还是可能会被其他事务影响。只有通过当前读的排它锁,才可以保证数据在同一时间只能被一个事务操作。
利用select * for update 可以锁表/锁行。自然锁表的压力远大于锁行。尽量采用锁行。FOR UPDATE仅适用于InnoDB,且必须在事务处理模块(BEGIN/COMMIT)中才能生效。那么什么时候锁表呢?
例1: (明确指定主键,并且有此记录,row lock)
SELECT * FROM wallet WHERE id=’3′ FOR UPDATE;
例2: (明确指定主键,若查无此记录,无lock)
SELECT * FROM wallet WHERE id=’-1′ FOR UPDATE;
例3: (无主键,table lock)
SELECT * FROM wallet WHERE name=’Mouse’ FOR UPDATE;
例4: (主键不明确,table lock)
SELECT * FROM wallet WHERE id<>’3′ FOR UPDATE;
例5: (主键不明确,table lock)
SELECT * FROM wallet WHERE id LIKE ‘3’ FOR UPDATE;
因此,通过幻读的定义得知,需要通过加排他锁的机制,才能保证数据资源在同一时间只能被一个事务操作。