事务的ACID和隔离级别

Posted by JimWang on 2021-02-20

事务的ACID和隔离级别

什么是事务以及ACID

事务简单来说:一个Session中所进行的所有操作,要么同时成功,要么同时失败;作为单个逻辑工作单元执行的一系列操作,满足四大特性:

  • 原子性(Atomicity):事务作为一个整体被执行 ,要么全部执行,要么全部不执行
  • 一致性(Consistency):保证数据库状态从一个一致状态转变为另一个一致状态
  • 隔离性(Isolation):多个事务并发执行时,一个事务的执行不应影响其他事务的执行
  • 持久性(Durability):一个事务一旦提交,对数据库的修改应该永久保存

事务的并发问题

  • 丢失更新:一个事务的更新覆盖了另一个事务的更新;
  • 脏读:一个事务读取了另一个事务未提交的数据;
    • ==其实就是读到了别的事务回滚前的脏数据。比如事务B执行过程中修改了数据X,在未提交前,事务A读取了X,而事务B却回滚了,这样事务A就形成了脏读。==
  • 不可重复读:不可重复读的重点是修改,同样条件下两次读取结果不同,也就是说,被读取的数据可以被其它事务修改;
    • ==事务A首先读取了一条数据,然后执行逻辑的时候,事务B将这条数据改变了,然后事务A再次读取的时候,发现数据不匹配了,就是所谓的不可重复读了。==
    • ==也就是说,当前事务先进行了一次数据读取,然后再次读取到的数据是别的事务修改成功的数据,导致两次读取到的数据不匹配,也就照应了不可重复读的语义。==
  • 幻读:幻读的重点在于新增或者删除,同样条件下两次读出来的记录数不一样。
    • ==事务A首先根据条件索引得到N条数据,然后事务B改变了这N条数据之外的M条或者增添了M条符合事务A搜索条件的数据,导致事务A再次搜索发现有N+M条数据了,就产生了幻读。==

隔离级别

隔离级别决定了一个session中的事务可能对另一个session中的事务的影响。

  • 读未提交(READ UNCOMMITTED):最低级别的隔离,通常又称为dirty read,它 ==允许一个事务读取另一个事务还没 commit 的数据,这样可能会提高性能==,但是会导致脏读问题
  • 读已提交(READ COMMITTED):在一个事务中只允许对其它事务已经 commit 的记录可见,该隔离级别不能避免不可重复读问题
    • 一个事务执行的过程中,其他事务修改了数据,从而导致该事务多次读取数据不匹配。
  • 可重复读(REPEATABLE READ,MySQL默认的隔离级别):==在一个事务开始后,其他事务对数据库的修改在本事务中不可见,直到本事务 commit 或 rollback。== 但是,其他事务的 insert/delete 操作对该事务是可见的,也就是说,该隔离级别并不能避免幻读问题。在一个事务中重复 select 的结果一样,除非本事务中 update 数据库。
    • ==可以解决不可重复读,事务过程中,其他事务对数据的修改不再可见。但是其他事务还能增删,所以还有幻读问题。==
  • 可串行化(SERIALIZABLE):最高级别的隔离,只允许事务串行执行。
    • 事物a 执行读写操作时,会锁定检索的数据行范围(范围锁),这种锁会阻止其他事物在本范围内的一切操作,只有事物a执行完毕,提交事物后,才会释放范围锁,这样就避免了幻读。

MySQL的事务支持不是绑定在MySQL服务器本身,而是与存储引擎相关:

  • MyISAM:不支持事务,用于只读程序提高性能;
  • InnoDB:支持ACID事务、行级锁、并发;

解决方案

MVCC

mvcc对版本并发控制(Multi-Version Conncurrency Control)是mysql中基于乐观锁原理实现的隔离级别的方式。==用于实现读已提交和可重复读取隔离级别。==

InnoDB的MVCC,是 ==通过在每行记录后面保存两个隐藏的列来实现的。这两个列,一个保存了行的创建时间,一个保存行的过期时间(或删除时间),当然存储的并不是实际的时间值,而是系统版本号(system version number).== 每开始一个新的事务,系统版本号都会自动递增,事务开始时刻的系统版本号会作为事务的版本号,用来和查询到的每行记录的版本号进行比较。下面看一下在repeatable Read 隔离级别下,MVCC具体是如何操作的。

  • SELECT
    • (1)InnoDB只查找版本遭遇当前事务版本的数据行(行的系统版本号小于或等于事务的系统版本号),这样可以确保事务读取的行,要么是在事务开始前已经存在的,要么是事务自身插入或者修改过的。
    • (2)行的删除版本要么未定义,要么大于当前事务版本号,这可以确保事务读取到的行,在事务开始之前没有被删除。
      只有符合上述两个条件的记录,才能作为查询结果。
  • INSERT
    • innoDB为新插入的每一行保存当前系统版本作为行版本号。
  • DELETE
    • innoDB为删除的每一行保存当前系统版本号作为行删除标识。
  • UPDATE
    • InnoDB为==插入一行新记录,保存当前系统版本号作为行版本号,同时保存当前系统版本号到原来的行作为行删除标识==

MVCC使用快照读和当前读解决可重复读

select 快照读
当执行select操作, ==innodb默认会执行快照读,会记录下这个select后的结果,之后select的时候就会返回这次快照的数据,即使其他事务提交了不会影响当前select的数据,这就实现了可重复读了。== 快照的生成当在第一次执行select的时候,也就是说假设当A开启了事务,然后没有执行任何操作,这个时候B insert了一条数据然后commit,这时候A执行select,满额返回的数据中心就会有B添加的那条数据,之后无论再有其他事务commit都没有关系,因为快照已经生成了,后面的select都是根据快照来的。

当前读
对于会对数据修改的操作(update,insert,delete)都是采用当前读的模式。在执行这几个操作时会读取最新的版本号记录,写操作后把版本号改为了当前事务的版本号,所以即使是被其他的事务提交的数据也可以查询到。==假设要update一条数据,但是在另一个事务中已经delete掉这条数据并且commit了,如果update就会产生冲突,所以在update的时候需要知道最新的数据,也正是因为这样才导致幻读。==

next-key锁解决幻读

InnoDB有三种行锁的算法:

1,Record Lock:单个行记录上的锁。

2,Gap Lock:间隙锁,锁定一个范围,但不包括记录本身。GAP锁的目的,是为了防止同一事务的两次当前读,出现幻读的情况。

3,Next-Key Lock:1+2,锁定一个范围,并且锁定记录本身。对于行的查询,都是采用该方法,主要目的是解决幻读的问题。

当我们用范围条件而不是相等条件索引数据,并请求共享或排他锁时,InnoDB会给符合条件的已有数据记录的索引项加锁;对于键值在条件范围内但并不存在的记录,叫做“间隙(GAP)”。

根据检索条件向下寻找最靠近检索条件的记录值A作为左区间,向上寻找最靠近检索条件的记录值B作为右区间

对于以上这个数据,

1
2
3
4
5
6
7
8
9
10
11
12
# session A
SELECT * FROM z WHERE b = 6 FOR UPDATE;

# session B
INSERT INTO z VALUES (2, 4);/*success*/
INSERT INTO z VALUES (2, 8);/*blocked*/
INSERT INTO z VALUES (4, 4);/*blocked*/
INSERT INTO z VALUES (4, 8);/*blocked*/
INSERT INTO z VALUES (8, 4);/*blocked*/
INSERT INTO z VALUES (8, 8);/*success*/
INSERT INTO z VALUES (0, 4);/*blocked*/
INSERT INTO z VALUES (-1, 4);/*success*/

索引 b 上的 next-key lock(间隙锁+行锁) 的范围是(b=4,id=3)到(b=6,id=5)这个左开右闭区间和(b=6,id=5)到(b=8,id=7)这个开区间。


索引会根据 b 和 id 的值进行排序,插入不同的值,锁的范围是不一样的;分别插入 (b=4,id=2) 和(b=4,id=4)以及其他数时,插入的位置如图所示:

因此,通过next-key锁锁住了范围内的间隙,就不会在出现幻读的现象了


部分转载自:https://www.yuque.com/fanzhengxu/tba6b8/dx0hvw#oWBVa