一、事务的4大特性(ACID)
1. 原子性(Atomicity)
事务是数据库的逻辑工作单位,它对数据库的修改要么全部执行,要么全部不执行。
2. 一致性(Consistemcy)
事务前后,数据库的状态都满足所有的完整性约束。
3. 隔离性(Isolation)
并发执行的事务是隔离的,不会相互影响。如果有两个事务,运行在相同的时间内,执行相同的功能,事务的隔离性将确保每一事务在系统中认为只有该事务在使用系统。这种属性有时称为串行化,为了防止事务操作间的混淆,必须串行化或序列化请求,使得在同一时间仅有一个请求用于同一数据。通过设置数据库的隔离级别,可以达到不同的隔离效果。
4. 持久性(Durability)
指的是只要事务成功结束,它对数据库所做的更新就必须永久保存下来。即使发生系统崩溃,重新启动数据库系统后,数据库还能恢复到事务成功结束时的状态。
二、并发事务引起的问题
1.脏读(dirty read)
A事务读取了B事务尚未提交的更改数据,并且在这个数据基础上进行操作。如果此时恰巧B事务进行回滚,那么A事务读到的数据是根本不被承认的。
以下是一个取款事务和转账事务并发时引起的脏读场景。
时间 | 转账事物A | 取款事物B |
---|---|---|
t1 | 开始事务 | |
t2 | 开始事务 | |
t3 | 查询账户余额为1000元 | |
t4 | 取出500元,把余额改为500元 | |
t5 | 查询账户余额为500元(脏读) | |
t6 | 撤销事务,余额恢复为1000元 | |
t7 | 汇入100元,余额改为600元 | |
t8 | 提交事务 | |
在这个场景中,B希望取款500元,而后有撤销了动作,而A往同一个账户转账100元,因为A事务读取了B事务尚未提交的数据,因而导致了账户白白丢失了500元。
2.不可重复读(unrepeatable read)
不可重复读是指A事务读取了B事务已经提交的更改数据。假设A在取款事务的过程中,B往该账户转账100元,A两次读取账户的余额发生不一致
时间 | 取款事务A | 转账事务B |
---|---|---|
T1 | 开始事务 | |
T2 | 开始事务 | |
T3 | 查询账户余额为1000元 | |
T4 | 查询账户余额为1000元 | |
T5 | 取出100元,把余额改为900元 | |
T6 | 提交事务 | |
T7 | 查询账户余额为900元 |
在同一个事务中T4和T7时间点读取的账户存款余额不一致
3.幻象读(phantom read)
A事务读取B提交的新增数据,这时A事务将出现幻想读的问题。幻读一般发生在计算统计数据的事务中。举个例子,假设银行系统在同一个事务中两次统计存款的总金额,在两次统计过程中,刚好新增了一个存款账户,并存入100元,这时两次统计的总金额将不一致。
时间 | 统计金额事务A | 转账事务B |
---|---|---|
T1 | 开始事务 | |
T2 | 开始事务 | |
T3 | 统计存款总金额为10000元 | |
T4 | 新增一个存款账户,存款为100元 | |
T5 | 提交事务 | |
T6 | 再次统计存款总金额为10100元(幻象读) |
如果新增的数据刚好满足事务的查询条件,那么这个新数据就会进入事务的视野,因而导致两次统计结果不一致的情况。
幻读和不可重复读是两个容易混淆的概念,前者是指读到了其他事物已经提交的新增数据,而后者是读到了已经提交事务的更改数据(更改或删除)。为了避免这两种情况,采取的策略是不同的:防止读到更改数据,只需对操作的数据添加行级锁,阻止操作过程中的数据发送变化,而防止读到新增数据,则往往需要添加一个表级锁–将整张表锁定,防止新增数据(Oracle使用多版本数据的方式实现)
4.第一类丢失更新
时间 | 取款事务A | 转账事务B |
---|---|---|
T1 | 开始事务 | |
T2 | 开始事务 | |
T3 | 查询账号余额为1000元 | |
T4 | 查询余额为1000元 | |
T5 | 汇入100元,把余额改为1100元 | |
T6 | 提交事务 | |
T7 | 取出100元,把余额改为900元 | |
T8 | 撤销事务 | |
T9 | 余额恢复为1000元(丢失更新) |
A事务在撤销时,“不小心”将B事务已经转入账号的金额给抹去了。
5.第二类丢失更新
A事务覆盖B事务已经提交的数据,造成B事务所操作丢失。
时间 | 转账事务A | 取款事务B |
---|---|---|
T1 | 开始事务 | |
T2 | 开始事务 | |
T3 | 查询账号余额为1000元 | |
T4 | 查询余额为1000元 | |
T5 | 取出100元,把余额改为900元 | |
T6 | 提交事务 | |
T7 | 汇入100元,把余额改为1100元 | |
T8 | 撤销事务 | |
T9 | 把余额改为1100元(丢失更新) |
三、事务的隔离级别
以上的4种问题(更新丢失、脏读、不可重复读、幻读)都和事务的隔离级别有关。通过设置事务的隔离级别,可以避免上述问题的发生。
1. 读未提交(Read Uncommitted)
读事务不阻塞其他读事务和写事务,未提交的写事务阻塞其他写事务但不阻塞读事务。
此隔离级别可以防止更新丢失,但不能防止脏读、不可重复读、幻读。
此隔离级别可以通过“排他写锁”实现。
2. 读已提交(Read Committed)
读事务允许其他读事务和写事务,未提交的写事务禁止其他读事务和写事务。
此隔离级别可以防止更新丢失、脏读,但不能防止不可重复读、幻读。
此隔离级别可以通过“瞬间共享读锁”和“排他写锁”实现。
3. 可重复读取(Repeatable Read)
以操作同一行数据为前提,读事务禁止其他写事务但不阻塞读事务,未提交的写事务禁止其他读事务和写事务。
此隔离级别可以防止更新丢失、脏读、不可重复读,但不能防止幻读。
此隔离级别可以通过“共享读锁”和“排他写锁”实现。
4. 序列化(Serializable)
提供严格的事务隔离,它要求事务序列化执行,事务只能一个接着一个地执行,不能并发执行。
此隔离级别可以防止更新丢失、脏读、不可重复读、幻读。
如果仅仅通过“行级锁”是无法实现事务序列化的,必须通过其他机制保证新插入的数据不会被刚执行查询操作的事务访问到。
可串行化:如果一个并行调度的结果等价于某一个串行调度的结果,那么这个并行调度是可串行化的。
隔离级别越高,越能保证数据的完整性和一致性,但是对并发性能的影响也越大。对于多数应用程序,可以优先考虑把数据库系统的隔离级别设为Read Committed。它能够避免更新丢失、脏读,而且具有较好的并发性能。尽管它会导致不可重复读、幻读这些并发问题,在可能出现这类问题的个别场合,可以由应用程序采用悲观锁或乐观锁来控制。
参考: