- 脏读
- 事务1 更改数据a的值为B,但没有提交, 其他事务读取a的值是B, 那么这就是脏读
- 危害:如果事务1 回滚了,那么其他事务读到的数据就是错误的。
- 不可重复读
- 事务1 第一次读取数据a的值是A,修改成C,提交事务。
- 事务2 第一次读取数据a的值是A,在事务2提交以后重新读取数据a的值是C,这种情况叫不可重复读
- 危害:
- 某订单已支付完成,这时候会有两个事务同时执行
- 给用户发短信
- 未发货状态:短信1:支付成功,订单在配货中
- 已发货状态:短信2:订单已发货
- 给用户发货
- 订单状态优先更改成了已发货,这时候短信服务发现状态是已发货,短信1就不会发送了
- 幻读
- 事务1 查询数据A不存在, 然后事务2, 插入了数据A, 这时候事务1再插入数据A, 会提示数据已存在,不能插入。 这就是幻读
- 危害:唯一键冲突,其中一个事务需要回滚
从Read committed到 Read read思路
要达到这样的目的:
处于事务中的查询,只能查到在创建事务之前commit的数据,不能查到之后commit的数据
解决思路:
- 引入版本号概念,每次修改都把修改之前的数据作为老版本保存到版本库里面
- 开启事务相当于创建了一个新版本,事务id == 版本id
- 通过事务id查询不同的版本数据
- 如果有数据C且C数据最后一次修改的事务id是c‘,简称Cc‘
- 开启事务A,
- 事务A的事务id tr_id = a‘
- A保存c‘ 叫做事务A下的数据C的最后一次修改事务id,简称:C_tr_id = c’
- 开启事务B,
- 事务B的事务id tr_id = b‘
- B保存c‘ 叫做事务B下的数据C的最后一次修改事务id,简称:C_tr_id = c’
- 事务B修改了数据C,C_tr_id = b’,新数据变成了Cb’
- 老数据Cc‘保存到版本库
- 事务A读取数据C,从版本库 匹配 C_tr_id = c’, 查到Cc’
- 事务B读取数据C,从版本库 匹配 C_tr_id = b’, 得到Cb‘
- 事务B commit; 数据C的最后一次修改的事务id变成了b‘
- 事务A读取数据C,由于C_tr_id并未更新,从版本库 匹配 C_tr_id = c’, 得到Cc‘
- 开启事务A,
画个图
-
开启事务A和B,都指向数据C
-
事务B修改了数据C,事务B修改之后事务F开启,事务F发现数据未commit,去查找上一版本,所以C_tr_id = c’
-
事务B commit了,在事务B commit后开启事务G,由于已commit,事务G的C_tr_id = b’
-
虽然实现了单个字段的多版本控制,但是如果要控制很多条数据, 就需要非常多的字段,显然这么设计不合理,所以要优化。从3总结出以下几点:
- c’对所有事务可见,因为c‘是最早的事务id,假设事务id是自增的,所以c‘是最小的事务id
- 事务B提交之后
- 因为事务A比事务B先开启,事务A对b‘不可见,所以a’ < b’
- 因为事务B比事务F先开启,事务F对b‘不可见, 所以要记录比事务F先开启的所有活跃事务集合,事务B显然在这个集合中的事务id都是不可见的
- 因为事务G在事务B后开启,且事务B已commit,所以事务B并不在当前活跃事务集合,所以事务G对b‘可见
- 所以开启事务之后要记录3个值
- 当前最小的活跃事务id
- 当前最大的活跃事务id
- 当前活跃队列集合
然后修改成这样子了
-
我把mvcc的一些名词对上号,我想你也可以写出来一个简化版的mvcc。PS:图中并非真实数据结构,只是模拟的!!!
-
最后 从 Read read 到 Read committed要怎么做?
只要每次读取的时候把 Read View 重置成最新的即可
比如:事务A在事务B提交之后,在查询数据C,把 tr_max = h’, tr_list = a’,f’,g’,这样就变成了事务G的状态,可以读取到Cb‘了