赞
踩
我们先来看一个例子,例如有一个火车售票系统:
当客户端A检查还有一张票时,将票卖掉,还没有执行更新数据库的时候,客户端B检查了票数,发现大于0,于是又买了一次票。然后客户端A将票数更新回数据库。于是就出现了同一张票被卖了两次的情况。
所以数据库的 CURD 应该满足什么属性能解决上面的问题?
事务就是一组 DML 语句组成,这些语句在逻辑上存在相关性,这一组 DML 语句要么全部成功,要么全部失败,是一个整体。MySQL 提供一种机制,保证我们达到这样的效果。事务还规定不同的客户端看到的
数据是不相同的。
事务就是要做的或所做的事情,主要用于处理操作量大,复杂度高的数据。假设一种场景:我给某个人转账,数据库必定需要将我账户上的金额 update ,然后给对方的账户做 add 操作等等,这样,就需要多条 MySQL 语句构成,那么所有这些操作合起来,就构成了一个事务。
正如我们上面所说,一个 MySQL 数据库,可不止一个事务在运行,同一时刻,甚至有大量的请求被包装成事务,在向 MySQL 服务器发起事务处理请求。而每条事务至少一条 SQL ,最多很多 SQL ,这样如果大家都访问同样的表数据,在不加保护的情况,就绝对会出现问题。甚至,因为事务由多条 SQL 构成,那么,也会存在执行到一半出错或者不想再执行的情况,那么已经执行的怎么办呢?
所以,一个完整的事务,绝对不是简单的 SQL 集合,还需要满足如下四个属性:
上面四个属性,可以简称为 ACID ;原子性(Atomicity,或称不可分割性);一致性(Consistency);隔离性(Isolation,又称独立性);持久性(Durability)。
所以总结,所谓的事务,就是在 ACID 四大属性的加持之下,由一条或者多条 SQL 共同构建成的,我们就称为事务。
事务被 MySQL 编写者设计出来,本质是为了当应用程序访问数据库的时候,事务能够简化我们的编程模型,不需要我们去考虑各种各样的潜在错误和并发问题。可以想一下当我们使用事务时,要么提交,要么回滚,我们不会去考虑网络异常了,服务器宕机了,同时更改一个数据怎么办对吧?因此事务本质上是为了应用层服务的,而不是伴随着数据库系统天生就有的。
备注:我们后面把 MySQL 中的一行信息,称为一行记录。
在 MySQL 中只有使用了 Innodb 数据库引擎的数据库或表才支持事务, MyISAM 不支持。我们可以使用指令 show engines\G
查看各种引擎的属性:
事务的提交方式常见的有两种:
查看事务提交方式:show variables like 'autocommit';
我们可以用 SET 来改变 MySQL 的自动提交模式:SET AUTOCOMMIT=0;
禁止自动提交。SET AUTOCOMMIT=1;
开启自动提交。
我们将隔离级别设置成最低是为了方便我们观察现象。
为了便于演示,我们将 mysql 的默认隔离级别设置成读未提交。具体操作我们后面会详细讲,现在以使用为主:
set global transaction isolation level read uncommitted;
设置好后需要重启终端,进行查看:select @@tx_isolation;
:
创建测试表
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;
以上是一个简单的员工工资表的表结构。
首先我们已经开启自动提交:
我们开始一个事务的语句是:start transaction;
或者 begin
,下面先使用第一个:
我们开始一个事务后,从该语句往后的所有 SQL 语句都是同一个事务。我们开启另一个终端同时也启动一个事务:
首先我们当前的表是空的,我们以左边的终端为主,我们先创建一个保存点 s1,对应的语句为 savepoint s1;
;然后我们往表里插入一个数据;接着再创建一个保存点 s2,然后再插入一个数据,如下图:
然后我们在另一个终端查看该表,是可以看见另一个终端插入的数据的:
上面所有的创建保存点、插入数据的操作都是一个事务,那么我们操作失误了,想要撤回 Mike 的数据,我们就可以定向地回滚,可以回滚到指定的位置,例如我们想撤回 Mike 的数据,我们回滚到 s2 的保存点即可,对应的语句为:rollback to s2;
,此时我们再从另一个终端查看该表时,就会发现 Mike 的数据已经没有了,如下:
如果我们直接 rollback;
,会回滚到最开始的地方。那么表中的数据也就没有了。现在我们把该事务提交,对应的语句为 commit;
。
如果我们插入数据后没有 rollback 而是 commit 那么数据就会持久化地保存到数据库中,这时候 rollback 也没有用了。
假设我们正常开始一个事务,正常插入数据,此时是可以看到插入的数据的:
但是如果当我们的 mysql 异常崩溃,还没有 commit 会怎样呢?下面我们让 mysql 异常崩溃,直接按下 *ctrl + * 即可:
如图,我们可以看到,数据会自动回滚。但是如果我们 commit 之后异常崩溃,数据不会再受影响,因为数据已经持久化。
我们上面手动启动一个事务并不会受 MySQL 是否自动提交影响,例如我们现在把自动提交关掉:
我们再启动一个事务,插入数据等,重复上面的操作:
如上,异常崩溃会自动回滚。
如上,commit 后再异常崩溃数据已经持久化。
我们知道,当我们启动一个事务的时候删除一个数据再手动 commit 之后,数据一定会被删除。但是我们将自动提交关闭,不启动事务,像我们正常一样使用单 SQL 语句删除呢?再异常崩溃会怎样呢?下面我们验证一下:
如上,我们自动提交关闭后,执行单条 SQL 语句异常退出后,数据也会回滚回来!这是为什么呢?因为自动提交已经被关闭了!需要我们手动提交才能保存数据!下面我们验证一下是否需要我们手动提交:
如上,我们手动 commit 后数据确实被删除了!说明我们的单 SQL 语句也是事务,只是以前系统默认打开自动提交,执行完 SQL 语句后就自动 commit 了!
所以根据上面四个场景,我们得出以下结论:
事务操作注意事项
下面举一个例子,假设有人向数据库进行 update 操作,另外一个人向数据库进行 select 数据,那么如果两个事务对同一个数据库操作,数据库先执行谁的呢?我们最先想到的可能是先 update 再 select 数据,因为需要保证数据是最新的,但是这是有问题的!为什么呢?比如我们在出生之前,能不能看到过去的世界呢?不能,我们也不应该看得到!又比如已经过世的故人,能不能看到我们今天的世界呢?也不能!所以回到事务,事务也是一样的!它不需要看到旧的数据,也不应该看到最新的数据,只需要看到每个事务到来时,它应该看到的数据,这就是隔离性!
那么回到上面的问题,update 和 select 谁先执行呢?这就要取决于它们谁先到来了,如果 update 先来,那么肯定先执行 update,因为要保持事务的原子性。如果 update 执行的很快,select 执行的很久,可能在 update 执行完毕之后,select 还在执行,那么此时 select 应不应该更新后的数据呢?不应该!因为要保证事务的隔离性!
查看全局隔级别:
select @@global.tx_isolation;
查看此次会话(登录)全局隔级别:
select @@session.tx_isolation;
或 select @@tx_isolation;
以上两种查看隔离级别的区别在于,select @@global.tx_isolation;
是全局的隔离级别;select @@tx_isolation;
在此次登录时默认读取全局的隔离级别,然后拷贝一份给自己,它的生命周期是在当我们开始登录到退出客户端。
设置隔离级别
set [session | global] transaction isolation level { read uncommitted | read
commited | repeatable read | serializable }
其中 [session | global]
是设置当前会话或者全局隔离级别;{}
内部是对应的隔离级别。
设置当前会话隔离性,另起一个会话,不会影响另一个会话,只影响当前会话;而设置全局隔离性,另起一个会话,会被影响。
我们在上面也设置过了我们当前的隔离级别是 RU,如下:
接下来我们开启两个事务并发起来,我们在其中一个事务中插入数据、删除数据、修改数据等,还没有 commit 前,在另一个事务中都可以查看得到,如下:
这就是读未提交,我们在另一个事务中读到了别人还没提交的事务!一个事务在执行中,读到另一个执行中事务的更新(或其他操作)但是未 commit 的数据,这种现象叫做脏读!
首先我们将隔离级别改成 RC:
当我们将两个终端的隔离级别都设置为 RC 后,下面我们开始做一些实验。首先我们在两个终端分别启动事务,在其中一个终端插入、修改数据,观察另一个终端是否能见:
如上图,我们发现在一个事务在进行期间,另一个事务进行查看是不能看见它的增加或修改的,而当前事务本身可以看见吗?我们试一下:
是可以的;当我们将第一个终端的事务 commit 之后,看看另一个事务能否看见修改之后的表:
如上图,我们发现在它进行 commit 之后,另一个事务还没有 commit 也能看到对应的修改!这就是读提交!
但是,第二个终端此时还在当前事务中,并未 commit,那么就造成了,同一个事务内,同样的读取,在不同的时间段(依旧还在事务操作中!),读取到了不同的值,这种现象叫做不可重复读。
那么不可重复读是问题吗?是的!在和我们并发运行的事务中,它做了修改表的数据并 commit,而导致我在每次查看数据的时候都是不一样的,这会导致一些问题出现,例如我们在用使用这个事务查看表进行统计数据时,统计到了一半,突然再查一下数据发现数据不一样了,就会导致我们需要重新统计!这就是因为不可重复读可能会引发的问题!
因为 MySQL 默认的隔离级别就是 RR 级别,所以我们重新启动 MySQL 服务即可更换为 RR 级别:
下面我们也并发启动两个事务,其中一个进行修改、新增数据,观察另一个事务查看的情况:
我们看到, 另一个事务是看不见的,这也正常,我们在 RC 级别都看不见,RR 级别也应该看不见,但是下面我们将修改的数据 commit 呢?再看看会有什么变化:
如图,当我们 commit 之后再查看,还是看不见修改的数据。如果我们也将这个事务 commit,再查看,就可以看到修改的数据了:
如上图,这就是可重复读。
在可重复读中,我们假设第一个终端为终端A,第二个为终端B,多次查看,发现终端A在对应事务中 insert 的数据,在终端B的事务周期中,也没有什么影响,也符合可重复的特点。但是,一般的数据库在可重复读情况的时候,无法屏蔽其他事务 insert 的数据,为什么呢?因为隔离性实现是对数据加锁完成的,而 insert 待插入的数据因为并不存在,那么一般加锁无法屏蔽这类问题,所以会造成虽然大部分内容是可重复读的,但是 insert 的数据在可重复读情况被读取出来,导致多次查找时,会多查找出来新的记录,就如同产生了幻觉。这种现象,叫做幻读(phantom read)。很明显,MySQL 在 RR 级别的时候,是解决了幻读问题的,具体的解决方法我们就不研究了。
串行化就是对所有操作全部加锁,进行串行化,不会有问题,但是只要串行化,效率很低,几乎完全不会被采用。
接下来我们将隔离级别更换为串行化:
接下来我们启动两个事务,分别进行查看数据,是没有问题的,因为两个读取不会串行化,共享锁:
然后我们在终端A修改数据,在终端B读取,即进行读写操作,终端A会卡住,因为终端B中的事务还没有结束:
当终端B中的事务结束,终端A中的事务才能继续:
反过来也一样,在终端A修改数据,在终端B读取,终端B中的读取也会卡住:
当终端A的事务结束终端B才能继续读取:
总结:
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。