
MySQL事物简析
参考:事务隔离级别中的可重复读能防幻读吗?_Happy Simon-CSDN博客
本文有参照其他博客的地方,若有侵权请告知本人删除
ACID原则
原子性
该事物内的操作要么都执行,要么都不执行
一致性
事务的执行不能破坏数据库数据的完整性和一致性,一个事务在执行之前和执行之后,数据库都必须处于一致性状态。如果数据库系统在运行过程中发生故障,有些事务尚未完成就被迫中断,这些未完成的事务对数据库所作的修改有一部分已写入物理数据库,这是数据库就处于一种不正确的状态,也就是不一致的状态
隔离性
多个事物进程之间互不干涉
持久性
一旦提交不可逆,数据就持久化到数据库
隔离性引发的问题
并发引发的问题
将MySQL隔离性按等级划分,由低到高为读未提交、读已提交、可重复度、串行化
脏读
- (针对未提交数据)如果一个事务中对数据进行了更新,但事务还没有提交,另一个事务可以“看到”该事务没有提交的更新结果,这样造成的问题就是,如果第一个事务回滚,那么,第二个事务在此之前所“看到”的数据就是一笔脏数据。
- 读已提交以上的隔离等级可以避免
不可重复读
- (针对其他提交前后,读取数据本身的对比)不可重复读取是指同一个事务在整个事务过程中对同一笔数据进行读取,每次读取结果都不同。如果事务1在事务2的更新操作之前读取一次数据,在事务2的更新操作之后再读取同一笔数据一次,两次结果是不同的,所以,Read Uncommitted也无法避免不可重复读取的问题。【举例:事物1执行期间有个事物2,事物2对事物1要操作的数据进行修改操作,而事物1在事物2前后都有查询操作,导致前后查询的那些条数的数据的内容不同】
- 可重复度以上的隔离等级可以避免
幻读
- “幻读”又叫"幻象读",是’‘不可重复读’‘的一种特殊场景
- (针对其他提交前后,读取数据条数的对比) 幻读是指同样一笔查询在整个事务过程中多次执行后,查询所得的结果集是不一样的。幻读针对的是多笔记录。在Read Uncommitted隔离级别下, 不管事务2的插入操作是否提交,事务1在插入操作之前和之后执行相同的查询,取得的结果集是不同的,所以,Read Uncommitted同样无法避免幻读的问题。【举例:事物1执行期间有个事物2,事物2对事物1要操作的数据进行新增或删除操作,而事物1在事物2前后都有查询操作,导致前后查询的数据条数不同】
- 可重复度以上的隔离等级可以避免(不能防止所有的幻读)
MySQL有的隔离性
Read Uncommitted
- 又称:读未提交
- 最低的隔离级别,Read Uncommitted最直接的效果就是一个事务可以读取另一个事务并未提交的更新结果。
Read Committed
- 又称:读已提交
- Read Committed通常是大部分数据库采用的默认隔离级别,它在Read Uncommitted隔离级别基础上所做的限定更进一步, 在该隔离级别下,一个事务的更新操作结果只有在该事务提交之后,另一个事务才可能读取到同一笔数据更新后的结果。 所以,Read Committed可以避免Read Uncommitted隔离级别下存在的脏读问题, 但,无法避免不可重复读取和幻读的问题。
Repeatable Read
- 又称:可重复读取
- Repeatable Read隔离级别可以保证在整个事务的过程中,对同一笔数据的读取结果是相同的,不管其他事务是否同时在对同一笔数据进行更新,也不管其他事务对同一笔数据的更新提交与否(允许其他事务继续访问该行数据,但是未提交的写事务将会禁止其他事务访问该行,会对该写锁一直保持直到到事务提交)。 Repeatable Read隔离级别避免了脏读和不可重复读取的问题,但无法避免幻读**。(**mysql默认隔离级别)
Serializable
-
又称:串行化
-
最为严格的隔离级别,所有的事务操作都必须依次顺序执行,可以避免其他隔离级别遇到的所有问题,是最为安全的隔离级别, 但同时也是性能最差的隔离级别,因为所有的事务在该隔离级别下都需要依次顺序执行,所以,并发度下降,吞吐量上不去,性能自然就下来了。 因为该隔离级别极大的影响系统性能,所以,很少场景会使用它。
-
通常情况下,我们会使用其他隔离级别加上相应的并发锁的机制来控制对数据的访问,这样既保证了系统性能不会损失太大,也能够一定程度上保证数据的一致性。
可重复读的实现
实现的两类锁
- 悲观锁
- 正如其名,它指的是对数据被外界(包括本系统当前的其他事务,以及来自外部系统的事务处理)修改持保守态度,因此,在整个数据处理过程中,将数据处于锁定状态。
- 读取数据时给加锁,其它事务无法修改这些数据。修改删除数据时也要加锁,其它事务无法读取这些数据。
- 悲观锁的数据库性能的大量开销,特别是对长事务而言,这样的开销往往无法承受。
- **排它锁(X锁)**就是一个典型的悲观锁(又称写锁。若事务T对数据对象A加上X锁,事务T可以读A也可以修改A,其他事务不能再对A加任何锁,直到T释放A上的锁。这保证了其他事务在T释放A上的锁之前不能再读取和修改A)
- 乐观锁
- 相对悲观锁而言,乐观锁机制采取了更加宽松的加锁机制【允许对其进行改动之外的操作,当发生改动之后才拒绝访问】。悲观锁大多数情况下依靠数据库的锁机制实现,以保证操作最大程度的独占性。
- 乐观锁大多采用基于数据版本记录机制实现
- **共享锁(S锁)**就是典型的乐观锁(又称读锁,若事务T对数据对象A加上S锁,则事务T可以读A但不能修改A,其他事务只能再对A加S锁,而不能加X锁,直到T释放A上的S锁。这保证了其他事务可以读A,但在T释放A上的S锁之前不能对A做任何修改)
当前读和快照读
- 当前读
- 像select lock in share mode(共享锁), select for update ; update, insert ,delete(排他锁)这些操作都是一种当前读
- 它读取的是记录的最新版本,读取时还要保证其他并发事务不能修改当前记录,会对读取的记录进行加锁
- 一种典型的悲观锁实现
- 快照读
- 像不加锁的select操作就是快照读,即不加锁的非阻塞读
- 快照读的前提是隔离级别不是串行级别,串行级别下的快照读会退化成当前读
- 为什么需要快照读?基于提高并发性能的考虑,快照读的实现是基于多版本并发控制,即MVCC,可以认为MVCC是行锁的一个变种,但它在很多情况下,避免了加锁操作,降低了开销
- 基于多版本,即快照读可能读到的并不一定是数据的最新版本,而有可能是之前的历史版本
undo日志和purge
undo log
- 在MySQL底层架构中有介绍,它是用于回滚时候找到修改的历史数据,只要分为插入insert undo log和修改update undo log
- insert undo log只在事务回滚时需要,并且在事务提交后可以被立即丢弃
- 事务在进行update或delete时产生的undo log; 不仅在事务回滚时需要,在快照读时也需要;所以不能随便删除,只有在快速读或事务回滚不涉及该日志时,对应的日志才会被purge线程统一清除
puage
- 从前面的分析可以看出,为了实现InnoDB的MVCC机制,更新或者删除操作都只是设置一下老记录的deleted_bit,并不真正将过时的记录删除。
- 为了节省磁盘空间,InnoDB有专门的purge线程来清理deleted_bit为true的记录。为了不影响MVCC的正常工作,purge线程自己也维护了一个read view(这个read view相当于系统中最老活跃事务的read view);如果某个记录的deleted_bit为true,并且DB_TRX_ID相对于purge线程的read view可见,那么这条记录一定是可以被安全清除的。
InnoDB三种锁
行数
- Record Locks(行锁),单个行记录上的锁
- 锁定的时候改行记录的设置的索引,而不是记录本身
- 如果表没有设置索引,InnoDB 会自动在主键上创建隐藏的聚簇索引,因此 Record Locks 依然可以使用
间隙锁
- Gap Lock(间隙锁),锁定一个范围,但不包括记录本身【更准确的说法是锁住索引B+Tree范围内的间隙,也就是叶节点中前后的空间】
- GAP锁的目的,是为了防止同一事务的两次当前读,出现幻读的情况
- 锁定索引之间的间隙,但是不包含索引本身
- 示例:SELECT * FROM 表1 WHERE 列1 BETWEEN 10 and 20 FOR UPDATE; – 在当前事物处理完之前其他事物不能在10-20之间插入数据
Nex-Key Lock
- 行锁 + 间隙锁,锁定一个范围,并且锁住记录本身,对于行的查询都采用了该方法,主要目的是解决幻读
- 当查询的索引含有唯一属性的时候,Next-Key Lock 会进行优化,将其降级为Record Lock,即仅锁住索引本身,不是范围
MVCC(多版本并发控制器)
概述
- MVCC在MySQL InnoDB中的实现主要是为了提高数据库并发性能,用更好的方式去处理读-写冲突,做到即使有读写冲突时,也能做到不加锁,非阻塞并发读
- 通过隐藏的列记录版本号,Read View 和 undoLog实现
实现原理
隐藏的列
- 在InnoDB中,会在每行数据后添加两个额外的隐藏的值来实现MVCC,这两个值一个记录这行数据何时被创建,另外一个记录这行数据何时过期(或者被删除)【MySQL若没有指定主键还是生成隐式主键列】。
- DB_TRX_ID:6byte,最近修改(修改/插入)事务ID【记录创建这条记录/最后一次修改该记录的事务ID】
- DB_ROLL_PTR:7byte,回滚指针,指向这条记录的上一个版本(存储于rollback segment里)
- DB_ROW_ID:6byte,隐含的自增ID(隐藏主键),如果数据表没有主键,InnoDB会自动以DB_ROW_ID产生一个聚簇索引
- 实际还有一个删除flag隐藏字段, 既记录被更新或删除并不代表真的删除,而是删除flag变了
Read View
-
Read View就是事务进行快照读操作的时候生产的读视图(Read View),在该事务执行的快照读的那一刻,会生成数据库系统当前的一个快照,记录并维护系统当前活跃事务的ID(当每个事务开启时,都会被分配一个ID, 这个ID是递增的,所以最新的事务,ID值越大)
-
Read View包含creator_trx_id,up_limit_id,low_limit_id,m_ids【版本号是递增的】
- creator_trx_id表示生成该readview的事务的事务id,就是当前版本号
- min_limit_id表示可见版本号当前最小id的事物
- max_limit_id表示下一个事物的版本号,即当前最大事物版本号+1
- m_ids表示当前所有可见的版本号
-
可见事物版本号规则【假设当前操作的事物a版本号为trx_id
,对应的阅读视图为ReadView
】
trx_id
属性值与ReadView
中的creator_trx_id
值相同 - 表示当前事务a访问修改过的记录,所以该版本可以被当前事务访问
trx_id
属性值 < ReadView
中的min_trx_id
值 - 表明生成该版本的事务在当前事务生成
ReadView
前已经提交,所以该版本可以被当前事务访问
trx_id
属性值 >= ReadView
中的max_trx_id
值 - 表明生成该版本的事务在当前事务生成
ReadView
后才开启,所以该版本不可以被当前事务访问
trx_id
属性值在ReadView
的min_trx_id
和max_trx_id
之间,那就需要判断一下trx_id
属性值是不是在m_ids
列表中, - 如果在,说明创建
ReadView
时生成该版本的事务还是活跃的,该版本不可以被访问; - 如果不在,说明创建
ReadView
时生成该版本的事务已经被提交,该版本可以被访问
- 重复上述四个判断,若当前所有历史记录都不符合,表明该事物完全不可见,查询结果就不会包含该记录
-
示例
当前事物版本号 5,4,3,2,1, 已提交事物 4,1
creator_trx_id = 5
min_limit_id = 1
max_limit_id = 6
trx_ids = [5,3,2]
-
生成ReadView的时机
- RC隔离级别:每次读[快照读]取数据前都会生成【同一事物下】
- RR隔离级别:在第一次读[快照读]取数据前生成【同一事物下】
-
分析为何RR能解决不重复度而RC不行?
- RR隔离级别
- 由于事物B内的快照读采用的是第一次的产生的ReadView,所以不会产生不可以重复读和幻读
事物A | 事物B |
---|
开启事物 | |
| 开启事物 |
快照读(无影响)查询x=5 | |
| 快照读(无影响)查询x=5 |
更改x=4 | |
提交事物 | |
| 快照读x=5[用的第一快照读的ReadView] |
| 当前读x=4 |
- RC隔离级别
- 由于事物B内的快照读采用的是每次都产生的新的ReadView,所以会产生不可以重复读和幻读
事物A | 事物B |
---|
开启事物 | |
| 开启事物 |
快照读(无影响)查询x=5 | |
| 快照读(无影响)查询x=5 |
更改x=4 | |
提交事物 | |
| 快照读x=4(新的ReadView) |
| 当前读x=4 |
MVCC小结
-
每开启一个新事务,事务的版本号就会递增。在可重读Repeatable reads事务隔离级别操作标准如下
- SELECT时,读取创建版本号<=当前事务版本号,删除版本号为空或>当前事务版本号。
- INSERT时,保存当前事务版本号为行的创建版本号
- DELETE时,保存当前事务版本号为行的删除版本号
- UPDATE时,插入一条新纪录,保存当前事务版本号为行创建版本号【存储的是地址,使得版本管理像个链表】,同时保存当前事务版本号到原来删除的行
-
通过这样的控制可以解决脏读、不可以重复读和部分幻读
-
脏读就不展示示例,不可重读和幻读示例如下【借用参考博客的图】:
-
出现幻读的快照读,ReadView无法排除的情况
- 借用参考博客的例子【】,事物1修改了事物2插入的数据,导致事物2的那一条数据对应的增加了一个版本号为事物1的记录,所以可以被事物1所查到
-
示例2
作者:陈广胜
链接:
来源:知乎
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
事物1 | 事物2 |
---|
start transaction; | start transaction; |
| select * from t;【结果为空】 |
insert into t values(1);【插入成功】 | |
commit; 【提交事物1】 | |
| select * from t;【结果为空】 |
| select * from t for update;【上锁,结果c=1】 |
| update t set c=2; |
| 【由于前面上了锁,这个事物没有提交前是修改不到的】 |
| commit; |
-
MCVV这种读取历史数据的方式称为快照读(snapshot read),而读取数据库当前版本数据的方式,叫当前读(current read)
-
快照读
-
当前读
-
假设要update一条记录,但是在另一个事务中已经delete掉这条数据并且commit了,如果update就会产生冲突,所以在update的时候需要知道最新的数据。读取的是最新的数据,需要加锁。
-
以下第一个语句需要加共享锁,其它都需要加排它锁。
-
select * from table where ? lock in share mode;
select * from table where ? for update;
insert;
update;
delete;
-
MVCC控制下的快照读不会出现脏读、不重复读、幻读等现象,但是在当前读的时候,仍会出现脏读、不重复读、幻读等现象,需要通过锁来解决【InnoDB采用Next-Key锁来解决】
-
MySQL RR下,如果没有locking read(select for update之类),它不会产生幻读,这种情形下是通过MVCC的快照保证的。如果存在locking read,那么可能会产生幻读。这种幻读一般会在一个locking read跟着一个non-locking read的时候发生,想避免这种情形,要么对所有的读都加锁,要么就是增加隔离级别到 serializable。这种情形,避免幻读是由锁保证的。【借用知乎哪一篇的结论】