MySQL事务

一、CRUD失控

首先我们来看这样一张图,有两个客户端,A,B两人分别在自己的系统上买票,发现票被买了两次,这就乱套了,为了不乱套,我们需要给CRUD限定一些特性,能让这一系列的操作稳定而有序。如果出现a已经买完票了,b就不能买票。

  1. 买票的过程得是原子的吧
  2. 买票互相应该不能影响吧
  3. 买完票应该要永久有效吧
  4. 买前,和买后都要是确定的状态吧

所以必须把上面的动作当成一个整体去执行,要么全部执行完,要么一条也不执行,绝对不能出现执行完部分最后失败的中间状态。我们把这五条语句当成一个整体,这个整体其实就是事务的概念,就是一个或者多个SQL语句的集合。事务本身并不是数据库类软件天然就有的,事物本质工作其实是为了简化程序员工作的模型,现在市面上主流的数据库软件一般都会提供事务管理机制。

二、什么是事务

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

事务就是要做的或所做的事情,主要用于处理操作量大,复杂度高的数据。假设有一种场景:你毕业了,学校的教务系统后台MySQL中不再需要你的数据了,要删除你的所有信息。那么要删除你的基本信息比如姓名、电话、籍贯等,也要删除和你相关的其它信息比如各科成绩、在校表现等等。这样就需要多条MySQL语句构成,那么所有这些操作合起来,就构成了一个事务。

一个MySQL数据库可不止一个事务在运行,同一时刻甚至有大量的请求被包装成事务,在向MySQL服务器发起事务处理请求。而每条事务至少一条SQL语句,最多有很多SQL语句,这样如果大家都访问同样的表数据,在不加保护的情况下,就绝对会出现问题。甚至,因为事务由多条SQL构成,那么也会存在执行到一半出错或者不想再执行的情况。所以一个完整的事务,绝对不是简单的SQL语句集合,还需要满足下面的四个属性:

  • 原子性:一个事务中的所有操作,要么全部完成,要么全部不完成,不会结束在中间某个环节。事务在执行过程中发生错误,会被回滚到事务开始前的状态,就像这个事务从来没有执行过一样。
  • 隔离性:数据库允许多个并发事务同时对其数据进行读写和修改的能力,隔离性可以防止多个事务并发执行时由于交叉执行而导致数据的不一致。事务隔离分为不同级别,包括读未提交、读提交、可重复读和串行化。
  • 持久性:事务处理结束后,对数据的修改就是永久的,即使系统故障也不会丢失。
  • 一致性:在事务开始之前和事务结束以后,数据库的完整性没有被破坏。这表示写入的资料必须完全符合所有的预设规则,这包含资料的精确度、串联性以及后续数据库可以自发性地完成预定的工作。

从这里我们也可以看出,只要事务满足了原子性,隔离性,持久性,就能保持数据的一致性

上面四个属性,可以简称为 ACID 。原子性(Atomicity,或称不可分割性) 一致性(Consistency) 隔离性(Isolation,又称独立性) 持久性(Durability)。

为什么会出现事务?

事务被MySQL编写者设计出来,本质是为了当应用程序访问数据库的时候,事务能够简化我们的编程模型不需要我们去考虑各种各样的潜在错误和并发问题。可以想一下当我们使用事务时,要么提交要么回滚,我们不会去考虑网络异常了,服务器宕机了,同时更改一个数据会怎么办等等的情况。因此事务本质上是为了应用层服务的,而不是伴随着数据库系统天生就有的。

三、事务的版本支持

在MySQL中只有使用了InnoDB存储引擎才支持事务管理机制MyISAM存储引擎是不支持事务的。我们可以输入指令show engines;查看一下所有引擎的详细信息,可以发现InnoDB是支持事务的,其它的都是不支持的。

四、事务的提交方式

事务的提交方式常见的有两种:自动提交、手动提交。我们可以查看一下当前MySQL的事务提交方式是什么,输入指令show variables like 'autocommit';,我们看到Value一列是on,代表是自动提交。

我们也可以将自动提交改成手动提交,输入指令set autocommit=0;即代表关闭自动提交,也就是变成手动提交。

五、事务的常见操作方式

下面我们来演示一下事务的操作方式,并且通过几个实验来验证事务的某一些特性。首先我们要创建一张用于测试的表:

mysql> create table account(
    -> id int primary key,
    -> name varchar(20) not null default '',
    -> blance decimal(10,2) not null default 0.0
    -> )engine=InnoDB;


1.正常演示

开始事务

开始一个事务可以使用start transaction;语句,也可以使用begin;语句,推荐使用后者,因为比较方便简单。

创建保存点

在手动操作事务时,我们可以创建保存点,通过保存点能让我们指定回滚到某一位置,这是可以由我们程序员自己控制的,创建保存点使用的语句是savepoint 保存点名字;

在创建保存点之后我们就可以插入记录了,我们创建两个保存点,并且在每个保存点之后依次插入一条记录。

mysql> savepoint save1;
Query OK, 0 rows affected (0.00 sec)

mysql> insert into account values (1, '张三', 100);
Query OK, 1 row affected (0.00 sec)

mysql> savepoint save2;
Query OK, 0 rows affected (0.00 sec)

mysql> insert into account values (2, '李四', 10000);
Query OK, 1 row affected (0.01 sec)

可以看到,在另外一个是可以看到的。

回滚

由于我们刚才手动设置了两个保存点,所以我们现在也能手动回滚。回滚有两种方式,第一种是直接使用语句roolback;可以直接回滚到最开始;第二种是使用语句roolback to 保存点名字;可以回滚到指定的保存点。例如下面的例子,我们先回滚到save2,再回滚到最开始:

mysql> rollback to save2;
Query OK, 0 rows affected (0.00 sec)

mysql> rollback;
Query OK, 0 rows affected (0.01 sec)

依次回滚后我们发现当前为空。


2.证明未commit,客户端崩溃,MySQL自动会回滚

(隔离级别设置为读未提交)

我们再来演示一下开始事务插入数据之后,如果遇到了客户端崩溃的情况,

MySQL会不会自动回滚?

首先还是先开启事务,然后向account表中插入一条记录。

可以发现,这个是时候,另外一个客户端还是可以看到的。

但当我们将启动事务插入数据的那个MySQL客户端强行异常终止之后,再看另一个MySQL客户端会发现,之前插入的数据没有了。原因是启动事务的客户端没有提交事务就被异常终止了,所以MySQL会自动回滚到事务最开始。

3.证明commit了,客户端崩溃,数据不会再受影响,已经持久化

这里我们把自动提交打开,然后插入一个id为2的数据,并提交。

然后我们终止客户端,再重新启动一个客户端,发现表中的数据依旧是存在的。也就是说一旦事务被提交了,即使客户端崩溃,插入的数据也不会受影响,这些数据已经具有持久性。

4.证明手动提交事务不会受自动提交的影响

我们刚刚确定了目前MySQL是默认打开了自动提交事务的:

也就是说MySQL如果会自动提交的话,那么我们手动启动事务,如果没有提交就关闭了客户端,MySQL的自动提交会不会帮我们执行提交呢?我们做个实验来演示一下。

这里我们在这个客户端可以看到,确确实实是插入了数据的,我们再用另外一个客户端看看。

再次查看表中的数据,我们发现MySQL帮我们自动回滚了,刚刚插入的数据没有了。也就是说,MySQL即使设置了自动提交,只要是手动启动的事务,必须手动提交,MySQL不会帮我们自动提交。

5.证明单条SQL与事务的关系

那么MySQL的自动提交方式到底有什么用呢?既然它在手动启动事务的情况下几乎是不会发生作用的,那么我们尝试一下不在手动启动事务的情况下,它是否会发生作用

首先我们先关闭自动提交,输入语句set autocommit=0;再插入一条数据。

这时我们确定已经插入,这个时候我们再强制关闭掉插入数据的MySQL终端,再从另一个MySQL终端中查看表中的数据,我们发现刚刚插入的数据消失了。这是因为我们一开始将自动提交关闭了,MySQL不会为我们自动提交事务,并且我们也没有手动启动事务,所以如果没有手动提交事务的话,在强制关闭客户端之后插入的数据就会消失。

相反,如果我们一开始打开了自动提交,那么即使我们没有手动启动事务,我们插入的每一条SQL都会被当成事务,MySQL会帮我们自动提交。

所以综合上述几个演示,我们可以得出以下的结论:

  1. 只要手动启动了事务,事务就必须手动commit提交,才会持久化,与MySQL中是否设置自动提交无关。
     
  2. 事务可以手动回滚,同时,当操作异常时,MySQL会自动回滚到事务最开始的位置。
     
  3. 对于InnoDB的每一条SQL语句都会默认被封装成事务,只要打开了自动提交,每一条语句都会被当成事务自动提交,如果没有打开自动提交,那么在执行完SQL语句后要手动commit提交,否则退出后数据不再显示。

六、事务的隔离级别

1.如何理解隔离性

  • MySQL服务可能会同时被多个客户端进程访问,访问的方式是以事务的方式。一个事务可能由多条SQL构成,也就意味着,任何一个事务,都有执行前、执行中和执行后的阶段。而所谓的原子性,其实就是让用户层要么看到执行前,要么看到执行后,不会看到执行中的出现的问题。如果执行中出现了问题,事务可以随时回滚。所以单个事务对用户表现出来的特性就是原子性
  • 但是所有事务都要有个执行过程,那么在多个事务各自执行多个SQL的时候,就还是有可能会 出现互相影响的情况。比如:多个事务同时访问同一张表,甚至同一行数据。 就如同你妈妈给你说:你要么别学,要学就学到最好。至于你怎么学,中间有什么困难,你妈妈不 关心。那么你的学习,对你妈妈来讲,就是原子的。那么你学习过程中,很容易受别人干扰,此 时,就需要将你的学习隔离开,保证你的学习环境是健康的。
  • 所以数据库中,为了保证事务执行过程中尽量不受干扰,就有了一个重要特征:隔离性
  • 数据库中,允许事务受不同程度的干扰,就有了一种重要特征:隔离级别

2.隔离级别

数据库一共有下面四种隔离级别,越往下隔离级别越高,相对应的效率也越低。

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

所谓的脏读指的是读未提交时,如果插入数据的客户端还没有提交插入的数据,就被其它客户端看到了,这些数据就叫作脏数据,这种现象就是脏读现象。

一般的数据库在可重复读情况的时候,无法屏蔽其他事务insert的数据,因为隔离性实现是对数据加锁完成的,而insert待插入的数据因为并不存在,那么一般加锁无法屏蔽这类问题,会造成虽然大部分内容是可重复读的,但是insert的数据在可重复读情况被读取出来,导致多次查找时,会多查找出来新的记录,就如同产生了幻觉。这种现象,叫做幻读(phantomread)。很明显,MySQL在RR级别的时候,是解决了幻读问题的。

七、一致性

事务执行的结果,必须使数据库从一个一致性状态,变到另一个一致性状态。当数据库只包含事务成功提交的结果时,数据库处于一致性状态。如果系统运行发生中断,某个事务尚未完成而被迫中 断,而改未完成的事务对数据库所做的修改已被写入数据库,此时数据库就处于一种不正确(不一致)的状态。因此一致性是通过原子性来保证的。 其实一致性和用户的业务逻辑强相关,一般MySQL提供技术支持,但是一致性还是要用户业务逻辑 做支撑,也就是,一致性,是由用户决定的。 而技术上,通过AID保证C

八、隔离性的原理

1.数据库并发的三种场景

  • 读-读 :不存在任何问题,也不需要并发控制
  • 读-写 :有线程安全问题,可能会造成事务隔离性问题,可能遇到脏读,幻读,不可重复读
  • 写-写 :有线程安全问题,可能会存在更新丢失问题,比如第一类更新丢失,第二类更新丢失

其中,读-写冲突问题是通过多版本并发控制(MVCC)来解决的,这是一种无锁并发控制的机制。

理解 MVCC 需要知道三个前提知识:

  • 3个记录隐藏字段
  • undo 日志
  • Read View

2.三个记录隐藏列字段

  • DB_TRX_ID:占6个字节,用来表示最近修改(修改/插入)的事务ID,记录创建这条记录/最后一次修改该记录的事务ID。
  • DB_ROLL_PTR:占7个字节,回滚指针,用来指向这条记录的上一个版本。这些数据一般存在undo log中。
  • DB_ROW_ID:占6个字节,是隐含的自增ID,即隐藏主键。如果数据表中没有设置主键,那么InnoDB会自动生成隐藏主键,并以此产生一个聚簇索引。

补充:实际还有一个删除flag隐藏字段, 既记录被更新或删除并不代表真的删除,而是删除flag变了

3.undo日志

MySQL是以服务进程的方式在内存中运行的。我们之前所讲的所有机制,包括索引、事务、隔离性、日志等,都是在内存中完成的。即在MySQL内部的相关缓冲区中,保存相关数据,完成各种操作。然后在合适的时候,将相关数据刷新到磁盘当中。所以,undo log可以简单理解为就是MySQL中的一段内存缓冲区,用来保存日志数据的。
 

如果这里我们创建一个表,插入张三这个数据:

mysql> create table if not exists student(
   name varchar(11) not null, 
   age int not null 
 );
 
mysql> insert into student (name, age) values ('张三', 28);
Query OK, 1 row affected (0.05 sec)
mysql> select * from student;
+--------+-----+
| name   | age |
+--------+-----+
| 张三   |  28 |
+--------+-----+
1 row in set (0.00 sec)

表中蕴含的意思还有:

我们目前并不知道创建该记录的事务ID,隐式主键,我们就默认设置成null,1。第一条记录也没有其他 版本,我们设置回滚指针为null。

假设我们现在有一个事务ID为10的事务,对student表中的记录进行修改(Update):将张三的name改为李四,修改过程如下:

  1. 因为要修改,所以要先给该记录加行锁。
  2. 在修改之前,要先将该行记录拷贝到undo log中,所以,undo log中就有了一行副本数据,这个原理就和写时拷贝类似,在要写入时先将原来的数据拷贝一份再写入修改。
  3. 拷贝好副本之后,现在MySQL中就有了两行同样的记录,undo log里的副本保存历史版本记录,原始记录的内容就可以开始修改了。并且原始记录的事务ID要变为10,隐藏主键不变,回滚指针指向undo log中副本数据的地址,即表示当前版本的上一个版本就是它。
  4. 提交事务,释放行锁,修改结束。

此时如果我们又要对student表中的记录做修改,这次想把age从28改为38,修改过程如下:

  1. 因为要修改,所以要先给该记录加行锁。
  2. 在修改之前,要先将该行记录拷贝到undo log中,所以,undo log中就有了一行副本数据,这个原理就和写时拷贝类似,在要写入时先将原来的数据拷贝一份再写入修改。
  3. 现在可以修改原始记录中的age,改成38.并且修改原始记录的事务ID,改为11,并且修改原始记录中的回滚指针,改为undo log中的副本数据的地址。
  4. 提交事务,释放行锁,修改结束。

这样,我们就有了一个基于链表记录的历史版本链。所谓的回滚,无非就是用历史数据,覆盖当前数据。 上面的一个一个版本,我们可以称之为一个一个的快照。

上面的例子是以update为主演示的,如果是delete呢?其实是一样的,因为delete删除数据并不是真正的删除数据,只是设置删除flag为删除,也可以形成历史版本。

如果是insert呢?因为insert是插入操作,也就是说之前并没有数据,那么insert按理说就不应该有历史版本。但是一般为了回滚操作,insert的数据的相反记录也是要被放入undo log中的,这个相反记录指的就是delete语句,因为你插入如果要回滚的话,就是将插入的数据删除了,就回到插入的上一个版本了。如果当前事务提交了,那么这个undo log的历史insert记录也就可以被清空了。

所以总结一下就是,update和delete可以形成版本链,insert暂时不考虑。

也就是说,增删改都是对最新版本的记录做操作,那么select读取记录是读取最新的版本还是读取历史版本呢?

般而言,select是读取历史版本,这种读取叫做快照读,读取历史版本的话,是不受加锁限制的,也就是可以并行执行,这样也提高了效率,这就是MVCC的意义所在。但select也可以读取最新的记录,叫做当前读。如果是当前读的话那么select必须要加锁,这就是串行化的读取。


那么,是什么决定了,select是当前读,还是快照读呢?

没错,就是隔离级别!

那为什么要有隔离级别呢?

事务都是原子的。所以,无论如何,事务总有先有后。 但是经过上面的操作我们发现,事务从begin->CURD->commit,是有一个阶段的。也就是事务有执行前,执 行中,执行后的阶段。但,不管怎么启动多个事务,总是有先有后的。 那么多个事务在执行中,CURD操作是会交织在一起的。那么,为了保证事务的“有先有后”,

是不是应该让不同 的事务看到它该看到的内容,这就是所谓的隔离性与隔离级别要解决的问题。

那么先来的事务,应不应该看到后来的事务所做的修改呢?

不能!就像革命先辈他并不知道这个时代的人在做什么,有什么变化,他的时间线便是上世纪的那一百年,而你也不知道抗战的先辈到底在那个年代付出了什么,每个人都有自己的快照读!

那么到底什么时候应该用快照读呢?

源码如下:

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值