本文主要是介绍02.09 Day 21 - 提炼 Day 6-10,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!
大家好,我是 Snow Hide,作为《MySQL 实战》这个专栏的学员之一,这是我打卡的第 21 天,也是我第 77 次进行这种操作。
今天我温习了该专栏里叫《事务到底是隔离的还是不隔离的?》、《为什么我只查一行的语句,也执行这么慢?》、《幻读是什么,幻读有什么问题?》、《为什么我只改一行的语句,锁这么多?》、《答疑文章(二):用动态的观点看加锁的文章》、《count(*)这么慢,我该怎么办?》的文章。
关键词总结:事务启动时机、MySQL 两个 “视图” 的概念、“快照” 在 MVCC 里是怎么工作的?(数据版本 row trx_id 的几种可能、对事务视图来说的数据版本)、更新逻辑(读提交和可重复读的区别)、第一类语句慢的原因,查询长时间不返回(等 MDL 锁、等 flush、等行锁)、第二类语句慢的原因,查询慢(当前读、一致性读)、幻读是什么?、幻读有什么问题?(语义问题、一致性问题)、如何解决幻读?(间隙锁、next-key lock、Supremum、间隙锁生效场景)、两个加锁原则、两个加锁优化、等值查询间隙锁、非唯一索引等值锁、主键索引范围锁、非唯一索引范围锁、唯一索引范围锁 bug、非唯一索引上存在 “等值” 的例子、limit 语句加锁、一个死锁的例子、不等号条件里的等值查询、等值查询过程、怎么看死锁?、怎么看锁等待?、update 的例子、count(*) 的实现方式(MyISAM、InnoDB、不支持事务、不准确、性能问题)、用缓存系统保存计数、在数据库保存计数、不同的 count 用法(count(主键 id)、count(1)、count(字段)、count(*)、按效率排列)。
所学总结:
《事务到底是隔离的还是不隔离的?》
事务启动时机
- 第一种启动方式,执行第一个快照读语句时创建一致性视图;
- 第二种启动方式,执行 start transaction with consistent snapshot 时创建一致性视图。
MySQL 两个 “视图” 的概念
- View 视图:通过查询语句定义的虚拟表,调用时执行查询语句并生成结果。语法是 create view … ,它的查询方法与表一样;
- Consistent Read 视图:InnoDB 在实现 MVCC 时用的一致性读视图,以支持读提交(RC,Read Committed)以及可重复读(RR,Repeatable Read)隔离级别的实现。
“快照” 在 MVCC 里是怎么工作的?
InnoDB 里每个事务有一个唯一的事务 ID,叫 transaction id。它是由事务开始时向 InnoDB 事务系统申请的,按申请顺序严格递增。
每行数据有多个版本。每次事务更新数据时,生成一个新的数据版本,并将 transaction id 赋予数据版本的事务 ID,也就是 row trx_id。保留旧数据版本,新数据版本中能直接获取它。
数据版本 row trx_id 的几种可能
- 在已提交事务(低水位):该版本是已提交或当前事务所生成的,该数据是可见的;
- 未开始事务(高水位):该版本是将来启动事务所生成的,该数据不可见;
- 未提交事务集合(当前事务):两种情况:
- row trx_id 在数组中:该版由还未提交的事务所生成,不可见;
- row trx_id 不在数组中:该版由已提交事务所生成,可见。
对事务视图来说的数据版本
- 版本未提交:不可见;
- 版本已提交:在视图创建后提交的,不可见;
- 版本已提交:在视图创建前提交的,可见。
更新逻辑
读提交和可重复读的区别
- 可重复读隔离级别:事务开始时创建一致性视图,之后事务的其他查询共用该一致性视图;
- 读提交隔离级别:语句执行前会重新计算一个新视图。
《为什么我只查一行的语句,也执行这么慢?》
第一类语句慢的原因,查询长时间不返回
通过 show processlist 命令查看当前语句的状态。
等 MDL 锁
当看到状态为 “Waiting for table metadata lock” 的语句。
某个线程正请求或持有表的 MDL 写锁,并拦截 select 语句。
等 flush
当看到状态为 “Waiting for table flush” 的语句。
某个线程正在对表做 flush 操作。
等行锁
由于访问某个记录时要加锁,如果这时已经有一个事务在这行记录上持有一个写锁,我们的查询语句就会被堵住。
第二类语句慢的原因,查询慢
为了把所有语句记录到 slow log 里,在连接后先执行 set long_query=0,将慢查询日志的时间阈值设置为 0。
当前读
速度很快,因为语句后加 lock in share mode。
一致性读
没有后加 lock in share mode 的语句,速度会慢一些。
《幻读是什么,幻读有什么问题?》
幻读是什么?
一个事务在前后两次查询同一个范围的时候,后一次查询看到了前一次查询没有看到的行。幻读在 “当前读” 下才会出现。幻读仅指 “新插入的行”。
幻读有什么问题?
语义问题
事务一的语句根据某个字段的值的条件查询到了数据,事务二根据另一个值的条件更改将事务一查询时所依赖的字段的值更改成与事务一查询时所匹配的值,导致事务三在执行事务一相通的查询语句时多查出一条数据,这个操作破坏了事务一里查询语句的加锁声明。随后,事务四插入了一条数据,在事务一查询所依赖的字段上插入了与事务一查询条件匹配的值,导致事务四在执行与事务一相同查询语句时查询出新插入的数据,一共查询出三条语句,这也破坏了事务一查询语句的加锁声明。
一致性问题
锁的设计是为了保证数据的一致性。而这个一致性,不止是数据库内部数据状态在此刻的一致性,还包含了数据和日志在逻辑上的一致性。即使把所有的记录都加上锁,还是阻止不了新插入的记录,这也就是为什么 “幻读” 会被单独拿出来解决的原因。
如何解决幻读?
间隙锁
为了解决幻读问题,InnoDB 引入了间隙锁(Gap Lock)。
将两个值之间的空隙锁住。数据行是可以加上锁的实体,数据行之间的间隙也可以加上锁的实体。
跟间隙锁存在冲突关系的,是 “往这个间隙中插入一个记录” 这个操作。
间隙锁 = 开区间
next-key lock
开后闭区间。
Supremum
+∞是开区间。InnoDB 给每个索引加了一个不存在的最大值 spuremum,也就是 “前开后闭区间”。
间隙锁生效场景
在可重读隔离级别下才会生效。
《为什么我只改一行的语句,锁这么多?》,《答疑文章(二):用动态的观点看加锁的文章》
两个加锁原则
- 原则 1:加锁基本单位是 next-key lock。它是前开后闭区间;
- 原则 2:查找过程中访问到的对象才加锁。
两个加锁优化
- 优化 1:索引等值查询,唯一索引加锁时 next-key lock 退化为行锁;
- 优化 2:索引等值查询,向右遍历且最后一个值不满足等值条件时,next-key lock 退化为间隙锁。
等值查询间隙锁
会话 A 根据一个值来查询时产生的加锁范围左侧为小于该值的值,右侧为大于该值的值。会话 B 往间隙插入间隙范围内的值是会被锁住的。会话 C 修改间隙右侧的大于会话 A 查询的值是可以的。
非唯一索引等值锁
lock in share mode 只锁覆盖索引。执行 for update 时系统会认为加下来要更新数据,因此会顺便给主键索引上满足条件的行加锁。
要用 lock in share mode 给行加锁避免数据被更新的话,必须的绕过覆盖索引的优化,查询字段中加入索引不存在的字段。
主键索引范围锁
首次会话 A 定位查找某个值对应的行时,是当作等值查询来片段的,而向右扫描到另一个值对应的行时,用的是范围查询判断。
非唯一索引范围锁
非唯一索引没有优化规则,不会蜕变为行锁。
唯一索引范围锁 bug
锁住的是大于范围右侧值的行,应该是 Bug。
非唯一索引上存在 “等值” 的例子
索引值相同但主键不同的情况下也会产生一个它们之间的锁范围,从而不会影响它们之外的其他行。
limit 语句加锁
在删除数据的时候尽量加 limit,可以控制删除数据的条数,让操作更安全,还可以减小加锁的范围。
一个死锁的例子
分析加锁规则时可以用 next-key lock 来分析。具体执行时要分成间隙锁和行锁来执行。
不等号条件里的等值查询
在执行过程中,通过树搜索的方式定位记录的时候,用的是 “等值查询” 的方法。
等值查询过程
锁是在执行过程中一个一个加上的,而不是一次性加上的。
怎么看死锁?
- 锁是一个个假的,要避免死锁,对同一组资源,按照尽量相同的顺序访问;
- 发生死锁时,for update 语句占有资源更多,回滚成本更大,所以 InnoDB 选择了回滚成本更小的 lock in share mode 语句,来回滚。
怎么看锁等待?
间隙其实是由其右边的记录定义的。
update 的例子
第一条更新语句在执行后上锁,第二条更新语句要更新的值处在第一条更新语句上锁的范围内,所以第二条更新语句会被阻塞。
《count(*)这么慢,我该怎么办?》
count(*) 的实现方式
MyISAM
把表的总行数存储在磁盘上,执行 count(*) 是会直接返回统计,效率很高。
InnoDB
执行 count(*) 时需要把数据逐行地从引擎里读出再累加。
不支持事务
MyIASM 表很快但不支持事务。
不准确
show table status 命令虽然很快,但不准确。
性能问题
InnoDB 表会遍历全表,结果准确但性能不佳。
用缓存系统保存计数
统计数据会有丢失以及不精确的问题。
在数据库保存计数
通过事务来确保逻辑上是一致的。
不同的 count 用法
count(主键 id)
InnoDB 遍历整张表,将 id 的值都取出返回给 server 层。server 层判断 id 后判断不可能为空就按行累加。
count(1)
InnoDB 遍历整张表但不取值。server 层将数字 “1” 放入返回的行,随后判断是不可能为空按行累加。
count(字段)
- 如果字段不允许 null,则逐行地读取,判断不能为 null,逐行累加;
- 如果字段允许 null,判断到有可能是 null 时要取出值再次判断,不是 null 时才累加。
count(*)
并不会读出多有字段,而是做了优化,不取值。肯定不是 null,按行累加。
按效率排列
count(字段) < count(主键 id) < count(1) ≈ count(*)
末了
重新总结了一下文中提到的内容:InnoDB 行数据有多个版本、数据版本有自己的 row trx_id、事务或语句有自己的一致性视图、普通查询语句是一致性读、一致性读会根据 row trx_id 和一致性视图确定数据版本可见性、可重复读查询认可在事务启动前就提交的数据、读提交查询认可在语句启动前就提交的数据、查一行可能出现被锁和执行慢的情况、表锁、行锁、一致性读、全表扫描的加锁方式、解决幻读问题、引入间隙锁、由于间隙锁导致的死锁现象、间隙锁会影响系统的并发度、可重复读隔离级别下的验证、可重复读隔离级别遵守两阶段锁协议、事务提交或回滚时才释放加锁的资源、next-key lock 是间隙锁加行锁、提交隔离级别外键场景的间隙锁相对比较复杂、语句执行过程中上行锁,执行完毕后释放不满足条件行的锁、读提交隔离级别的锁范围更小时间更短、用可重复读隔离级别最大限度地提升系统并行处理事务的能力、获得表行数的两种方法、不同引擎中 count(*) 的实现、缓存系统统计存在问题、InnoDB 解决了一致性视图的问题、利用事务的原子性和隔离性以简化在业务开发时的逻辑。
这篇关于02.09 Day 21 - 提炼 Day 6-10的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!