关于Myspl 事务

对于MySQL的一般应用,基本都是单表写入,但是有时候,我们需要通过一组SQL语句处理业务,且同时往多个表中写入,且写入结果相互依赖、相互影响,这个时候,如果其中有失败的操作,则会造成整个数据的不正确,因此我们保证所用命令执行的同步,产生依靠关系的SQL语句能够同时操作成功或同时返回初始状态。在此情况下,我们就需要用到MySQL的事务。

在MySQL中,事务由单独单元的一个或多个SQL语句组成。在这个单元中,每个MySQL语句是相互依赖的。而整个单独单元作为一个不可分割的整体,如果单元中某条SQL语句一旦执行失败或产生错误,整个单元将会回滚。所有受到影响的数据将返回到事务开始以前的状态;如果单元中的所有SQL语句均执行成功,则事务被顺利执行。目前在MySQL中使用最广泛的InnoDB存储引擎是完全支持事务的。

定义:事务是数据库管理系统执行过程中的一个逻辑单元,有有限的操作序列构成的。

并不是任意对数据库的操作序列都是数据库事务,数据库事务拥有以下四个特征,通常我们称之为ACID特性。

0. 事务的基本要素(ACID)

1. 原子性(Atomicity):事务开始后所有操作,要么全部做完,要么全部不做,不可能停滞在中间环节。事务执行过程中出错,会回滚到事务开始前的状态,所有的操作就像没有发生一样。也就是说事务是一个不可分割的整体,就像化学中学过的原子,是物质构成的基本单位。
比如:INSERT INTO user (name,password) VALUES ('张三','123456'),('李四','654321'),只要任何一个值插入失败,那么整个事务就是败了。对于不支持事务的比如MyISAM引擎的表而言,出错之前的值是可以正常插入到表中的。再比如,我们需要连续成功两条插入语句,才算真正成功……

2. 一致性(Consistency):事务开始前和结束后,数据库的完整性约束没有被破坏,即事务应确保数据库的状态从一个一致状态转变为另一个一致状态。比如A向B转账,不可能A扣了钱,B却没收到。

3. 隔离性(Isolation):同一时间,只允许一个事务请求同一数据,不同的事务之间彼此没有任何干扰。比如A正在从一张银行卡中取钱,在A取钱的过程结束前,B不能向这张卡转账。

4. 持久性(Durability):事务完成后,事务对数据库的所有更新将被保存到数据库,不能回滚。

1. 事务的开启、回滚、提交

# 开启事务
start transaction;
begin;
# 回滚事务
rollback;
# 提交事务
commit;

2. 事务的4种隔离级别

所谓事务的隔离级别主要解决:当事务之间发生交叉时如何处理的问题。

事务的隔离级别越高,月能保证数据的完整性和一致性,但是对于并发性能的影响也会越大,MySQL的事务包含4个隔离级别,按照级别从低到高排列如下:

2.1 read uncommitted (dirty read)

即是:读未提交内容,事务可以看到其他事务更改了但是还没有提交的数据。即存在脏读的情况。

脏读 脏读是指在一个事务处理过程里读取了另一个未提交的事务中的数据。

当一个事务正在多次修改某个数据,而在这个事务中这多次的修改都还未提交,这时一个并发的事务来访问该数据,就会造成两个事务得到的数据不一致。例如:用户A向用户B转账100元,对应SQL命令如下:

update account set money=money+100 where name=’B’;  
update account set money=money - 100 where name=’A’;

当只执行第一条SQL时,A通知B查看账户,B发现确实钱已到账(此时即发生了脏读),而之后无论第二条SQL是否执行,只要该事务不提交,则所有操作都将回滚,那么当B以后再次查看账户时就会发现钱其实并没有转。

测试步骤:

说明:以下所有测试使用的软件版本为:mysql 5.1.73、CentOS 6.8

修改隔离级别:

mysql> SET GLOBAL TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;

事务1:

mysql> begin;
mysql> select * from account;
# 可以的到这样的结果
+----+----------+-------+
| id | username | money |
+----+----------+-------+
|  1 | A        |  1000 |
|  2 | B        |  1300 |
+----+----------+-------+

事务2:

mysql> begin;
mysql> select * from account;
# 可以的到这样的结果
+----+----------+-------+
| id | username | money |
+----+----------+-------+
|  1 | A        |  1000 |
|  2 | B        |  1300 |
+----+----------+-------+

事务1:

mysql> update account set money=money+100 where username='B';
mysql> select * from account;
+----+----------+-------+
| id | username | money |
+----+----------+-------+
|  1 | A        |  1000 |
|  2 | B        |  1400 |
+----+----------+-------+

事务2:

mysql> select * from account;
# 事务2可以看到事务1已经更新但未提交的事务的数据
+----+----------+-------+
| id | username | money |
+----+----------+-------+
|  1 | A        |  1000 |
|  2 | B        |  1400 |
+----+----------+-------+

事务1:

# 事务1如果回滚事务
mysql> rollback;
mysql> select * from account;
# 可以看到事务1的数据恢复到事务之前了
+----+----------+-------+
| id | username | money |
+----+----------+-------+
|  1 | A        |  1000 |
|  2 | B        |  1300 |
+----+----------+-------+

事务2:

mysql> select * from account;
# 可以看到数据变成从前的了,但是中途确实能看到money变成1400。出现了脏读现象。
+----+----------+-------+
| id | username | money |
+----+----------+-------+
|  1 | A        |  1000 |
|  2 | B        |  1300 |
+----+----------+-------+

2.2 read committed

即是:读已提交内容,这是大多数数据库默认的隔离级别,不过MySQL默认的不是它。它满足隔离的一般定义:一个事务只能看见已提交事务的更改,也就是说,对于一个事务而言,在它提交之前,它所做的任何修改对其它事务来说都是不可见的。

不过该级别的隔离不能解决不可重复读的问题,所谓不可重复读是指:对于数据库中的某个数据,一个事务范围内多次查询却返回了不同的数据值,这是由于在两次查询间隔中,该数据被另一个事务修改并提交了。

测试步骤

修改隔离级别:

mysql> SET GLOBAL TRANSACTION ISOLATION LEVEL READ COMMITTED;

事务1:

mysql> select * from account;
# 这是我们看到的原始数据
+----+----------+-------+
| id | username | money |
+----+----------+-------+
|  1 | A        |  1000 |
|  2 | B        |  1300 |
+----+----------+-------+
# 开启事务1
mysql> start transaction;

事务2:

# 开启事务2
mysql> start transaction;
# 查询数据
mysql> select * from account;
+----+----------+-------+
| id | username | money |
+----+----------+-------+
|  1 | A        |  1000 |
|  2 | B        |  1300 |
+----+----------+-------+

事务1:此时如果我们改变一条数据的内容,但是我们事务未提交

mysql> update account set money=100 where username='B';
# 该事务未提交状态下,我们只能在本事务中看到数据更改了
mysql> select * from account;
+----+----------+-------+
| id | username | money |
+----+----------+-------+
|  1 | A        |  1000 |
|  2 | B        |   100 |
+----+----------+-------+

事务2:由于事务1的事务未提交,我们无法在本事务中看到事务1更改的内容,脏读问题解决

# 查询数据库的内容,我们可以看到,
mysql> select * from account;
+----+----------+-------+
| id | username | money |
+----+----------+-------+
|  1 | A        |  1000 |
|  2 | B        |  1300 |
+----+----------+-------+

事务1:提交事务1

mysql> commit;

事务2:

# 查询数据,我们可以看到事务1更新的数据,但是由于事务2尚未提交,在本次事务中同样的sql语句出现了不同的结果
mysql> select * from account;
+----+----------+-------+
| id | username | money |
+----+----------+-------+
|  1 | A        |  1000 |
|  2 | B        |   100 |
+----+----------+-------+

从我们上面的测试可以看到,事务2在事务1修改数据但未提交时,查询不到数据,当直到事务1提交事务之后,事务2才可以查询到新增的数据。这说明“读已提交”这个隔离级别解决了脏读的问题,但是出现了“不可重复读”的问题,即事务2在两次查询的数据不一致,因为在两次查询之间事务1更新了一条数据。

2.3 repeatable read (mysql的默认级别)

即:可重复读,同一个事务内,同样的查询请求,若多次执行,获得的结果是一样的。通俗来讲,可重复读就是在一个事务里读取数据,怎么读都不会变,除非提交了该事务,再次进行读取。

测试步骤:

修改隔离级别:

mysql> SET GLOBAL TRANSACTION ISOLATION LEVEL REPEATABLE READ;

事务1:

# 开启事1
mysql> start transaction;
# 查询数据
mysql> select * from account;
+----+----------+-------+
| id | username | money |
+----+----------+-------+
|  1 | A        |  1000 |
|  2 | B        |   100 |
+----+----------+-------+

事务2

# 开启事务2
mysql> start transaction;
# 查询数据
mysql> select * from account;
+----+----------+-------+
| id | username | money |
+----+----------+-------+
|  1 | A        |  1000 |
|  2 | B        |   100 |
+----+----------+-------+

事务1

# 更改数据信息
mysql> update account set money=300 where username='B';
# 在本事务内可以看到已更新的内容
mysql> select * from account;
+----+----------+-------+
| id | username | money |
+----+----------+-------+
|  1 | A        |  1000 |
|  2 | B        |   300 |
+----+----------+-------+

事务2

# 查询数据,由于事务1并没有提交,所以事务2中并看不到更新,脏读问题不存在
mysql> select * from account;
+----+----------+-------+
| id | username | money |
+----+----------+-------+
|  1 | A        |  1000 |
|  2 | B        |   100 |
+----+----------+-------+

事务1,提交事务

mysql> commit;

事务2:

# 查询数据,我们可以看到,虽然事务1中将数据更改了,但是在事务2中多次查询,看到的结果依然是以前的。
mysql> select * from account;
+----+----------+-------+
| id | username | money |
+----+----------+-------+
|  1 | A        |  1000 |
|  2 | B        |   100 |
+----+----------+-------+

# 提交事务2
mysql> commit;
# 只有当事务2也提交事务后,才能看到事务1更新的数据
mysql> select * from account;
+----+----------+-------+
| id | username | money |
+----+----------+-------+
|  1 | A        |  1000 |
|  2 | B        |   300 |
+----+----------+-------+

该隔离级别可能会出现**幻读(虚读)**现象。一般会发生在操作一批数据的时候,比如:我们在事务1中需要修改一批数据的某个字段的值,但是在这期间,事务2中新插入一条新数据,而且该数据符合事务1的修改条件,那么事务1结束后会发现自己多修改了一条数据,就像产生了幻觉一样。

幻读情景再现

原始数据

mysql> select * from account;
+----+----------+-------+
| id | username | money |
+----+----------+-------+
|  1 | A        |  1000 |
|  2 | B        |   300 |
+----+----------+-------+

事务1

# 开启事务1
mysql> begin;

事务2

# 开启事务2,此时事务2和事务1交叉了,
mysql> begin;
# 此时我们插入一条数据
INSERT INTO account (username,money) VALUES('C',800);
# 在本事务中我们查看数据,可以看到新数据
mysql> select * from account;
+----+----------+-------+
| id | username | money |
+----+----------+-------+
|  1 | A        |  1000 |
|  2 | B        |   300 |
|  3 | C        |   800 |
+----+----------+-------+
# 然后提交事务
mysql> commit;

事务1

# 查看数据,此时事务1并不能看到事务2更新的数据,说明解决了不可重复读的问题。
mysql> select * from account;
+----+----------+-------+
| id | username | money |
+----+----------+-------+
|  1 | A        |  1000 |
|  2 | B        |   300 |
+----+----------+-------+
# 事务1对数据进行修改
mysql> update account set money=money+100 where money>100;
# 然后查询,发现自己多改了一条,和自己之前的预测不一样,好像出现了幻觉。
mysql> select * from account;
+----+----------+-------+
| id | username | money |
+----+----------+-------+
|  1 | A        |  1100 |
|  2 | B        |   400 |
|  3 | C        |   900 |
+----+----------+-------+

mysql> commit;

mysql> select * from account;
+----+----------+-------+
| id | username | money |
+----+----------+-------+
|  1 | A        |  1100 |
|  2 | B        |   400 |
|  3 | C        |   900 |
+----+----------+-------+

以上只是插入新数据造成的“幻读”现象,更改和删除的处理与这个类似。不过该级别下可以使用“多版本并发控制(mvcc)”来进行部分解决,有兴趣的可以自行了解。

2.4 serializable

即:串行化,这是最高的隔离级别,它通过强制事务排序,使之不可能相互交叉冲突,从而解决幻读问题。其基本原理是:在每个读的数据行上加上共享锁。通俗地讲就是:假如两个事务都操作到同一数据行,那么这个数据行就会被锁定,只允许先操作到该数据行的事务优先操作,只有当该事务提交了,数据行才会解锁,后一个事务才能成功操作这个数据行,否则只能一直等待。也正因为这样,可能导致大量的超时现象和锁竞争。

测试步骤

修改隔离级别:

mysql> SET GLOBAL TRANSACTION ISOLATION LEVEL SERIALIZABLE;

查看原始数据

mysql> select * from account;
+----+----------+-------+
| id | username | money |
+----+----------+-------+
|  1 | A        |  1100 |
|  2 | B        |   400 |
|  3 | C        |   900 |
+----+----------+-------+

事务1

# 开启事务1
mysql> begin;

事务2

# 开启事务2
mysql> begin;
# 此时我们插入一条数据
INSERT INTO account (username,money) VALUES('D',600);
# 在本事务中我们查看数据,可以看到新数据
mysql> select * from account;
+----+----------+-------+
| id | username | money |
+----+----------+-------+
|  1 | A        |  1000 |
|  2 | B        |   300 |
|  3 | C        |   800 |
+----+----------+-------+

事务1

# 由于事务2先操作的account表,同时尚提交,我们在事务1中涉及到事务2操作的数据时就无法操作
mysql> select * from account;
ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction
# 虽然事务2未提交,但是事务1操作的数据并不涉及事务2中的数据时,可以正常操作
mysql> select * from account where id=2;
+----+----------+-------+
| id | username | money |
+----+----------+-------+
|  2 | B        |   400 |
+----+----------+-------+

事务1和事务2同时操作

# 事务1先查询,会有一个等待期,但是没有结果,此时如果快速的提交事务2,会发现事务1紧接着就出现了数据。
mysql> select * from account;
# 提交事务2
mysql> commit;

3. 查看事务的隔离级别

# 查看MySQL当前的事务隔离级别(使用系统变量)
SELECT @@tx_isolation;
SELECT @@session.tx_isolation;
# 或者
SHOW VARIABLES LIKE 'tx_isolation';
# 查看全局的事务隔离级别
SHOW GLOBAL VARIABLES LIKE 'tx_isolation';
SELECT @@global.tx_isolation;

4. 设置事务的隔离级别

方法1:

# 基本语法
SET [GLOBAL | SESSION] TRANSACTION ISOLATION LEVEL
  {
       REPEATABLE READ
     | READ COMMITTED
     | READ UNCOMMITTED
     | SERIALIZABLE
   }
# 例子:
SET GLOBAL TRANSACTION ISOLATION LEVEL READ COMMITTED;

说明:GLOBAL:设置全局的事务隔离级别、SESSION:设置当前session的事务隔离级别,如果语句没有指定GLOBAL或SESSION,默认值为SESSION

说明:如果设置了session级别的更改,可能需要退出后在进入才生效。

发布了22 篇原创文章 · 获赞 0 · 访问量 1147

猜你喜欢

转载自blog.csdn.net/bigpatten/article/details/103961913