《丁奇-MySQL45讲-03》之归纳总结
03 | 事务隔离:为什么你改了我还看不见
-
事务的特性:ACID,即对应原子性、一致性、隔离性、持久性。通过undo log来保证原子性,能够撤销事务内的所以操作来保证原子性,要么是全部都成功,通过redo log来保证持久性,会根据策略进行刷脏,通过锁+MVCC的方式来保证隔离性,而一致性指的是从一个正确的状态迁移到另外一个正确的状态下,其实就是通过事务中的AID来保证C。
-
并发事务可能出现的问题:在不同的隔离级别下,多个事务同时执行可能会出现
脏读、不可重复读、幻读。其中脏读指的是A事务读到了B事务未提交的数据,也就是说B事务对数据的修改(还未提交)对于A事务可以直接看到,这要是B事务发生了回滚,那A事务所做的一系列操作岂不是凉凉;不可重复读指的是A事务多次读取同一条记录的结果值不同,也就是说在同一个事务A中,我多次读取同一条记录,结果竟然是不一样的,那我还怎么写业务;幻读指的是A事务多次读取数据的结果集数量不同,也就是说一会是3条记录,一会是10条记录,两者的区别在于,不可重复读代表的是多次读一条记录的结果值不同,幻读代表的是多次读一批记录的数量不同。 -
事务的隔离级别:
读未提交(read uncommited)、读已提交(read commited)、可重复读(repeatable read)、串行化(serializable)。读未提交指的是当前事务可以读到未提交事务的修改,这种隔离级别就会造成脏读、不可重复读、幻读;读已提交指的是当前事务能够读取已提交事务的修改,这种隔离级别会造成不可重复读、幻读;可重复读指的是当前事务一开始读的数据是什么样子,那在它提交之前一直都是这样子,即使多次读取,个人认为它不会造成幻读(通过MVCC + 锁的方式);串行化指的是对于同一行记录,写操作会加写锁,读操作会加读锁,当出现读写锁冲突的时候,后访问的事务必须等前一个事务执行完成,才能继续执行。隔离级别越高,其性能越低。 -
MVCC:每一行记录都有两个隐藏列,一个是data_trx_id(更新这条行记录的事务ID),一个是data_roll_ptr(指向undo log的指针,每次修改记录都会生成undo log,每条undo log会指向更早版本的undo log,从而形成一条版本链)。在RU级别下,直接读取版本的最近记录即可,对于串行化来说,则是通过加锁互斥,并不需要MVCC的帮助,因此MVCC只针对RC和RR,这两个级别最关键的点就在于能看到哪些版本,我们都知道RC能看到已提交事务的修改,而RR则从一一开始是什么样子后面就都是什么样子,为了决定哪些版本对当前事务可见性问题,设计了
ReadView。ReadView有trx_ids,代表当前活跃的事务ID列表(未提交的事务),其中最小值为up_limit_id,最大值为low_limit_id,当前事务为creator_trx_id,事务ID是开启事务时InnoDB分配的,其大小决定了事务开启的先后顺序,因此可以根据事务ID的大小关系来决定版本记录的可见性,如下判断过程:
在RR级别下,每执行第一个select语句时都会将当前系统中的所有活跃事务(未提交的事务)拷贝到一个列表来生成ReadView,后续所有的select都会复用这个ReadView,这也是造成可重复读的关键点。
在RC级别下,每次执行select语句时,都会重新将当前系统中的所有活跃事务拷贝到一个列表来重新生成ReadView,所以它才会造成不可重复读。
- data_trx_id < up_limit_id || data_trx_id == creator_trx_id(可见)
如果该行记录的事务ID小于ReadView中的最小活跃事务ID,那么可以说明的是该行记录在开始当前事务之前就已经存在了(事务ID大小决定了开始事务的顺序);该行记录的事务ID等于creator_trx_id,说明该行记录是当前事务自己生成的,自己生成的数据当然能看见。
- data_trx_id > low_limit_id(不可见)
如果该行记录的事务ID大于ReadView中的最大活跃事务ID,那么可以说明的是该行记录是在创建ReadView之后才提交的,是不可见的。
- data_trx_id是否在trx_ids列表中
如果该行记录的事务ID在trx_ids列表中存在,那么说明生成该行记录的事务还在活跃中(还在提交中,个人认为这样子描述不是很准确,在RR级别下,就算在后续提交了,其事务ID仍然在trx_ids列表中,提交只不过更新了data_trx_id,所以在其他事务看来,认为它仍然还是未提交的,但实际上它已经提交了...),那么我自然是看不见修改的数据,则需要根据data_roll_ptr找到该行记录的上一个版本,然后根据上一个版本的data_trx_id重新按照如上的过程进行判断;如果该行记录的事务ID在trx_id列表中不存在,说明生成该行记录的事务已经提交了,是具有可见性的。RR级别下每次的查询都不会使trx_ids列表发生改变,而RC级别下每次的查询都会重新生成ReadView,导致trx_ids列表发生变化,也就造成了不可重复读问题。
-
undo log:专门用于事务的回滚和MVCC。在事务开始前,MySQL会保存待修改的记录,而在事务提交后并不会马上删除undo log,而是会打上删除标记,而后由purge线程去判断是否可以删除,对于插入语句来说,由于只有当前事务能够看到其插入的记录,所以产生的undo loh对于其他事务并没有用,那么在事务提交后就可以直接删除了。我比较好奇的是怎么判断是否不在需要undo log,undo log会拿记录的事务ID跟ReadView中的up_limit_id进行比较,如果事务ID小于up_limit_id,那么说明没有事务需要当前的undo log,那么将会打上删除标记(该结论纯粹个人理解)。MySQL中使用回滚段(rollback segment)来存储undo log,当某一个undo log被删除后并不会马上释放空间,删除掉undo log的空间将会被复用,只有整个回滚段被删掉了,才有可能使空间释放,文件变小。
-
如何避免长事务:首先长事务会导致保留大量的回滚日志,占用大量的存储空间,为了避免长事务可以设置SQL语句的执行时间:SET_MAX_EXECUTION_TIME;监控innodb_trx表,设置长事务阈值,超过就报警或者kill;可以单独为undo log分配空间,清理较为方便。
浙公网安备 33010602011771号