Skip to content

mysql事务

什么是事务呢?

在MySQL中,**事务(Transaction)**是一系列数据库操作,这些操作要么全部执行,要么全部不执行,从而保证数据库的完整性和一致性。事务是一个不可分割的工作单位,它包含了对数据库的一组更改。

事务四大特性(ACID)

  • 原子性(Atomicity)

    事务是不可分割的最小操作单元,要么全部成功,要么全部失败。转账交易场景,要么A账户扣款成功并且B帐户加款成功,要么A帐号不扣款并且B账户不加款。

  • 一致性(Consistency)

    事务完成时,必须使所有数据都保持一致状态。转账交易场景,无论事务提交还是回滚,事务结束后A账户+B账户的总余额等于交易开始前的总余额。

  • 隔离性(Isolation)

    在并发环境中,当不同的事务同时操纵相同的数据时,每个事务都有各自的隔离空间,一个事务的内部操作对其他事务是不可见的。

  • 持久性(Durability)

    事务一但提交或者回滚,它对数据库中的数据的改变就是永久的。

事务异常回滚

准备测试环境数据

sql
CREATE DATABASE IF NOT EXISTS testdb CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;

use testdb;

create table if not exists t_balance(
    id bigint primary key auto_increment,
    name varchar(64) default '' not null,
    amount int default 0 not null
) ENGINE=INNODB DEFAULT CHARSET=utf8mb4 collate=utf8mb4_general_ci;

insert into t_balance(name,amount) values ('zhangsan', 10000),('lisi', 10000);

测试事务异常回滚

shell
mysql> start transaction;
Query OK, 0 rows affected (0.00 sec)

mysql> update t_balance set amount=amount-1000 where name='zhangsan';
Query OK, 1 row affected (0.00 sec)
Rows matched: 1  Changed: 1  Warnings: 0

mysql> select * from t_balance;
+----+----------+--------+
| id | name     | amount |
+----+----------+--------+
|  1 | zhangsan |   9000 |
|  2 | lisi     |  10000 |
+----+----------+--------+
2 rows in set (0.00 sec)

# 事务回滚
mysql> rollback;
Query OK, 0 rows affected (0.01 sec)

# 事务回滚后余额恢复原值
mysql> select * from t_balance;
+----+----------+--------+
| id | name     | amount |
+----+----------+--------+
|  1 | zhangsan |  10000 |
|  2 | lisi     |  10000 |
+----+----------+--------+
2 rows in set (0.00 sec)

事务的隔离级别

为何需要学习事务隔离级别呢?

todo 没有找到开发中实际遇到的场景解析这个问题,但是可能能够从如下角度解析这个问题:

  • 不同的事务级别对性能有不同的影响
  • 在没有锁的情况下可能可以使用事务隔离级别解决问题

mysql并发事务有哪些问题存在呢?

参考链接

MySQL并发事务中可能存在的问题主要有以下几个方面:

  1. 脏读(Dirty Read):
    • 定义:一个事务读取到另一个事务尚未提交的数据。
    • 示例:事务B正在更新某个字段但尚未提交,此时事务A读取这个字段的值。如果事务B发生错误并执行回滚操作,那么事务A读取到的就是脏数据(即最终不会生效的数据)。
    • 解决方法:提高事务的隔离级别,例如使用“Read committed”或更高的隔离级别。
    • 原理:mysql通过mvcc机制解决此问题。
  2. 不可重复读(Non-repeatable Read):
    • 定义:一个事务内多次读取同一数据但读取结果不同,因为读取到另一个事务已提交的数据。
    • 示例:事务A两次读取同一数据,在两次读取之间,事务B修改了该数据并提交。因此,事务A两次读取到的数据不同。
    • 解决方法:同样可以通过提高事务的隔离级别来避免,如使用“Repeatable Read”或更高的隔离级别。MySQL的默认隔离级别就是“Repeatable Read”。MySQL底层使用行锁解决此问题。
    • 原理:mysql通过mvcc机制解决此问题。
  3. 幻读(Phantom Read):
    • 定义:一个事务按照某个条件读取数据时,没有数据符合该条件,但之后另一个事务插入数据,使得第一个事务再次按照该条件查询时出现了新的数据,就像出现了幻影一样。
    • 示例:事务A查询满足某条件的记录,发现没有记录。此时,事务B插入了一条满足该条件的记录并提交。然后事务A再次查询,发现有一条记录满足条件。
    • 解决方法:使用最高级别的隔离级别“Serializable”,它会对每一行数据加锁,从而解决幻读问题。但需要注意的是,这种级别的并发性能较低。
    • 原理:MVCC通过为每个事务提供一个Read View来确保事务在读取数据时的一致性和隔离性,从而解决了幻读问题。

有哪些事务隔离级别呢?

Mysql-详解脏读、不可重复读、幻读

提示:读未提交性能最好,可串行化性能最差。

事务隔离级别用于解决并发事务所引发的问题:

  • 读未提交(read uncommitted):脏读存在、不可重复读存在、幻读存在。
  • 读已提交(read committed):不可重复读存在、幻读存在。
  • 可重复读(repeatable read 默认级别):幻读存在。
  • 串行化(serializable):所有都不存在。

演示各个事务隔离级别存在的问题

准备测试环境

sql
CREATE DATABASE IF NOT EXISTS testdb CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;

use testdb;

create table if not exists t_balance(
    id bigint primary key auto_increment,
    name varchar(64) default '' not null,
    amount int default 0 not null
) ENGINE=INNODB DEFAULT CHARSET=utf8mb4 collate=utf8mb4_general_ci;

insert into t_balance values(1,'zhangsan',10000);

读未提交隔离级别

bash
# session1开启事务
mysql> start transaction;
Query OK, 0 rows affected (0.00 sec)
# session1修改金额
mysql> update t_balance set amount=amount-1000 where name='zhangsan';
Query OK, 1 row affected (0.00 sec)
Rows matched: 1  Changed: 1  Warnings: 0

# session2查看当前事务隔离级别
mysql> select @@transaction_isolation;
+-------------------------+
| @@transaction_isolation |
+-------------------------+
| REPEATABLE-READ         |
+-------------------------+
1 row in set (0.00 sec)

# session2设置当前事务隔离级别为read uncommitted
mysql> set session transaction isolation level read uncommitted;
Query OK, 0 rows affected (0.00 sec)

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

# session2开启事务
mysql> start transaction;
Query OK, 0 rows affected (0.00 sec)
# session2能够读取session1未提交的修改
mysql> select * from t_balance;
+----+----------+--------+
| id | name     | amount |
+----+----------+--------+
|  1 | zhangsan |   9000 |
+----+----------+--------+
1 row in set (0.00 sec)

# session1 回滚修改
mysql> rollback;
Query OK, 0 rows affected (0.00 sec)

# session2查询到session1回滚后的数据
mysql> select * from t_balance;
+----+----------+--------+
| id | name     | amount |
+----+----------+--------+
|  1 | zhangsan |  10000 |
+----+----------+--------+
1 row in set (0.00 sec)

# session2提交事务
mysql> commit;
Query OK, 0 rows affected (0.00 sec)

读已提交隔离级别

bash
# session1设置当前事务隔离级别为read committed
mysql> set session transaction isolation level read committed;
Query OK, 0 rows affected (0.00 sec)
# session1开启事务
mysql> start transaction;
Query OK, 0 rows affected (0.00 sec)
# session1在session2操作前查询数据
mysql> select * from t_balance;
+----+----------+--------+
| id | name     | amount |
+----+----------+--------+
|  1 | zhangsan |  10000 |
+----+----------+--------+
1 row in set (0.00 sec)

# session2开启事务
mysql> start transaction;
Query OK, 0 rows affected (0.00 sec)
# session2修改数据
mysql> update t_balance set amount=amount-1000 where name='zhangsan';
Query OK, 1 row affected (0.00 sec)
Rows matched: 1  Changed: 1  Warnings: 0
# session2提交修改
mysql> commit;
Query OK, 0 rows affected (0.01 sec)

# session1再次查询数据,发现金额被修改为9000
mysql> select * from t_balance;
+----+----------+--------+
| id | name     | amount |
+----+----------+--------+
|  1 | zhangsan |   9000 |
+----+----------+--------+
1 row in set (0.00 sec)
# session1提交事务
mysql> commit;
Query OK, 0 rows affected (0.00 sec)

可重复读隔离级别

bash
# session1设置当前事务隔离级别为repeatable read
mysql> set session transaction isolation level repeatable read;
Query OK, 0 rows affected (0.00 sec)
# session1开启事务
mysql> start transaction;
Query OK, 0 rows affected (0.00 sec)

# session2开启事务
mysql> start transaction;
Query OK, 0 rows affected (0.00 sec)
# session2插入id=2的记录
mysql> insert into t_balance values(2,'王五',10000);
Query OK, 1 row affected (0.01 sec)
# session2提交事务
mysql> commit;
Query OK, 0 rows affected (0.00 sec)

# session1查询没有id=2的记录,因为mvcc机制
mysql> select * from t_balance;
Empty set (0.00 sec)
# 但是session1插入id=1的记录提示主键冲突错误,这就是换读现象了
mysql> insert into t_balance values(2,'王五',10000);
ERROR 1062 (23000): Duplicate entry '2' for key 't_balance.PRIMARY'

# session1提交事务
mysql> commit;
Query OK, 0 rows affected (0.00 sec)

# session1再次查询却能够看到id=2的数据
mysql> select * from t_balance;
+----+--------+--------+
| id | name   | amount |
+----+--------+--------+
|  2 | 王五   |  10000 |
+----+--------+--------+
1 row in set (0.00 sec)

串行化隔离级别

bash
# session1设置当前事务隔离级别为serializable
mysql> set session transaction isolation level serializable;
Query OK, 0 rows affected (0.00 sec)
# session1开启事务
mysql> start transaction;
Query OK, 0 rows affected (0.00 sec)
mysql> select * from t_balance;
Empty set (0.00 sec)

# session2开启事务
mysql> start transaction;
Query OK, 0 rows affected (0.00 sec)
# session2执行下面insert语句一直等待状态
mysql> insert into t_balance values(3,'lisi',10000);

# session1提交事务后,session2的insert语句才能继续执行
mysql> commit;
Query OK, 0 rows affected (0.00 sec)

# session2提交事务
mysql> commit;
Query OK, 0 rows affected (0.00 sec)