SQL92将事务隔离级别分为了4种,读未提交,读已提交,可重复读,可串行化。不同的隔离级别,对于数据的可见性不同,就会导致不同的正确性。事务的核心在于控制多个客户端在读写共享数据时候的并发问题。这篇文章通过实例研究隔离级别,我们将从读未提交开始,一步一步分析。
step1 创建表和基础数据

CREATE TABLE `traning`.`account` (
  `id` INT NOT NULL AUTO_INCREMENT COMMENT  '主键id',
  `uid` INT NOT NULL COMMENT '用户uid',
  `amount` INT NOT NULL COMMENT '金额',
  PRIMARY KEY (`id`));

INSERT INTO `traning`.`account` (`uid`, `amount`) VALUES ('1', '1000');
INSERT INTO `traning`.`account` (`uid`, `amount`) VALUES ('2', '2000');

step2 修改隔离级别

场景1 读未提交导致脏读

Client1:SET GLOBAL TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
Client2:SET GLOBAL TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
关闭会话重新进入MySQL客户端。
Client2:show variables like 'transaction_isolation';
Client1: start TRANSACTION;
Client2: start TRANSACTION;
Client2: select amount from account where uid=1; (1000)
update account set amount=amount-500 where uid=1;
Client1: select amount from account where uid=1; (读到别人没提交事务:500)
-- update account set amount=(amount+100) where uid=1;(数据被锁,不能更新)
Client2: rollback;(Client1再次读到的是1000,但是没有读)
-- Client1: select amount from account where uid=1; (500)
Client1: update account set amount=(500+100) where uid=1; (读到旧数据去进行更新操作,导致账户丢失500)

场景2 读已提交解决脏读问题

Client1:SET GLOBAL TRANSACTION ISOLATION LEVEL READ COMMITTED;
Client2:SET GLOBAL TRANSACTION ISOLATION LEVEL READ COMMITTED;
关闭会话重启。
Client1:start TRANSACTION;
Client2:start TRANSACTION;
Client2: select amount from account where uid=1; (1000)
update account set amount=amount-500 where uid=1; (500)
Client1:select amount from account where uid=1 (读取不到别人没提交的事务:1000);
update account set amount=1000-500 where uid=1;(数据被锁不能更新)
Client2: rollback;
Client1: update account set amount=(1000+100) where uid=1;
Client1: commit;

场景3 读已提交导致不可重复读问题

Client1:SET GLOBAL TRANSACTION ISOLATION LEVEL READ COMMITTED;
Client2:SET GLOBAL TRANSACTION ISOLATION LEVEL READ COMMITTED;
关闭会话重启。
Client1:start TRANSACTION;
Client2:start TRANSACTION;
Client1:select amount from account where uid=1; (1000);
Client2:update account set amount=amount-100 where uid=1;
Client2: commit;
Client1: select amount from account where uid=1(amount=900,发现自己钱少了100);

场景4 可重复读解决不可重复读问题

Client1:SET GLOBAL TRANSACTION ISOLATION LEVEL REPEATABLE READ;
Client2:SET GLOBAL TRANSACTION ISOLATION LEVEL REPEATABLE READ;
Client1:start TRANSACTION;
Client2:start TRANSACTION;
Client2: select amount from account where uid=1;(1000)
Client1:select amount from account where uid=1; (1000);
Client2:commit;
Client1:select amount from account where uid=1;(amount=1000);

场景5 可重复读导致幻读问题

Client1:SET GLOBAL TRANSACTION ISOLATION LEVEL REPEATABLE READ;
Client2:SET GLOBAL TRANSACTION ISOLATION LEVEL REPEATABLE READ;
Client1:start TRANSACTION;
Client2:start TRANSACTION;
Client2: select amount from account ; (2行数据)
Client1:select amount from account ; (2行数据);
Client2:INSERT INTO traning.account (uid, amount) VALUES ('3', '3000');
Client2:commit;
Client1:select amount from account ;(3条数据);
Clinet1 :update account set amount=1 where id=3(Client读不到但是可以更新);
Clinet1 :select amount from account ;(4条数据-phantom read here!!!!影响了client1的判断和数据);
Clinet1 :commit;

场景6 可序列化解决幻读问题

Client1:SET GLOBAL TRANSACTION ISOLATION LEVEL SERIALIZABLE;
Client2:SET GLOBAL TRANSACTION ISOLATION LEVEL SERIALIZABLE;
Client1:start TRANSACTION;
Client2:start TRANSACTION;
Client2: select amount from account ; (2行数据)
Client1:select amount from account ; (2行数据);
Client2:INSERT INTO traning.account (uid, amount) VALUES ('3', '3000'); (插入不进去,数据被锁)
Lock wait timeout exceeded; try restarting transaction

以下是整理的图:
MySQL事务隔离级别-案例驱动_第1张图片

当然还有第一类丢失更新(脏写)和第二类丢失更新,第一类丢失更新永远不会发生,第二类丢失更新会在RU和RC情况下发生。

这是完整的图
包含了更多的隔离级别和可能发生数据异常情况:
MySQL事务隔离级别-案例驱动_第2张图片