为什么需要隔离级别?
当数据库上有多个事务同时执行的时候,就可能出现脏读(dirty read)、不可重复读(non-repeatable read)、幻读(phantom read)的问题,为了解决这些问题,就有了“隔离级别”的概念。
隔离级别有哪几种?
SQL标准的事务隔离级别包括:读未提交(read uncommitted)、读提交(read committed)、可重复读(repeatable read)和串行化(serializable )。
- 读未提交:一个事务还没提交时,它做的变更就能被别的事务看到。
- 读提交:一个事务提交之后,它做的变更才会被其他事务看到。
- 可重复读:一个事务执行过程中看到的数据,总是跟这个事务在启动时看到的数据是一致的。MySQL默认的隔离级别是RR。
- 串行化:对于同一行记录,“写”会加“写锁”,“读”会加“读锁”。当出现读写锁冲突的时候,后访问的事务必须等前一个事务执行完成,才能继续执行。
隔离级别可以简单理解为在查询的某个时间点,给数据库做了一个快照(一般叫做视图,这里为了区分数据库中的2个视图的概念,所以这里我把它叫做快照)
隔离级别的实现
undo log
在 MySQL 中,实际上每条记录在更新的时候都会同时记录一条回滚操作。记录上的最新值,通过回滚操作,都可以得到前一个状态的值。除了记录变更记录(redo log),还会记录一条变更相反的回滚操作记录(undo log)。
对于一条记录,在不同的时刻启动事务,会有不同的 read-view。
隐藏列
数据表增加两个隐藏列DATA_TRX_ID
和DATA_ROLL_PTR
,用于实现MVCC
ReadView
主要属性
- mIds:代表生成ReadView时,当前活跃所有的事务ID,活跃的意思就是事务开启了还没提交,事务开启,事务ID会自增,事务ID是一个全局自增的数字。
- min_trx_id:表示当前活跃的mIds中最小的事务ID。
- max_trx_id:表示生成ReadView时,最大的事务ID。(这里一定不要理解成mIds中最大的ID。因为事务ID虽然是全局递增的,但是并不代表事务ID大的一定要在事务ID小的后面提交,也就是事务开启有先后,但是事务结束的先后和开启的先后并不是完全一致的,毕竟事务有长有短。)
creator_trx_id:该ReadView在哪个事务里创建的。
数据访问规则
如果被访问版本的 data_trx_id > min_trx_id,说明生成该版本的事务在 ReadView 生成前就已经提交了,那么该版本可以被当前事务访问。
- 如果被访问版本的 data_trx_id < max_trx_id,说明生成该版本数据的事务在生成 ReadView 后才生成,那么该版本不可以被当前事务访问。
- 如果被访问版本的 data_trx_id属性值在最大值和最小值之间(包含),那就需要判断一下 trx_id 的值是不是在 m_ids 列表中。如果在,说明创建 ReadView 时生成该版本所属事务还是活跃的,因此该版本不可以被访问;如果不在,说明创建 ReadView 时生成该版本的事务已经被提交,该版本可以被访问。
ReadView中通过最大事务ID,mIds最小事务ID,mIds活跃事务列表,将当前要读的数据的事务ID分成了3种情况:
- 要么小于mIds的最小事务ID;
- 要么大于生成ReadView的当前的最大事务ID;
- 要么处于最大最小值之间,这时候就有两种情况,因为并不是最大最小值之间就一定是活跃的,毕竟先开启的事务并不一定会先结束,事务有大小长短。在mIds中就是还没提交的活跃版本,不可被读取,不在就是已经提交的版本,可以被读取。
当一个事务要读取一行数据,首先用上面规则判断数据的最新版本,如果发现可以访问就直接读取了,如果发现不能访问,就通过DATA_ROLL_PTR指针找到undo log,递归往下去找每个版本,直到读取到自己可以读取的版本为止,如果读取不到那就返回空。
ReadView生成时机
RR和RC这两个隔离级别的一个很大不同就是:生成ReadView的时机不同,READ COMMITTD在每一次进行普通SELECT操作前都会生成一个ReadView,而REPEATABLE READ只在第一次进行普通SELECT操作前生成一个ReadView,数据的可重复读其实就是ReadView的重复使用。
什么是MVCC?
MVCC(Multi-Version Concurrency Control ,多版本并发控制)指的就是,在使用读已提交(READ COMMITTD)、可重复读(REPEATABLE READ)这两种隔离级别的事务,在执行普通的SELECT
操作时,访问记录的版本链的过程,这样子可以使不同事务的读-写、写-读操作并发执行,从而提升系统性能。