一个事务(transaction)中的所有操作,或者全部完成,或者全部不完成,不会结束在中间某个环节.
在事务开始之前和完成之后,数据都必须保持一致状态,必须保证数据库的完整性。也就是说,数据必须符合数据库的规则。
数据库允许多个并发事务同时对数据进行操作,隔离性保证各个事务相互独立,事务处理时的中间状态对其它事务是不可见的,以此防止出现数据不一致状态。可通过事务隔离级别设置:包括读未提交(Read uncommitted)、读提交(read committed)、可重复读(repeatable read)和串行化(Serializable)
事务处理结束后,对数据的修改就是永久的,即便系统故障也不会丢失。
在 InnoDB 中,默认为 Repeatable (可重复的)级别,InnoDB 中使用一种被称为 next-key locking 的策略来避免phantom(幻读)现象的产生。
使用 select @@tx_isolation;
可以查看 MySQL 默认的事务隔离级别。
不同的事务隔离级别会导致不同的问题:
隔离级别 | 脏读 | 不可重复读 | 幻读 |
---|---|---|---|
Read uncommittied(读未提交数据) | √ | √ | √ |
Read committied(读已提交数据) | × | √ | √ |
Repea read(可重复读) | × | × | √ |
Serializable(可序列化) | × | × | × |
事务传播行为类型 | 说明 |
---|---|
PROPAGATION_REQUIRED | 如果当前没有事务,就创建一个新事务,如果当前存在事务,就加入该事务。这是最常见的选择。 |
PROPAGATION_SUPPORTS | 支持当前事务,如果当前没有事务,就以非事务方式执行。 |
PROPAGATION_MANDATORY | 使用当前的事务,如果当前没有事务,就抛出异常。 |
PROPAGATION_REQUIRED_NEW | 新建事务,如果当前存在事务,就把当前事务挂起。 |
PROPAGATION_NOT_SUPPORTED | 以非事务方式执行操作,如果当前存在事务,就把当前事务挂起。 |
PROPAGATION_NEVER | 以非事务方式执行操作,如果当前存在事务,则抛出异常。 |
PROPAGATION_NESTED | 如果当前存在事务,则在嵌套事务内执行。如果当前没有事务,则执行与PROPAGATION_REQUIRED类似的操作。 |
所谓脏读是指一个事务中访问到了另外一个事务未提交的数据,如下图:
事务A | 事务B |
---|---|
账户5000 | 账户5000 |
小明老婆花了3000 | |
小明查看账户2000(产生脏读) | |
取款操作发生未知错误,事务B回滚,账户还是5000(rollback) | |
commit | commit |
事务B更新账户为2000,但是在事务B commit之前,事务A查看账户时是事务B还未提交的数据,此时事务B出现错误执行rollback,而事务A拿到的还是2000,这就是脏读。
事务A | 事务B |
---|---|
账户5000 | 账户5000 |
小明查看账户5000 | |
小明老婆花了3000(修改该账户) | |
commit | |
小明查看账户2000 | |
commit |
在事务A中事务B修改了该账户,所以两次得到的值不同。
事务A | 事务B |
---|---|
查询id>100的用户(只有102) | |
插入id=101的用户(John) | |
commit | |
插入id=1的用户(主键冲突,幻读) | |
commit |
由于很多人(当然也包括本人), 容易搞混 不可重复读
和 幻读
, 这两者确实非常相似。
不可重复读
主要是说多次读取一条记录, 发现该记录中某些列值被修改过。幻读
主要是说多次读取一个范围内的记录(包括直接查询所有记录结果或者做聚合统计), 发现结果不一致(标准档案一般指记录增多, 记录的减少应该也算是幻读)。(可以参考MySQL官方文档对 Phantom Rows 的介绍) 其实对于 幻读
, MySQL的InnoDB引擎默认的RR
级别已经通过MVCC自动帮我们解决了
, 所以该级别下, 你也模拟不出幻读的场景; 退回到 RC
隔离级别的话, 你又容易把幻读
和不可重复读
搞混淆, 所以这可能就是比较头痛的点吧!
具体可以参考《高性能MySQL》对 RR
隔离级别的描述, 理论上RR级别是无法解决幻读的问题, 但是由于InnoDB引擎的RR级别还使用了MVCC(多版本并发控制), 所以也就避免了幻读的出现!
总结:
(1)不可重复读是读取了其他事务更改的数据,针对update操作
解决:使用行级锁,锁定该行,事务A多次读取操作完成后才释放该锁,这个时候才允许其他事务更改刚才的数据。
(2)幻读是读取了其他事务新增的数据,针对insert与delete操作
解决:使用表级锁,锁定整张表,事务A多次读取数据总量之后才释放该锁,这个时候才允许其他事务新增数据。
解决方案也就是上文提到的四种隔离级别,他们可以最大程度避免以上三种情况的发生:
隔离级别越高,越能保证数据的完整性和一致性,但是对并发性能的影响也越大。对于多数应用程序,可以优先考虑把数据库系统的隔离级别设为Read Committed。它能够避免脏读取,而且具有较好的并发性能。尽管它会导致不可重复读、幻读和第二类丢失更新这些并发问题,在可能出现这类问题的个别场合,可以由应用程序采用悲观锁或乐观锁来控制。
第一类丢失更新(回滚丢失,Lost update) : A事务撤销时,把已经提交的B事务的更新数据覆盖了。
事务A | 事务B |
---|---|
查询账户余额1000 | |
查询并汇入1000元,余额改为2000元 | |
commit | |
取出1000元,把余额改为0元 | |
撤销事务 | |
余额1000(丢失更新) |
事务A | 事务B |
---|---|
查询账户余额1000 | |
查询账户余额1000 | |
取出1000元,把余额改为0元 | |
commit | |
汇入1000元 | |
commit | |
余额2000(丢失更新) |
每次获取数据的时候,都会担心数据被修改,所以每次获取数据的时候都会进行加锁,确保在自己使用的过程中数据不会被别人修改,使用完成后进行数据解锁。由于数据进行加锁,期间对该数据进行读写的其他线程都会进行等待。
每次获取数据的时候,都不会担心数据被修改,所以每次获取数据的时候都不会进行加锁,但是在更新数据的时候需要判断该数据是否被别人修改过。如果数据被其他线程修改,则不进行数据更新,如果数据没有被其他线程修改,则进行数据更新。由于数据没有进行加锁,期间该数据可以被其他线程进行读写操作。
悲观锁:比较适合写入操作比较频繁的场景,如果出现大量的读取操作,每次读取的时候都会进行加锁,这样会增加大量的锁的开销,降低了系统的吞吐量。
乐观锁:比较适合读取操作比较频繁的场景,如果出现大量的写入操作,数据发生冲突的可能性就会增大,为了保证数据的一致性,应用层需要不断的重新获取数据,这样会增加大量的查询操作,降低了系统的吞吐量。
总结:两种所各有优缺点,读取频繁使用乐观锁,写入频繁使用悲观锁。
悲观锁比较适合强一致性的场景,但效率比较低,特别是读的并发低。乐观锁则适用于读多写少,并发冲突少的场景。
RC级别,提供了读锁和写锁,解决了脏读问题。
RR级别,在已有读锁和写锁的基础上,增加了gap锁,即间隙锁,解决了幻读的问题(间隙锁解决了幻读,由此就能明白到底什么是幻读了)。