参考资料
https://dev.mysql.com/doc/refman/5.7/en/innodb-locking-transaction-model.html
ACID模型
ACID模型是一组数据库设计原则, 强调业务数据存储的可靠和关键型应用程序运行的稳定 。InnoDB存储引擎遵循了ACID设计,可以保证数据不会因软件崩溃和硬件故障等异常情况而丢失。其中ACID分别是
- A : atomicity 原子性:指一个事务中的所有操作,要么全部完成,要么全部失败
- C : consistency 一致性:指事务操作前和操作后,数据均满足一组约束,同时数据库保持一致性状态,只是从一个有效状态转移到另一个有效状态。以A、B两个账户互相转账为例,A、B两个账户总额为5000,那么不管他们之间怎么转账,两个账户的总额都是5000,这就是有效状态;同时A、B两个账户的余额不小于0,这就是满足约束。
- I: : isolation 隔离性:指多个事务并行时,事务之间互相不影响
- D : durability 持久性:指事务提交后,即使系统崩溃,数据仍然不会丢失
InnoDB 实现ACID模型所使用的一些组件工具
- redo log:用于事务数据恢复,与持久性相关
- undo log:用于事务回滚和一致性读,与原子性相关
- MVCC 和 Lock:用于控制事务并发,与隔离性有关
事务隔离
隔离级别是一种设置,用于在多个事务同时进行更改和执行查询时微调性能与可靠性、一致性和结果可再现性之间的平衡。InnoDB提供了SQL 1992标准中描述的所有四种事务隔离级别:
- READ UNCOMMITTED
- READ COMMITTED
- REPEATABLE READ
- SERIALIZABLE
REPEATABLE READ是InnoDB默认的隔离级别,但在实际应用中,READ COMMITTED使用会更加广泛。
InnoDB使用MVCC和锁策略支持各个事务隔离级别。使用READ COMMITTED隔离级别,可以最小化锁的开销,提高并发性能,尤其是在批量更新时。
在命令行或配置文件中使用 --transaction-isolation可配置全局事务隔离级别;也可以在连接MySQL时动态设置当前连接的隔离级别,单独配置将覆盖全局配置。
https://dev.mysql.com/doc/refman/5.7/en/server-options.html#option_mysqld_transaction-isolation
并行事务问题
当并行事务操作同一个数据时,存在以下问题
脏读
指的是事务A读取了事务B修改了但还未提交的数据。如果事务B之后回滚了,事务A读取到的就是脏数据。
不可重复读
指的是在一个事务中,前后多次执行同一条查询语句(查询单个记录)时,查询到数据不一致的情况。在事务前后查询的间隙,如果数据被其它事务修改,就会产生不可重复读问题。
幻读
指的是,在一个事务中,前后多次查询符合条件的记录数量时,查询到记录数量不一致的情况。在事务A前后查询的间隙,如果事务B增加了一条符合事务A查询条件的记录,事务A中前后查询符合条件的记录数时就会产生幻读问题。
不同隔离级别下并行事务存在的问题
可以看到,性能开销最小的读未提交,所有并发问题都存在;性能开销最大的串行,解决了所有事务并发问题。
可重复读 REPEATABLE READ
在可重复读级别下,解决了脏读、不可重复读,并且很大程度上防止了幻读。加锁的基本单位是临键锁(有效防止幻读),进行一致性读、加锁读、新增数据时处理逻辑如下
- 新增数据
进行Insert时,先针对要插入索引记录中的间隙申请插入意向锁,如果该间隙已经被加锁,则等待;若没被加锁则获得插入意向锁,然后插入数据。
- 一致性读
解决不可重复读和脏读,事务中的普通读都是一致性读,并且一致性读是基于当前事务第一次查询时建立的快照。这表示在事务中执行普通读不会读取到未提交的数据,并且前后多次执行同一个普通读语句时,结果是一致的。有一个例外是,针对事务本身更新的数据,在更新之后进行的查询,获取的数据是最新的。
事务中第一次创建快照的时间点为:
- start transaction启动事务后中第一次使用select语句
- 启动事务时创建start transaction with consistent snapshot
- 加锁读
很大程度上防止了幻读,事务中进行加锁读,如(SELECT with FOR UPDATE or LOCK IN SHARE MODE)、UPDATE和DELETE语句时,加锁策略取决于语句上的搜索条件
- 对于唯一索引上的等值查询,有值时,只会锁定查询的记录本身。如select id where id = 2 for update,其中id为主键索引。如果有id等于2的数据时,这条语句只会锁定id=2的记录[2],不会锁定间隙;如果没有则会锁定最近的间隙,例如有1,3两条记录,则间隙锁区间为(1, 3)。
- 对于其他的搜索条件,使用间隙锁或者临键锁来锁定扫描到的索引记录,阻止其他会话插入到临键锁所覆盖的间隙中。只在唯一索引上会使用间隙锁,因为唯一索引本身的约束就阻止了该记录的新增,无需重复锁定。
读提交 READ COMMITTED
在读提交隔离级别下,只解决了脏读问题,存在不可重复读和幻读问题,但提高了数据库并发性能。加锁的基本单位是记录锁,进行一致性读、加锁读、新增数据时处理逻辑如下
- 新增数据
进行Insert时,不需要加插入意向锁,直接插入数据,通过page latch控制并发。
- 一致性读
解决脏读,在事务中,每次普通SELECT都是一致性读。但是每次一致性读都会创建新的ReadView,然后再进行一致性读。这表示事务中进行普通SELECT不会读取到未提交的数据,但执行多个普通SELECT语句时,执行结果可能是不同的。
- 加锁读
事务中进行加锁读,如(SELECT with FOR UPDATE or LOCK IN SHARE MODE)、UPDATE和DELETE语句时,只锁定记录本身,不会锁定索引间的间隙。由于加锁读时,只使用了记录锁,所以完全无法避免幻读。
READ COMMITTED隔离级别只支持基于row的binlog。如果使用READ COMMITTED和binlog format=MIXED,数据库将自动使用基于row的binlog。
可重复读和读提交加锁区别
在读提交隔离级别下
- 对于UPDATE或DELETE语句,InnoDB只为更新或删除的行保留锁。MySQL评估WHERE条件后,将释放不匹配行的记录锁,这可以降低死锁的概率。
- 执行UPDATE时,如果该行已经锁定,InnoDB会执行半一致性(semi-consistent)读,将该行最新提交的版本返回给MySQL,以便MySQL可以确定该行是否符合UPDATE的WHERE条件。如果符合,MySQL再次读取该行,InnoDB尝试对该行加x锁,若锁冲突,则等待加锁。
半一致性可以提高并发性能,但也可能导致并发更新冲突,需要使用乐观锁或者悲观锁来控制并发访问,确保在更新数据时避免出现冲突,从而保证数据的一致性和正确性。
例如,创建一个table,并新增数据。表中没有设置主键索引,所以会有一个内部的自增索引。在进行搜索时,索引扫描会使用这个隐藏的内部索引。
CREATE TABLE t (a INT NOT NULL, b INT) ENGINE = InnoDB;
INSERT INTO t VALUES (1,2),(2,3),(3,2),(4,3),(5,2);
COMMIT;
启动一个session A,执行update
# Session A
START TRANSACTION;
UPDATE t SET b = 5 WHERE b = 3;
启动一个session B,执行update
# Session B
UPDATE t SET b = 4 WHERE b = 2;
当使用默认的REPEATABLE READ隔离级别时,执行Update语句时,会首先对读取到的每一行添加x排他锁(如果有锁冲突则等待),然后再决定是否更新它,并且需要等到事务结束后才释放锁。因为第一个Update语句没有走索引,所以执行时会扫描隐藏索引上的全部记录,并将索引上的记录和间隙全部锁定,直到事务结束才释放。
x-lock(1,2); retain x-lock
x-lock(2,3); update(2,3) to (2,5); retain x-lock
x-lock(3,2); retain x-lock
x-lock(4,3); update(4,3) to (4,5); retain x-lock
x-lock(5,2); retain x-lock
第二个UPDATE语句在试图获取任一行的x锁时阻塞(因为第一次UPDATE保留了所有行和间隙的锁,而在REPEATABLE READ隔离级别时,需要先对扫描到的索引记录和间隙加锁,再进行更新,所以第二个UPDATE直接阻塞),直到第一次UPDATE提交或回滚后才继续执行
x-lock(1,2); block and wait for first UPDATE to commit or roll back
如果使用READ COMMITTED,执行UPDATE时,索引扫描记录后,先对扫描到的记录申请加X记录锁
- 如果索引记录已经被其它事务加锁,则使用半一致性读,先去读取行的最新版本数据,根据where子句判断该行是否符合更新条件,符合则申请X记录锁;不符合则放弃该行。
- 如果索引记录没有锁定,则先加X记录锁,然后读取该行数据,根据where子句判断该行是否符合更新条件,符合则更新,不符合则先释放该行的锁。
执行第一个UPDATE会对它读取的每一行获取x锁,并释放未修改的行上面的锁
x-lock(1,2); unlock(1,2)
x-lock(2,3); update(2,3) to (2,5); retain x-lock
x-lock(3,2); unlock(3,2)
x-lock(4,3); update(4,3) to (4,5); retain x-lock
x-lock(5,2); unlock(5,2)
执行第二个UPDATE语句。针对已锁定的行,InnoDB使用半一致性读,MySQL判断该行是否匹配where语句,如果匹配再去申请对该行加x锁;不匹配则直接放弃。针对未锁定的行,先加锁,再判断是否是需要更新的行,是则更新;不是则放弃。
x-lock(1,2); update(1,2) to (1,4); retain x-lock
x-lock(2,3); unlock(2,3)
x-lock(3,2); update(3,2) to (3,4); retain x-lock
x-lock(4,3); unlock(4,3)
x-lock(5,2); update(5,2) to (5,4); retain x-lock
读未提交 READ UNCOMMITTED
与READ COMMITTED类似,只是存在脏读问题。
串行化 SERIALIZABLE
与REPEATABLE READ类似,只是会隐式的将普通的SELECT转换为SELECT ... LOCK IN SHARE MODE。
标签:事务,加锁,READ,lock,模型,UPDATE,索引,InnoDB From: https://www.cnblogs.com/cd-along/p/18107332