MySQL-事务05

事务日志

事务有4种特性:原子性、一致性、隔离性和持久性
事务的隔离性由锁机制实现
事务的原子性、一致性和持久性由事务的redo日志和undo日志保证
redo log称为重做日志,提供再写入操作,恢复提交事务修改的页操作,用来保证事务的持久性
undo log称为回滚日志,回滚行记录到某个特定版本,用来保证事务的原子性、一致性
redo和undo都可以视为是一种恢复操作,但是:
redo log:是存储引擎层(innodb)生成的日志,记录的是“物理级别”上的页修改操作,比如页号XXX,偏移量YYY,写入了ZZZ数据。主要为了保护数据的可靠性
undo log:是存储引擎层(innodb)生成的日志,记录的是逻辑操作日志,比如对某一行数据进行了INSERT语句操作,那么undo log就记录一条与之相反的DELETE操作。主要用于事务的回滚(undo log记录的是每个修改操作的逆操作)和一致性非锁定读(undo log回滚记录到某种特定的版本–MVCC,即多版本并发控制)
redo日志的好处特点
1.好处
redo日志降低了刷盘频率
redo日志占用的空间非常小:存储表空间ID、页号、偏移量以及需要更新的值,所需的存储空间是很小的,刷盘快
2.特点
redo日志是顺序写入磁盘的:在执行事务的过程种,每执行一条语句,就可能产生若干条redo日志,这些日志是按照产生的顺序写入磁盘的,也就是使用顺序IO,效率比随机IO快
事务执行过程种,redo log不断记录:redo log跟bin log的区别,redo log是存储引擎层产生的,而bin log是数据库层产生的。假设一个事务,对表做10万行的记录插入,在这个过程中,一直不断的往redo log顺序记录,而bin log不会记录,直到这个事务提交,才会一次写入到bin log文件中
redo的组成
1.重做日志的缓冲(redo log buffer),保存在内存中,是易丢的
参数设置:innodb_log_buffer_size:
redo log buffer大小,默认16M,最大值是4096M,最小值1M
2.重做日志文件(redo log file),保存在硬盘中,是持久的
用一个执行update操作:
第1步:先将原始数据从磁盘中读入内存中来,修改数据的内存拷贝
第2步:生成一条重做日志并写入redo log buffer,记录的是数据被修改后的值
第3步:当事务commit时,将redo log buffer中的内容刷新到redo log file,对redo log file采用追加写的方式
第4步:定期将内存中修改的数据刷新到磁盘中
redo log的刷盘策略
Innodb_flush_log_at_trx_commit参数,该参数控制commit提交事务时,如何将redo log buffer中的日志刷新到redo log file中
设置0:表示每次事务提交时不进行刷盘操作(系统默认master thread每隔1s进行一次重做日志的同步)
设置1:表示每次事务提交时都将进行同步,刷盘操作(默认值)
设置2:表示每次事务提交时都只把redo log buffer内容写入page cache,不进行同步。由os自己决定什么时候同步到磁盘文件
checkpoint
在整个日志文件组中还有两个重要的属性,分别是write pos、checkpoint
write pos 是当前记录的位置,一边写一边后移
checkpoint 是当前要擦除的位置,也是往后推移
redo log小结
InnoDB 的更新操作采用的是Write Ahead Log(预先日志持久化)策略,即先写日志,再写入磁盘

Undo日志
每当我们要对一条记录做改动时(INSERT,DELETE,UPDATE)都需要“留一手”–把回滚时所需的东西记下来
插入一条记录时,需要把这个主键值对应的记录删掉就好了(对于每个INSERT,InnoDB存储引擎会完成一个DELETE)
删除了一条记录,要把这条记录中的内容都记下来,回滚时再把记录插入到表中(对于每个DELETE,InnoDB存储引擎会执行一个INSERT)
修改了一条记录,要把修改这条记录前的旧值都记录下来,回滚时再把这条记录更新为旧值(对于每个UPDATE,InnoDB存储引擎会执行一个相反的UPDATE,将修改前的行放回去)
MySQL把这些为了回滚而记录的这些内容称之为撤销日志或者回滚日志(即undo log)
此外,undo log会产生redo log,也就是undo log的产生会伴随着redo log的产生,这是因为undo log也需要持久性的保护
Undo日志的作用
1、作用1:回滚数据
undo是逻辑日志,只是将数据库逻辑地恢复到原来的样子,但是数据结构和页本身在回滚之后可能大不相同
2、作用2:MVCC
undo的另一个作用是MVCC,即在InnoDB存储引擎中MVCC的实现是通过undo来完成。当用户读取一行记录时,若该记录已经被其他事务占用,当前事务可以通过undo读取之前的行版本信息,以此实现非锁定读取
undo的存储结构
1、回滚段与undo页
InnoDB对undo log的管理采用段的方式,也就是回滚段(rollback segment)每个回滚段记录了1024个undo log segment,而在每个undo log segment段中进行undo 页的申请
undo页的重用:当事务提交时,并不会立刻删除undo页,因为重用,所以这个undo页可能混杂其他事务的undo log 。当undo log在commit后,会被放到一个链表中,然后判断undo 页的使用空间是否小于3/4,如果是,则表示当前页可以被重用,由于undo log 是离散的,所以清理对应的磁盘空间时,效率不高
2、回滚段与事务
每个事务只会使用一个回滚段,一个回滚段在同一时刻可能会服务于多个事务
当一个事务开始的时候,会制定一个回滚段,在事务进行的过程中,当数据被修改时,原始的数据会被复制到回滚段
在回滚段中,事务会不断填充盘区,直到事务结束或所有的空间被用完。如果当前的盘区不够用,事务会在段中请求扩展下一个盘区,如果所有已分配的盘区都被用完,事务会覆盖最初的盘区或者在回滚段允许的情况下扩展新的盘区来使用
回滚段存在于undo 表空间中,在数据库中可以存在多个undo 表空间,但同一时刻只能使用一个undo表空间
当事务提交时,InnoDB存储引擎会做以下两件事情:一是将undo log放入列表中,以供之后的purge操作,二是判断undo log所在的页是否可以重用,若可以分配给下个事务使用
3、回滚段中的数据分类
未提交的回滚数据:该数据所关联的事务并未提交,用于实现读一致性,所以该数据不能被其他事务的数据覆盖
已经提交但未过期的回滚数据:该数据关联的事务已经提交,但是受到undo retention 参数的保持时间的影响
事务已经提交并过期的数据:事务已经提交,而且数据保存时间已经超过undo retention参数指定的时间,属于已经过期的数据。当回滚段满了之后,会优先覆盖
事务提交后并不能马上删除undo log及undo log 所在的页。是否可以最终删除undo log及undo log所在页由purge线程来判断
undo的类型
在InnoDB存储引擎中,undo log分为:
insert undo log:insert undo log是指在insert操作中产生的undo log。因为insert操作的记录,只对事物本身可见,对其他事务不可见(这是事务隔离性的要求),故该undo log可以在事务提交后直接删除。不需要进行purge操作
update undo log:update undo log记录的是对delete和update操作产生的undo log。该undo log可能需要提供MVCC机制,因此不能在事务提交时就进行删除。提交时放入undo log链表,等待purge线程进行最后的删除
undo log的生命周期
简要生成过程
更新数据到buffer pool中
不存在则从磁盘加载
记录undo log
更新数据
写入到redo log buffer中
写入到redo log到文件
写入binlog文件
详细生成过程
DB_ROW_ID:如果没有为表显式的定义主键,并且表中也没有定义唯一索引,那么InnoDB会自动为表添加一个row_id的隐藏列作为主键
DB_TRX_ID:每个事务都会分配一个事务ID,当对某条记录发生变更时,就会将这个事务的事务ID写入trx_id中
DB_ROLL_PTR:回滚指针,本质上就是指向undo log的指针
undo log是如何回滚的
以上面的例子来说,假设执行rollback,那么对应的流程应该是这样:
通过undo no=3的日志把id=2的数据删除
通过undo no=2的日志把id=1的数据的deleteMark还原成0
通过undo no=1的日志把id=1的数据的name还原成Tom
通过undo no=0的日志把id=1的数据删除
undo log的删除
1、针对于insert undo log
因为insert操作的记录,只对事务本身可见,对其他事务不可见。故该undo log可以在事务提交后直接删除,不需要进行purge操作
2、针对于update undo log
该undo log可能需要提供MVCC机制,因此不能事务提交时就进行删除。提交时放入undo log链表,等待purge线程进行最后的删除
补充:purge线程两个主要作用是:清理undo页和清除page里面带有Delete_Bit标识的数据行
小结

undo log是逻辑日志,对事务回滚时,只是将数据库逻辑地恢复到原来的样子
redo log是物理日志,记录的是数据页的物理变化,undo log不是redo log的逆过程

事务的隔离性由锁来实现
概述
锁是计算机协调多个进程或线程并发访问某一资源的机制。我们并发范文某个数据的时候,最多只有一个线程在访问,保证数据的完整性和一致性。同时需要对并发操作进行控制,因此产生了锁。同时锁机制也为实现MySQL的各个隔离级别提供了保证。锁冲突也是影响数据库并发访问性能的一个重要因素

MySQL并发事务访问相同记录
情况大致分为3种:
1、读-读情况
无影响
2、写-写情况
并发事务相继对相同的记录做出改动,可以会发生脏写
需要排队执行
当一个事务想对这条记录做改动时,会看看内存中有没有与这条记录关联的锁结构,在锁结构里有很多信息,其中比较重要的属性
trx信息:代表这个锁结构是那个事务生成的
is_waiting:代表当前事务是否在等待
当事务T1改动了这条记录后,就生成了一个锁结构与该记录关联,因为之前没有别的事务为这条记录加锁,所以is_waiting属性就是false,我们把这个场景就称之为获取锁成功,或者加锁成功,然后就可以继续执行操作了
3、读-写或写-读情况
一个事务进行读取操作,另一个进行改动操作。这种情况下可能发生脏读、不可重复读、幻读的问题
4、并发问题的解决方案
怎么解决脏读、不可重复读、幻读这些问题?
方案一:读操作利用多版本并发控制【MVCC,下章讲解】,写操作进行加锁
所谓的MVCC,就是生成一个ReadView,通过ReadView找到符合条件的记录版本(历史版本由undo日志构建)查询语句只能读到在生成ReadView之前已提交事务所做的更改,而写操作肯定针对的是最新版本的记录,读记录的历史版本和改动记录的最新版本本身并不冲突,也就是采用MVCC时,读-写操作并不冲突
在READ COMMITTED隔离级别下,一个事务在执行过程中每次执行SELECT操作时都会生成一个ReadView,ReadView的存在本身就保证了事务不可以读取到未提交的事务所做的更改,也就是避免了脏读现象
在REPEATABLE READ隔离级别下,一个事务在执行过程中只有第一次执行SELECT操作才会生成一个ReadView,之后的SELECT操作都复用这个ReadView,这样也就避免了不可重复读和幻读的问题
方案二:读、写操作都采用加锁方式
这样操作是排队执行,那么所有问题都可以解决
小结对比发现:
采用MVCC方式的话,读-写操作并不冲突,性能更高
采用加锁方式的话,读-写操作需要排队执行,影响性能

锁的不同角度分类
对数据的操作类型划分
读锁/共享锁:英文用S表示。针对同一份数据,多个事务的读操作可以同时进行二不会互相影响,相互不堵塞的
写锁/排它锁:英文用X表示。当前写操作没有完成前,它会阻断其他写锁和读锁。这样就能确保在给定的时间里,只有一个事务能执行写入,并防止其他用户读取正在写入的同一资源
需要注意的是对于InnoDB引擎来说,读锁和写锁可以加在表上,也可以加在行上
写操作:insert情况下,新插入一条记录的操作并不加锁,通过一种称之为隐式锁的结构来保护这条新插入的记录在未提交之前不会被其他事务看到
锁粒度角度划分
1、表级锁
表级别的S锁、X锁:一些特殊情况下,如崩溃恢复过程中用到
意向锁:InnoDB支持多粒度锁,它允许行级锁与表级锁共存,而意向锁就是其中的一种表锁
意向锁的存在是为了协调行锁和表锁的关系,支持多粒度(表锁与行锁)的锁共存
意向锁是一种不与行级锁冲突表级锁,这一点非常重要
表明"某个事务正在某些行持有了锁或该事务准备去持有锁"
意向锁分为两种:
意向共享锁(intention shared lock,IS):事务有意向对表中的某些行加共享锁(S锁)–事务要获取某些行的S锁,必须先获得表的IS锁
意向排他锁(intention shared lock,IX):事务有意向对表中的某些行加排他锁(X锁)
意向锁是由存储引擎自己维护的,用户无法手动操作意向锁,在为数据行加共享、排他锁之前,InnoDB会先获取该数据行所在数据表的对应意向锁
意向锁要解决的问题:如果我们给某一行数据加上了排他锁,数据库会自动给更大一级的空间,比如数据页或数据表加上意向锁,告诉其他人这个数据页或者数据表已经有人上过排它锁了
自增锁:数据库设置了自增ID的时候,需要表级锁
元数据锁-MDL锁:当对一个表做增删查改操作的时候,加MDL读锁;当要对表做结构变更操作的时候,加MDL写锁
2、行级锁
行级锁只在存储引擎层实现
优点:锁定力度小,发生锁冲突概率低,可以实现的并发度高
缺点:对于锁的开销比较大,加锁会比较慢,容易出现死锁情况
记录锁Record Locks:是有S锁和X锁之分
当一个事务获取了一条记录的S型记录锁后,其他事务也可以继续获取该记录的S型记录锁,但不可以继续获取X型记录锁
当一个事务获取了一条记录的X型记录锁后,其他事务既不可以继续获取该记录的S型记录锁,也不可以继续获取X型记录锁
间隙锁Gap Locks:解决幻读的一种方式,仅仅是为了防止插入幻影记录而提出的
举个例子:比如现在主键有5 ,8 ,15 ,20
情况一:当占有select * from student where id = 3 lock in share mode;时间隙锁的范围是(0,5)就是1,2,3,4
情况二:当占有select * from student where id = 10 lock in share mode;时间隙锁的范围是(8,15)就是9,10,11,12,13,14
情况三:当占有select * from student where id = 25 lock in share mode;时间隙锁的范围是(20,+∞)就是大于20的所有范围
临键锁Next-Key Locks:本质就是一个记录锁和一个间隙锁的合体,它既能保护该条记录,又能阻止别的事务将新记录插入被保护记录前边的间隙
插入意向锁Insert Intention Locks:InnoDB规定事务在等待的时候也需要在内存中生成一个锁结构,表明有事务想在某个间隙中插入新记录,但是现在在等待。插入意向锁是一种gap锁,不是意向锁,在insert操作时产生
3、页级锁
页锁的开销介于表锁和行锁之间,会出现死锁;锁的粒度也是介于表锁和行锁之间,并发度一般
对待锁的态度划分
悲观锁
乐观锁
加锁方式
隐式锁:插入一条记录或者间隙锁、临键锁
显示锁:显示加的锁
共享锁
select * lock in share mode;
排它锁
select * for update;
其他
全局锁:整个库实例加锁,场景:全库逻辑备份,命令为 flush tables with read lock
死锁
产生必要条件
两个或者两个以上事务
每个事务都已经持有锁并且申请新的锁
锁资源同时只能被同一个事务持有或者不兼容
事务之间因为持有锁和申请锁导致彼此循环等待
处理死锁
方式1:等待,直到超时innodb_lock_with_timeout=50
方式2:使用死锁检测进行死锁处理
如何避免死锁
合理设计索引
调整业务逻辑SQL执行顺序
避免大事务
在并发比较高的系统中,不要显示加锁
降低隔离级别

锁的内存结构
一条记录加锁的本质就是在内存中创建一个锁结构与之关联
什么时候加锁的记录会放到一个锁结构中
在同一个事务中进行加锁操作
被加锁的记录在同一个页面中
加锁的类型是一样的
等待状态是一样的

结构解析:
1、锁所在的事务信息:
锁所在的事务信息在内存结构中只是一个指针,通过指针可以找到内存中关于该事务的更多信息
2、索引信息:
对于行锁来说,需要记录一下加锁的记录是属于那个索引的。这里也是一个指针
3、表锁/行锁信息:
表锁结构和行锁结构在这个位置的内容是不同的
表锁:记载着是对那个表加的锁,还有其他的一些信息
行锁:记载了三个重要的信息:
Space ID:记录所在表空间
Page Number:记录所在页号
n_bits:对于行锁来说,一条记录就对应着一个比特位,一个页面中包含很多记录,用不同的比特位来区分到底是哪一条记录加了锁。
4、type_mode:
这是一个32位的数,被分成了lock_mode、lock_type和rec_lock_type三个部分;
锁的模式,占用低4位,可选的值如下:
LOCK_IS(十进制0):表示共享意向锁,也就是IS锁
LOCK_IX(十进制1):表示独占意向锁,也就是IX锁
LOCK_S(十进制2):表示共享锁,也就是S锁
LOCK_X(十进制3):表示独占锁,也就是X锁
LOCK_AUTO_INC(十进制4):表示AUTO-INC锁
在innodb存储引擎中,LOCK_IS、LOCK_IX、LOCK_AUTO_INC都算是表级锁的模式,LOCK_S、LOCK_X既可以算是表级锁的模式,也可以是行级锁的模式
锁的类型(lock_type),占用5~8位,不过现阶段只有第5位和第6位被使用
LOCK_TABLE(十进制16),也就是当第5个比特位置为1时,表示表级锁
LOCK_REC(十进制32),也就是当第6个位置为1时,表示行级锁
行锁的具体类型(rec_lock_type),使用其余的位来表示。只有在lock_type的值为lock_rec时,也就是只有该锁为行级锁时,才会被细分为更多的类型
LOCK_ORDINARY(十进制0):表示next-key锁
LOCK_GAP(十进制512):也就是当第10比特位置为1时,表示gap锁
LOCK_REC_NOT_GAP(十进制1024):也就是当第11个比特位置为1时,表示正经记录锁
LOCK_INSERT_INTENTION(十进制2048):也就是当第12个比特位置为1时,表示插入意向锁
is_waiting属性?基于内存空间的节省,所以把is_waiting属性放到了type_mode这个32位的数字中:
LOCK_WAIT(十进制256):当第9个比特位置为1时,表示is_waiting为true,也就是当前事务尚未获取到锁,处在等待状态;当这个比特位为0时,表示is_waiting为false,也就是当前事务获取锁成功
5、其他信息:
为了更好的管理系统运行过程中生成的各种锁结构而设计了各种哈希表和链表
6、一堆比特位:
如果是行锁结构的话,在该结构末尾还放置了一堆比特位,比特位的数量是由上边提到的n_bits属性表示的

锁监控
关于MySQL锁的监控,我们一般可以通过检查InnoDB_row_lock等状态变量来分析系统上的行锁的争夺情况
show status like ‘innodb_row_lock%’;
查询出的各个状态说明:
Innodb_row_lock_current_waits:正在等待锁的数量
Innodb_row_lock_time:等待总时长
Innodb_row_lock_time_avg:等待平均时长
Innodb_row_lock_time_max:等待最长的一次所花时间
Innodb_row_lock_waits:等待总次数
其他监控方法
MySQL5.7把事务和锁的信息记录在了information_schema库中,涉及到的三张表分别是INNODB_TRX、INNODB_LOCKS和INNODB_LOCK_WAITS
MySQL8.0用DATA_LOCKS和DATA_LOCK_WAITS分别替换了INNODB_LOCKS和INNODB_LOCK_WAITS

多版本并发控制MVCC

什么是MVCC?
MVCC(Multiversion Concurrency Control),多版本并发控制。

快照读与当前读
MVCC在MySQL的InnoDB中的实现主要是为了提高数据库并发性能,用更好的方式去处理读-写冲突,做到即使有读-写冲突时,也能做到不加锁,非阻塞并发读,而这个读指的即使快照读,而非当前读。当前读实际上是一种加锁的操作,是悲观锁的实现。而MVCC本质是采用乐观锁思想的一种方式
1、快照读
快照读又叫一致性读,读取的是快照数据,不加锁的简单SELECT都属于快照读,即不加锁的非阻塞读
2、当前读
当前读读取的是记录的最新版本,读取时还要保证其他并发事务不能修改当前记录,会对读取的记录进行加锁。加锁的SELECT,或者对数据进行增删查改都会进行当前读

MVCC实现原理之ReadView
MVCC的实现依赖于:隐藏字段(trx_id,roll_pointer)、Undo Log、Read View
1、什么是ReadView?
ReadView就是事务A在使用MVCC机制进行快照读操作时产生的读视图。当事务启动时,会生成数据库系统当前的一个快照,InnoDB为每个事务构造了一个数据,用来记录并维护当前活跃事务的ID(“活跃”指的就是,启动了但还没提交)
2、设计思路
主要是针对提交读和可重复读隔离级别的事务使用
creator_trx_id,创建这个ReadView的事务ID
trx_ids,表示在生成ReadView时当前系统中活跃的读写事务的事务ID列表
up_limit_id,活跃的事务中最小的事务ID
low_limit_id,表示生成ReadView时系统中应该分配给下一事务的ID值。low_limit_id是系统最大的事务ID值,这里要注意是系统中的事务ID,需要区别于正在活跃的事务ID
3、ReadView的规则
trx_id属性值与creator_trx_id值相同,可以访问
trx_id属性值小于活跃的事务中最小的事务ID,也可以访问
trx_id属性值大于low_limit_id值,不可以访问
trx_id属性值位于up_limit_id和low_limit_id之间,判断trx_id是否在trx_ids列表中,如果在,则不可访问,不在则可以访问
4、MVCC整体操作流程
首先获取事务自己的版本号,也就是事务ID
获取ReadView
查询得到的数据,然后于ReadView中的事务版本号进行比较
如果不符合ReadView规则,就需要从Undo Log中获取历史快照
最后返回符合规则的数据
当隔离级别为读已提交(Read Committed)时,一个事务中的每一次SELECT查询都会重新获取一次ReadView
当隔离级别为可重复读时,只在第一次SELECT的时候会生成一次ReadView

总结
MVCC在READ COMMITTD、REPEATABLE READ这两种隔离级别的事务在执行快照操作时访问记录的版本链过程
READ COMMIT:在每次进行SELECT的时候生成一个ReadView
REPEATABLE READ:只在第一次进行SELECT操作前生成一个ReadView
通过MVCC我们解决:
读写之间阻塞的问题:通过MVCC可以让读写互相不阻塞,即读不阻塞写,写不阻塞读,这样就可以提升事务并发处理能力
降低了死锁的概率:这是因为MVCC采用了乐观锁的方式,读取数据时并不需要加锁,对于写操作,也只锁定必要的行
解决快照读的问题:当我们查询数据在某个时间点的快照时,只能看到这个时间点之前事务提交更新的结果,而不能看到这个时间点之后事务提交的更新结果

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值