事务是什么

数据库事务( transaction)是访问并可能操作各种[数据项]的一个数据库操作[序列],这些操作要么全部执行,要么全部不执行,是一个不可分割的工作单位。事务由事务开始与事务结束之间执行的全部数据库操作组成。

事务能否生效数据库引擎是否支持事务是关键。比如常用的 MySQL 数据库默认使用支持事务的innodb引擎。但是,如果把数据库引擎变为 myisam,那么程序也就不再支持事务了!

例如:银行转账操作,A账户转账给B账户,从A账户扣款并使B账户增款,扣款操作和增款操作视为一组操作,在这个过程中,两个环节是关联的。第一个账户划出款项必须保证正确的存入第二个账户,如果第二个环节没有完成,整个的过程都应该取消,否则就会发生丢失款项的问题。整个交易过程,可以看作是一个事物,成功则全部成功,失败则需要全部撤消,这样可以避免当操作的中间环节出现问题时,产生数据不一致的问题。

数据库事务 - 图1

事务的边界包括:

  • 事务的开始边界
  • 事务的正常结束边界(COMMIT),提交事务,永久保存被事务更新后的数据库状态。
  • 事务的异常结束边界(ROLLBACK):撤销事务,使数据库退回到执行事务前的初始状态。

事务四大特性:ACID

1. 原子性(Atomicity)

事务的原子性是指事务必须是一个原子的操作序列单元。事务中包含各项操作在事务执行过程中,只允许存在两种状态:
1、全部成功执行;
2、全部不执行。

任何一项操作失败都将导致整个事务失败。同时其他已经被执行的操作都将被撤销并回滚,只有所有操作全部成功,整个事务才算完成。

2. 一致性(Consistency)

  1. 一致性原则要求事务的执行不改变数据库的一致。即事务执行前如果数据库一致,事务执行后,这种一致性仍然存在。`一致性是指事务是否产生非预期中间状态或结果`。比如脏读和不可重复读,产生了非预期中间状态,脏写与丢失修改则产生了非预期结果。
  2. T事务为例,T执行前,AB的账户余额总和为200元,那么我们要保证在T执行后,AB的账户余额总合仍然为200元。

一致性实际上是由隔离性去进一步保证的,隔离性达到要求,则可以满足一致性。也就是说,隔离不足会导致事务不满足一致性要求,所以务必理解各个隔离级别,才能少写Bug。

3. 隔离性(Isolation)

事务的隔离性是指在并发环境中,并发的事务式相互隔离的,一个事务的执行不能被其他事务干扰。不同事务并发操作相同数据时,每个事务都有各自完整的数据空间,即一个事务内部操作及使用的数据对其他并发事务是隔离的,并发执行的各个事务之间不能相互干扰。

在多线程编程中,如果大家都读写同一块数据,那么久可能出现最终数据不一致,也就是每条线程都可能被别的线程影响了。按理说,最严格的隔离性实现就是完全感知不到其他并发事务的存在,多个并发事务无论如何调度,结果都与串行执行一样。

在标准SQL规范中,定义了四个事务隔离级别,不同的隔离级别对事务处理不同,从高到低依次是:

  • ⬇️可串行化(Serializable)
  • ⬇️可重复读(Read Repeatable)
  • ⬇️已提交读(Read Committed)
  • ⬇️未提交读(Read Uncommitted)

每一个级别都只是指导标准,每个数据库对其的实现都有差异,有的数据库在Read Committed级别时,就已经实现了Read Repeatable的效果,有的数据库干脆不提供Read Uncommitted级别。 在隔离级别为Serializable时,就会感觉到事务像一个完完全全的原子操作,不被任何中断、并发所影响。

4. 持久性(Duration)

即事务成功提交后,对数据的修改永久的,即使系统发生故障,也不会丢失,这里所说的故障,也只是一般错误比如宕机、系统Bug、断电,如果是硬盘损毁,那就没办法,数据一定会丢失。

事务并发问题与隔离级别

在事务并发执行时,隔离不足(隔离级别设置低)会导致的问题。

丢失更新

1.1 第一类丢失更新

定义:A事务撤销时,把已经提交的B事务的更新数据覆盖了。

时间点 事务A 事务B
T1 开始事务
T2 开始事务
T3 查询账户余额为1000元
T4 查询账户余额为1000元
T5 存入100元把余额改为1100元
T6 提交事务
T7 取出100元把余额改为900元
T8 撤销事务
T9 余额恢复为1000元(丢失更新)

以上的示例演示了第一类丢失更新问题,事务B虽然成功了,但是它所做的更新没有被永久存储,这种并发问题是由于完全没有隔离事务造成的。当两个事务更新相同的数据时,如果一个事务被提交,另一个事务却撤销,那么会连同第一个事务所做的更新也被撤销了。(这是绝对避免出现的事情) 事务A的开始时间和结束时间包含事务B的开始和结束时间,事务A回滚事务的同时,把B的已经提交的事务也回滚的,这是避免的,这就是第一类丢失更新.

1.2 第二类丢失更新

定义:A事务提交时,把已经提交的B事务的更新数据覆盖了。

时间点 事务A 事务B
T1 开始事务
T2 开始事务
T3 查询账户余额为1000元
T4 查询账户余额为1000元
T5 取出100元把余额改为900元
T6 提交事务
T7 存入100元把余额改为1100
T8 提交事务
T9 余额恢复为1100元(丢失更新)

第二类丢失更新和第一类的区别实际上是对数据的影响是由A事务的撤销还是提交造成的,它和不可重复读(下面介绍)本质上是同一类并发问题,通常把它看做是不可重复读的一个特例。两个或多个事务查询同一数据。然后都基于自己的查询结果更新数据,这时会造成最后一个提交的更新事务,将覆盖其它已经提交的更新事务。

脏读

定义:读到未提交更新的数据

时间点 事务A 事务B
T1 开始事务
T2 开始事务
T3 查询账户余额为1000元
T4 取出500元把余额改为500元
T5 查询账户余额为500元(脏读)
T6 撤销事务,余额恢复为1000元
T7 存入100元把余额改为600元
T8 提交事务

A事务查询到了B事务未提交的更新数据,A事务依据这个查询结果继续执行相关操作。但是接着B事务撤销了所做的更新,这会导致A事务操作的是脏数据,以上的示例中T5时刻产生了脏读,最终导致A事务提交时账户余额的不正确,可能有人会有疑问,B事务还没有提交或撤销,T5时刻A事务为什么能读到已经改变的数据,这里要说的是,数据表中的数据是实时改变的,事务只是控制数据的最终状态,也就是说如果没有正确的隔离级别,在更新操作语句结束后,即使事务未完成,其他事务就已经可以读取到改变的数据值了。
现在为止:所有的数据库都避免脏读操,可以用两个Mysql会话试验一下以上的操作,在默认的隔离级别下(REPEATABLE-READ),A事务在T5时刻读取到的余额为1000元,不会是500元。

不可重复读

定义:读到已经提交更新的数据,但一个事务范围内两个相同的查询却返回了不同数据。

时间点 事务A 事务B
T1 开始事务
T2 开始事务
T3 查询账户余额为1000元
T4 查询账户余额为1000元
T5 取出100元把余额改为900元
T6 提交事务
T7 查询账户余额为900元(与T4读取的一不一致,不可重复读)

幻读

定义:读到已提交插入数据,幻读与不可重复读类似,幻读是查询到了另一个事务已提交的新插入数据,而不可重复读是查询到了另一个事务已提交的更新数据。

时间点 事务A 事务B
T1 开始事务
T2 开始事务
T3 统计用户Z总存款数为1000元
T4 新增Z的一个存款账号,存款100元
T5 提交事务
T6
T7 再次统计用户Z总存款数为1100元(与T4读取的一不一致,幻读)

A事务第一次查询时,没有问题,第二次查询时查到了B事务已提交的新插入数据,这导致两次查询结果不同。
不可重复读和幻读的区别: 简单来说,不可重复读是由于数据修改引起的,幻读是由数据插入或者删除引起的。

不可重复读,是指在数据库访问中,一个事务范围内两个相同的查询却返回了不同数据。这是由于查询时系统中其他事务修改的提交而引起的。比如事务T1读取某一数据,事务T2读取并修改了该数据,T1为了对读取值进行检验而再次读取该数据,便得到了不同的结果。
一种更易理解的说法是:在一个事务内,多次读同一个数据。在这个事务还没有结束时,另一个事务也访问该同一数据。那么,在第一个事务的两次读数据之间。由于第二个事务的修改,那么第一个事务读到的数据可能不一样,这样就发生了在一个事务内两次读到的数据是不一样的,因此称为不可重复读,即原始读取不可重复。
所谓幻读,是指事务A读取与搜索条件相匹配的若干行。事务B以插入或删除行等方式来修改事务A的结果集,然后再提交。
幻读是指当事务不是独立执行时发生的一种现象,例如第一个事务对一个表中的数据进行了修改,比如这种修改涉及到表中的“全部数据行”。同时,第二个事务也修改这个表中的数据,这种修改是向表中插入“一行新数据”。那么,以后就会发生操作第一个事务的用户发现表中还有没有修改的数据行,就好象发生了幻觉一样.一般解决幻读的方法是增加范围锁RangeS,锁定检锁范围为只读,这样就避免了幻读。

事务隔离级别

数据库并发事务导致的五大问题,总结来说其中两类是更新问题,三类是读问题,数据库是如何避免这种并发事务问题的呢?答案就是通过不同的事务隔离级别,在不同的隔离级别下,并发事务读取数据的结果是不一样的,比如在脏读小节里介绍的,如果是在REPEATABLE-READ隔离级别下,A事务在T5时刻读取是读取不到B事务未提交的数据的。我们需要根据业务的要求,设置不同的隔离级别,在效率和数据安全性中找到平衡点。
SQL标准定义了4类隔离级别,包括了一些具体规则,用来限定事务内外的哪些改变是可见的,哪些是不可见的。低级别的隔离级一般支持更高的并发处理,并拥有更低的系统开销。

四种隔离级别的安全性与性能成反比!最安全的性能最差,最不安全的性能最好!

SERIALIZABLE(串行化)

当数据库系统使用SERIALIZABLE隔离级别时,一个事务在执行过程中完全看不到其他事务对数据库所做的更新。当两个事务同时操作数据库中相同数据时,如果第一个事务已经在访问该数据,第二个事务只能停下来等待,必须等到第一个事务结束后才能恢复运行。因此这两个事务实际上是串行化方式运行。

REPEATABLE READ(可重复读)

当数据库系统使用REPEATABLE READ隔离级别时,一个事务在执行过程中可以看到其他事务已经提交的新插入的记录,但是不能看到其他事务对已有记录的更新。

READ COMMITTED(读已提交数据)

当数据库系统使用READ COMMITTED隔离级别时,一个事务在执行过程中可以看到其他事务已经提交的新插入的记录,而且还能看到其他事务已经提交的对已有记录的更新。

READ UNCOMMITTED(读未提交数据)

当数据库系统使用READ UNCOMMITTED隔离级别时,一个事务在执行过程中可以看到其他事务没有提交的新插入的记录,而且还能看到其他事务没有提交的对已有记录的更新。

隔离级别 第一类丢失更新 第二类丢失更新 脏读 不可重复读 幻读
SERIALIZABLE (串行化) 避免 避免 避免 避免 避免
REPEATABLE READ(可重复读) 避免 避免 避免 避免 允许
READ COMMITTED (读已提交) 避免 允许 避免 允许 允许
READ UNCOMMITTED(读未提交) 避免 允许 允许 允许 允许

我们通过隔离级别的定义很容易自己分析出这张表,比如可重复读隔离级别的定义是一个事务在执行过程中可以看到其他事务已经提交的新插入的记录,但是不能看到其他事务对已有记录的更新。所以,在这种隔离级别下,在脏读示例的T5时刻和不可重复读的T7时刻,事务A都是无论事务B是否提交,事务A都是无法读取到事务B对已有记录的更新的,所以不会产生脏读和不可重复读,而又由于这种隔离级别下可以看到其他事务已经提交的新插入记录,自然是无法避免幻读的产生。另外,值得注意的是所有隔离级别都可以避免第一类丢失更新的问题。

大多数关系数据库默认使用Read committed的隔离级别,Mysql InnoDB默认使用Read repeatable的隔离级别,这和Mysql replication 机制使用Statement日志格式有关。各数据库隔离级别的实现也是有差别的,例如Oracle支持Read committed 和Serializable两种隔离级别,另外可以通过使用读快照在Read committed级别上禁止不可重复读问题;MySQL默认采用RR隔离级别,SQL标准是要求RR解决不可重复读的问题,但是因为MySQL采用了gap lock,所以实际上MySQL的RR隔离级别也解决了幻读的问题,也就是Mysql InnoDB在Read repeatable级别上使用next-key locking 策略来避免幻读现象的产生。