事务是作为单个逻辑工作单元执行的一系列操作。一个逻辑工作单元必须有四个属性,称为原子性、一致性、隔离性和持久性 (ACID) 属性,只有这样才能成为一个事务。
- 原子性
- 事务必须是原子工作单元;对于其数据修改,要么全都执行,要么全都不执行。
- 一致性
- 事务在完成时,必须使所有的数据都保持一致状态。在相关数据库中,所有规则都必须应用于事务的修改,以保持所有数据的完整性。事务结束时,所有的内部数据结构(如 B 树索引或双向链表)都必须是正确的。
- 隔离
- 由并发事务所做的修改必须与任何其他并发事务所做的修改隔离。事务识别数据时数据所处的状态,要么是另一并发事务修改它之前的状态,要么是第二个事务修改它之后的状态,事务不会识别中间状态的数据。这称为可串行性,因为它能够重新装载起始数据,并且重播一系列事务,以使数据结束时的状态与原始事务执行的状态相同。
- 持久性
- 事务完成之后,它对于系统的影响是永久性的。该修改即使出现系统故障也将一直保持。
数据库引擎隔离级别
ISO 标准定义了关系型数据库的隔离级别:
- 未提交读 Read Uncommitted(隔离事务的最低级别,只能保证不读取物理上损坏的数据) : 允许脏读取,但不允许更新丢失。如果一个事务已经开始写数据,则另外一个数据则不允许同时进行写操作,但允许其他事务读此行数据。该隔离级别可以通过“排他写锁”实现。
- 已提交读 Read Committed(数据库引擎的默认级别,只能读取到已经提交的数据) : 允许不可重复读取,但不允许脏读取。这可以通过“瞬间共享读锁”和“排他写锁”实现。读取数据的事务允许其他事务继续访问该行数据,但是未提交的写事务将会禁止其他事务访问该行。
- 可重复读 Repeated Read : 禁止不可重复读取和脏读取,但是有时可能出现幻影数据。这可以通过“共享读锁”和“排他写锁”实现。读取数据的事务将会禁止写事务(但允许读事务),写事务则禁止任何其他事务。
- 可序列化(串行读) Serializable(隔离事务的最高级别,事务之间完全隔离) : 提供严格的事务隔离。它要求事务序列化执行,事务只能一个接着一个地执行,但不能并发执行。如果仅仅通过“行级锁”是无法实现事务序列化的,必须通过其他机制保证新插入的数据不会被刚执行查询操作的事务访问到。
* SQL Server 和Oracle 默认隔离级别为Read Commited ;MySQL InnoDB存储引擎默认的隔离级别为Repeatable Read。
* 隔离级别越高,越能保证数据的完整性和一致性,但是对并发性能的影响也越大。对于多数应用程序,可以优先考虑把数据库系统的隔离级别设为Read Committed,它能够避免脏读取,而且具有较好的并发性能。尽管它会导致不可重复读、虚读和第二类丢失更新这些并发问题,在可能出现这类问题的个别场合,可以由应用程序采用悲观锁或乐观锁来控制。
下表显示了不同隔离级别导致的并发副作用。
| 隔离级别 | 脏读(Dirty Read) | 不可重复读(NonRepeatable Read) | 幻读(Phantom Read) |
| 未提交读 Read Uncommitted | 是 | 是 | 是 |
| 已提交读 Read Committed | 否 | 是 | 是 |
| 可重复读 Repeated Read | 否 | 否 | 是 |
| 可序列化 Serializable | 否 | 否 | 否 |
- 脏读 Dirty Read ( 一个事务读取了另一个未提交的并行事务写的数据)
- 不可重复读 NonRepeatable Read (一个事务重新读取前面读取过的数据,发现该数据已经被另一个已提交的事务修改过)
- 幻读 Phantom Read (一个事务重新执行一个查询,返回一套符合查询条件的行,发现这些行因为其他最近提交的事务而发生了改变)
* Dirty Read 如事务A的未提交(还依然缓存)的数据被事务B读走,如果事务A失败回滚,会导致事务B所读取的的数据是错误的。
解决办法:如果在第一个事务提交前,任何其他事务不可读取其修改过的值,则可以避免该问题。
* NonRepeatable Read 如事务A中两处读取数据 price 的值,在第一读的时候,price 是10,然后事务B就把 price 的数据改成20,事务A再读一次,结果就发现,price 竟然就变成20了,造成事务A数据混乱。
解决办法:如果只有在修改事务完全提交之后才可以读取数据,则可以避免该问题。
* Phantom Read 与NonRepeatable Read相似,也是同一个事务中多次读不一致的问题。NonRepeatable Read的不一致是因为它所要取的数据集被改变了(比如price 的数据),但 Phantom Read 所要读的数据的不一致却不是它所要读的数据集改变,而是它的条件数据集改变。比如事务A Select count(*) from students where class='1' ; 第一次读了班级1的学生人数为35,第二次读取的时候,由于事务B插入或者删除一个班级为1的学生,结果取出来了36或34。
解决办法:如果在操作事务完成数据处理之前,任何其他事务都不可以添加新数据,则可避免该问题。