带你理解事务(上)、基本概念

语言: CN / TW / HK

简介和特性

对于程序猿来说,编程就是将大千世界的各种业务用代码表示出来,所以代码输出的最终结果也要和现实世界能够对应的上。我们用银行转账来举例子,比如我给我的朋友A转100元,最终的结果无非就是有两种,一是成功,我的账户余额减少100,我朋友的账户余额增加100,二是失败,我们两账户余额都没有变。肯定是不允许出现我的账户余额减少了但是我朋友的账户余额没有增加,或者是我朋友的账户余额增加了我的账户余额却没有减少,出现这两种现象都会让我们和银行的关系变得极不和谐。

例子中转账这件事情虽然看起来很简单,但是当变成代码的时候就比较麻烦了,我们来看看变成代码之后会是什么样子:

image.png

乍看之下感觉很简单好像没有什么问题,但是如果在2和3中间增加了一步检查朋友账户是否为合法账户的步骤变成如下:

image.png

我们假定,第3步这步出了问题,我朋友的账户是个洗钱账户,这时候转账流程要结束,但是有个问题是我的账户已经扣掉了100元,如果不还我100的话那我不亏死,这样估计银行招牌早就被砸了。所以这个转账流程必须有如下几个特点:

1、原子性(Atomic)

要么转账成功,要不就没转,不能存在转了一半的这种情况。换句话说,转账这个过程的所有环节是不可分割的,不管转账的过程如何复杂,最后的结果是要么成功,我的账户少了钱,我朋友账户钱增加了对应数量的钱,要么是我的账户和我朋友的账户的钱都没有改变。这个特点我们称它为原子性。

2、一致性(Consistency)

这一点网上的很多博客解释的都不正确。这个特点其实指的是,转账这个过程中的数据是要能和现实世界的规则对应起来,或者说是需要满足一定的规则约束,比如,我的账户钱必须大于0,并且扣完钱也是必须大于0的。不满足这样的约束的话则说明数据是有问题的

3、隔离性(Isolation)

现实世界中的同一时刻,是有多个转账同时发生的,这同时发生的多个转账之间的数据不能相互影响,要把这个问题说清楚会稍微复杂一些,我们慢慢来说,我们把我们之前提的转账流程拆的更细一点如下:

  • (1)读取我账户上的金额,假如此时金额101
  • (2)将余额扣除100
  • (3)把剩余的余额写到磁盘上
  • (4)读取朋友账户金额
  • (5)给朋友账户金额增加100
  • (6)将新的余额写到磁盘上 假设现在有两个这样的转账过程同时发生,在计算机中,由于多处理器的原因,这两个转账并不会严格的按照彻底做完一个

| 时间线 |转账1 |转账2 | | --- | --- |--- | | 1| 读取我账户上的金额,此时金额101| | | 2| | 读取我账户上的金额,此时金额101| | 3| 将余额扣除100| | | 4| 把剩余的余额写到磁盘上| | | 5| | 将余额扣除1| | 6| | 把剩余的余额写到磁盘上| | 7| 读取朋友账户金额| | | 8| | 读取朋友账户金额| | 9| 给朋友账户金额增加100| | | 10| 将新的余额写到磁盘上| | | 11| | 给朋友账户金额增加1| | 12| | 将新的余额写到磁盘上|

相信不少人已经看出问题了吧,由于在时间线2处读到的钱还是101,因此在第6处写到磁盘上的我的余额是100,这时候银行不得亏死,因此,正确的流程是时间线134这三个事情必须先做,然后时间线2处的才允许发生,正确流程如下:

| 时间线 |转账1 |转账2 | | --- | --- |--- | | 1| 读取我账户上的金额,假如此时金额101| | | 2| 将余额扣除100| | | 3| 把剩余的余额写到磁盘上| | | 4| | 读取我账户上的金额,此时金额是1| | 5| | 将余额扣除1| | 6| | 把剩余的余额写到磁盘上| | 7| 读取朋友账户金额| | | 8| | 读取朋友账户金额| | 9| 给朋友账户金额增加100| | | 10| 将新的余额写到磁盘上| | | 11| | 给朋友账户金额增加1| | 12| | 将新的余额写到磁盘上|

4、持久性(Durability)

一旦转账完成之后,数据就永久的被持久化到磁盘上不会丢失。

我们所说的事务(transcation)必须具有这四种特性,否则就会出现问题。

事务并发读写会遇到的问题

我们用修改数据库中我的账户的余额为例子,来看看多个事务同时发生都有可能出现什么问题:

脏写

| 时间线 |事务1 |事务2 | | --- | --- |--- | |1 | start | | |2 | | start| |3 | update user set money=100 where id=1 | | |4 | | update user set money=50 where id=1| |5 | | commit| |6 |select money from user where id=1 (此时查出来money是50) | | |7 | commit | |

这个案例中,事务2修改了事务1还没有提交的数据,事务1在时间线6的重新查询的时候一脸懵逼,明明自己的钱应该是100的,却不知道为什么变成了50,就这样白白的损失了50块钱,和银行的关系变得极其不和谐。

脏读

| 时间线 |事务1 |事务2 | | --- | --- |--- | |1 |start | | |2 | | start| |3 |update user set money=100 where id=1 | | |4 | |select money from user where id=1 (查出来的money是100)| |5 |update user set money=50 where id=1 | | |6 |commit | | |7 | |commit |

这个案例中,事务1先把数据库中我的账户余额更新为了100,然后事务2来查询发现余额是100,就告诉我余额是100,这种读到了别的事务未提交的数据就叫做脏读。有人不理解这个“脏”字体现在了哪里,因为事务1还是有可能继续修改money字段的,比如在上面的时间线6的时候事务1修改了money,这样的话事务2读到的数据其实是个错误的数据。

不可重复读

| 时间线 |事务1 |事务2 | | --- | --- |--- | |1 |start | | |2 | | start| |3 | select money from user where id=1 (此时查出来的money是100)| | |4 | | update user set money=50 where id=1| |5 |select money from user where id=1(此时查出来的money是50) | | |6 |commit | | |7 | | commit|

事务1在时间线5的时候读到了事务2还没有提交的数据,并且和自己在时间线3读到的数据不一样,这时候我们称这种现象称为不可重复读。

幻读

| 时间线 |事务1 |事务2 | | --- | --- |--- | |1 | start | | |2 | | start| |3 | select * from user where money>100 (假设查到了一条数据) | | |4 | | insert into user (money,id) values (200,2) | |5 | select * from user where money>100 (此时会查到两条数据) | | |6 | commit | | |7 | | commit|

事务1在时间线3和时间线5查到的数据不一样,并且时间线5读到了时间线3没有读到的数据,我们称这种现象为幻读。

四种现象的严重程度排序 脏写 > 脏读 > 不可重复读 > 幻读

隔离级别

注意,这里是以MySQL为例子,说的是MySQL中使用Innodb时候的事务隔离级别,不同的数据库对隔离级别的支持是不一样的。在MySQL中,支持下面四种事务隔离级别: * READ UNCOMMITTED: 读未提交 * READ COMMITTED: 读已提交 * REPEATABLE READ: 可重复读 * SERIALIZABLE: 串行化

事实上这是sql标准中的四种隔离级别,sql标准中还规定了不同隔离级别中可以发生什么问题和不可以发生什么问题,具体如下:

|隔离级别|脏写|脏读|不可重复读|幻读| | --- | --- |--- |--- |--- | |READ UNCOMMITTED |❎ |可能出现 |可能出现 |可能出现 | |READ COMMITTED |❎ |❎ |可能出现 |可能出现 | |REPEATABLE READ |❎ |❎ |❎ |可能出现 | |SERIALIZABLE |❎ |❎ |❎ |❎ |

我们可以看到,由于脏写太过于严重,所以在哪个事务隔离级别下都不允许发生。