数据库的事务和并发问题

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/starexplode/article/details/82899290

数据库事务

事务(Transaction)是并发控制的基本单位。所谓的事务,它是一个操作序列,这些操作要么全部执行,要么全部都不执行。比如,银行转账,从一个账号扣钱,然后另一个账号余额增加,这两个操作要么都执行,要么都不执行。这两个操作组合在一起就是事务。

数据库事务有严格的定义,它必须同时满足4个特性:

  1. 原子性,Atomic
  2. 一致性,Consistency
  3. 隔离性, Isolation
  4. 持久性,Durabiliy

简称ACDI。下面是对每一个特性的说明:

  • 原子性:表示组成一个事务的多个数据库操作是一个密不可分的原子单元,只有所有的操作执行成功,整个事务才提交。事务中的任何一个数据库操作失败,已经执行的任何操作都必须撤销(回滚),让数据库恢复到事务提交之前的状态。
  • 一致性:数据库总是从一个一致性状态装换到另一个一致性状态。一致性状态的含义是数据库中的数据应该满足数据库约束。
  • 隔离性:在并发数据操作时,不同的事务拥有各自的数据空间,它们的操作不会对对方产生干扰。但是也并非要做到完全无干扰。数据库规定了多个隔离级别,不同的隔离级别的干扰程度是不同,隔离级别越高,数据一致性越好,但并发性越弱。
  • 持久性:一旦数据库提交之后,事务中的所有操作都必须被持久化都数据库中。即使在提交事务后,数据库重启时,也必须保证能够通过某种机制恢复数据。

在这些事务的特征中,数据”一致性“是最终目标,其他特性都是为达到这个目标而采取的措施。

数据库并发的问题

一个数据库可能会有多个客户端同时访问,数据库中相同的数据就有可能同时被多个事务访问,如果没有采取必要的隔离措施,就会导致各种问题,破坏数据的完整性,这些问题可以分为5中,两类:

  1. 数据读取的问题:
    • 脏读
    • 不可重复读
    • 幻想读
  2. 数据更新问题
    • 第一类丢失更新
    • 第二类丢失更新

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个事务级别,每一个级别都规定了一个事务中所做的修改,哪些在事务中是可见的,哪些是不可见的。较低的隔离通常可以执行更高的并发,系统开销也更低。

下面的是四中数据库事务的介绍:

  1. READ UNCOMMITED(未提交读)
    事务中的修改,即使没有提交对其它事务都是可见的。事务可以读取未提交的数据,这也被称为脏读。一般很少使用。
  2. READ COMMITED(提交读)
    大多数的数据库的默认隔离级别都是READ COMMITED。READ_COMMITED从一个事务开始时,只能”看见“已经提交的修改。也就是说:一个事务从开始到提交前,所做的任何修改对其他事务是不可见的。这个级别有时候也叫做不可重复读,因为两次执行查询可能会得到不同的结果。
  3. REPEATABLE READ(可重复读)
    REPEATABLE READ解决了脏读的问题,该级别保证在同一个事务中多次读取同样的记录的结果是一致的。但是这个级别还是没有解决另一个问题:幻读。
  4. SERIALIZABLE(可串行化)
    SERIALIZABLE是最高的隔离级别。它通过事务串执行,避免了前面所说的幻读问题。简单来说SERIALIZABLE会为每一行数据都加锁,所以会导致大量的锁超时和竞争。实际中很少使用这个隔离级别。

下表为事务隔离级别对并发问题的解决情况:

隔离级别 脏读 不可重复读 幻想读 第一类丢失更新 第二类丢失更新
READ UNCOMMITED 允许 允许 允许 不允许 允许
READ COMMITED 不允许 允许 允许 不允许 允许
REPEATABLE READ 不允许 不允许 允许 不允许 不允许
SERIALIZABLE 不允许 不允许 不允许 不允许 不允许

其中READ UNCOMMITED并发性和吞吐量最好,SERIALIZABLE的最差,所以事务的隔离级别和数据库的并发行是对立的。

猜你喜欢

转载自blog.csdn.net/starexplode/article/details/82899290