Mysql:事务

作者: 程序员小杰 | 来源:发表于2020-06-14 16:12 被阅读0次

    一、事务简介

    1、在 MySQL 中只有使用了 Innodb 数据库引擎的数据库或表才支持事务。
    2、事务处理可以用来维护数据库的完整性,保证成批的 SQL 语句要么全部执行,要么全部不执行。
    3、事务只和DML语句有关,或者说DML语句才有事务。DML:Data Manipulation Language的缩写,意思是数据操纵语言,也就是INSERT、UPDATE、DELETE。

    二、事务四大特征(ACID)

    一般来说,事务是必须满足4个条件(ACID)::原子性(Atomicity,或称不可分割性)、一致性(Consistency)、隔离性(Isolation,又称独立性)、持久性(Durability)。

    原子性:一个事务(transaction)中的所有操作,要么全部完成,要么全部不完成,不会结束在中间某个环节。事务在执行过程中发生错误,会被回滚(Rollback)到事务开始前的状态,就像这个事务从来没有执行过一样。

    一致性:在事务开始之前和事务结束以后,数据库的完整性没有被破坏。这表示写入的资料必须完全符合所有的预设规则,这包含资料的精确度、串联性以及后续数据库可以自发性地完成预定的工作。

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

    持久性:事务处理结束后,对数据的修改就是永久的,即便系统故障也不会丢失。

    在 MySQL 命令行的默认设置下,事务都是自动提交的,即执行 SQL 语句后就会马上执行 COMMIT 操作。因此要显式地开启一个事务,须使用命令 BEGIN 或 START TRANSACTION,或者执行命令 SET AUTOCOMMIT=0,用来禁止使用当前会话的自动提交。

    三、事务控制语句:

    事务其实可以划分为两大类:隐式的事务和显示的事务

    隐式的事务很简单,比如我们的insert、delete、update这些语句都是隐式的事务。

    显示的事务指的是带有很明显的开始和结束的标记
    BEGIN 或 START TRANSACTION 显式地开启一个事务;

    COMMIT 会提交事务,并使已对数据库进行的所有修改成为永久性的;

    ROLLBACK 回滚会结束用户的事务,并撤销正在进行的所有未提交的修改;

    SAVEPOINT 保存点名称,SAVEPOINT允许在事务中创建一个保存点,一个事务中可以有多个 SAVEPOINT;

    RELEASE SAVEPOINT identifier 删除一个事务的保存点,当没有指定的保存点时,执行该语句会抛出一个异常;

    ROLLBACK TO 保存点名称 把事务回滚到标记点;

    set [ global | session ] transaction isolation level 用来设置事务的隔离级别。InnoDB 存储引擎提供事务的隔离级别有READ UNCOMMITTED(读未提交)、READ COMMITTED(读已提交)、REPEATABLE READ (可重复读)和 SERIALIZABLE(串行)。
    如果选择global,意思是此语句将应用于之后的所有session,而当前已经存在的session不受影响。

    如果选择session,意思是此语句将应用于当前session内之后的所有事务。

    创建表

    CREATE TABLE `salary`  (
      `id` int(11) NOT NULL,
      `name` varchar(25) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL,
      `money` decimal(11, 2) DEFAULT NULL,
      PRIMARY KEY (`id`) USING BTREE
    ) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
    

    查看当前事务是否开启:

    show variables like '%autocommit%';
    
    image.png

    值为 0 和值为 OFF:关闭事务自动提交。
    值为 1 和值为 ON:开启事务自动提交。
    我们这就不关闭了,因为都是使用begin显式的开启事务。
    当然也可以修改事务是否开启

    set autocommit = 1 ;  0或者1
    

    查看当前事务级别:

    1.查看当前会话隔离级别
    select @@tx_isolation;
    2.查看系统当前隔离级别
    select @@global.tx_isolation;
    
    image.png

    当前为可重复读。也是mysql的默认级别。

    commit示例:

    mysql> begin;  #显示的开启事务
    Query OK, 0 rows affected (0.00 sec)
    
    mysql> INSERT INTO salary VALUES(1,'张三',2000);
    Query OK, 1 row affected (0.00 sec)
    mysql> select * from salary;  //在当前事务是可以查询数据的
    +----+------+---------+
    | id | name | money   |
    +----+------+---------+
    |  1 | 张三 | 2000.00 |
    +----+------+---------+
    1 row in set (0.02 sec)
    

    这时重新打开一个 cmd 窗口,查看 salary 数据表

    mysql> select * from salary;
    Empty set
    

    可以看到数据是空的。
    下面在之前的窗口中使用 COMMIT 语句提交事务,如下所示:

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

    然后再次在第2个窗口执行查询命令,之后都叫2窗口了。

    mysql> select * from salary;
    +----+------+---------+
    | id | name | money   |
    +----+------+---------+
    |  1 | 张三 | 2000.00 |
    +----+------+---------+
    1 row in set (0.01 sec)
    

    在本例中,关闭自动提交后,该位置会作为一个事务起点,直到执行 COMMIT 语句和 ROLLBACK 语句后,该事务才结束。

    ROLLBACK示例:

    在1窗口继续执行insert语句:

    mysql> begin;
    Query OK, 0 rows affected (0.00 sec)
    
    mysql> INSERT INTO salary VALUES(2,'李四',5000);
    Query OK, 1 row affected (0.00 sec)
    mysql> select * from salary;
    +----+------+---------+
    | id | name | money   |
    +----+------+---------+
    |  1 | 张三 | 2000.00 |
    |  2 | 李四 | 5000.00 |
    +----+------+---------+
    2 rows in set (0.03 sec)
    

    2窗口:

    mysql> select * from salary;
    +----+------+---------+
    | id | name | money   |
    +----+------+---------+
    |  1 | 张三 | 2000.00 |
    +----+------+---------+
    1 row in set (0.02 sec)
    
    

    数据还是一条。
    1窗口:

    mysql> rollback;  #执行回滚
    Query OK, 0 rows affected (0.00 sec)
    
    mysql> select * from salary;
    +----+------+---------+
    | id | name | money   |
    +----+------+---------+
    |  1 | 张三 | 2000.00 |
    +----+------+---------+
    1 row in set (0.02 sec)
    

    数据变为了一条。
    当我们在一个事务中执行了多次insert或者update语句之后,然后想执行回滚,发现它回到了最开始的数据,那能不能指定回到那个事务呢?答案是可以的,事务提供了事务保存点。

    SAVEPOINT示例:

    下面演示将向表salary中连续插入3条数据,在插入第2条数据的后面定义一个保存点,最后看看能否回滚到此保存点。
    1窗口:

    mysql> begin;
    Query OK, 0 rows affected (0.00 sec)
    
    mysql> INSERT INTO salary VALUES(3,'王五',5000);
    Query OK, 1 row affected (0.00 sec)
    
    mysql> INSERT INTO salary VALUES(4,'赵六',5000);
    Query OK, 1 row affected (0.00 sec)
    
    mysql> select * from salary;
    +----+------+---------+
    | id | name | money   |
    +----+------+---------+
    |  1 | 张三 | 2000.00 |
    |  3 | 王五 | 5000.00 |
    |  4 | 赵六 | 5000.00 |
    +----+------+---------+
    3 rows in set (0.02 sec)
    

    定义保存点

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

    保存第三条数据

    mysql> INSERT INTO salary VALUES(5,'田七',6000);
    Query OK, 1 row affected (0.00 sec)
    
    mysql> select * from salary;
    +----+------+---------+
    | id | name | money   |
    +----+------+---------+
    |  1 | 张三 | 2000.00 |
    |  3 | 王五 | 5000.00 |
    |  4 | 赵六 | 5000.00 |
    |  5 | 田七 | 6000.00 |
    +----+------+---------+
    4 rows in set (0.03 sec)
    

    回滚到保存点s4

    mysql> rollback to savepoint s4;  #回滚到保存点
    Query OK, 0 rows affected (0.00 sec)
    
    mysql> select * from salary;
    +----+------+---------+
    | id | name | money   |
    +----+------+---------+
    |  1 | 张三 | 2000.00 |
    |  3 | 王五 | 5000.00 |
    |  4 | 赵六 | 5000.00 |
    +----+------+---------+
    3 rows in set (0.03 sec)
    

    可以看到在保存点之后的数据就消失了,保存点之前的数据就保存了。
    2窗口:

    mysql> select * from salary;
    +----+------+---------+
    | id | name | money   |
    +----+------+---------+
    |  1 | 张三 | 2000.00 |
    +----+------+---------+
    1 row in set (0.02 sec)
    

    在二窗口我们可以看到,竟然没有最新数据,这是为什么呢?因为事务回滚到指定保存点并不代表事务结束了,需要执行commit或者rollback.
    1窗口:

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

    2窗口

    mysql> select * from salary;
    +----+------+---------+
    | id | name | money   |
    +----+------+---------+
    |  1 | 张三 | 2000.00 |
    |  3 | 王五 | 5000.00 |
    |  4 | 赵六 | 5000.00 |
    +----+------+---------+
    3 rows in set (0.03 sec)
    

    然后如果保存点不想要了,可以撤销保存点

    release savepoint 保存点名  --撤销保存点
    

    四、事务的隔离级别

    隔离性有隔离级别(4个)
    读未提交:read uncommitted
    读已提交:read committed
    可重复读:repeatable read
    串行化:serializable


    image.png

    4.1 读未提交(read uncommitted)

    事务A和事务B,事物A未提交的数据,事物B可以读取到
    这里读取到的数据叫做“脏数据”
    这种隔离级别最低,这种级别一般是在理论上存在,数据库隔离级别一般都高于该级别。
    修改全局隔离级别为读未提交

    mysql> select @@global.tx_isolation;  #查看系统当前隔离级别
    +-----------------------+
    | @@global.tx_isolation |
    +-----------------------+
    | REPEATABLE-READ       |
    +-----------------------+
    1 row in set (0.02 sec)
    mysql> set global transaction isolation level read uncommitted;  #修改事务级别为读未提交
    Query OK, 0 rows affected (0.00 sec)
    mysql> commit
        -> ;
    Query OK, 0 rows affected (0.02 sec)
    
    mysql> select @@global.tx_isolation;
    +-----------------------+
    | @@global.tx_isolation |
    +-----------------------+
    | READ-UNCOMMITTED      |
    +-----------------------+
    1 row in set (0.02 sec)
    

    不知道为什么执行SELECT @@session.tx_isolation;查看session的事务等级还是REPEATABLE-READ,并没有变为READ-UNCOMMITTED,只有当我关闭navicat工具之后,重新打开命令行查询才会变为READ-UNCOMMITTED。
    示例:
    窗口1

    mysql> begin;  #开启事务
    Query OK, 0 rows affected (0.00 sec)
    mysql> select * from salary;
    +----+------+---------+
    | id | name | money   |
    +----+------+---------+
    |  1 | 张三 | 2000.00 |
    |  3 | 王五 | 5000.00 |
    |  4 | 赵六 | 5000.00 |
    +----+------+---------+
    3 rows in set (0.02 sec)
    
    mysql> update salary set money = money + 5000 where name = '张三';   #张三的薪资增加了5000
    Query OK, 1 row affected (0.00 sec)
    Rows matched: 1  Changed: 1  Warnings: 0
    
    mysql> select * from salary;
    +----+------+---------+
    | id | name | money   |
    +----+------+---------+
    |  1 | 张三 | 7000.00 |
    |  3 | 王五 | 5000.00 |
    |  4 | 赵六 | 5000.00 |
    +----+------+---------+
    3 rows in set (0.03 sec)
    

    窗口2

    mysql> select * from salary;
    +----+------+---------+
    | id | name | money   |
    +----+------+---------+
    |  1 | 张三 | 7000.00 |
    |  3 | 王五 | 5000.00 |
    |  4 | 赵六 | 5000.00 |
    +----+------+---------+
    3 rows in set (0.02 sec)
    

    可以看到窗口1都没有进行提交,窗口2就已经查询出来了。

    窗口1进行回滚

    mysql> rollback;
    Query OK, 0 rows affected (0.00 sec)
    mysql> select * from salary;
    +----+------+---------+
    | id | name | money   |
    +----+------+---------+
    |  1 | 张三 | 2000.00 |
    |  3 | 王五 | 5000.00 |
    |  4 | 赵六 | 5000.00 |
    +----+------+---------+
    3 rows in set (0.03 sec)
    

    窗口2

    mysql> select * from salary;
    +----+------+---------+
    | id | name | money   |
    +----+------+---------+
    |  1 | 张三 | 2000.00 |
    |  3 | 王五 | 5000.00 |
    |  4 | 赵六 | 5000.00 |
    +----+------+---------+
    3 rows in set (0.03 sec)
    

    出现上述情况,即我们所说的脏读 ,两个并发的事务,“事务A:领导给张三发工资”、“事务B:张三查询工资账户”,事务B读取了事务A尚未提交的数据。

    4.2 读已提交(read committed)

    事务A和事务B,事务A已经提交的数据,事务B才能读取到
    这种隔离级别高于读未提交,这种级别可以避免脏读
    但是这种隔离级别会导致“不可重复读”。
    示例:

    mysql> set global transaction isolation level read committed;  #隔离级别修改为读已提交
    mysql> SELECT @@global.tx_isolation;
    
    +-----------------------+
    | @@global.tx_isolation |
    +-----------------------+
    | READ-COMMITTED        |
    +-----------------------+
    1 row in set (0.03 sec)
    mysql> select * from salary;
    +----+------+---------+
    | id | name | money   |
    +----+------+---------+
    |  1 | 张三 | 2000.00 |
    |  3 | 王五 | 5000.00 |
    |  4 | 赵六 | 5000.00 |
    +----+------+---------+
    3 rows in set (0.03 sec)
    

    窗口1显示开启事务

    mysql> begin;
    Query OK, 0 rows affected (0.00 sec)
    
    mysql> update salary set money = money + 5000 where name = '张三'; 
    Query OK, 1 row affected (0.00 sec)
    Rows matched: 1  Changed: 1  Warnings: 0
    
    mysql> select * from salary;
    +----+------+---------+
    | id | name | money   |
    +----+------+---------+
    |  1 | 张三 | 7000.00 |
    |  3 | 王五 | 5000.00 |
    |  4 | 赵六 | 5000.00 |
    +----+------+---------+
    3 rows in set (0.04 sec)
    

    窗口2

    mysql> SELECT * from salary;
    +----+------+---------+
    | id | name | money   |
    +----+------+---------+
    |  1 | 张三 | 2000.00 |
    |  3 | 王五 | 5000.00 |
    |  4 | 赵六 | 5000.00 |
    +----+------+---------+
    3 rows in set (0.03 sec)
    

    窗口2张三的余额还是2000,这已经避免了脏读。
    窗口1进行提交操作

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

    窗口2

    mysql> SELECT * from salary;
    +----+------+---------+
    | id | name | money   |
    +----+------+---------+
    |  1 | 张三 | 2000.00 |
    |  3 | 王五 | 5000.00 |
    |  4 | 赵六 | 5000.00 |
    +----+------+---------+
    3 rows in set (0.03 sec)
    
    mysql> SELECT * from salary;
    +----+------+---------+
    | id | name | money   |
    +----+------+---------+
    |  1 | 张三 | 7000.00 |
    |  3 | 王五 | 5000.00 |
    |  4 | 赵六 | 5000.00 |
    +----+------+---------+
    3 rows in set (0.03 sec)
    

    这样就会发现前后两次查询结果不一致。就出现了不可重复读。
    不可重复读是指在一个事务内,多次读同一数据。在这个事务还没有结束时,另外一个事务也访问该同一数据。那么,在第一个事务中的两次读数据之间,由于第二个事务的修改,那么第一个事务两次读到的的数据可能是不一样的。这样就发生了在一个事务内两次读到的数据是不一样的,因此称为是不可重复读。(即不能读到相同的数据内容)
    在读已提交(read committed)这种隔离级别下还会出现幻读,那也演示一下
    示例:
    窗口2查询id大于的数据为两条

    mysql> begin;
    Query OK, 0 rows affected (0.00 sec)
    
    mysql> select * from salary where id > 1;
    +----+------+----------+
    | id | name | money    |
    +----+------+----------+
    |  3 | 王五 | 5000.00  |
    |  4 | 赵六 | 20000.00 |
    +----+------+----------+
    2 rows in set (0.03 sec)
    

    窗口1

    mysql> begin;
    Query OK, 0 rows affected (0.00 sec)
    
    mysql> insert into salary values(2,'二狗',5000);
    Query OK, 1 row affected (0.00 sec)
    
    mysql> commit;
    Query OK, 0 rows affected (0.00 sec)
    

    窗口2

    mysql> select * from salary where id > 1;
    +----+------+----------+
    | id | name | money    |
    +----+------+----------+
    |  2 | 二狗 | 5000.00  |
    |  3 | 王五 | 5000.00  |
    |  4 | 赵六 | 20000.00 |
    +----+------+----------+
    3 rows in set (0.03 sec)
    

    前后两次的数据量不一致,这就产生了幻读。

    4.3可重复读(repeatable read)

    事务A和事务B,事务A提交之后的数据,事务B读取不到,事务B是可重复读取数据,这种隔离级别高于读已提交。可重复读是MySQL的默认级别。
    示例

    mysql> set global transaction isolation level repeatable read;   #修改事务隔离级别为可重复读
    mysql> select @@global.tx_isolation;
    +-----------------------+
    | @@global.tx_isolation |
    +-----------------------+
    | REPEATABLE-READ       |
    +-----------------------+
    1 row in set (0.02 sec)
    

    窗口1

    mysql> begin;
    Query OK, 0 rows affected (0.00 sec)
    
    mysql> select * from salary;
    +----+------+---------+
    | id | name | money   |
    +----+------+---------+
    |  1 | 张三 | 7000.00 |
    |  3 | 王五 | 5000.00 |
    |  4 | 赵六 | 5000.00 |
    +----+------+---------+
    3 rows in set (0.03 sec)
    
    mysql> update salary set money = money + 15000 where name = '赵六';
    Query OK, 1 row affected (0.00 sec)
    Rows matched: 1  Changed: 1  Warnings: 0
    

    窗口2

    mysql> begin;
    Query OK, 0 rows affected (0.00 sec)
    
    mysql> select * from salary;
    +----+------+---------+
    | id | name | money   |
    +----+------+---------+
    |  1 | 张三 | 7000.00 |
    |  3 | 王五 | 5000.00 |
    |  4 | 赵六 | 5000.00 |
    +----+------+---------+
    3 rows in set (0.02 sec)
    

    窗口1进行提交

    mysql> commit;
    Query OK, 0 rows affected (0.00 sec)
    mysql> select * from salary;
    +----+------+----------+
    | id | name | money    |
    +----+------+----------+
    |  1 | 张三 | 7000.00  |
    |  3 | 王五 | 5000.00  |
    |  4 | 赵六 | 20000.00 |
    +----+------+----------+
    3 rows in set (0.03 sec)
    

    窗口2

    mysql> select * from salary;
    +----+------+---------+
    | id | name | money   |
    +----+------+---------+
    |  1 | 张三 | 7000.00 |  
    |  3 | 王五 | 5000.00 |
    |  4 | 赵六 | 5000.00 |  #数据还是5000
    +----+------+---------+
    3 rows in set (0.03 sec)
    

    这样可以看出不可重复读就避免了。事务A进行提交了之后,事务B查询的数据还是第一次查询的数据。

    再开一个命令行窗口,也就代表新开了一个事务
    窗口3

    mysql> select * from salary;
    +----+------+----------+
    | id | name | money    |
    +----+------+----------+
    |  1 | 张三 | 7000.00  |
    |  3 | 王五 | 5000.00  |
    |  4 | 赵六 | 20000.00 |
    +----+------+----------+
    3 rows in set (0.01 sec)
    

    如果窗口1进行新增数据,然后进行提交,窗口2还是查询不出来的,在mysql的可重复读这种隔离级别下是避免了幻读的。

    4.4 Serializable 序列化

    Serializable 是最高的事务隔离级别,同时代价也花费最高,性能很低,一般很少使用,在该级别下,事务顺序执行,不仅可以避免脏读、不可重复读,还避免了幻像读。

    下一篇就介绍读已提交和可重复读是怎么实现的?

    相关文章

      网友评论

        本文标题:Mysql:事务

        本文链接:https://www.haomeiwen.com/subject/natgtktx.html