数据库事务
事务(Transaction)是并发控制的基本单位。所谓的事务,它是一个操作序列,这些操作要么全部执行,要么全部都不执行。比如,银行转账,从一个账号扣钱,然后另一个账号余额增加,这两个操作要么都执行,要么都不执行。这两个操作组合在一起就是事务。
数据库事务有严格的定义,它必须同时满足4个特性:
- 原子性,Atomic
- 一致性,Consistency
- 隔离性, Isolation
- 持久性,Durabiliy
简称ACDI。下面是对每一个特性的说明:
- 原子性:表示组成一个事务的多个数据库操作是一个密不可分的原子单元,只有所有的操作执行成功,整个事务才提交。事务中的任何一个数据库操作失败,已经执行的任何操作都必须撤销(回滚),让数据库恢复到事务提交之前的状态。
- 一致性:数据库总是从一个一致性状态装换到另一个一致性状态。一致性状态的含义是数据库中的数据应该满足数据库约束。
- 隔离性:在并发数据操作时,不同的事务拥有各自的数据空间,它们的操作不会对对方产生干扰。但是也并非要做到完全无干扰。数据库规定了多个隔离级别,不同的隔离级别的干扰程度是不同,隔离级别越高,数据一致性越好,但并发性越弱。
- 持久性:一旦数据库提交之后,事务中的所有操作都必须被持久化都数据库中。即使在提交事务后,数据库重启时,也必须保证能够通过某种机制恢复数据。
在这些事务的特征中,数据”一致性“是最终目标,其他特性都是为达到这个目标而采取的措施。
数据库并发的问题
一个数据库可能会有多个客户端同时访问,数据库中相同的数据就有可能同时被多个事务访问,如果没有采取必要的隔离措施,就会导致各种问题,破坏数据的完整性,这些问题可以分为5中,两类:
- 数据读取的问题:
- 脏读
- 不可重复读
- 幻想读
- 数据更新问题
- 第一类丢失更新
- 第二类丢失更新
1. 脏读(direct read)
A事务读取B事务尚未提交更改的数据,并在这个数据的基础上进行操作。如果恰巧B事务回滚,那么A事务读取到的数据是不被承认的。通过一个取款事务和转账事务来说明这个问题。
时间 | 转账事务A | 取款事务B |
---|---|---|
T1 | 开始事务 | |
T2 | 开始事务 | 查询账户余额1000元 |
T3 | 取出500元,把余额改为500元 | |
T4 | 查询余额500元(脏读) | |
T5 | 撤销事务,余额恢复为1000元 | |
T6 | 汇入100元,把余额改为600元 | |
T8 | 提交事务 |
在这个场景中转账事务A读取到取款事务B中的未提交的数据,导致脏读。
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事务将出现幻想读现象。幻想读一般发生在计算统计数据的事务中。
举个例子,比如在银行系统的同一个事务中有两次统计存款用户的总金额,在两次统计中刚好新增了一个存款,这时,两次统计的结构将会不一致。
时间 | 统计金额事务A | 转账事务B |
---|---|---|
T1 | 开始事务 | |
T2 | 开始事务 | |
T3 | 统计总存款为1000元 | |
T4 | 新增一个存款账户,存款100元 | |
55 | 提交事务 | |
T6 | 再次统计总存款数为10100元(幻想读) |
如果新增的数据刚好满足事务的查询条件,那么这个新数据就会出现事务的视野中,因而产生了两次结构不一致。
幻想读和不可重复读这两个概念比较容易混淆
,幻想读是指读到了其他已经提交的事务的新增数据,而不可重复读是指读到了已经提交的更改数据(更改或者删除)。
为了避免这两种情况,采取的对策是不同的:防止读到更新的数据,只需要对操作的数据添加行级锁,阻止操作中的数据发生改变;而防止读到新增的数据,则往往需要添加表级锁——将整张表加锁,防止新增数据。
4. 第一类丢失更新
第一类丢失更新是一个事务撤销时把另一个事务的数据覆盖了。下面通过一个转账的例子来看一下这个问题。
时间 | 取款事务A | 转账事务B |
---|---|---|
T1 | 开始事务 | |
T2 | 开始事务 | |
T3 | 查询余额为1000元 | |
T4 | 查询余额为1000元 | |
T5 | 汇入100元,把余额修改为1100元 | |
T6 | 提交事务 | |
T7 | 取出100元,把余额修改为900元 | |
T8 | 撤销修改 | |
T9 | 把余额恢复为1000元(丢失更新) |
5. 第二类丢失更新
一个事务覆盖另一个事务已经提交的数据。造成另一个事务所做的操作丢失。
时间 | 取款事务A | 转账事务B |
---|---|---|
T1 | 开始事务 | |
T2 | 开始事务 | |
T3 | 查询账户余额为1000元 | |
T4 | 查询账户余额为1000元 | |
T5 | 取出100元,把余额修改为900元 | |
T6 | 提交事务 | |
T7 | 汇入100元 | |
T8 | 提交事务 | |
T9 | 把余额修改为1100元(丢失更新) |
总结:第一类为撤销时覆盖,第二类为提交时覆盖。
数据库锁机制
数据库的并发会引起很多问题,当然有些问题还可以容忍,但是有的问题却是致命的。并发问题一般都会用锁解决,在数据库中也是用锁解决的,但是不同的数据库对于锁的实现是不同的,但基本的原理是相同。
按锁定的对象可以分为:
- 表锁定:对于整张表锁定
- 行锁定:对于表中的特定行锁定
从并发的数据关系中又可以分为
- 独占锁:共享锁会防止独占锁,但允许其他共享锁的访问。
- 共享锁:独占锁独自占领表或行,防止其他共享锁的访问,当然也访问其他独占锁。
在数据更新的时候,数据库必须在进行更改的行上施加行独占锁,也就是说INSERT,UPDATE,DELETE等语句都会隐式采用必要的行锁定。
事务的隔离级别
尽管数据库为用户提供了锁的DML操作方式,但是直接使用还是很麻烦的,因此数据库为用户提供了自动锁的机制。也就是隔离级别,只要用户指定的回话的隔离级别,数据库就会分析SQL语句,然后进行合适的加锁,当数据锁的数据太多的时候,自动进行锁升级来提高系统的,性能,这一过程对用户是透明的(不可见)的。
SQL标准定义了4个事务级别,每一个级别都规定了一个事务中所做的修改,哪些在事务中是可见的,哪些是不可见的。较低的隔离通常可以执行更高的并发,系统开销也更低。
下面的是四中数据库事务的介绍:
- READ UNCOMMITED(未提交读)
事务中的修改,即使没有提交对其它事务都是可见的。事务可以读取未提交的数据,这也被称为脏读。一般很少使用。 - READ COMMITED(提交读)
大多数的数据库的默认隔离级别都是READ COMMITED。READ_COMMITED从一个事务开始时,只能”看见“已经提交的修改。也就是说:一个事务从开始到提交前,所做的任何修改对其他事务是不可见的。这个级别有时候也叫做不可重复读,因为两次执行查询可能会得到不同的结果。 - REPEATABLE READ(可重复读)
REPEATABLE READ解决了脏读的问题,该级别保证在同一个事务中多次读取同样的记录的结果是一致的。但是这个级别还是没有解决另一个问题:幻读。 - SERIALIZABLE(可串行化)
SERIALIZABLE是最高的隔离级别。它通过事务串执行,避免了前面所说的幻读问题。简单来说SERIALIZABLE会为每一行数据都加锁,所以会导致大量的锁超时和竞争。实际中很少使用这个隔离级别。
下表为事务隔离级别对并发问题的解决情况:
隔离级别 | 脏读 | 不可重复读 | 幻想读 | 第一类丢失更新 | 第二类丢失更新 |
---|---|---|---|---|---|
READ UNCOMMITED | 允许 | 允许 | 允许 | 不允许 | 允许 |
READ COMMITED | 不允许 | 允许 | 允许 | 不允许 | 允许 |
REPEATABLE READ | 不允许 | 不允许 | 允许 | 不允许 | 不允许 |
SERIALIZABLE | 不允许 | 不允许 | 不允许 | 不允许 | 不允许 |
其中READ UNCOMMITED并发性和吞吐量最好,SERIALIZABLE的最差,所以事务的隔离级别和数据库的并发行是对立的。