数据库的事务一直以来是数据库最核心的基础知识,熟悉事务知识是深入学习数据库的前提;同时,数据库的事务也是互联网面试最最最常问的知识之一。
本文我们将从以下几个角度深入分析:
- 事务的四大特性
- 数据库并发环境下的一致性问题
- 数据库的隔离级别分别所解决的一致性问题。
话不多说,小伙伴们,上车吧!
事务
什么是事务?
事务是数据库系统里面非常重要的术语。它可以由一行简单的SQL来实现,也可以由一组复杂的SQL来实现。对于MySQL来说,有两种事务实现方式,一种是显式事务,另一种则是隐式事务。显式事务需要我们自己手动使用begin或start transaction开启事务,执行完中间的SQL语句后使用commit提交事务,即手动提交。
InnoDB默认为隐式事务,即自动提交,每一行insert、update、delete的SQL语句操作都默认为一个独立的事务。如果想关闭自动提交,可以set autocommit = 0来实现。
本质上来说,事务其实就是一系列逻辑操作,为了保证这一系列逻辑操作能够被准确、统一、安全地执行,就需要通过规范和技术来实现事务,让事务具备特性。
事务有什么特性?
1. 原子性(Atomicity)
一个事务被视为不可分割的最小单元,一个事务的所有操作要么全部提交成功,要么全部失败回滚。当你为一组行为开启事务时,这组行为的全部操作要么同时成功,要么同时失败,不存在某一步行为成功执行而另一步执行失败的场景。一旦某个行为操作失败,那么这组行为里包括执行成功和执行失败的会全部回滚,就像什么事都没发生过一样。
2. 隔离性(Isolation)
一个事务所做的修改在最终提交以前,对其它事务是不可见的。
3. 一致性(Consistency)
数据库的数据在事务执行前后都保持一致性状态。在一致性状态下,所有事务对同一个数据的读取结果都是相同的。不能存在同一个事务对某一组数据前后两次读取的内容不一致的场景。
4. 持久性(Durability)
一旦事务提交,则其所做的修改将会永远保存到数据库中。即使系统发生崩溃,事务执行的结果也不能丢失。
对于一个事务,想要实现它必须遵循以上ACID四个特性,也就是这四个特性必须全部得到满足才可以称之为一个完整的数据库“事务”。
那么InnoDB如何去实现这四个特性呢?不同的特性其实有不同的实现方式:
并发一致性问题
什么是数据库的并发一致性问题?
并发一致性问题,指的是在并发环境下,因为事务的隔离性很难保证,所以会出现很多并发问题。
数据库的并发一致性问题一共有三种,分别是:
1. 脏读(Dirty Read)
指的是事务A读取到了事务B已经修改但是还未提交的数据。
假设A和B同时读取一个数据,事务A读取到这个数据的值为10,随后将其值修改为20,按道理,事务A对这个数据的修改在事务未提交之前是不会被其他事务看到的,但是由于数据库的隔离性未能保证,此时事务B也去读取这个数据的值,就会直接读取到事务A修改完之后的值20,那么此时如果事务A进行了数据的回滚,不提交了。那么事务B最终读取到的就是一个过期的值20。
这种情况就称为脏读。
2. 不可重复读(Nnrepeatable Read)
指的是在一个事务里面两次读取到的内容不一样。
事务B读取到某个数据S的值为10,随后事务A将S的值修改为20。如果B再次读取这个数据,读取到的值就变为20。此时读取的结果和第一次读取的结果不同,这就是不可重复读。按道理,事务B都还没有提交,所读取到的数据应该是对别的事务不可见的,换句话来说应该是安全的。但是由于并发环境下事务的隔离型未能满足,多个事务在某一个相同时刻对同一个数据进行修改,就会出现这样的并发冲突问题。
3. 幻读(Phantom Read)
指的是在一个事务内查询某个数据范围的数据,如果出现了两次查询的结果不一样,就称为“幻读”。
事务A根据条件查询到某个范围的数据[10,20,30,40.50],此时B在这个符合条件的范围内插入新的数据,A再次读取这个范围的数据后,发现该范围多出了一条数据60,此时就发生了“幻读”现象。幻读现象发生的本质,也是由于事务的隔离型未能保证导致的。
所以,MySQL如何解决并发一致性问题?
首先强调一下,MySQL并不等于InnoDB。
InnoDB是MySQL5.5版本之后默认使用的存储引擎。InnoDB使用MVCC可以解决脏读和不可重复读问题。但是,MVCC并不是唯一可以解决并发一致性问题的措施。MVCC本质上是一种乐观锁,通过比较不同事务的版本号的方式来解决问题。可以使用乐观锁,那么一样也可以使用悲观锁。MySQL的其他存储引擎比如Myisam甚至无法使用事务,所以它一般用锁来解决并发一致性问题。
在这里,我先不赘述InnoDB的MVCC和MySQL各种各样的锁,我们放到之后的文章来讲。本篇文章主要强调事务本身。
隔离级别
什么是数据库的隔离级别?
指的是实现了数据库中的安全级别。从对ACID的实现程度上分为四个隔离级别。隔离级别越高的数据库越安全,能解决的并发一致性问题也就越多。
那么数据库有几种隔离级别呢?
1. 未提交读(Read Uncommitted)
事务中的修改,即使没有提交,对其它事务也是可见的。该隔离级别会发生脏读、不可重复读、幻读。所以是最差的一个隔离级别。
2. 提交读(Read Committed)
一个事务只能读取已经提交的事务所做的修改。换句话说,一个事务所做的修改在提交之前对其它事务是不可见的,所以该隔离级别解决了脏读问题,也就是说,当你的数据库实现到了提交读这个隔离级别时,脏读现象就不会再发生。
3. 可重复读(Repeatable Read)
保证在同一个事务中多次读取同一数据的结果是一样的。这是第三个隔离级别,也是InnoDB默认实现的隔离级别。
当你的数据库实现到了提交读这个隔离级别时,脏读和不可重复读现象就都不会再发生。
4. 可串行化(Serializable)
强制事务串行执行,这样多个事务互不干扰,自然而然就不会出现并发一致性问题。
该隔离级别需要加锁实现,因为要使用加锁机制保证同一时间只有一个事务执行。
因为可串行化是串行执行,所以不会有并发问题。这也是最安全的,第四个隔离级别。
总结一下:
- 读未提交就是一种最差的数据库隔离级别, 说明你这个数据库在多事务的时候非常不安全;
- 提交读能解决脏读问题;
- 可重复读能解决不可重复读和脏读问题;
- 串行化能解决脏读、不可重复读、幻读问题。
为什么InnoDB不默认实现可串行化?
数据库提出了这四种隔离级别分别来解决不同的并发一致性问题。
但是,难道隔离级别越高就越好吗?对于各大编程语言,不仅仅要考虑“安全”,还要考虑“性能”,对于数据库一样如此。
隔离级别越高,就代表着越安全,但是同时性能效率也就越低。你想想,当你的数据库做到了串行化,就意味没有并发问题产生。但是此时你读取的数据身上挂着一把锁,一个数据同一时刻只能被一个事务访问,那么剩下的事务获取不到就只能排队。实际生产环境中往往都是在并发环境中对数据库进行操作,业务高峰的时候甚至会有几万、几十万个事务同时存在,所以串行化往往得不到业务上的满足。这就需要在“安全”和“性能”之间做一个衡量,于是MySQL的InnoDB存储引擎默认实现的隔离级别为“可重复读”,而非可串行化。
总结
事务本质上就是一系列逻辑操作,不同数据库、不同存储引擎对事务的支持强度都是不一样的。比如Mysql数据库InnoDB引擎天然支持事务,而Myisam引擎则不支持事务。
数据库事务只有满足了ACID四大特性,才能安全的被我们执行。如果是在某一个时刻只有一个事务在操作,那么就不会出现并发一致性问题,那么ACID就很容易满足。因为隔离性是可以满足的,我们只要满足了原子性,就可以满足一致性。
但是在多事务的并发环境下,由于事务的隔离性很难满足,就会产生脏读、不可重复读、幻读的并发一致性问题。为了解决这些并发一致性问题,数据库系统规范了四个隔离级别:未提交读、提交读、可重复读、可串行化。
隔离级别越高,并发环境下数据库越安全,但是性能也越低。所以为了权衡安全和性能,InnoDB默认实现的隔离级别是“可重复读”。
那么如何实现可重复读呢?可以使用锁,也可以使用MVCC。