MySQL数据库 第9章:事务

9.1 事务处理

9.1.1 事务的概念

举例来了解什么是事务:

转账可以分为两部分来完成:转入转出
只有这两个部分都完成才认为转账成功.

A账户给B账户转账100元,那么:
A账户减少100元
B账户增加100元

在数据库中,这个转账的过程是使用两条SQL语句来完成的,如下:

# A 账户减少100元
UPDATE account SET money = money - 100 WHERE name = 'A';
# B 账户增加100元
UPDATE account SET money = money + 100 WHERE name = 'B';

正常情况下,如果这两条SQL语句都能正常执行的话,那这笔交易就算完成了,但如果其中任意一条语句出现异常没有执行,则会导致两个账户的金额不同步,造成错误。

为了防止这种情况发生,就需要使用MySQL中的事务Transaction

事务是针对数据库的一组操作,它可以由一条或多条SQL语句组成。

9.1.2 事务的基本操作

在默认情况下,用户执行的每一条SQL语句都会被当成单独的事务自动提交。

如果要将一组SQL语句作为一个事务,则需要先执行以下语句显式地开启一个事务。

START TRANSACTION;

此时,每一条SQL语句不再自动提交,用户需要手动提交,操作才会生效,手动提交的命令如下:

COMMIT;

如果不想提交当前事务,可以取消事务(即回滚):

ROLLBACK;

事务处理案例演示:
先创建一个数据表sh_user,里面有两个字段name和money:

CREATE TABLE sh_user(
NAME CHAR(20),
money INT)

然后插入两条数据:

INSERT INTO sh_user VALUES('a',1000),('b',1000)

查询一下:

# 查看用户数据
SELECT name, money FROM sh_user;
+------+---------+
| name | money   |
+------+---------+
| a    | 1000.00 |
| b    | 1000.00 |
+------+---------+
2 rows in set (0.00 sec)

将a用户的100元钱转给b用户

# ① 开启事务
mysql> START TRANSACTION;
# ② a减少100元
mysql> UPDATE sh_user SET money = money - 100 WHERE name = 'a';
# ③ b增加100元
mysql> UPDATE sh_user SET money = money + 100 WHERE name = 'b';
# ④ 提交事务
mysql> COMMIT;

查询Alex和Bill的金额:

mysql> SELECT name, money FROM sh_user;
+------+---------+
| name | money   |
+------+---------+
| a    |  900.00 |
| b    | 1100.00 |
+------+---------+
2 rows in set (0.00 sec)

事务的回滚

# ① 开启事务
mysql> START TRANSACTION;
# ② b扣除100元
mysql> UPDATE sh_user SET money = money - 100 WHERE name = 'b';
# ③ 查询b的金额
mysql> SELECT name, money FROM sh_user WHERE name = 'b';
+------+---------+
| name | money   |
+------+---------+
| b    | 1000.00 |
+------+---------+
1 row in set (0.00 sec)

发现b的钱从1100,减少100后成为1000元,然后我们开始回滚

# ① 回滚事务
mysql> ROLLBACK;
# ② 查看Bill的金额
mysql> SELECT name, money FROM sh_user WHERE name = 'b';
+------+---------+
| name | money   |
+------+---------+
| b    | 1100.00 |
+------+---------+
1 row in set (0.00 sec)

发现回滚后,钱数又回到了1100元(事务之前的状态)

事务的执行要么成功,要么就返回到事务开始前的状态,这就保证了同一事务操作的同步性和数据的完整性。

MySQL中的事务必须满足A、C、I、D这4个基本特性。

  • Atomicity:原子性
  • Consistency:一致性
  • Isolation:隔离性
  • Durability:持久性

原子性(Atomicity)

  • 一个事务必须被视为一个不可分割的最小工作单元,只有事务中所有的数据库操作都执行成功,才算整个事务执行成功。
  • 事务中如果有任何一个SQL语句执行失败,已经执行成功的SQL语句也必须撤销,数据库的状态退回到执行事务前的状态。

一致性(Consistency)

  • 一致性是指在事务处理时,无论执行成功还是失败,都要保证数据库系统处于一致的状态,保证数据库系统从不返回到一个未处理的事务中。
  • MySQL中的一致性主要由日志机制实现的,通过日志记录数据库的所有变化,为事务恢复提供了跟踪记录。

隔离性(Isolation)

  • 隔离性是指当一个事务在执行时,不会受到其他事务的影响。保证了未完成事务的所有操作与数据库系统的隔离,直到事务完成为止,才能看到事务的执行结果。
  • 隔离性相关的技术有并发控制、可串行化、锁等。当多个用户并发访问数据库时,数据库为每一个用户开启的事务,不能被其他事务的操作数据所干扰,多个并发事务之间要相互隔离。

持久性(Durability)

  • 持久性是指事务一旦提交,其对数据库的修改就是永久性的。
  • 事务的持久性不能做到百分百的持久,只能从事务本身的角度来保证永久性,而一些外部原因导致数据库发生故障,如硬盘损坏,那么所有提交的数据可能都会丢失。

attention:

  • MySQL中的事务不允许嵌套的,若在执行START TRANSACTION语句前上一个事务还未提交,会隐式地执行提交操作
  • 事务处理主要是针对数据表中数据的处理,不包括创建或删除数据库、数据表,修改表结构等操作,而且执行这类操作时会隐式地提交事务
    例如:在使用START TRANSACTION开启事务后执行ALTER TABLE修改表结构,会隐式提交事务。
  • 当执行COMMIT或ROLLBACK后,当前事务就会自动结束.
    ROLLBACK只能针对未提交的事务回滚,已提交的事务无法回滚
  • MySQL 5.7默认的存储引擎为InnoDB,该存储引擎支持事务,而另一个常见的存储引擎MyISAM不支持事务。
    对于MyISAM存储引擎的数据表,无论事务是否提交,对数据的操作都会立即生效,不能回滚。

事务的自动提交
MySQL默认是自动提交模式。
如果没有显式开启事务(START TRANSACTION),每一条SQL语句都会自动提交(COMMIT)。

查看事务是否自动提交

mysql> SELECT @@autocommit;
+--------------+
| @@autocommit |
+--------------+
|            1 |
+--------------+
1 row in set (0.00 sec)

1:开启自动提交
0:关闭自动提交

关闭自动提交:

SET AUTOCOMMIT = 0;

关闭后:用户需要手动执行提交(COMMIT)操作。
若直接终止MySQL会话,MySQL会自动进行回滚。

9.1.3 事务的保存点

在回滚事务时,若希望只撤销一部分,可以用保存点来实现。

SAVEPOINT 保存点名;

在设置保存点后,可以将事务回滚到指定保存点。

ROLLBACK TO SAVEPOINT 保存点名;

若不再需要一个保存点,使用如下语句删除:

RELEASE SAVEPOINT 保存点名;

实例演示:

mysql> SELECT name, money FROM sh_user WHERE name = 'a';
+------+--------+
| name | money  |
+------+--------+
| a    | 900.00 |
+------+--------+
1 row in set (0.00 sec)
# ① 开启事务
mysql> START TRANSACTION;
# ② a扣除100元
mysql> UPDATE sh_user SET money = money - 100 WHERE name = 'a';
# ③ 创建保存点s1
mysql> SAVEPOINT s1;
# ④ a再扣除50元
mysql> UPDATE sh_user SET money = money - 50 WHERE name = 'a';

保存点s1的时候a的money为800元
后续又更新了一次后,a的money为750元

# ① 回滚到保存点s1
mysql> ROLLBACK TO SAVEPOINT s1;
# ② 查询a的金额
mysql> SELECT name, money FROM sh_user WHERE name = 'a';
+------+--------+
| name | money  |
+------+--------+
| a    | 800.00 |
+------+--------+
1 row in set (0.00 sec)

此时恢复为800元,但事务仍未提交

回滚之后呢,就会回复到原来的900元

# ① 回滚事务
mysql> ROLLBACK;
# ② 查看a的金额
mysql> SELECT name, money FROM sh_user WHERE name = 'a';
+------+--------+
| name | money  |
+------+--------+
| a    | 900.00 |
+------+--------+
1 row in set (0.00 sec)

一个事务中可以创建多个保存点,在提交事务后,事务中的保存点就会被删除。


在回滚到某个保存点后,在该保存点之后创建过的保存点也会消失。

9.2 事务隔离级别

9.2.1 查看隔离级别

事务隔离级的意义:

数据库是一个多用户的共享资源,MySQL允许多线程并发访问,用户可以通过不同的线程执行不同的事务。

为了保证这些事务之间不受影响,对事务设置隔离级是十分必要的。

# ① 查看全局隔离级
SELECT @@global.transaction_isolation;
# ② 查看当前会话中的隔离级
SELECT @@session.transaction_isolation;
# ③ 查看下一个事务的隔离级
SELECT @@transaction_isolation;

全局的隔离级:影响所有连接MySQL用户。
当前会话隔离级:只影响当前正在登录MySQL服务器的用户。(不会影响其他用户)
下一个事务的隔离级:仅对当前用户的下一个事务操作有影响。

默认情况下3种方式的结果都是REPEATABLE READ

mysql> SELECT @@transaction_isolation;
+-------------------------+
| @@transaction_isolation |
+-------------------------+
| REPEATABLE-READ         |
+-------------------------+
1 row in set (0.00 sec)

MySQL中事务的隔离级别:

  • REPEATABLE READ:可重复读
  • READ UNCOMMITTED:读取未提交
  • READ COMMITTED:读取提交
  • SERIALIZABLE:可串行化

9.2.2 修改隔离级别

设置事务的隔离级别:

SET [SESSION | GLOBAL] TRANSACTION ISOLATION LEVEL 参数值

SESSION:当前会话
GLOBAL:全局
直接省略:下一个事务的隔离级
TRANSACTION:事务
ISOLATION:隔离
LEVEL:级别

使用演示:修改为READ UNCOMMITTED

# ① 修改事务隔离级别
mysql> SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
Query OK, 0 rows affected (0.00 sec)
# ② 查看是否修改成功
mysql> SELECT @@session.transaction_isolation;
+---------------------------------+
| @@session.transaction_isolation |
+---------------------------------+
| READ-UNCOMMITTED                |
+---------------------------------+
1 row in set (0.00 sec)

事务的默认访问模式为 READ WRITE(读/写模式),
事务可以执行:读(查询)或 写(更改、插入、删除等)操作。

若开发需要,可以将事务的访问模式设置为READ ONLY(只读模式),禁止对表进行更改。

# ① 设置只读事务
SET [SESSION | GLOBAL] TRANSACTION READ ONLY
# ② 恢复成读写事务
SET [SESSION | GLOBAL] TRANSACTION READ WRITE

9.2.3 MySQL的4中隔离级别

READ UNCOMMITTED(读取未提交):
事务中最低的级别,可以读取到其他事务中未提交的数据。
也称为脏读(Dirty Read):一个事务读取了另外一个事务未提交的数据。

情景模拟:a给b转账100元购买商品。
a开启事务后转账,但不提交事务,通知b来查询,
如果b的隔离级别较低,就会读取到a的事务中未提交的数据,
发现a确实给自己转了100元,就给a发货。
等b发货成功后,a将事务回滚,b就会受到损失。

脏读效果演示:
在这里插入图片描述
设置客户端B的隔离级别,允许脏读

# 客户端B
mysql> SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;

在客户端B中查询b当前的金额

# 客户端B
mysql> SELECT name, money FROM sh_user WHERE name = 'b';
+------+---------+
| name | money   |
+------+---------+
| b    | 1100.00 |
+------+---------+
1 rows in set (0.00 sec)

客户端A开启事务并转账

# 客户端A
mysql> START TRANSACTION;
mysql> UPDATE sh_user SET money = money - 100 WHERE name = 'a';
mysql> UPDATE sh_user SET money = money + 100 WHERE name = 'b';

客户端A未提交事务,客户端B查询金额

# 客户端B
mysql> SELECT name, money FROM sh_user WHERE name = 'Bill';
+------+---------+
| name | money   |
+------+---------+
| Bill | 1200.00 |
+------+---------+
1 row in set (0.00 sec)

此时,b读取到了未提交的数据,显示自己是1200元,其实还是1100元,因为a给他转的100元还没提交呢

提高隔离级别,解决脏读问题

mysql> SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
mysql> SELECT name, money FROM sh_user WHERE name = 'Bill';
+------+---------+
| name | money   |
+------+---------+
| Bill | 1100.00 |
+------+---------+
1 row in set (0.00 sec)

实验结束,回滚客户端A的事务

# 客户端A
mysql> ROLLBACK;

READ COMMITTED(读取提交)

大多数DBMS(如SQL ServerOracle)的默认隔离级,但不包括MySQL
只能读取其他事务已经提交的数据,避免了脏读问题。

但是会出现不可重复读(NON-REPEATABLE READ)问题。

不可重复读:一个事务中多次查询的结果不一致,原因是查询的过程中数据发生了改变。

情景模拟:在网站后台统计所有用户的总金额。
第1次查询a有900元,
第2次查询a有800元。

问题:在同一个事务中,同样的两次查询结果不同,
原因:第2次查询前a取出了100元。

设置隔离级别,查询a的金额

# 客户端B
mysql> SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
mysql> START TRANSACTION;
mysql> SELECT name, money FROM sh_user WHERE name = 'a';
+------+--------+
| name | money  |
+------+--------+
| a    | 900.00 |
+------+--------+
1 row in set (0.00 sec)

a取出100元,再次查询a金额

# 客户端A
mysql> UPDATE sh_user SET money = money - 100 WHERE name = 'a';
# 客户端B
mysql> SELECT name, money FROM sh_user WHERE name = 'a';
+------+--------+
| name | money  |
+------+--------+
| a    | 800.00 |
+------+--------+
1 row in set (0.00 sec)

两次查询结果不一致

实验结束,提交事务

# 客户端B
mysql> COMMIT;

更改隔离级别,避免客户端B的不可重复读

# 客户端B
mysql> SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ;
mysql> START TRANSACTION;
mysql> SELECT name, money FROM sh_user WHERE name = 'a';
+------+--------+
| name | money  |
+------+--------+
| a    | 800.00 |
+------+--------+
1 row in set (0.00 sec)

更改Alex的金额,客户端B两次查询的结果一致

# 客户端A
mysql> UPDATE sh_user SET money = money + 100 WHERE name = 'a';
# 客户端B
mysql> SELECT name, money FROM sh_user WHERE name = 'a';
+------+--------+
| name | money  |
+------+--------+
| a    | 800.00 |
+------+--------+
1 row in set (0.00 sec)

实验结束,提交事务

# 客户端B
mysql> COMMIT;

REPEATABLE READ(可重复读)

MySQL的默认事务隔离级,它解决了脏读和不可重复读的问题,
确保了同一事务的多个实例在并发读取数据时,会看到同样的结果。

该级别理论上会出现幻读(PHANTOM READ)问题。
幻读又被称为虚读,是指在一个事务内两次查询中数据条数不一致,
如:其他事务做了插入记录的操作,导致记录数有所增加。
不过,MySQL的InnoDB存储引擎已经解决了幻读问题。

情景模拟:在网站后台统计所有用户的总金额时,
当前只有两个用户,总金额为2000元,
此时新增一个用户,并且存入1000元。

再次统计会发现总金额变为3000元,造成了幻读的情况

通过READ COMMITTED演示幻读问题

# 客户端B
mysql> SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
mysql> START TRANSACTION;
mysql> SELECT SUM(money) FROM sh_user;
+------------+
| SUM(money) |
+------------+
|    2000.00 |
+------------+
1 row in set (0.00 sec)

客户端A新增一个用户,客户端B就会幻读

# 客户端A
mysql> INSERT INTO sh_user (id, name, money) VALUES 
    -> (3, 'Tom', 1000);
# 客户端B
mysql> SELECT SUM(money) FROM sh_user;
+------------+
| SUM(money) |
+------------+
|    3000.00 |
+------------+
1 row in set (0.00 sec)

实验结束,提交事务

# 客户端B
mysql> COMMIT;

利用REPEATABLE READ避免幻读

# 客户端B
mysql> SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ;
mysql> START TRANSACTION;
mysql> SELECT SUM(money) FROM sh_user;
+------------+
| SUM(money) |
+------------+
|    3000.00 |
+------------+
1 row in set (0.00 sec)

客户端A新增一个用户,客户端B没有幻读

# 客户端A
mysql> INSERT INTO sh_user (id, name, money) VALUES 
    -> (4, 'd', 1000);
# 客户端B
mysql> SELECT SUM(money) FROM sh_user;
+------------+
| SUM(money) |
+------------+
|    3000.00 |
+------------+
1 row in set (0.00 sec)

实验结束,提交事务

# 客户端B
mysql> COMMIT;

SERIALIZABLE(可串行化)
隔离级的最高级别,它在每个读的数据行上加锁,使之不会发生冲突,
解决了脏读、不可重复读和幻读的问题。
由于加锁可能导致超时(Timeout)和锁竞争(Lock Contention)现象,
性能是4种隔离级中最低的。
除非为了数据的稳定性,需要强制减少并发的情况时,才会选择此种隔离级。

演示SERIALIZABLE:

# 客户端B
mysql> SET SESSION TRANSACTION ISOLATION LEVEL SERIALIZABLE;
mysql> START TRANSACTION;

客户端B开启事务后查询数据,此时会锁表

# 客户端B
mysql> SELECT name, money FROM sh_user WHERE name = 'a';
+------+--------+
| name | money  |
+------+--------+
| a   | 900.00 |
+------+--------+
1 row in set (0.00 sec)

客户端A无法立即写入,等待表解锁

# 客户端A
mysql> UPDATE sh_user SET money = money + 100 WHERE name = 'Alex';
(此时光标在不停闪烁,进入等待状态)

客户端B提交事务后,客户端A才可以写入成功

# 客户端B
mysql> COMMIT;
# 客户端A
Query OK, 1 row affected (3.99 sec)
Rows matched: 1  Changed: 1  Warnings: 0

若客户端B一直未提交事务,客户端A会等待到超时后报错

# 客户端A
ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting
transaction

锁等待超时默认为50秒,可进行更改

mysql> SELECT @@innodb_lock_wait_timeout;
+----------------------------+
| @@innodb_lock_wait_timeout |
+----------------------------+
|                         50 |
+----------------------------+
1 row in set (0.00 sec)

猜你喜欢

转载自blog.csdn.net/PoGeN1/article/details/122993631