【MySQL】深入理解MySQL事务(上篇)

前言

在实际业务场景中,如何保证操作的完整性是一个重要的议题,依次执行一系列逻辑强关联的操作,如果在中途发生了错误,就很有可能导致数据的错乱。

设想一下在 ATM 取钱的场景,当我们取出一千元的时候,ATM 会在清点完成后一次性吐出一千元,而不是分十次每次吐出一百元,这就是为了保证操作的完整性,要么完整的取走一千元,扣除余额,要么一分钱都没有取走,余额不变,而不会出现中途机器故障导致数据不一致的情况。这样的一次完整操作叫做事务 transaction一个事务中的所有操作要么全部成功执行,要么完全不执行。

事务一般由多条 MySQL 语句构成,这些语句在逻辑上存在相关性,这一组DML语句要么全部成功,要么全部失败,是一个整体,这些操作合起来,就构成了一个事务。MySQL提供一种机制,保证我们达到这样的效果。事务还规定不同的客户端看到的数据是不相同的。

事务是由 MySQL 的引擎来实现的,我们常见的 InnoDB 引擎它是支持事务的。不过并不是所有的引擎都能支持事务,MySQL 5.5 之后,默认的存储引擎从 MyISAM 替换成了 InnoDB,这其中的一个重要原因就是因为 InnoDB 支持事务。

我们可以用 SHOW ENGINES 来看一下 MySQL 中对各种存储引擎的描述。
在这里插入图片描述

可以看到InnoDB 支持事务


事务的ACID 特性

一个 MySQL 数据库,可不止你一个事务在运行,同一时刻,甚至有大量的请求被包装成事务,在向 MySQL 服务器发起事务处理请求。而每条事务至少一条 SQL ,最多很多 SQL ,这样如果大家都访问同样的表数据,在不加保护的情况,就绝对会出现问题。甚至,因为事务由多条 SQL 构成,那么,也会存在执行到一半出错或者不想再执行的情况,那么已经执行的怎么办呢?

所以,一个完整的事务,绝对不是简单的 sql 集合,还需要满足如下四个属性:

A - Atomicity 原子性: 一个事务是一个不可分割的最小单位,事务中的所有操作要么全部成功,要么全部失败,没有中间状态。原子性主要是通过事务日志中的回滚日志(undo log)来实现的,当事务对数据库进行修改时,InnoDB 会根据操作生成相反操作的 undo log,比如说对 insert 操作,会生成 delete 记录,如果事务执行失败或者调用了 rollback(回滚),就会根据 undo log 的内容恢复到执行之前的状态。

C - Consistency 一致性: 事务执行之前和执行之后数据都是合法的一致性状态,即使发生了异常,也不会因为异常引而破坏数据库的完整性约束,比如唯一性约束等。

I - Isolation 隔离性: 数据库允许多个并发事务同时对其数据进行读写和修改的能力,隔离性可以防止多个事务并发执行时由于交叉执行而导致数据的不一致。事务隔离分为不同级别,包括读未提交( Read uncommitted )、读提交( read committed )、可重复读( repeatable read )和串行化( Serializable )

D - Durability 持久性: 事务提交之后对数据的修改是持久性的,即使数据库宕机也不会丢失,通过事务日志中的重做日志(redo log)来保证。事务修改之前,会先把变更信息预写到 redo log 中,如果数据库宕机,恢复后会读取 redo log 中的记录来恢复数据。

InnoDB 引擎通过什么技术来保证事务的这四个特性的呢?

  • 持久性是通过 redo log (重做日志)来保证的;
  • 原子性是通过 undo log(回滚日志) 来保证的;
  • 隔离性是通过 MVCC(多版本并发控制) 或锁机制来保证的;
  • 一致性则是通过持久性+原子性+隔离性来保证;

事务提交方式

事务的提交方式常见的有两种:

  • 自动提交
  • 手动提交

查看事务提交方式

mysql> show variables like 'autocommit';

在这里插入图片描述
我们可以看到,当前自动提交是开启的

用 SET 来改变 MySQL 的自动提交模式

我们可以通过SET命令来控制是否启用自动提交模式:

mysql> SET AUTOCOMMIT=0; #SET AUTOCOMMIT=0 禁止自动提交

在这里插入图片描述

mysql> SET AUTOCOMMIT=1; #SET AUTOCOMMIT=1 开启自动提交

在这里插入图片描述

事务常见操作方式

注意:为了便于演示,我们将mysql的默认隔离级别设置成读未提交,关于隔离级别我们后面专门会讲,现在以使用为主。

mysql> set global transaction isolation level READ UNCOMMITTED;
Query OK, 0 rows affected (0.00 sec)
mysql> quit
Bye

##需要重启终端,进行查看
mysql> select @@tx_isolation;
+------------------+
| @@tx_isolation |
+------------------+
| READ-UNCOMMITTED |
+------------------+
1 row in set, 1 warning (0.00 sec)

创建测试表

create table if not exists account(
id int primary key,
name varchar(50) not null default '',
blance decimal(10,2) not null default 0.0
)ENGINE=InnoDB DEFAULT CHARSET=UTF8;

在这里插入图片描述


正常演示 - 证明事务的开始与回滚

mysql> show variables like 'autocommit'; -- 查看事务是否自动提交。我们故意设置成自动提交,看看该选项是否影响begin

在这里插入图片描述

mysql> start transaction; -- 开始一个事务,begin也可以,推荐begin
Query OK, 0 rows affected (0.00 sec)
mysql> savepoint save1; -- 创建一个保存点save1
Query OK, 0 rows affected (0.00 sec)
mysql> insert into account values (1, '张三', 100); -- 插入一条记录
Query OK, 1 row affected (0.05 sec)
mysql> savepoint save2; -- 创建一个保存点save2
Query OK, 0 rows affected (0.01 sec)
mysql> insert into account values (2, '李四', 10000); -- 在插入一条记录
Query OK, 1 row affected (0.00 sec)
mysql> select * from account; -- 两条记录都在了
+----+--------+----------+
| id | name   | blance   |
+----+--------+----------+
|  1 | 张三   |   100.00 |
|  2 | 李四   | 10000.00 |
+----+--------+----------+
2 rows in set (0.00 sec)
mysql> rollback to save2; -- 回滚到保存点save2
Query OK, 0 rows affected (0.03 sec)

在这里插入图片描述

mysql> rollback; -- 直接rollback,回滚在最开始
Query OK, 0 rows affected (0.00 sec)

在这里插入图片描述

现在再插入一条数据,然后提交事务。
在这里插入图片描述
commit命令是提交事务,注意:一旦事务已经提交,就不能回滚了,因此,在代码执行过程中捕获到异常的时候需要直接执行 rollback 而不是 commit。


非正常演示1 - 证明未commit,客户端崩溃,MySQL自动会回滚(隔离级别设置为读未提交)

我们利用终端A演示事务,终端B进行查看当前表数据

终端A

mysql> select * from account; --当前表中只有一个王五
+----+--------+----------+
| id | name   | blance   |
+----+--------+----------+
|  3 | 王五   | 20000.00 |
+----+--------+----------+
1 row in set (0.00 sec)
mysql> show variables like 'autocommit'; --依旧自动提交
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| autocommit    | ON    |
+---------------+-------+
1 row in set (0.00 sec)
mysql> begin; --开启事务
Query OK, 0 rows affected (0.00 sec)
--插入两条记录
mysql> insert into account values (1, '张三', 100);
Query OK, 1 row affected (0.00 sec)
mysql> insert into account values (2, '张三', 200);
Query OK, 1 row affected (0.01 sec)
mysql> select * from account;
+----+--------+----------+
| id | name   | blance   |
+----+--------+----------+
|  1 | 张三   |   100.00 |
|  2 | 张三   |   200.00 |
|  3 | 王五   | 20000.00 |
+----+--------+----------+
3 rows in set (0.00 sec)

当前插入的两条数据已经存在,但没有commit,此时同时查看终端B

--终端B
mysql> select * from account;
+----+--------+----------+
| id | name   | blance   |
+----+--------+----------+
|  1 | 张三   |   100.00 |
|  2 | 张三   |   200.00 |
|  3 | 王五   | 20000.00 |
+----+--------+----------+
3 rows in set (0.00 sec)

终端A上插入的数据在终端B上已经同步。

此时我们在终端A上异常终止MyQSL:

mysql> Aborted -- ctrl + \ 异常终止MySQL

然后我们从终端B查看表数据:
在这里插入图片描述

我们发现一个现象,事务在未提交之前,如果因出现异常等原因而造成客户端崩溃,mysql会自动进行回滚,这也体现了事务的原子性

非正常演示2 - 证明commit了,客户端崩溃,MySQL数据不会在受影响,已经持久化

终端 A

mysql> show variables like 'autocommit'; -- 依旧自动提交
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| autocommit | ON |
+---------------+-------+
1 row in set (0.00 sec)
mysql> select * from account; -- 当前表内无数据
Empty set (0.00 sec)
mysql> begin; -- 开启事务
Query OK, 0 rows affected (0.00 sec)
mysql> insert into account values (1, '张三', 100); -- 插入记录
Query OK, 1 row affected (0.00 sec)
mysql> commit; --提交事务
Query OK, 0 rows affected (0.04 sec)
mysql> Aborted -- ctrl + \ 异常终止MySQL

终端 B

mysql> select * from account; 
+----+--------+--------+
| id | name | blance |
+----+--------+--------+
| 1 | 张三 | 100.00 |
+----+--------+--------+
1 row in set (0.00 sec)

可以看到虽然终端A异常终止了,但是终端A中插入的数据却依旧存在,所以commit的作用是将数据持久化到MySQL中。

通过演示1,2我们能够明白,当手动启动一个事务(begin or start transsaction,commit)的时候,会自动更改提交方式,不会受MySQL是否自动提交影响

那有一个问题,那设置自动提交有什么用呢,如果有用,是给谁设置的,会影响谁呢?我们接下来通过演示3来简单证明一下。

非正常演示3 - 证明单条 SQL 与事务的关系

实验一

-- 终端A
mysql> select * from account;
+----+--------+--------+
| id | name | blance |
+----+--------+--------+
| 1 | 张三 | 100.00 |
+----+--------+--------+
1 row in set (0.00 sec)

mysql> show variables like 'autocommit';
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| autocommit | ON |
+---------------+-------+
1 row in set (0.00 sec)

mysql> set autocommit=0; --关闭自动提交
Query OK, 0 rows affected (0.00 sec)
mysql> insert into account values (2, '李四', 10000); --插入记录

此时在终端B可以查看数据已经插入:
在这里插入图片描述

然后终止终端A:

mysql> ^DBye --ctrl + \ or ctrl + d,终止终端

再次查看终端B:
在这里插入图片描述
我们发现之前在终端A中执行的单条sql(即插入李四的信息)随着终端A崩溃而没有被保存下来,这就是我们设置关闭自动提交(set autocommit=0)起了作用。

如果我们设置为自动提交(set autocommit=1),那李四的信息就会被保存下来,而不会随着终端A崩溃而消失。

通过这个示例其实就能够证明,其实我们之前的所有的单条sql,本质在mysql中,全部各自会被以事务的方式进行提交。

结论

  • 只要输入begin或者start transaction,事务便必须要通过commit提交,才会持久化,与是否设置setautocommit无关。
  • 事务可以手动回滚,同时,当操作异常,MySQL会自动回滚
  • 对于 InnoDB 每一条 SQL 语言都默认封装成事务,自动提交。(select有特殊情况,因为 MySQL 有MVCC )
  • 从上面的例子,我们能看到事务本身的原子性(回滚),持久性(commit)

事务操作注意事项

  • 如果没有设置保存点,也可以回滚,只能回滚到事务的开始。直接使用 rollback(前提是事务还没有提交)
  • 如果一个事务被提交了(commit),则不可以回退(rollback)
  • 可以选择回退到哪个保存点
  • InnoDB 支持事务, MyISAM 不支持事务
  • 开始事务可以使 start transaction 或者 begin

我们通过上面的演示看到了事务的两个特性:原子性(回滚),持久性(commit)。那么隔离性?一致性?从哪里体现呢,我们接着讲…

事务隔离级别

如何理解隔离性

  • MySQL服务可能会同时被多个客户端进程(线程)访问,访问的方式以事务方式进行

  • 一个事务可能由多条SQL构成,也就意味着,任何一个事务,都有执行前,执行中,执行后的阶段。而所谓的原子性,其实就是让用户层,要么看到执行前,要么看到执行后。执行中出现问题,可以随时回滚。所以单个事务,对用户表现出来的特性,就是原子性。

  • 但,毕竟所有事务都要有个执行过程,那么在多个事务各自执行多个SQL的时候,就还是有可能会出现互相影响的情况。比如:多个事务同时访问同一张表,甚至同一行数据。

  • 数据库中,为了保证事务执行过程中尽量不受干扰,就有了一个重要特征:隔离性

  • 数据库中,允许事务受不同程度的干扰,就有了一种重要特征:隔离级别

隔离级别

  • 读未提交【Read Uncommitted】: 在该隔离级别,所有的事务都可以看到其他事务没有提交的执行结果。(实际生产中不可能使用这种隔离级别的),但是相当于没有任何隔离性,也会有很多并发问题,如脏读,幻读,不可重复读等,我们上面为了做实验方便,用的就是这个隔离性。
  • 读提交【Read Committed】 :该隔离级别是大多数数据库的默认的隔离级别(不是 MySQL 默认的)。它满足了隔离的简单定义:一个事务只能看到其他的已经提交的事务所做的改变。这种隔离级别会引起不可重复读,即一个事务执行时,如果多次 select, 可能得到不同的结果。
  • 可重复读【Repeatable Read】: 这是 MySQL 默认的隔离级别,它确保同一个事务,在执行中,多次读取操作数据时,会看到同样的数据行。但是会有幻读问题。
  • 串行化【Serializable】: 这是事务的最高隔离级别,它通过强制事务排序,使之不可能相互冲突,从而解决了幻读的问题。它在每个读的数据行上面加上共享锁,。但是可能会导致超时和锁竞争(这种隔离级别太极端,实际生产基本不使用)
  • 隔离级别如何实现:隔离,基本都是通过锁实现的,不同的隔离级别,锁的使用是不同的。常见有,表锁,行锁,读锁,写锁,间隙锁(GAP),Next-Key锁(GAP+行锁)等。不过,我们目前现有这个认识就行。

  • 在实际产线环境下,可能会存在大规模并发请求的情况,如果没有妥善的设置事务的隔离级别,就可能导致一些异常情况的出现,最常见的几种异常为脏读(Dirty Read)幻读(Phantom Read)不可重复读(Unrepeatable Read)

查看与设置隔离级别

在大致解释了隔离级别之后,我们来看看如何查看和设置隔离级别:

查看

mysql> SELECT @@global.tx_isolation; --查看全局隔级别

在这里插入图片描述

mysql> SELECT @@session.tx_isolation; --查看会话(当前)全局隔离级别

在这里插入图片描述

mysql> SELECT @@tx_isolation; --默认同上(查看的也是当前会话隔离级别)

在这里插入图片描述

设置

-- 设置当前会话 or 全局隔离级别语法
SET [SESSION | GLOBAL] TRANSACTION ISOLATION LEVEL 
{READ UNCOMMITTED | READ COMMITTED | REPEATABLE READ | SERIALIZABLE}

1.设置当前会话隔离级别(设置为串行化),然后另起一个会话,对比查看,发现设置当前会话隔离级别后此隔离级别只影响当前会话

mysql> set session transaction isolation level serializable; -- 设置当前会话隔离级别为串行化
Query OK, 0 rows affected (0.00 sec)

注意,以下图片提及到的隔离级别均误写成隔离性。

在这里插入图片描述
同理,我们来设置全局隔离级别看看:

2.设置全局隔离级别,另起一个会话,会被影响

--设置全局隔离性为串行化
mysql> set global transaction isolation level serializable;
Query OK, 0 rows affected (0.00 sec)

在这里插入图片描述

– 注意,如果没有现象,关闭mysql客户端,重新连接。

下面,我们通过示例来进一步理解一下四种隔离级别的作用。

读未提交【Read Uncommitted】

几乎没有加锁,虽然效率高,但是问题太多,严重不建议采用

终端A


-- 设置隔离级别为 读未提交
mysql> set global transaction isolation level read uncommitted;
Query OK, 0 rows affected (0.00 sec)

--重启客户端
mysql> select @@tx_isolation;
+------------------+
| @@tx_isolation |
+------------------+
| READ-UNCOMMITTED |
+------------------+
1 row in set, 1 warning (0.00 sec)
mysql> select * from account;
+----+--------+----------+
| id | name | blance |
+----+--------+----------+
| 1 | 张三 | 100.00 |
| 2 | 李四 | 1000.00 |
+----+--------+----------+
2 rows in set (0.00 sec)
mysql> begin; --开启事务
Query OK, 0 rows affected (0.00 sec)
mysql> update account set blance=123.0 where id=1; --更新指定行
Query OK, 1 row affected (0.05 sec)
Rows matched: 1 Changed: 1 Warnings: 0
--没有commit哦!!!

终端B

mysql> begin;
mysql> select * from account;
+----+--------+---------+
| id | name   | blance  |
+----+--------+---------+
|  1 | 张三   |  123.00 |
|  2 | 李四   | 1000.00 |
+----+--------+---------+
2 rows in set (0.00 sec)
--读到终端A更新但是未commit的数据[insert,delete同样]

在这里插入图片描述

一个事务在执行中,读到另一个执行中事务的更新(或其他操作)但是未commit的数据,这种现象叫做脏读(dirty read)

读提交【Read Committed】

-- 终端A
mysql> set global transaction isolation level read committed;
Query OK, 0 rows affected (0.00 sec)
--重启客户端
mysql> select * from account;--查看当前数据
+----+--------+---------+
| id | name   | blance  |
+----+--------+---------+
|  1 | 张三   |  100.00 |
|  2 | 李四   | 1000.00 |
+----+--------+---------+
mysql> begin; --手动开启事务,同步的开始终端B事务
Query OK, 0 rows affected (0.00 sec)
mysql> update account set blance=321.0 where id=1; --更新张三数据
Query OK, 1 row affected (0.00 sec)
Rows matched: 1 Changed: 1 Warnings: 0
--切换终端到终端B,查看数据

在这里插入图片描述
这次我们看到,在终端A事务中更新但未提交的数据,在终端B正在执行的事务中就读不到了。
接下来我们在终端A中提交事务,然后在终端B中再次查看数据:
在这里插入图片描述
commit后,在终端B的事务中读到更新的数据了。

但是,因为终端B此时还在当前事务中,并未commit,那么就造成了,同一个事务内,同样的读取,在不同的时间段(依旧还在事务操作中!),读取到了不同的值,这种现象叫做不可重复读(non reapeatable read)!!

可重复读【Repeatable Read】

--终端A
--设置全局隔离级别RR
mysql> set global transaction isolation level repeatable read; 
Query OK, 0 rows affected (0.01 sec)
--关闭终端重启
mysql> select @@tx_isolation;
+-----------------+
| @@tx_isolation |
+-----------------+
| REPEATABLE-READ | --隔离级别RR
+-----------------+
1 row in set, 1 warning (0.00 sec)
mysql> select *from account; --查看当前数据
+----+--------+---------+
| id | name   | blance  |
+----+--------+---------+
|  1 | 张三   |  321.00 |
|  2 | 李四   | 1000.00 |
+----+--------+---------+
2 rows in set (0.00 sec)
mysql> begin; --开启事务,同步的,终端B也开始事务
Query OK, 0 rows affected (0.00 sec)
mysql> update account set blance=4321.0 where id=1; --更新数据
Query OK, 1 row affected (0.00 sec)
Rows matched: 1 Changed: 1 Warnings: 0

切换到终端B,查看另一个事务是否能看到
在这里插入图片描述
我们发现,终端A事务在未提交时,它的更新数据在终端B同时正在执行的事务里读不到。
我们接下来将终端A事务提交,并再次查看终端B:
在这里插入图片描述

可以看到,终端A 提交事务后,终端B读取数据依旧没有变化。
此时我们结束终端B事务,再次查看:
在这里插入图片描述
当终端B事务提交后,终端A事务之前更新的数据才得以在终端B中读取到。
也就是说,这样保证了终端B的同一事务在相同查询条件下任意不同时间两次查询得到的数据结果一致。

这种确保同一个事务,在执行中,多次读取操作数据时,会看到同样的数据行的行为就叫做可重复读!与读提交产生的不可重复读恰好相反!

如果将上面的终端A中的update操作,改成insert操作,会有什么问题??

直接上结论:

我们将上面的终端A中的update操作,改成insert操作,发现终端A在对应事务中insert的数据,在终端B的事务周期中,也没有什么影响,也符合可重复的特点。但是,一般的数据库在可重复读情况的时候,无法屏蔽其他事务insert的数据(为什么?因为隔离性实现是对数据加锁完成的,而insert待插入的数据因为并不存在,那么一般加锁无法屏蔽这类问题),会造成虽然大部分内容是可重复读的,但是insert的数据在可重复读情况被读取出来,导致多次查找时,会多查找出来新的记录,就如同产生了幻觉。这种现象,叫做幻读(phantom read)

很明显,MySQL在RR级别的时候,是解决了幻读问题的(解决的方式是用
Next-Key锁(GAP+行锁)解决的。这块比较难,有兴趣同学了解一下)。

串行化【serializable】

使用表级锁来保证事务与事务之间的串行化,可以防止所有的异常情况,但是牺牲了系统的并发性,效率很低,几乎完全不会被采用。

示例:

我们将全局隔离级别设置为串行化

--终端A
mysql> set global transaction isolation level serializable;

在这里插入图片描述

在终端A和终端B同时分别开启一个事务:
在这里插入图片描述
注意:在串行化下,两个同时执行的事务的读取(select)不会串行化,是共享锁的。

此时,我们在终端A事务中对表数据进行更新,查看现象:

mysql> update account set blance=1.00 where id=1;

在这里插入图片描述
可以发现,终端A事务的表更新操作被阻塞住了。

我们将终端B事务进行提交:
在这里插入图片描述
可以看到,此时终端A事务的表更新操作不再被阻塞,而是执行成功。

通过此示例看出,串行化的效率很低,由于锁的原因,同一时间只有竞争到锁的事务才能执行操作,而其他事务就需要等待。

总结

  • 隔离级别越严格,安全性越高,但数据库的并发性能也就越低,往往需要在两者之间找一个平衡点。
  • 不可重复读的重点是修改和删除:同样的条件, 你读取过的数据,再次读取出来发现值不一样了; 幻读的重点在于新增:同样的条件, 第1次和第2次读出来的记录数不一样。
  • 说明: mysql 默认的隔离级别是可重复读,一般情况下不要修改
  • 上面的例子可以看出,事务也有长短事务这样的概念。事务间互相影响,指的是事务在并行执行的时候,即都没有commit的时候,影响会比较大。

并发事务引发的问题

前面我们提到,当多个事务并发执行时可能会遇到「脏读、不可重复读、幻读」的现象,这些现象会对事务的一致性产生不同程序的影响。

  • 脏读:读到其他事务未提交的数据;
  • 不可重复读:前后读取的数据不一致;
  • 幻读:前后读取的记录数量不一致。

SQL 标准提出了四种隔离级别来规避这些现象,隔离级别越高,性能效率就越低,按隔离水平高低排序如下:

图片

针对不同的隔离级别,并发事务时可能发生的现象也会不同:

在这里插入图片描述

因此,要解决脏读现象,就要升级到「读提交」以上的隔离级别;要解决不可重复读现象,就要升级到「可重复读」的隔离级别,要解决幻读现象不建议将隔离级别升级到「串行化」

MySQL 在「可重复读」隔离级别下,可以很大程度上避免幻读现象的发生(注意是很大程度避免,并不是彻底避免),所以 MySQL并不会使用「串行化」隔离级别来避免幻读现象的发生,因为使用「串行化」隔离级别会影响性能。

  • MySQL InnoDB 引擎的默认隔离级别虽然是「可重复读」,但是它很大程度上避免幻读现象(并不是完全解决了),解决的方案有两种:
  • 针对快照读(普通 select 语句),是通过 MVCC 方式解决了幻读,因为可重复读隔离级别下,事务执行过程中看到的数据,一直跟这个事务启动时看到的数据是一致的,即使中途有其他事务插入了一条数据,是查询不出来这条数据的,所以就很好了避免幻读问题。
  • 针对当前读(select … for update 等语句),是通过 next-key lock(记录锁+间隙锁)方式解决了幻读,因为当执行 select … for update 语句的时候,会加上 next-key lock,如果有其他事务在 next-key lock 锁范围内插入了一条记录,那么这个插入语句就会被阻塞,无法成功插入,所以就很好了避免幻读问题。

再谈一致性(Consistency)

在上面的讲解中,我们基本对事务特性中的AID有了基本认识,但是对于一致性我们需要单独拿出来简单说一说:

ACID一致性是指事务的执行不会破坏数据库的完整性约束,所谓的完整性约束包括数据关系的完整性和业务逻辑的完整性。因此,这里的“一致性”指的是完整性约束不会破坏。

其实,一致性是事务的最终目的,原子性、隔离性、持久性都是为了实现一致性。

我们可以简单验证一番。

怎么验证呢?

假设,这个事务系统如果是由我们来设计的话。

首先,场景是这样的,小范转100块钱给小黄,那么这个事务系统必须要保证小范扣了100块钱,而且小黄也必须要加了100块钱。

这个我们要怎么保证呢?

我们可以先用一本本子记下来,小范扣100块钱,小黄加100块钱,然后,我们再根据本子上写的,顺序执行,这样的话,小范或者小黄就没法耍赖了。

OK,那么我们现在就开干,把这个事务系统开发出来,下面是伪代码:

//事务系统
abstract class transaction{
 
    void transaction(){
 
        /* todo:将所有操作写进日志
         * args: 事务名称, 事务操作, 事务写入状态(0 未写完 1已写完)
         */
        setLog("小范转100块钱给小黄", "小范-100", 0);
        setLog("小范转100块钱给小黄", "小黄+100", 1);
 
        //获取日志
        Log logs = getLog("小范转100块钱给小黄");
 
        //解析日志,获取操作事件
        Event events = parseLog(logs);
        
        //执行操作并回写日志状态标记该事务已完成
        doEvent(events, logs);
    }
}

OK,系统开发出来了,我们把它放到应用上去跑起来试下。

但是,可能是因为计算机内存不够,系统跑到一半,闪退了。。。

也就是doEvent的时候,小范扣了100块钱,这个时候闪退了。。。

上数据库一看,完了,小范已经扣了100,但是小黄并没有增加100,事务也没有执行下去。

所以我们这个事务系统是有问题的,我们的事务系统,应该要保证小范扣100,而且小黄也要加100,我们姑且称这种状态为一致性,因为我们要保证这两个操作对数据而言是一致的。

从目前来看,我们这个事务系统,没有完全实现一致性,那如果发生了这种状况,系统闪退停机等等异常情况,我们该怎么处理,才能保证一致性呢?

有了,我们可以在日志中多加一个状态,用来标记该操作有没有执行,然后用一个定时器,每隔几秒找出日志中没有完成的事务,把它执行完,这样一来,就能保证小范扣了100,小黄加了100了,哪怕中途停机了,也能用定时器把事务执行完。

就这样测试了十来次,结果跟操作都一致,确实能保证一致性了,就正式给用上生产环境了。

可是才不到一天,就出问题了,怎么呢?有个业务,小张向老李转账300元,可是小张的账户上只有298,该死的初级程序员又没有对小张的金额作校验,直接就给执行了。

这下小张的账户余额变成了-2,老李的账户变成了300。闹了个大笑话。

这虽然主要责任不在我们开发的事务系统,但是,我们也要做处理,也就是在小张的余额做加减的时候,减成了负数,这个时候程序应该需要抛出异常的,不能让程序再执行下去了,所以,这就需要我们的事务系统,可以在执行到一半的时候,回滚到初始状态。

也就是说,如果同一个事务中,有操作ABC三个顺序操作,操作A成功了,操作B失败了,那这操作C还要执行吗?当然不能,这种情况,B失败了,我们就只能把A给回滚到操作之前。

这样一来,我们这个事务系统就是,要么事务都完成,要么事务都不完成,我们姑且就把这个叫做原子性吧。

增加了原子性的功能后,事务系统又开始跑了。

过了几天,又出问题了,怎么呢?原来啊,小范有300块钱,小张向小范转了500块钱,事务还没操作完呢,小刘又给小范转了300块钱,这样一来,问题就来了,小张给小范转500,本应该事务结束的时候小范有800块钱,可是小刘又给小范转了300,还是用小范原有的300去增加的,这样一来,小刘的事务结束,小范就有600块钱,小张的事务执行完,把800写回给小范,接着,小刘的事务也执行完,把600写回给小范,导致最终小范账上只有600块钱,小张的500被吞了。

这样,数据完全混乱了。问题出在哪呢?在于小张事务执行的时候,读取到小范有300,事务没完,小刘也读取到小范有300,这样就错乱了,我们应该要让小张在转账的时候,小刘要等小张转完了,才能转。这样,才能解决掉数据混乱的问题,我们,姑且把这个叫做隔离性。

隔离性修复完之后,项目又开始运作了,事务系统运行了很长一段时间,也没有出现问题。

到这里,验证就结束了,上面写日志的行为其实就是事务的持久性,也可以看到,上面出现的隔离性、原子性、持久性,也都是为了彻底实现一致性而产生的

所以,总的来说,一致性是一个比较笼统的概念,是事务的基础,一致性和原子性的区别就是,原子性强调的是操作的完整,要么都成功、要么都不成功,而一致性包含的比较多。

这个验证示例也可以看出:其他三个特性均是事务内在保证的,而一致性的约束条件是由外部业务逻辑规定的。这就意味者,同样的一个操作,根据外部业务逻辑规定的完整性约束的不同,可能满足事务要求,也可能不满足。


the end

到这里,事务的讲解已经完成了大部分了,但是,对于本节文中提到的比如:当前读、快照读、MVCC 它到底们具体是什么,数据库并发的场景有什么,这些场景会造成什么问题,如何解决等等我们都没有说

剩下的部分我会之后出文章专门讲解…

期待下次的见面~

  • 9
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 9
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 9
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

…狂奔的蜗牛~

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值