2.3.4 丢失更新
脏读、不可重复读、幻读都是一个事务写,一个事务读,由于一个事务的写导致另一个事务读到了不该读的数据。
丢失更新是两个事务都是写。丢失更新分为提交覆盖和回滚覆盖;回滚覆盖会被数据库拒绝,所以不可能产生,重点关注提交覆盖。
seq | session A | session B |
1 | SET TRANSACTION ISOLATION LEVEL READ COMMITTED; | SET TRANSACTION ISOLATION LEVEL READ COMMITTED; |
2 | BEGIN; | BEGIN; |
3 | SELECT money FROM account_t WHERE name = ‘A’; | |
4 | SELECT money FROM account_t WHERE name = ‘A’; |
5 | UPDATE account_t SET money = 1100 WHERE name = ‘A’; | |
6 | COMMIT; |
7 | UPDATE account_t SET money = 1100 WHERE name = ‘A’; | |
8 | COMMIT; |
2.4 区别
1)脏读和不可重复读的区别在于,脏读是读取了另一个事务未提交的数据,而不可重复读是读取了另一个事务提交后的修改。本质上都是其他事务的修改影响了本事务的读取。
2)不可重复读和幻读比较类似;不可重复读是两次读取同一条记录,得到不一样的结果;而幻读是两次读取同一个范围内的记录得到的结果集不一样(可能不同个数,也可能相同个数内容不一样,比如x一行后又添加新行)。
3)不可重复读是因为其他事务进行了 update 操作,幻读是因为其他事务进行了 insert或者 delete 操作。
隔离级别 | 回滚覆盖 | 脏读 | 不可重复读 | 幻读 | 提交覆盖 |
READ UNCOMMITTED | NO | YES | YES | YES | YES |
READ COMMITTED | NO | NO | YES | YES | YES |
REPEATABLE REA | NO | NO | NO | YES (手动加锁) | YES (手动加锁) |
SERIALIZABLE | NO | NO | NO | NO | NO |
MySQL InnoDB 引擎的默认隔离级别是可重复读,但是它可以很大程度上避免幻读现象。
1)针对快照读:通过 MVCC 方式解决了幻读。因为可重复读隔离级别下,事务执行过程中看到的数据,一直跟这个事务启动时看到的数据是一致的,即使中途有其他事务插入了一条数据,是查询不出来这条数据的,所以就很好了避免幻读问题。
2)针对当前读:是通过 next-key lock(记录锁+间隙锁)方式解决了幻读。因为当执行 select 语句的时候,会加上 next-key lock,如果有其他事务在 next-key lock 锁范围内插入了一条记录,那么这个插入语句就会被阻塞,无法成功插入,所以就很好了避免幻读问题。
2.5 测试代码
DROP TABLE IF EXISTS `account_t`; CREATE TABLE `account_t` ( `id` INT(11) NOT NULL, `name` VARCHAR(255) DEFAULT NULL, `money` INT(11) DEFAULT 0, PRIMARY KEY (`id`), KEY `idx_name` (`name`) )ENGINE = INNODB AUTO_INCREMENT=0 DEFAULT CHARSET = utf8; SELECT * from account_t; rollback; INSERT INTO `account_t` VALUES (7,'M',1000), (1, 'C', 1000),(2, 'B', 1000),(3, 'A', 1000); insert into -- 脏读读取了另一个事务未提交的修改 (其他事务的修改影响了本事务的读取) SET TRANSACTION ISOLATION LEVEL READ COMMITTED; BEGIN -- 脏读事务1 UPDATE account_t SET money = money - 100 WHERE name = 'A'; -- 脏读事务2 -- SELECT money FROM account_t WHERE name = 'A'; -- SELECT money FROM account_t WHERE name = 'B'; -- 脏读事务1 UPDATE account_t SET money = money + 100 WHERE name = 'B'; -- 脏读事务1 COMMIT; -- 脏读事务2 -- COMMIT -- 不可重复读读取了另一个事务提交之后的修改(其他事务的修改影响了本事务的读取) SET TRANSACTION ISOLATION LEVEL READ COMMITTED; BEGIN -- 不可重复读事务2 SELECT money FROM account_t WHERE name = 'A'; -- 不可重复读事务1 UPDATE account_t SET money = money - 100 WHERE name = 'A'; -- 不可重复读事务1 COMMIT; -- 不可重复读事务2 -- SELECT money FROM account_t WHERE name = 'A'; -- COMMIT -- 幻读两次读取得到的结果集不一样 SET TRANSACTION ISOLATION LEVEL REPEATABLE READ; BEGIN -- 幻读事务2 -- SELECT * FROM account_t WHERE id >= 2; -- 幻读事务1 INSERT INTO account_t(id,name,money) VALUES (4,'D',1000); -- 幻读事务1 COMMIT -- 幻读事务2 -- SELECT * FROM account_t WHERE id >= 2; -- COMMIT; -- 丢失更新(提交覆盖) SET TRANSACTION ISOLATION LEVEL REPEATABLE READ; BEGIN -- 丢失更新事务1 SELECT money FROM account_t WHERE name = 'A'; -- 丢失更新事务2 -- SELECT money FROM account_t WHERE name = 'A'; -- UPDATE account_t SET money = 1100 WHERE name = 'A'; -- COMMIT; -- 丢失更新事务1 UPDATE account_t SET money = 900 WHERE name = 'A'; COMMIT;
三、MVCC
MVCC 是啥?
一般解决不可重复读和幻读问题,是采用锁机制实现,有没有一种乐观锁的问题去处理,可以采用 MVCC 机制的设计,可以用来解决这个问题。取代行锁,降低系统开销。
MVCC 的英文全称是 Multiversion Concurrency Control ,中文意思是多版本并发控制技术。原理是,通过数据行的多个版本管理来实现数据库的并发控制,简单来说就是保存数据的历史版本。可以通过比较版本号决定数据是否显示出来。读取数据的时候不需要加锁可以保证事务的隔离效果。它是通过数据库记录中的隐式字段,undo 日志 ,Read View 来实现的。
MVCC 可以解决什么问题?
1、读写之间阻塞的问题,通过 MVCC 可以让读写互相不阻塞,读不相互阻塞,写不阻塞读,这样可以提升数据并发处理能力。
2、降低了死锁的概率,这个是因为 MVCC 采用了乐观锁的方式,读取数据时,不需要加锁,写操作,只需要锁定必要的行。
3、解决了一致性读的问题,也就是事务启动时根据某个条件读取到的数据,直到事务结束时,再次执行相同条件,还是读到同一份数据,不会发生变化。
多版本并发控制 MVCC(Multiversion Concurrency Control),用来实现一致性的非锁定读。非锁定读是指不需要等待访问记录 X 锁(排他锁)的释放。因此 MVCC 没有读写阻塞,只有写写阻塞,提高了并发性。
在 read committed 和 repeatable read 下,innodb 使用MVCC;但是它们对于快照数据的定义不同。
1)在 read committed 隔离级别下,对于快照数据总是读取被锁定行的最新一份快照数据;
2)而在 repeatable read 隔离级别下,对于快照数据总是读取事务开始时的行数据版本;
3.1 read view
read view 是事务进行快照读的时候产生的读视图,保存了当前事务开启时所有活跃事务的列表。
在 read committed 和 read repeatable 隔离级别下,MVCC 采用read view 来实现的,它们的区别在于创建 read view 时机不同:
1)read committed 隔离级别会在事务中每个 select 都会生成一个新的 read view,也意味着在同一个事务多次读取同一条数据可能出现数据不一致;因为在多次读取期间可能有其他事务修改了该条记录,并提交了;
2)read repeatable 隔离级别是启动事务时生成一个 readview,在整个事务读取数据都才使用这个 read view,这样保证了在事务期间读到的数据都是事务启动前的记录。
3.1.1 构成
1) m_ids :创建 read view 时,当前数据库活跃事务(已启动但未提交的事务)的事务 id 列表;
2)min_trx_id :创建 read view 时, m_ids 中的最小事务 id;
3)max_trx_id :创建 read view 时,当前数据库将为下一个事务分配的事务 id;并不一定是 m_ids 中的最大事务 id;
4)creator_trx_id :创建 read view 所在事务的 id;
3.2 当前读 & 快照读
3.2.1 当前读(Current Read)
当前读是指读取最新提交的数据。当进行当前读时,事务会读取已经提交并且已经写入到数据库中的最新数据。当前读可以获取到最新的数据,但也可能会受到并发事务的影响,因为正在执行的事务可能会修改正在被读取的数据,从而导致一致性问题。对此,当前读使用行级锁定来确保读取最新提交的数据的一致性。
-- 出现当前读的情况 -- 1 select * from table where ... lock in share mode; -- 2 select * from table where ... for update; -- 3 insert into table values(...); -- 4 update table set ?=? where ...; -- 5 delete from table where ...;
3.2.2 快照读(Snapshot Read)
快照读是指读取一个指定时间点的数据快照。当进行快照读时,事务会读取在事务开始之前已提交的数据状态,即使在事务执行期间其他事务进行了更改或提交。这意味着快照读可以提供一种一致性的视图,不受并发事务的干扰。
MVCC 的读指的是快照读(非锁定读),因为没有事务需要对历史数据进行 DML 操作。
在读已提交和可重复读的隔离级别下,对于快照数据的定义不同
∘ RC 级别:读取当前事务锁定行的最新行记录。
∘ RR 级别:读取启动事务时的行记录版本。
-- 出现快照读的情况 select * from table where ......
3.3 聚集索引隐藏列
1)trx_id :当某个事务对某条聚集索引记录进行修改时,将会把当前事务的 id 赋值给 trx_id;
2)roll_pointer :当某个事务对某条聚集索引记录进行修改时,会将上一个版本的记录写到 undo log,然后通过roll_pointer 指向旧版本记录,通过它可以找到修改前的记录;
3.4 事务的可见性问题
3.4.1 事务状态
1)已提交的事务
2)已启动未提交的事务
3)还没开始的事务
3.4.2 事物的可见性
事务可以看到事物本身的修改。具体可以通过如下判断:
1)trx_id < min_trx_id :说明该记录在创建 read_view 之前已经提交,所以对当前事务可见;
2)trx_id >= max_trx_id :说明该记录是在创建 read_view 之后启动事务生成的,所以对当前事务不可见;
3)min_trx_id <= trx_id < max_trx_id :此时需要判断是否在 m_ids 列表中:
∘ 在列表中:生成该版本记录的事务仍处于活跃状态,该版本记录对当前事务不可见;
∘ 不在列表中:生成该版本记录的事务已经提交,该版本记录对当前事务可见;
四、redo log
redo 日志用来实现事务的持久性;
内存中包含 redo logbuffer,磁盘中包含 redo log file;
当事务提交时,必须先将该事务的所有日志写入到重做日志文件进行持久化,待事务的 commit 操作完成才完成了事务的提交;
redo log 顺序写,记录的是对每个页的修改(页、页偏移量、以及修改的内容);在数据库运行时不需要对 redo log 的文件进行读取操作;只有发生宕机的时候,才会拿redo log 进行恢复。
五、undo log
undo 日志用来帮助事务回滚以及 MVCC 的功能;存储在共享表空间中;
undo 是逻辑日志,回滚时将数据库逻辑地恢复到原来的样子,根据 undo log 的记录,做之前的逆运算;比如事务中有 insert 操作,那么执行 delete 操作;对于 update 操作执行相反的 update 操作;
同时 undo 日志记录行的版本信息,用于处理 MVCC 功能