事务隔离级别
在mysql中,如果有大量用户同时访问修改同一条数据,将会产生数据一致性的问题,本文来探讨mysql是如何解决这个问题的。
场景预设
CREATE DATABASE IF NOT EXISTS `isolation_lab`;USE `isolation_lab`;--DROP TABLE IF EXISTS `account`; CREATE TABLE `account` ( `id` INT PRIMARY KEY AUTO_INCREMENT, `name` VARCHAR(50) NOT NULL, `balance` DECIMAL(10,2) NOT NULL DEFAULT 0.00, `version` INT DEFAULT 0 ) ENGINE=InnoDB;--INSERT INTO `account` (`name`, `balance`) VALUES ('张三', 1000.00), ('李四', 500.00), ('王五', 2000.00);-- #MySQL #SQL #每天一个知识点DROP TABLE IF EXISTS `transaction_log`; CREATE TABLE `transaction_log` ( `id` INT PRIMARY KEY AUTO_INCREMENT, `from_account` VARCHAR(50), `to_account` VARCHAR(50), `amount` DECIMAL(10,2), `create_time` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB;并发事务的三大问题
脏读
脏读 就是读取到了其他事务尚未提交的数据,下面来创建一个脏读的情况 脏数据 是指 未提交的、不稳定的临时数据 。
---- 需要两个独立的 MySQL 客户端会话(会话 A 和会话 B)----SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; START TRANSACTION;--UPDATE account SET balance = balance - 200 WHERE name = '张三';----SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; START TRANSACTION;--SELECT balance FROM account WHERE name = '张三'; -- 结果:800.00(但会话 A 可能随时回滚!)----ROLLBACK;----SELECT balance FROM account WHERE name = '张三'; -- 结果:1000.00(刚才读到的800是"脏数据")这种情况会导致会话 B 很可能根据读取到的800来做出一些决策,但是会话 A 又回滚数据了,导致会话 B 的后续决策(针对800)是对于实际的1000这个数据做的。
或者会话 A 插入一条新的数据,但未提交,会话 B 根据这条新数据做了操作,会话 A 回滚,导致业务逻辑完全错误。
不可重读
不可重读 就是同一事务中多次读取同一数据,结果 不一致
--UPDATE account SET balance = 1000.00 WHERE name = '张三';--SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED; START TRANSACTION;--SELECT balance FROM account WHERE name = '张三'; -- 结果:1000.00----START TRANSACTION; UPDATE account SET balance = 800.00 WHERE name = '张三'; COMMIT;----SELECT balance FROM account WHERE name = '张三'; -- 结果:800.00(与第一次读取不一致!)----COMMIT;有人可能会很疑惑,同一事务中多次读取同一数据,结果不一致 ,是不可重读,那 脏读 不一样是在一个事务中可能读到不同的数据吗?
其本质区别是:

- 脏读 是读取了 事务未提交 的数据, 不可重读 是读取了 其他事务已经提交的数据 2. 脏读 的数据是临时的,可能被回滚,从未真正生效, 不可重读 的数据是真实的
幻读
幻读 是同一事务中多次查询,返回的 行数 不一致
----SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ; --START TRANSACTION;--SELECT COUNT(*) FROM account WHERE balance > 600; -- 结果:2(张三1000,王五2000)----START TRANSACTION; INSERT INTO account (name, balance) VALUES ('赵六', 700.00); COMMIT;----SELECT COUNT(*) FROM account WHERE balance > 600; -- 结果:2(在 REPEATABLE READ 下看不到新插入的赵六)--UPDATE account SET version = version +SELECT ROW_COUNT(); -- 影响了3行!包括赵六!--SELECT COUNT(*) FROM account WHERE balance > 600; --COMMIT;这个结果非常的神奇,那么到底发生了什么,会产生这种情况?
因为在 可重复读 这个事务隔离级别中,具有 快照读 和 当前读 两种机制:
- 第一次使用 select 进行查询时,使用的是 快照读 ,它读取的是事务开始的时候 数据的快照 ,因此看不见之前由其他事务提交的新增事务。
- 在使用 update 语句进行更新时,因为要确保更新的数据是基于最新的、已经提交的状态,因此数据库必须进行 当前读 ,因此 update 语句能够感知到其他事务新提交的那条记录。
- 并且,当数据库发现新记录是符合 update 中的 where 语句,就会对其也进行更新,还会将这条记录 创建版本 的 trx_id (事务ID)设置为当前更新事务的ID。
- 第二次使用 select 进行查询时,依旧使用的是 快照读 ,快照读的规则是: 只读取在事务开始时就已经提交的数据版本,或者由本事务自身创建的数据版本 ,因此现在可以读取到新记录。
四种隔离级别
- 读未提交(READ UNCOMMITTED) - 性能最好,一致性最差
--SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;-------- 使用场景:对数据一致性要求极低,如实时性要求极高的监控系统- 读已提交(READ COMMITTED) - Oracle/PostgreSQL默认
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;---------- 使用场景:大多数 OLTP 系统- 可重复读(REPEATABLE READ) - MySQL默认
SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ;---------- 使用场景:需要事务内数据一致性的场景- 串行化(SERIALIZABLE) - 一致性最好,性能最差
SET SESSION TRANSACTION ISOLATION LEVEL SERIALIZABLE;---------- 用场景:对一致性要求极高,如金融核心系统隔离级别实际影响
C READ UNCOMMITTED
--UPDATE account SET balance = 1000.00 WHERE name = '张三'; UPDATE account SET balance = 500.00 WHERE name = '李四'; UPDATE account SET balance = 2000.00 WHERE name = '王五'; DELETE FROM account WHERE name = '赵六';--SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; START TRANSACTION; --SELECT SUM(balance) AS total_before FROM account; -- 假设看到3500--START TRANSACTION; UPDATE account SET balance = balance --- 还没提交!-UPDATE account SET balance = balance *--COMMIT;--ROLLBACK; -- 转账失败,但利息已错误计算!REPEATABLE READ
--UPDATE account SET balance = 1000.00 WHERE name = '张三'; UPDATE account SET balance = 500.00 WHERE name = '李四'; UPDATE account SET balance = 2000.00 WHERE name = '王五';--SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ; START TRANSACTION; SELECT SUM(balance) AS total_before FROM account; -- 看到3500--START TRANSACTION; UPDATE account SET balance = balance -COMMIT; -- 提交成功--SELECT SUM(balance) AS total_now FROM account; --UPDATE account SET balance = balance *COMMIT; -- 提交后,其他事务看到更新 文章版权声明:除非注明,否则均为边学边练网络文章,版权归原作者所有