事务的 ACID
事务具有四个特征:原子性( Atomicity )、一致性( Consistency )、隔离性( Isolation )和持续性( Durability )。这四个特性简称为 ACID 特性。
1 、原子性。事务是数据库的逻辑工作单位,事务中包含的各操作要么都做,要么都不做
2 、一致性。事 务执行的结果必须是使数据库从一个一致性状态变到另一个一致性状态。因此当数据库只包含成功事务提交的结果时,就说数据库处于一致性状态。如果数据库系统 运行中发生故障,有些事务尚未完成就被迫中断,这些未完成事务对数据库所做的修改有一部分已写入物理数据库,这时数据库就处于一种不正确的状态,或者说是 不一致的状态。
3 、隔离性。一个事务的执行不能其它事务干扰。即一个事务内部的操作及使用的数据对其它并发事务是隔离的,并发执行的各个事务之间不能互相干扰。
4 、持续性。也称永久性,指一个事务一旦提交,它对数据库中的数据的改变就应该是永久性的。接下来的其它操作或故障不应该对其执行结果有任何影响。
Mysql的四种隔离级别
-
Read Uncommitted(读取未提交内容)
-
Read Committed(读取提交内容)
-
Repeatable Read(可重读)
-
Serializable(可串行化)
-
脏读(Drity Read):某个事务已更新一份数据,另一个事务在此时读取了同一份数据,由于某些原因,前一个RollBack了操作,则后一个事务所读取的数据就会是不正确的。
-
不可重复读(Non-repeatable read):在一个事务的两次查询之中数据不一致,这可能是两次查询过程中间插入了一个事务更新的原有的数据。
-
幻读(Phantom Read):在一个事务的两次查询中数据笔数不一致,例如有一个事务查询了几列(Row)数据,而另一个事务却在此时插入了新的几列数据,先前的事务在接下来的查询中,就会发现有几列数据是它先前所没有的。
-- 查看表结构
desc tb_test;
-- 客户端A: 将A的会话隔离级别设置为read uncommitted(未提交读)
SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
-- A 查看全局 / 会话事务隔离级别
-- mysql 5
SELECT @@global.tx_isolation, @@tx_isolation;
-- mysql 8
SELECT @@global.transaction_isolation,@@transaction_isolation;
# 设定全局的隔离级别 设定会话 global 替换为 session 即可 把set语法温习一下
# SET [GLOABL] config_name = 'foobar';
# SET @@[session|global].config_name = 'foobar';
# SELECT @@[global.]config_name;
SET @@gloabl.tx_isolation = 0;
SET @@gloabl.tx_isolation = 'READ-UNCOMMITTED';
SET @@gloabl.tx_isolation = 1;
SET @@gloabl.tx_isolation = 'READ-COMMITTED';
SET @@gloabl.tx_isolation = 2;
SET @@gloabl.tx_isolation = 'REPEATABLE-READ';
SET @@gloabl.tx_isolation = 3;
SET @@gloabl.tx_isolation = 'SERIALIZABLE';
1.未提交度导致的脏读数据
测试SQL:
-- A 启动事务更新数据,不提交
START TRANSACTION;
-- A 查询数据: (数据为初始状态)
SELECT * FROM tb_test;
-- ---->>>转客户端-B
-- A 再次查询数据: 发现数据已经被修改了,这就是所谓的“脏读”
SELECT * FROM tb_test;
-- ---->>>转客户端-B
-- A 再次读取数据,发现数据变回初始状态
SELECT * FROM tb_test;
-- 客户端B: 启动事务
START TRANSACTION;
-- B 更新数据:不提交
UPDATE tb_test SET num = 10 WHERE id = 1;
-- ---->>>转客户端-A
COMMIT;
-- OR
ROLLBACK;
-- ---->>>转客户端-A
事务B更新了一条记录,但是没有提交,此时事务A可以查询出未提交记录。造成脏读现象。未提交读是最低的隔离级别。
2.已提交读导致的不可重复读
-- 客户端A: 将A的会话隔离级别设置为read committed(已提交读)
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
测试SQL:
-- A 启动事务更新数据,不提交
START TRANSACTION;
-- A 查询数据: (数据为初始状态)
SELECT * FROM tb_test;
-- ---->>>转客户端-B
-- A 再次查询数据: 发现数据未被修改
SELECT * FROM tb_test;
-- ---->>>转客户端-B
-- A 再次读取数据:发现数据已发生变化,说明B提交的修改被事务中的A读到了,这就是所谓的“不可重复读”
SELECT * FROM tb_test;
-- 客户端B: 启动事务
START TRANSACTION;
-- B 更新数据:不提交
UPDATE tb_test SET num = 10 WHERE id = 1;
-- ---->>>转客户端-A
COMMIT;
-- OR
ROLLBACK;
-- ---->>>转客户端-A
已提交读隔离级别解决了脏读的问题,但是出现了不可重复读的问题,即事务A在两次查询的数据不一致,因为在两次查询之间事务B更新了一条数据。已提交读只允许读取已提交的记录,但不要求可重复读。
3.可重复读级别导致的幻读
-- 客户端A: 将A的会话隔离级别设置为read committed(已提交读)
SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ;
幻读错误的理解:说幻读是 事务A 执行两次 select 操作得到不同的数据集,即 select 1 得到 10 条记录,select 2 得到 11 条记录。这其实并不是幻读,这是不可重复读的一种,只会在 R-U R-C 级别下出现,而在 mysql 默认的 RR 隔离级别是不会出现的。
幻读,并不是说两次读取获取的结果集不同,幻读侧重的方面是某一次的 select 操作得到的结果所表征的数据状态无法支撑后续的业务操作。更为具体一些:select 某记录是否存在,不存在,准备插入此记录,但执行 insert 时发现此记录已存在,无法插入,此时就发生了幻读。
# step-1:开启A客户端事务,并且查询(没有数据)
BEGIN;
select *from tb_test where id = 9
# step-2: 开启B客户端事务,插入一条数据,提交事务
BEGIN;
insert into tb_test values(9,999);
COMMIT;
# step-3:再次查询该条数据(没有显示初B客户端提交的数据,出现了幻读),且尝试insert该ID的数据(出现主键冲突)
select *from tb_test where id = 9
insert into tb_test values(9,999);
RR 也是可以避免幻读的,通过对 select 操作手动加 行X锁(SELECT ... FOR UPDATE 这也正是 SERIALIZABLE 隔离级别下会隐式为你做的事情),同时还需要知道,即便当前记录不存在,比如 id = 1 是不存在的,当前事务也会获得一把记录锁(因为InnoDB的行锁锁定的是索引,故记录实体存在与否没关系,存在就加 行X锁,不存在就加 next-key lock间隙X锁),其他事务则无法插入此索引的记录,故杜绝了幻读。
# 这里需要用 X锁, 用 LOCK IN SHARE MODE 拿到 S锁 后我们没办法做 写操作
SELECT `id` FROM `users` WHERE `id` = 1 FOR UPDATE;
4.可串行化
-- 客户端A: 将A的会话隔离级别设置为SERIALIZABLE(串行化)
SET SESSION TRANSACTION ISOLATION LEVEL SERIALIZABLE;
serializable完全锁定字段,若一个事务来查询同一份数据就必须等待,直到前一个事务完成并解除锁定为止。是完整的隔离级别,会锁定对应的数据表格,因而会有效率的问题。
在 SERIALIZABLE 隔离级别下,step1 执行时是会隐式的添加 行(X)锁 / gap(X)锁的,从而 step2 会被阻塞,step3 会正常执行,待 T1 提交后,T2 才能继续执行(主键冲突执行失败),对于 T1 来说业务是正确的,成功的阻塞扼杀了扰乱业务的T2,对于T1来说他前期读取的结果是可以支撑其后续业务的。
id = 1 的记录不存在,开始执行事务: step1: T1 查询 id = 1 的记录并对其加 X锁 step2: T2 插入 id = 1 的记录,被阻塞 step3: T1 插入 id = 1 的记录,成功执行(T2 依然被阻塞中),T1 提交(T2 唤醒但主键冲突执行错误)。
T1事务符合业务需求成功执行,T2干扰T1失败。
# step-1:开启A客户端事务,并且查询(没有数据),加X锁
BEGIN;
select *from tb_test where id = 9 for update;
# step-2: 开启B客户端事务,插入一条数据,会出现“主键冲突的提示”
BEGIN;
insert into tb_test values(9,999);
# step-3:A客户端insert该ID的数据(正常)
insert into tb_test values(9,999);
commit;
(参考:https://segmentfault.com/a/1190000016566788?utm_source=tag-newest)