支持事务的数据库管理系统(transactional DBMS)就是要确保以上操作(整个“事务”)都能完成,或一起取消;否则就会出现200美元平白消失或出现的情况。
但在现实情况下,失败的风险很高。在一个数据库事务的执行过程中,有可能会遇上事务操作失败、数据库系统/操作系统出错,甚至是存储介质出错等情况。这便需要DBMS对一个执行失败的事务执行恢复操作,将其数据库状态恢复到一致状态(数据的一致性得到保证的状态)。为了实现将数据库状态恢复到一致状态的功能,DBMS通常需要维护事务日志以追踪事务中所有影响数据库数据的操作。



我们通过mysql数据库来模拟一下以上场景:
1.首先创建一张t_wallet钱包表,初始化一条balance为1000,id为1的数据。
mysql> create table t_wallet( -> id int primary key auto_increment comment '主键id', -> account varchar(10) comment '账户', -> balance decimal(6, 2) comment '账户余额' -> ) comment = '账户余额表'; Query OK, 0 rows affected (0.03 sec) mysql> insert into t_wallet(account, balance) values('小明', 1000); Query OK, 1 row affected (0.01 sec)
2.设置事务自动提交关闭,set autocommit = 0 | OFF;
mysql> show variables like 'autocommit'; +---------------+-------+ | Variable_name | Value | +---------------+-------+ | autocommit | ON | +---------------+-------+ 1 row in set, 1 warning (0.01 sec) mysql> set autocommit = OFF; Query OK, 0 rows affected (0.00 sec)
3.我们打开两个session会话,修改当前MySQL事务隔离级别为未提交读,分别开启事务A和事务B,事务A执行查询余额操作,获取到账户小明的当前balance=1000.
注意我本地用的是mysql 8,事务隔离级别属性是 transaction_isolation,mysql 8 之前是 tx_isolation.
事务A:
mysql> select @@transaction_isolation; +-------------------------+ | @@transaction_isolation | +-------------------------+ | REPEATABLE-READ | +-------------------------+ 1 row in set (0.00 sec) 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) mysql> select * from t_wallet where account = '小明'; +----+---------+---------+ | id | account | balance | +----+---------+---------+ | 1 | 小明 | 1000.00 | +----+---------+---------+ 1 row in set (0.00 sec)
5.开启事务B,先查询到小明账户有1000余额,然后事务B执行扣除1000余额操作,此时事务B还未提交。事务A查询余额,此时可以看到余额为0,事务A读取到了事务B还未提交的数据。
事务B:
mysql> begin; Query OK, 0 rows affected (0.00 sec) mysql> select * from t_wallet; +----+---------+---------+ | id | account | balance | +----+---------+---------+ | 1 | 小明 | 1000.00 | +----+---------+---------+ 1 row in set (0.00 sec) mysql> update t_wallet set balance = balance - 1000 where id = 1; Query OK, 1 row affected (0.00 sec) Rows matched: 1 Changed: 1 Warnings: 0 mysql> select * from t_wallet; +----+---------+---------+ | id | account | balance | +----+---------+---------+ | 1 | 小明 | 0.00 | +----+---------+---------+ 1 row in set (0.00 sec)
事务A:
获取到当前余额为0。
mysql> select * from t_wallet where account = '小明'; +----+---------+---------+ | id | account | balance | +----+---------+---------+ | 1 | 小明 | 0.00 | +----+---------+---------+ 1 row in set (0.00 sec)
事务B:
最后再回滚事务B。这就是脏读
mysql> rollback; Query OK, 0 rows affected (0.00 sec) mysql> select * from t_wallet; +----+---------+---------+ | id | account | balance | +----+---------+---------+ | 1 | 小明 | 1000.00 | +----+---------+---------+ 1 row in set (0.00 sec)
6.修改事务隔离级别为已提交读。再次执行以上SQL.
设置session事务隔离级别为已提交读:
mysql> set session transaction isolation level read committed; Query OK, 0 rows affected (0.00 sec)
事务B:
mysql> begin; Query OK, 0 rows affected (0.00 sec) mysql> select * from t_wallet; +----+---------+---------+ | id | account | balance | +----+---------+---------+ | 1 | 小明 | 1000.00 | +----+---------+---------+ 1 row in set (0.00 sec) mysql> update t_wallet set balance = balance - 1000 where id = 1; Query OK, 1 row affected (0.00 sec) Rows matched: 1 Changed: 1 Warnings: 0 mysql> select * from t_wallet; +----+---------+---------+ | id | account | balance | +----+---------+---------+ | 1 | 小明 | 0.00 | +----+---------+---------+ 1 row in set (0.00 sec)
事务A:
当前隔离级别为已提交读,再次查看余额,发现仍是1000。脏读问题解决
mysql> select @@transaction_isolation; +-------------------------+ | @@transaction_isolation | +-------------------------+ | READ-COMMITTED | +-------------------------+ 1 row in set (0.00 sec) mysql> select * from t_wallet; +----+---------+---------+ | id | account | balance | +----+---------+---------+ | 1 | 小明 | 1000.00 | +----+---------+---------+ 1 row in set (0.00 sec)
7.当前为已提交读隔离级别,验证不可重复读问题。开始事务A,第一次读取余额为1000。
事务A:
第一次读取余额1000.
mysql> select * from t_wallet where id = 1; +----+---------+---------+ | id | account | balance | +----+---------+---------+ | 1 | 小明 | 1000.00 | +----+---------+---------+ 1 row in set (0.00 sec)
8.事务B开启,并扣除余额1000,并提交事务。
事务B:
mysql> begin; Query OK, 0 rows affected (0.00 sec) mysql> select * from t_wallet where id = 1; +----+---------+---------+ | id | account | balance | +----+---------+---------+ | 1 | 小明 | 1000.00 | +----+---------+---------+ 1 row in set (0.00 sec) mysql> update t_wallet set balance = balance - 1000 where id = 1; Query OK, 1 row affected (0.00 sec) Rows matched: 1 Changed: 1 Warnings: 0 mysql> commit; Query OK, 0 rows affected (0.00 sec)
9.此时,事务A再次读取,发现余额为0。两次读取的结果不一致。
事务A:
mysql> select * from t_wallet where id = 1; +----+---------+---------+ | id | account | balance | +----+---------+---------+ | 1 | 小明 | 0.00 | +----+---------+---------+ 1 row in set (0.00 sec)
10.我们再次修改隔离级别为可重复读,重置小明账户余额为1000。再次执行 7 ~ 9 步执行的SQL。
修改事务隔离级别为可重复读:
mysql> set session transaction isolation level repeatable read; Query OK, 0 rows affected (0.00 sec) mysql> select @@transaction_isolation; +-------------------------+ | @@transaction_isolation | +-------------------------+ | REPEATABLE-READ | +-------------------------+ 1 row in set (0.00 sec)
事务A:
第一次读取余额1000.
mysql> select * from t_wallet where id = 1; +----+---------+---------+ | id | account | balance | +----+---------+---------+ | 1 | 小明 | 1000.00 | +----+---------+---------+ 1 row in set (0.00 sec)
事务B:
mysql> select @@transaction_isolation; +-------------------------+ | @@transaction_isolation | +-------------------------+ | REPEATABLE-READ | +-------------------------+ 1 row in set (0.00 sec) mysql> begin; Query OK, 0 rows affected (0.00 sec) mysql> select * from t_wallet where id = 1; +----+---------+---------+ | id | account | balance | +----+---------+---------+ | 1 | 小明 | 1000.00 | +----+---------+---------+ 1 row in set (0.00 sec) mysql> update t_wallet set balance = balance - 1000 where id = 1; Query OK, 1 row affected (0.00 sec) Rows matched: 1 Changed: 1 Warnings: 0 mysql> commit; Query OK, 0 rows affected (0.00 sec)
事务A:
第二次读取余额仍然是1000。不可重复读问题解决
mysql> select * from t_wallet where id = 1; +----+---------+---------+ | id | account | balance | +----+---------+---------+ | 1 | 小明 | 1000.00 | +----+---------+---------+ 1 row in set (0.00 sec)
11.假如此时,我们不结束事务A,事务B新增一条记录 (小王,500)
事务B:
mysql> insert into t_wallet(account, balance) values('小王', 500); Query OK, 1 row affected (0.00 sec) mysql> commit; Query OK, 0 rows affected (0.00 sec) mysql> select * from t_wallet; +----+---------+---------+ | id | account | balance | +----+---------+---------+ | 1 | 小明 | 0.00 | | 2 | 小王 | 500.00 | +----+---------+---------+ 2 rows in set (0.00 sec)
事务A:
查询表数据,发现仍然只查询到余额为1000的小明的账户记录。发现没有出现幻读现象,这个主要是依赖MVCC多版本控制帮我们解决的,后续笔记会详细说明。
mysql> select * from t_wallet; +----+---------+---------+ | id | account | balance | +----+---------+---------+ | 1 | 小明 | 1000.00 | +----+---------+---------+ 1 row in set (0.00 sec)
(四)问题思考
(1)为什么MySQL的默认隔离级别是RR? 为什么实际生产环境MySQL隔离级别使用是RC?
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 分享4款.NET开源、免费、实用的商城系统
· 全程不用写代码,我用AI程序员写了一个飞机大战
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 记一次.NET内存居高不下排查解决与启示
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了