# 8. 事务

# 8.1 事务的隔离性

多事务操作之间不会产生影响。

# 8.2 三个读问题

  • 脏读

    一个未提交的事务读取到另一个未提交的事务的数据。

    image-20200916161119710

    如上:东方不败想从5000改到100,而岳不群想从5000改到60000。这个时候岳不群先改了,然后东方不败读取到数据已经改成60000了,所以东方不败就会继续在60000的基础上进行修改。但是这个时候,岳不群的事务并没有进行提交,而且进行了事务回滚,所以真实的数据现在还是5000,而东方不败操作的数据是60000。这就叫脏读。

  • 不可重复读

    一个未提交事务读取到另一提交事务修改数据。

    image-20200916161326921

    如上:东方不败先读取到数据是5000,想对数据进行操作,但是这个时候岳不群已经将数据改成900了。而东方不败又检测到了数据已经改成900了,读两次,数据不一致,这就是不可重复读(因为不知道再读的话是不是又会不一样了)。

  • 虚(幻)读

    一个未提交事务读取到另一提交事务增加的数据。

    如:本来该事务只修改了 3 条数据,这个时候另外一个事务 insert 了一条新的数据,就变成修改了 4 条数据了。

# 8.3 解决读问题 —— 设置事务的隔离性

isolation 属性值 意思 脏读 不可重复读 虚读 作用
READ UNCOMMITTED 读未提交 效率高,但是啥也避免不了
READ COMMITTED 读已提交 常用,可避免脏读
REPEATABLE READ 可重复读 可以用在非 insert 方法上
SERIALIZABLE 串行化 三个问题都解决了,但效率低
DEFAULT 使用数据库默认 MySQL 的话就是 REPEATABLE READ
Oracle 的话就是 READ COMMITTED

# 8.4 当前读/快照读

# 当前读

当前读:读取的是最新版本,并且对读取的记录加锁,阻塞其他事务同时改动相同记录,避免出现安全问题。

select ... lock in share mode (共享读锁)

select ... for update

update, delete, insert

例如,假设要 update 一条记录,但是另一个事务已经 delete 这条数据并且 commit 了,如果不加锁就会产生冲突。所以 update 的时候肯定要是当前读,得到最新的信息并且锁定相应的记录。

关于 for update

使用 select ... for update 可以锁表也可以锁行。锁表的压力自然是比锁行的压力要大的,所以应尽量采用锁行。

for update 仅适用于 InnoDB(因为 MyISAM 不支持行锁),且必须在事务处理模块(BEGIN/COMMIT)中才能生效。

  • 例1: (明确指定主键,并且有此数据,row lock)

    SELECT * FROM wallet WHERE id='3' FOR UPDATE;

  • 例2: (明确指定主键,若查无此数据,无 lock)

    SELECT * FROM wallet WHERE id='-1' FOR UPDATE;

  • 例3: (无主键,table lock)

    SELECT * FROM wallet WHERE name='Mouse' FOR UPDATE;

  • 例4: (主键不明确,table lock)

    SELECT * FROM wallet WHERE id<>'3' FOR UPDATE;

  • 例5: (主键不明确,table lock)

    SELECT * FROM wallet WHERE id LIKE '3' FOR UPDATE;

实现方式: 当前读是通过 Next-Key Lock 来实现的。

下面通过一个例子来说明当前读的实现方式,例如下面这条 SQL:

delete from T where age = 7;

进行下面的实验:

img

测试可知 delete from T where age = 7; 语句在 age 上的加锁区间为 (4,10),图解如下:

img

# 快照读

快照读:读取的是记录数据的可见版本(可能是过期的数据),不用加锁。

单纯的 select 操作,不包括上述

select ... lock in share mode;

select ... for update。    

READ COMMITTED 隔离级别:每次 select 都生成一个快照读

REPEATABLE READ 隔离级别:开启事务后第一个 select 语句才是快照读的地方,而不是一开启事务就快照读

实现方式: undo log + MVCC

下图右侧黄色部分是数据:一行数据记录,主键 ID 是 10,object = 'Goland' ,被 update 更新为 object = 'Python' 。

img

  1. 事务会先使用“排他锁”锁定该行,将该行当前的值复制到 undo log 中;
  2. 然后再真正地修改当前行的值;
  3. 最后填写事务的 DB_TRX_ID ,使用回滚指针 DB_ROLL_PTR 指向 undo log 中修改前的行。
  • DB_TRX_ID : 6 字节 DB_TRX_ID 字段,表示最后更新的事务 id ( update , delete , insert ) 。此外,删除在内部被视为更新,其中行中的特殊位被设置为将其标记为已软删除。
  • DB_ROLL_PTR : 7 字节回滚指针,指向前一个版本的 undo log 记录,组成 undo 链表。如果更新了行,则撤消日志记录包含在更新行之前重建行内容所需的信息。
  • DB_ROW_ID:行标识(隐藏单调自增 ID),大小为 6 字节,如果表没有主键,InnoDB 会自动生成一个隐藏主键,因此会出现这个列。另外,每条记录的头信息(record header)里都有一个专门的 bit(deleted_flag)来表示当前记录是否已经被删除。

补充

insert undo log 只在事务回滚时需要,事务提交就可以删掉了。

update undo log 包括 update 和 delete,回滚和快照读都需要。

MVCC 解决幻读的原理:

  1. 其实它是用来替代行锁的,进一步提升并发能力
  2. InnoDB 的 MVCC,是通过在每行记录后面保存两个隐藏的列 DB_TRX_IDDB_ROLL_PTR 来实现的,它们是自动加上的,程序无法控制;
  3. MVCC 把一个个事务都隔离开来,自己玩自己的 CURD,在最后提交到数据库时再比较版本号;
  4. 不会读取事务版本号大于当前事务 ID 的数据,顺便解决了幻读问题;
  5. 主要解决的是写时加锁不能读的问题,但并没有解决写并发的问题。

# 8.5 InnoDB 可重复读隔离级别下如何避免幻读

  • 表现:快照读(非阻塞读) —— MVCC
  • 内在:next-key 锁(行锁 + gap 锁)
上次更新: 8/5/2021, 9:08:33 PM