引言
事务,体现的是部分与整体的思想,多个部分组成一个整体,要么全部生效,要么全部失效。事务就是实现这种效果的一个抽象概念。这种实现反映到数据库中,就是多条SQL语句,要么所有执行成功,要么所有执行失败。
ACID
在数据库当中,一个事务必须同时满足4个特性即:原子性(Atomic)、一致性(Consistency)、隔离性(Isolation)和持久性(Durabiliy)简称ACID。
原子性:表示组成一个事务的多个数据库操作是一个不可分割的原子单元,只有所有的操作执行成功,整个事务才提交。事务中的任何一个数据库操作失败,已经执行的任何操作都必须撤销,让数据库返回到初始状态。
一致性:事务执行的结果必须是使数据库从一个一致性状态变到另一个一致性状态。
假设数据库的初始状态为C0,事务T1的提交就会导致数据库状态从C0转变成C1,执行事务T2的时候数据库状态就从C1变成C2了,以此类推,执行T(n)次事务的时候数据库状态就从C(n-1)变成C(n)了。
定义一致性主要有2个方面,一致读和一致写。
一致写: 事务执行的数据变更只能基于上一个一致的状态,且只能体现在一个状态中。T(n)的变更结果只能基于C(n-1),C(n-2),...C(1)状态,且只能体现在C(n)状态中。也就是说,一个状态只能有一个事务变更数据,不允许有2个或者2个以上事务在一个状态中变量数据。至于具体一致写基于哪个状态,需要判断T(n)事务是否和T(n-1),T(n-2),....T1有无依赖关系。
例如:
定义100个事务T(1)...T(100)实现相同的逻辑 update table set i=i+1,i的初始值是0,那么并发执行这100个事务之后i的值是多少?
可能很容易想到是100。那么怎么从一致性角度去理解呢?
数据库随机调度到T(50)执行,此时数据库状态是C(0),而其它事务都和T(50)有依赖关系,根据写一致性原理,其它事务必须等到T(50)执行完毕后数据库状态变为C(1)才可以执行。因此数据库利用锁机制阻塞其它事务的执行。直到T(50)执行完毕,数据库状态从C(0)迁移到C(1)。数据库唤醒其它事务后随机调度到T(89)执行,以此类推直到所有事务调度执行完毕,数据库状态最终变为C(100)。
一致读:事务读取数据只能从一个状态中读取,不能从2个或者2个以上的状态读取。也就是T(n)只能从C(n-1),C(n-2)....C1中的一个状态读取数据,不能一部分数据读取自C(n-1),而另一部分数据读取自C(n-2)。
例如:
还是上面的例子,假设T(1)...T(100)顺序执行,在不同的时机执行select i from table,我们看到i的值是什么?
1、T(1)的执行过程中。数据库状态尚未迁移,读到的i=0
2、T(1)执行完毕,T(2)的执行过程中,数据库状态迁移至C(1),读到的i=1隔离性:在并发数据操作时,不同的事务拥有各自的数据空间,它们的操作不会对对方产生干扰。准确地说,并非要求做到完全不干扰。数据库规定了多种事务隔离级别,不同的隔离级别对应不同的干扰程度,隔离级别越高,数据一致性越好,但并发性越弱。
持久性:一旦事务提交成功后,事务中所有的数据操作都必须被持久化到数据库中。即使在提交事务后,数据库马上崩溃,在数据库重启时,也必须保证能够通过某种机制恢复数据。
在这些事务特性中,数据“一致性”是最终目标,其他特性都是为达到这个目标而采取的措施,要求或手段。
数据库管理系统一般采用重执行日志来保证原子性、一致性、持久性。重执行日志记录了数据库变化的每一个动作,数据库在一个事务中执行一部分操作后发生错误退出,数据库即可根据重执行日志撤销已经执行的操作,此外,对于已经提交的事务,即使数据库崩溃,在重启数据库时也能够根据日志对尚未持久化的数据进行相应的重执行操作。
采用数据库锁机制 保证事务的隔离性。当多个事务试图对相同的数据进行操作时,只有持有锁的事务才能操作数据,直到前一个事务完成后,后面的事务才有机会对数据进行操作。
并发
一个数据库可能拥有多个访问客户端,这些客户端都可用并发的方式访问数据库。数据库中的相同数据可能同时被多个事务访问,如果没有采取必要的隔离措施,就会导致各种并发问题,破坏数据的完整性。这些问题可以归结为读与写两类操作上。
读并发
1、脏读
A事务读取B事务尚未提交的更改数据,并在这个数据的基础上进行操作。如果恰巧B事务回滚,那么A事务读到的数据根本是不被承认的,如下:
在这个场景中,B希望取款500元,而后又撤消了动作,而A往相同的账户中转账100元,就因为A事务读取了B事务尚未提交的数据,因而造成账户白白丢失了500元。
2、不可重复读
不可重复读是指A事务读取了B事务已经提交的更改数据。假设A在取款事务的过程中,B往该账户转账100元,A两次读取账户的余额发生不一致。
在同一事务中,T4时间点和T7时间点读取的账户存款余额不一致。
3、幻象读
A事务读取B事务提交的新增数据,这时A事务将出现幻象读的问题。幻象读一般发生在计算统计数据的事务中。如下:
在这个场景中,假设银行系统在同一个事务中两次统计存款账户的总金额,在两次统计过程中,刚好新增了一个存款账户,并存入100元,这时两次统计的总金额将不一致。
注:
不可重复读是指读到了已经提交事务的更改数据(UPDATE 或DELETE),只需对操作的数据添加行锁就可以解决了。
幻象读是指读到了其他已经提交事务的新增数据(INSERT),而为了防止读到新增的数据,往往需要添加表锁,将整张表锁定。
写并发
1、第一类丢失更新(回滚丢失更新)
A事务撤消时,把已经提交的B事务的更新数据覆盖了。这种错误可能造成很严重的问题,如下:
A事务在撤销时,“不小心”将B事务已经转入账户的金额给抹去了。
2、第二类丢失更新(提交丢失更新)
A事务覆盖B事务已经提交的数据,造成B事务所做的操作丢失。
在上面这个场景中,由于支票转账事务覆盖了取款事务对存款余额所做的更新,导致银行最后损失了100元。相反,如果转账事务先提交,那么用户账户将损失100元。
隔离级别
要解决上面的问题,我们很容易想到的解决办法可能就是给数据加锁,但如果直接给数据加锁是非常麻烦的,而且容易出错,因此数据库为用户提供了自动锁机制,这种自动锁机制就是我们常说的事务隔离级别,只要用户指定会话的事务隔离级别,数据库就会分析事务中的SQL语句,然后自动为事务操作的数据资源添加适合的锁。
ANSI/ISO SQL92标准定义了4个等级的事务隔离级别,在相同的数据环境下,使用相同的输入,执行相同的工作,根据不同的隔离级别,可能导致不同的结果。如下所示:
注:
事务的隔离级别和数据库并发性是对立的。隔离级别越高并发性和吞吐量越低,发生的问题也就越少。
参考资料
1、《精通Spring4.x 企业应用开发实战--第11章 Spring的事务管理》
2、知乎--如何理解数据库事务中的一致性的概念?
3、《Hibernate5讲义》