事务
事务的ACID原则
A/Atomicity:原子性
要么全部执行成功,要么全部执行失败
C/Consistency:一致性
数据只允许从一个一致性状态变化为另一个一致性状态
I/Isolation:独立性/隔离性
事务之间都是独立的
D/Durability:持久性
事务一旦被提交,它会保持永久性
MySQL的事务机制综述
MySQL默认将一条SQL视为单个事务,同时默认开启自动提交事务,SQL执行完了就会自动提交
手动管理事务
- start transaction | begin | begin work:开启一个事务
- commit:提交一个事务
- rollback:回滚一个事务
MySQL事务的隔离机制
默认为第三级别:Repeatable read可重复读
隔离级别
- Read uncommitted/RU:读未提交
- Read committed/RC:读已提交
- Repeatable read/RR:可重复读
- Serializable:序列化/串行化
脏读、幻读、不可重复读问题
- 脏读
- 一个事务读到了其他事务还未提交的数据
- 当前事务读到的数据,由于还未提交,因此有可能会回滚
- 不可重复读
- 一个事务中,多次读取同一数据,先后读取到的数据不一致
- 幻读
- 一个事务内多次查询返回的结果集不一样
事务的四大隔离级别
- 解决的问题
- 读未提交:处于该隔离级别的数据库,脏读、不可重复读、幻读问题都有可能发生。
- 读已提交:处于该隔离级别的数据库,解决了脏读问题,不可重复读、幻读问题依旧存在。
- 可重复读:处于该隔离级别的数据库,解决了脏读、不可重复读问题,幻读问题依旧存在。
- 序列化/串行化:处于该隔离级别的数据库,解决了脏读、不可重复读、幻读问题都不存在。
- 读未提交
- 基于「写互斥锁」实现,写同一数据时会互斥,但读操作却并不是互斥的
- 也就是当一个事务在写某个数据时,就算没有提交事务,其他事务来读取该数据时,也可以读到未提交的数据,因此就会导致脏读、不可重复读、幻读一系列问题出现
- 读操作不加锁,写操作加排他锁。
- 读已提交
- 使用「写互斥锁」,两个事务操作同一数据时,会出现排他性
- 读操作,MVCC机制不会让另一个事务读取正在修改的数据,而是读取上一次提交的数据(也就是读原本的老数据)
- 当A正在更新数据但还未提交时,事务B开始读取数据,此时MVCC机制则会基于表数据的快照创建一个ReadView,然后读取原本表中上一次提交的老数据。然后等事务A提交之后,事务B再次读取数据,此时MVCC机制又会创建一个新的ReadView,然后读取到最新的已提交的数据,此时事务B中两次读到的数据并不一致,因此出现了不可重复读问题
- 读操作使用MVCC机制,每次SELECT生成快照,写操作加排他锁。
- 可重复读
- 和【读已提交】稍微有些不同MVCC机制会
- 不会每次查询时都创建新的ReadView,而是在一个事务中,只有第一次执行查询会创建一个ReadView,在这个事务的生命周期内,所有的查询都会从这一个ReadView中读取数据,从而确保了一个事务中多次读取相同数据是一致的,也就是解决了不可重复读问题。
- 读操作使用MVCC机制,首次SELECT生成快照,写操作加临键锁。
- 序列化/串行化
- 将所有的事务按序排队后串行化处理
- 操作同一张表的事务只能一个一个执行,事务在执行前需要先获取表级别的锁资源,拿到锁资源的事务才能执行,其余事务则陷入阻塞,等待当前事务释放锁
- 读操作加共享锁,写操作加临键锁。
MySQL的事务实现原理
事务机制是基于日志实现的
两个个日志
- undo-log:主要记录SQL的撤销日志,比如目前是insert语句,就记录一条delete日志。
- redo-log:记录当前SQL归属事务的状态,以及记录修改内容和修改页的位置。
多条SQL的事务机制
- ①当MySQL执行时,碰到start transaction;的命令时,会将后续所有写操作全部先关闭自动提交机制,也就是后续的所有写操作,不管有没有成功都不会将日志记录修改为commit状态。
- ②先在redo-log中为第一条SQL语句,记录一条prepare状态的日志,然后再生成对应的撤销日志并记录到undo-log中,然后执行SQL,将要写入的数据先更新到缓冲区。
- ③再对第二条SQL语句做相同处理,如果有更多条SQL则逐条依次做相同处理.....,
- ④直到碰到了rollback、commit命令时,再对前面的所有写SQL做相应处理
- 如果是commit提交事务的命令,则先将当前事务中,所有的SQL的redo-log日志改为commit状态,然后由MySQL后台线程做刷盘,将缓冲区中的数据落入磁盘存储
- 如果是rollback回滚事务的命令,则在undo-log日志中找到对应的撤销SQL执行,将缓冲区内更新过的数据全部还原,由于缓冲区的数据被还原了,因此后台线程在刷盘时,依旧不会改变磁盘文件中存储的数据。
- 事务的恢复机制
- MySQL重启时,依旧可以通过redo-log日志重新恢复未落盘的数据,从而确保数据的持久化特性
MVCC机制综述
- MVCC机制的全称为Multi-Version Concurrency Control,即多版本并发控制技术
- 采用更好的方式处理了读-写并发冲突,做到即使有读写冲突时,也可以不加锁解决,从而确保了任何时刻的读操作都是非阻塞的
- MySQL-MVCC多版本并发控制
- 仅在RC读已提交级别、RR可重复读级别才会使用MVCC机制
- RU读未提交级别,既然都允许存在脏读问题,因此无需MVCC介入
- Serializable串行化级别,所有的并发事务串行化处理,不存在多线程并发问题,因此无需MVCC介入
MVCC机制实现原理剖析
MVCC三剑客实现的
隐藏字段、Undo-log日志、ReadView
InnoDB表的隐藏字段
MySQL除开会构建你显式声明的字段外,通常还会构建一些InnoDB引擎的隐藏字段,在InnoDB引擎中主要有DB_ROW_ID、DB_Deleted_Bit、DB_TRX_ID、DB_ROLL_PTR这四个隐藏字段
- 隐藏主键 - ROW_ID(6Bytes)
- 主键、具备唯一非空属性的字段
- 当两者都不存在时,InnoDB就会隐式定义一个顺序递增的列ROW_ID来作为聚簇索引列
- 删除标识 - Deleted_Bit(1Bytes)
- 执行delete语句时,只会改变将隐藏字段中的删除标识改为1/true,如果后续事务出现回滚动作,直接将其标识再改回0/false即可,这样就避免了索引树的结构调整
- purger线程会自动清理Deleted_Bit=1/true的行数据。
- 最近更新的事务ID - TRX_ID(6Bytes)
- MySQL对于所有包含写入SQL的事务,会为其分配一个顺序递增的事务ID,但如果是一条select查询语句,则分配的事务ID=0
- 不过对于手动开启的事务,MySQL都会为其分配事务ID,就算这个手动开启的事务中仅有select操作
- 回滚指针 - ROLL_PTR(7Bytes)
- 当一个事务对一条数据做了改动后,都会将旧版本的数据放到Undo-log日志中,而rollback_pointer就是一个地址指针,指向Undo-log日志中旧版本的数据,当需要回滚事务时,就可以通过这个隐藏列,来找到改动之前的旧版本数据
InnoDB引擎的Undo-log日志
- 存储旧版本的数据
- 版本链
MVCC核心 - ReadView
一个事务在尝试读取一条数据时,MVCC基于当前MySQL的运行状态生成的快照,也被称之为读视图,即ReadView,在这个快照中记录着当前所有活跃事务的ID(活跃事务是指还在执行的事务,即未结束(提交/回滚)的事务)
- 一个事务与一个ReadView属于一对一的关系
- 核心内容
- creator_trx_id:代表创建当前这个ReadView的事务ID。
- trx_ids:表示在生成当前ReadView时,系统内活跃的事务ID列表。
- up_limit_id:活跃的事务列表中,最小的事务ID。
- low_limit_id:表示在生成当前ReadView时,系统中要给下一个事务分配的ID值。
MVCC机制实现原理
- 当一个事务尝试改动某条数据时,会将原本表中的旧数据放入Undo-log日志中。
- 当一个事务尝试查询某条数据时,MVCC会生成一个ReadView快照。
- Undo-log主要实现数据的多版本,ReadView则主要实现多版本的并发控制
RC、RR不同级别下的MVCC机制
- RC级别下,MVCC机制是会在每次select语句执行前,都会生成一个ReadView
- RR级别中,一个事务只会在首次执行select语句时生成快照,后续所有的select操作都会基于这个ReadView来判断
锁
锁的分类
总的来说其实就共享锁、排他锁两种,只是加的方式不同,加的地方不同,因此就演化出了这么多锁的称呼
以锁粒度的维度划分:
①表锁:
- 全局锁:加上全局锁之后,整个数据库只能允许读,不允许做任何写操作。
- 元数据锁 / MDL锁:基于表的元数据加锁,加锁后整张表不允许其他事务操作。
- 意向锁:这个是InnoDB中为了支持多粒度的锁,为了兼容行锁、表锁而设计的。
- 自增锁 / AUTO-INC锁:这个是为了提升自增ID的并发插入性能而设计的。
②页面锁
③行锁:
- 记录锁 / Record锁:也就是行锁,一条记录和一行数据是同一个意思。
- 间隙锁 / Gap锁:InnoDB中解决幻读问题的一种锁机制。
- 临建锁 / Next-Key锁:间隙锁的升级版,同时具备记录锁+间隙锁的功能。
以互斥性的维度划分:
- 共享锁 / S锁:不同事务之间不会相互排斥、可以同时获取的锁。
- 排他锁 / X锁:不同事务之间会相互排斥、同时只能允许一个事务获取的锁。
- 共享排他锁 / SX锁:MySQL5.7版本中新引入的锁,主要是解决SMO带来的问题。
以操作类型的维度划分:
- 读锁:查询数据时使用的锁。
- 写锁:执行插入、删除、修改、DDL语句时使用的锁。
以加锁方式的维度划分:
- 显示锁:编写SQL语句时,手动指定加锁的粒度。
- 隐式锁:执行SQL语句时,根据隔离级别自动为SQL操作加锁。
以思想的维度划分:
- 乐观锁:每次执行前认为自己会成功,因此先尝试执行,失败时再获取锁。
- 悲观锁:每次执行前都认为自己无法成功,因此会先获取锁,然后再执行。
共享锁与排他锁
共享锁
- 不同事务之间不会排斥,可以同时获取锁并执行
- 这里所谓的不会排斥,仅仅只是指不会排斥其他事务来读数据,但其他事务尝试写数据时,就会出现排斥性
排他锁
当一个线程获取到独占锁后,会排斥其他线程,如若其他线程也想对共享资源/同一数据进行操作,必须等到当前线程释放锁并竞争到锁资源才行
锁的释放
- MySQL中释放锁的动作都是隐式的(MySQL自己来干)
- 对于锁的释放时机,在不同的隔离级别中也并不相同,比如在“读未提交”级别中,是SQL执行完成后就立马释放锁,而在“可重复读”级别中,是在事务结束后才会释放
页面锁、乐观锁与悲观锁
页面锁
- 以页为粒度,锁住的是一页数据
- 了解即可
乐观锁
- 无锁思想,仅是一种锁的思想
- 每次执行都认为只会有自身一条线程操作,因此无需拿锁直接执行
- 乐观锁机制,一般都是基于CAS思想实现的
- 乐观锁更加适用于读大于写的业务场景,频繁写库的业务则并不适合加乐观锁
悲观锁
- 仅是一种锁的思想
- 每次执行都认为会有其他线程一起来操作,因此每次都需要先拿锁再执行
- 想要使用悲观锁,对一个事务加排他锁for update即可
行锁
记录锁(Record Lock)
也就是行锁,一条记录和一行数据是同一个意思。
间隙锁(Gap Lock)
- InnoDB中解决幻读问题的一种锁机制
- ID字段之间从4跳到了9,那么4~9两者之间的范围则被称为“间隙”,而间隙锁则主要锁定的是这块范围
临键锁(Next-Key Lock)
- 记录锁+间隙锁组成的锁
- 锁定左开右闭的区间
插入意向锁(Insert Intention Lock)
- 插入意向锁是一种间隙锁,这种锁是一种隐式锁,也就是咱们无法手动的获取这种锁
- 知道即可
行锁的粒度粗化
- 在内存中专门分配了一块空间存储锁对象,当该区域满了后,就会将行锁粗化为表锁。
- 当做范围性写操作时,由于要加的行锁较多,此时行锁开销会较大,也会粗化成表锁。
表锁
将锁加在表上,一张表只能存在一个同一类型的表锁
不同引擎的表锁的实现也不同
InnoDB
- 支持多粒度锁的存储引擎,它的锁机制是基于聚簇索引实现的
- 当SQL执行时,如果能在聚簇索引命中数据,则加的是行锁,如无法命中聚簇索引的数据则加的是表锁
MyISAM
- 使用表锁,需要使用额外的语法
- MyISAM引擎中获取读锁(具备读-读可共享特性)LOCK TABLES
table_nameREAD; - MyISAM引擎中获取写锁(具备写-读、写-写排他特性)LOCK TABLES
table_nameWRITE; - 查看目前库中创建过的表锁(in_use>0表示目前正在使用的表锁)SHOW OPEN TABLES WHERE in_use > 0;
- 释放已获取到的锁UNLOCK TABLES;
- MyISAM引擎中获取读锁(具备读-读可共享特性)LOCK TABLES
元数据锁(Meta Data Lock)
- 简称MDL锁
- MDL锁是基于.frm文件中的元数据加锁的
- 主要是用于,更改表结构时使用
意向锁(Intention Lock)
意向锁实际上也是一种特殊的表锁,意向锁其实是一种“挂牌告知”的思想,好比日常生活中的出租车,一般都会有一个牌子,表示它目前是“空车”还是“载客”状态,而意向锁也是这个思想
自增锁(AUTO-INC Lock)
- 专门为了提升自增ID的并发插入性能而设计的
- 模式
- innodb_autoinc_lock_mode = 0:传统模式。同时只允许一条线程执行,这种形式显然性能较低。
- innodb_autoinc_lock_mode = 1:连续模式(MySQL8.0以前的默认模式)。
- innodb_autoinc_lock_mode = 2:交错模式(MySQL8.0之后的默认模式)。
全局锁
- 全局锁其实是一种尤为特殊的表锁,其实将它称之为库锁也许更合适
- 全局锁是基于整个数据库来加锁的,加上全局锁之后,整个数据库只能允许读,不允许做任何写操作
- 一般全局锁是在对整库做数据备份时使用
共享排他锁
- 主要是解决SMO带来的问题
- 更新操作触发B+Tree叶子节点分裂,那么就会对整棵B+Tree加排它锁,这不但阻塞了后续这张表上的所有的更新操作,同时也阻止了所有试图在B+Tree上的读操作,也就是会导致所有的读写操作都被阻塞,其影响巨大
MySQL中的死锁现象
死锁是指两个或两个以上的线程(或进程)在运行过程中,因为资源竞争而造成相互等待、相互僵持的现象
MySQL中死锁如何解决呢?
- 锁超时机制:事务/线程在等待锁时,超出一定时间后自动放弃等待并返回。
- 死锁检测算法 - wait-for graph
- 外力介入打破僵局:第三者介入,将死锁情况中的某个事务/线程强制结束,让其他事务继续执行。
- 如何避免死锁产生?
- 缩短事务持有锁的时间、减小锁的粒度以及锁的数量
- 业务允许的情况下,将隔离级别调低,因为级别越低,锁的限制会越小
- 尽量不要手动在事务中获取排他锁,否则会造成一些不必要的锁出现,增大产生死锁的几率
- 调整业务SQL的逻辑顺序,较大、耗时较长的事务尽量放在特定时间去执行(如凌晨对账...)
锁机制的底层实现原理
锁的内存结构
- 锁的事务信息
- 索引的信息
- 锁粒度信息
- 锁类型信息
- 其他信息
- 锁的比特位