mysql的mvcc

链接
mysql每次事务创建时都会分配一个自增id作为事务的唯一标志,就是事务id

数据表每一行数据都会有一个隐藏字段DB_TRX_ID,用来存储创建或者最后一次修改此记录的事物ID

数据表里还有另外一个隐藏字段,DB_ROLL_PTR回滚指针,指向这条记录的上一个版本在undo log中的数据。
undo log可以作为mvcc中查找对应可读记录,也可以作为当前事物的rollback依据。
有两种undo_log:

  1. insert undo_log:insert新记录时产生的undo log, 只在事务回滚时需要,并且在事务提交后可以被立即丢弃
  2. update undo_log :不仅在事务回滚时需要,在快照读时也需要;所以不能随便删除,只有在快速读或事务回滚不涉及该日志时,对应的日志才会被purge线程统一清除

当前读:当前读指的是读取数据当前最新数据。update、insert、delete、select for update(排他锁)、select lock in share mode。读取数据需要保证其他并发事务不能修改当前记录,会对读取的记录进行加锁。

快照读:
快照读指的是在读取数据时,生成读取快照,在同一个事物中可能会一直读取此快照的数据。快照读读到的数据可能不是最新的,可能是历史版本的数据,这些历史版本的数据就是从undo log中获取的。事物中的select 不加锁的情况会执行快照读,快照读依赖readview来实现。快照读的前提是隔离级别不是串行级别,串行级别下的快照读会退化成当前读。

  • 什么是read_view? 由当前活跃事物ID的列表trx_list、当前活跃最小事物ID low_limit_id、下一个即将分配的事物ID up_limit_id,三个部分组成。
  • 执行快照读的时候,会创建一个ReadView。定义被读取行的DB_TRX_ID 为trx_id。

基于ReadView的可见性分析逻辑

执行快照读的时候,会创建一个ReadView。定义被读取行的DB_TRX_ID 为trx_id。

  1. 比较 trx_id是否小于low_limit_id 或者为当前事物ID,如果为true,则代表修改此行数据的事物早已提交或者就是当前事务进行的修改,当前记录可见。否则进入下一步判断。
  2. 比较trx_id是否大于等于up_limit_id,如果为true,则代表修改此行记录的事物晚于当前读视图创建,当前记录不可见,根据DB_ROLL_PTR undo log指针找到上一条记录,从新进行可见性分析。否则进入下一步判断
  3. 判断trx_id是否在trx_list列表中,如果在,代表修改此行记录的事物还未提交,当前事务不可以读取当前记录,根据DB_ROLL_PTR undo log指针找到上一条记录,从新进行可见性分析。如果不在,说明数据在readview生成的时候已经提交,当期事物可以读取当前记录。

mvcc在RR级别下,事物进行快照读时会检查当前事物是否已经创建过ReadView,如果存在,则使用已经创建的,这也是实现可重复的方法。
在RC级别下,事物每一次快照读都会创建一个新的ReadView,这样就会造成不可重复的和幻读的问题。

mysql的锁

参考

锁类型(锁的粒度,把锁具体加在什么地方)

全局锁

给整个数据库实例加锁。如果给数据库实例加全局锁会导致整个库处于只读状态(这是非常危险的)。全局锁的典型使用场景是用于全库备份,即把数据库中所有的表都select出来。

元数据锁

元数据锁(MetaData Lock),也叫MDL锁,是用来保护元数据信息,系统级的锁无法主动控制。主要是为了在并发环境下对DDL、DML同时操作下保持元数据的一致性。

页级锁

表级锁速度快,但冲突多,行级冲突少,但速度慢。所以取了折中的页级,一次锁定相邻的一组记录。

表级锁

包含五种模式:

  1. 表意向读锁
  2. 表意向写锁
  3. 表读锁
  4. 表写锁
  5. 自增锁

行级锁

行锁容易产生死锁现象(表锁就不存在死锁现象),所以使用行锁需要注意加锁的顺序和锁定的范围。
行级锁分类:

  1. 记录锁
    1. 只是加在单行记录上,记录锁永远都是加在索引上的,就算一个表没有建索引,数据库也会隐式的创建一个索引。如果 WHERE 条件中指定的列是个二级索引,那么记录锁不仅会加在这个二级索引上,还会加在这个二级索引所对应的聚簇索引上。
  2. 间隙锁
    1. 间隙锁是一种区间锁。锁加在不存在的空闲空间上,或者两个索引记录之间,或者第一个索引记录之前,或者最后一个索引之后的空间,用来表示只锁住一段范围(一般在进行范围查询时且隔离级别在RR或Serializable隔时)。
    2. 一般在RR隔离级别下会使用到GAP锁。使用GAP锁,主要是为了防止幻读产生,在被GAP锁锁住的区间,不允许插入数据或者更新数据。
  3. 下一键锁
    1. InnoDB工作在可重复读隔离级别(RR)下,并且会以Next-Key Lock的方式对数据行进行加锁,这样可以有效防止幻读的发生。Next-Key Lock是行锁和间隙锁的组合,当InnoDB扫描索引记录的时候,会首先对索引记录加上行锁(Record Lock),再对索引记录两边的间隙加上间隙锁(Gap Lock)。加上间隙锁之后,其他事务就不能在这个间隙修改或者插入记录。
  4. 插入意向锁
    1. 插入意向锁,插入记录时使用,是一种特殊的间隙锁。这个锁表示插入的意向,只有在执行insert语句的时候才会有这个锁。就相当于我插入之前加个插入意向锁试探一下子
    2. 插入意向锁之间是不互相冲突,但插入意向锁会跟间隙锁或者Next-Key锁冲突:间隙锁的作用是锁住区间防止其他事务插入数据导致幻读。假设提前有事务A获取了id 在(1,5)区间的间隙锁,那么事务B尝试插入 id = 2时,会先尝试获取插入意向锁,但是由于插入意向锁和间隙锁冲突,导致插入失败,也就避免了幻读产生。

锁模式(到底加的什么锁,是读锁还是写锁)

读锁(共享锁/s锁)

读锁是某个事务(比如事务A)在进行读取操作(比如读一张表或者读取某一行)时创建出来的锁,其他的事务可以并发地读取这些数据(被加了锁的),但是不能修改这些数据(除非持有锁的用户已经释放锁)。
事务A对数据加上读锁之后,其他事务依然可以对其添加读锁(共享),但是不能添加写锁。
InnoDB支持表锁和行锁,在行(也就是记录)上加锁,并不是锁住该条记录,而是在记录对应的索引上加锁。如果where条件中不走索引,则会对所有的记录加锁。

写锁(排他锁/x锁)

一个事务对数据添加写锁之后,其他的事务对该数据,既不能读取也不能更改。与读锁加锁的范围相同,写锁既可以加在记录上,也可以加在表上。
当引擎选择myisam时,insert/update/delete语句,会自动给该表加上排他锁。

意向锁

意向锁是一种不与行级锁冲突的表级锁,表示表中的记录所需要的锁(S锁或X锁)的类型,如果某一事务对数据表加了行锁(共享/排他)或者表锁(共享/排他),那么除此之外还会再加一个对应类型的意向锁

意向锁分类
  1. 意向共享锁(IS锁):IS锁表示当前事务意图在表中的行上设置共享锁下面语句执行时会首先获取IS锁,因为这个操作在获取S锁:获取S锁:select … lock in share mode
  2. 意向排它锁(IX锁):IX锁表示当前事务意图在表中的行上设置排它锁下面语句执行时会首先获取IX锁,因为这个操作在获取X锁:获取X锁:select … for update

事务要获取某个表上的S锁和X锁之前,必须先分别获取对应的IS锁和IX锁。

意向锁作用

如果另一个事务试图在该表级别的共享锁或排它锁,则受到由第一个事务控制的表级别意向锁的阻塞。第二个事务在锁定该表前不必检查各个页或行锁,而只需检查表上的意向锁。

有表user
如果事务a对user表的某一行数据加了排他锁,那么会同时给user表加一个意向排他锁。这时候user表就不能再加表锁了(共享锁和排他锁都不行,如果a加的是共享锁,那么还可以加共享锁)

这时事务b来,想加一个表级共享锁,这时候他要保证以下两点:

  1. 当前没有其他事务持有 users 表的排他锁(表排他锁)。
  2. 当前没有其他事务持有 users 表中任意一行的排他锁(行排他锁)。

其中为了检测是否满足第二个条件,事务 B 必须在确保user表不存在任何排他锁的前提下,去检测表中的每一行是否存在排他锁。很明显这是一个效率很差的做法,但是有了意向锁之后,情况就不一样了:虽然a只是加了行排他锁,但是他也同时在整个表加了一个意向排他锁,这时候b不需要逐行扫描,只扫描当前存在的事务a加了什么锁就可以了。

自增锁

在InnoDB存储引擎中,针对每个自增长的字段都设置了一个自增长的计数器。插入操作会根据自增长计数器的当前值进行+1操作

undolog和binlog的区别

参考链接

如何排查bug

参考链接
线上的故障主要包括cpu、磁盘、内存以及网络问题,而大多数故障可能会包含不止一个层面的问题,所以进行排查时候尽量四个方面依次排查一遍。
使用tcpdump进行抓包