Mysql锁机制与优化实践以及MVCC底层原理剖析

锁机制

  • 从性能上分为乐观锁(用版本比对或cas机制,自旋或抛异常)和悲观锁,前者适合读多写少的场景。
  • 从数据操作粒度分为表锁、页锁、行锁
  • 操作类型分为读锁和写锁和意向锁
  1. 读锁(共享锁,S锁)
    多个事务可以同时加读锁,不互斥,但如果有事务加了写锁,其他事务不能再加读锁。
  2. 写锁(排他锁,X锁)
    一个事务加了写锁后,其他事务不能加读锁或写锁,写锁是互斥的。
  3. 意向锁(Intention Lock)
    表级别的锁,不是行级锁,是 InnoDB 自动加的“标记锁”,标志事务打算对某些行加读锁或写锁,意向锁不会阻塞读写操作,只和表级锁冲突。
    比如事务一需要加表锁,需要一行行检查有没有行锁。只要每次在需要加行锁之前也加上意向锁,那么表锁来的时候只需要检查有没有意向锁了。
名称含义
意向共享锁(IS)表示“我打算对某些行加共享锁”
意向排他锁(IX)表示“我打算对某些行加排他锁”

意向锁的意义是什么?
优化表级锁与行级锁的兼容性检查
比如:某事务要加表级读锁,先看有没有意向写锁(IX),有就不能加。

表锁(Table Lock)

对整张表加锁,操作时锁定整张表中的所有数据。适合读多写少,表不大或批量操作时。

lock table tablename read; -- 读锁,写锁将read改为write;
show open tables; -- 查看所有上了锁的表
unlock tables; -- 解锁所有表

页锁(Page Lock)

对数据库中的一个数据页(如某一组数据行)加锁,锁粒度和开销介于表锁和行锁之间。开销小,加锁快,会出现死锁,锁粒度也一般只有BDB存储引擎支持页锁

行锁(Row Lock)

只对某一行数据加锁,是最细粒度的锁。并发性最高,不同事务可以操作同一表的不同记录。开销大,加锁慢;会出现死锁锁粒度最小,发生锁冲突的概率最低并发最高。InnoDB相对于Myisam的最大不同有两点:

  • InnoDB支持事务(TRANSACTION)
  • InnoDB支持行级锁

** 在 MySQL InnoDB 中,行锁是通过索引来加的**,不是锁“表中某一行的实际物理位置”,而是锁满足查询条件的那部分索引记录。

加锁的条件

  1. 表使用的是 InnoDB 引擎。
  2. 使用了索引(尤其是主键或唯一索引)。
  3. 语句在事务中执行。
  4. 使用了加锁语句,如:
    • SELECT … FOR UPDATE
    • SELECT … LOCK IN SHARE MODE
    • UPDATE / DELETE(这些天然加排他锁)
      一定要命中索引,否则会锁整表!(RR事务会升级为表锁。RC不会)
SELECT * FROM user WHERE name = 'Tom' FOR UPDATE;

如果 name 字段没有索引,InnoDB 会扫描全表,对所有行加锁(变成表锁效果) ,这就叫“锁升级”或“锁扩大”。

关于RR事务行锁升级为表锁的原因分析
因为在RR隔离级别下需要解决不可重复读和幻读问题,所以在扫描聚集索引记录时,为了防止扫描过的索引被其他事务修改(被修改就变成了不可重复读)或间隙被其他事务插入记录(幻读),从而导致数据不一致,所以mysql的解决方案时把所有扫描过的索引记录和间隙都锁上。

间隙锁(Gap Lock)

锁定一个索引值之间的“间隙”,不包括实际存在的行。

-- 假设 age 字段有索引,表中有记录:10, 30,50
SELECT * FROM user WHERE age > 10 AND age < 30 FOR UPDATE;

SELECT * FROM user WHERE age = 21 FOR UPDATE;

这两条 SQL 会加上:间隙锁:(10, 30),不能在这个事务未提交前,插入 age=15 或 age=25 等在这区间(开区间不包括10和30)的新记录。
只锁间隙,不锁已有记录。防止其他事务在这个间隙内插入新记录。在 RR 隔离级别中生效,在 RC 中不会加此锁。用于解决幻读问题

临键锁(Next-Key Lock)

当前行的记录锁(Record Lock) + 前面的间隙锁(Gap Lock)它是一种复合锁,由 InnoDB 引擎在 Repeatable Read(RR) 隔离级别下使用,用来防止幻读。

-- 假设数据表中 id 字段有如下值:(5), (10), (15), (20)
SELECT * FROM user WHERE id = 10 FOR UPDATE;

在 RR 隔离级别下,InnoDB 加的锁是:

  • 行锁:锁住 id = 10 这一行
  • 间隙锁:锁住 (5, 10) 这个间隙(即 5 < id < 10)
    所以加锁范围是:(5, 10] ,注意:包含了当前记录 + 它前面的间隙

何时会加临键锁?

  • 只在 RR 隔离级别下
  • 且查询语句带有 FOR UPDATE 或 LOCK IN SHARE MODE
  • 或更新语句 UPDATE / DELETE 使用索引扫描时
  • 查询必须使用索引(否则可能退化成表锁)

总结

InnoDB存储引擎由于实现了行级锁,虽然在锁定机制的实现方面所带来的性能损耗可能比表级锁更高一些,但是在整体并发处理能力要远远优于Myisam的表级锁的。当系统并发量高的时候,InnoDB的性能与Myisam对比就有明显的优势了。同时,InnoDB的行级锁如果使用不当,可能反而导致InnoDB的整体性能不如Myisam或者更差。所以索引的选择是系统性能的关键之一

锁等待分析

这个命令可以查看 InnoDB 层面的行锁相关统计信息,适合做全局锁等待的监控与分析。

show status like 'innodb_row_lock%';

输出字段说明:

参数名含义说明
Innodb_row_lock_current_waits当前正在等待锁的事务数量(实时)
** Innodb_row_lock_waits(重要)**自服务器启动以来累计发生的锁等待次数
Innodb_row_lock_time(重要)自服务器启动累计锁等待的总时间(毫秒)
Innodb_row_lock_time_max单次锁等待的最长时间(毫秒)
** Innodb_row_lock_time_avg(重要)**平均每次锁等待耗时(毫秒)
查看INFORMATION_SCHEMA系统库锁相关数据表
 -- 查看事务
select * from INFORMATION_SCHEMA.INNODB_TRX;
 -- 查看锁,8.0之后需要换成这张表performance_schema.data_locks
select * from INFORMATION_SCHEMA.INNODB_LOCKS;
 -- 查看锁等待,8.0之后需要换成这张表performance_schema.data_lock_waits
select * from INFORMATION_SCHEMA.INNODB_LOCK_WAITS;
 -- 释放锁,trx_mysql_thread_id可以从INNODB_TRX表里查看到
kill trx_mysql_thread_id
 -- 查看锁等待详细信息
show engine innodb status;
  • 死锁问题分析
set tx_isolation='REPEATABLE-READ';

set transaction_isolation='REPEATABLE-READ'; -- 8.0
  1. Session_1执行:select * from account where id=1 for update;
  2. Session_2执行:select * from account where id=2 for update;
  3. Session_1执行:select * from account where id=2 for update;
  4. Session_2执行:select * from account where id=1 for update;
  5. 查看近期死锁日志信息:show engine innodb status;
    mysql大多数情况会自动检测到死锁自动让产生死锁的事务失败(其他事务返回0行)
    在这里插入图片描述
    但是有些情况检测不到可以通过日志分析找到对应事务线程的id,可以通过kill杀掉。
    索引优化实践
  • 尽可能让所有数据检索都通过索引完成,避免无索引导致行锁升级为表锁
  • 合理设计索引,尽量缩小锁的范围
  • 尽可能减少检索条件范围,避免间隙锁
  • 尽可能控制事务大小,减少锁定资源量和时间,涉及事务加锁的sql尽量放在事务的最后执行(这样获取锁以后事务很快就可以提交释放锁)
  • 尽可能用低的事务隔离级别(RC适用大多数情况)

MVCC多版本并发控制机制

可见性判断算法(Repeatable Read 隔离级别)
Read View 是 InnoDB 在执行快照读时生成的一个结构,它描述了当前事务可以看到哪些事务已经提交的版本,用来决定哪些数据版本对当前事务“可见”。

read-view包含的字段(四个核心)

字段名含义
m_ids活跃的事务 ID 列表(未提交的)
min_trx_idm_ids 中最小的事务 ID
max_trx_id系统中下一个即将分配的事务 ID(也代表当前系统最大事务 ID)
creator_trx_id当前创建此 Read View 的事务 ID

每当一个事务第一次执行一致性读时(比如第一次 SELECT),InnoDB 就会创建一个read-view,之后这个事务的所有一致性读都会使用它(RR 隔离级别下,RC每次SELECT都会创建Read View)。

每个参数是怎么得来的?

  1. m_ids(活跃事务列表)

    活跃事务指的是:当前正在运行中,尚未提交或回滚的事务。

    • 在 InnoDB 的事务系统中,有一个“全局事务列表”(类似一个链表结构),保存着所有活跃事务(未提交)
    • 创建 Read View 时会遍历这个链表,把所有未提交事务的 ID 加入 m_ids

    举个例子:
    系统当前活跃事务:ID = 85, 90, 95
    你当前事务是 ID = 100,刚刚开始(未创建视图)
    则:m_ids = [85, 90, 95]

  2. min_trx_id

    • 就是 m_ids 中的最小事务 ID,表示最早还没提交的事务
    • 通过遍历 m_ids 得到
  3. max_trx_id

    • 这是一个全局自增事务 ID,InnoDB 维护了一个变量 next_trx_id
    • 每创建一个新事务,就会从这个变量获取,并加 1
    • 该值表示“当前系统中还没有分配的最大事务 ID”
    • 当前最大已分配事务 ID 是 100
    • 那么 next_trx_id = 101,这个就是 max_trx_id 的值
  4. creator_trx_id

    • 当前事务的事务 ID(trx_id),在事务开始时由 next_trx_id 分配
    • Read View 生成时,系统会直接取这个值作为当前事务的标识

有些老师讲的版本是最大id就是最后一个活跃的id,而m_ids则是[min_trx_id,max_trx_id]左右都是闭区间,大于max_trx_id的就是未开始的事务(在创建read-view那一时刻还没有新的事务),小于min_trx_id就是不活跃的事务(已提交或已经回滚)

可重复读隔离级别,当事务开启,执行任何查询sql时会生成当前事务的一致性视图read-view该视图在事务结束之前永远都不会变化(如果是读已提交隔离级别在每次执行查询sql时都会重新生成read-view),这个视图由执行查询时所有未提交事务id数组(数组里最小的id为min_id)和已创建的最大事务id(max_id)组成,事务里的任何sql查询结果需要从对应版本链里的最新数据开始逐条跟read-view做比对从而得到最终的快照结果。

  • 版本链比对规则(以下称undolog版本链中的一行链数据为row):
    • 如果 row 的 trx_id 落在绿色部分( trx_id<min_id ),表示这个版本是已提交的事务生成的,这个数据是可见的;
    • 如果 row 的 trx_id 落在红色部分( trx_id>max_id ),表示这个版本是由将来启动的事务生成的,是不可见的(若 row 的trx_id 就是当前自己的事务是可见的);
    • 如果 row 的 trx_id 落在黄色部分(min_id <=trx_id<= max_id),那就包括两种情况:
      1. 若 row 的 trx_id 在视图数组中,表示这个版本是由还没提交的事务生成的,不可见(如 row 的 trx_id 就是当前自己的事务是可见的);
      2. 若 row 的 trx_id 不在视图数组中,表示这个版本是已经提交了的事务生成的,可见
  • 对于删除的情况可以认为是update的特殊情况,会将版本链上最新的数据复制一份,然后将trx_id修改成删除操作的trx_id,同时在该条记录的头信息(record header)里的(deleted_flag)标记位写上true,来表示当前记录已经被删除,在查询时按照上面的规则查到对应的记录如果delete_flag标记位为true,意味着记录已被删除,则不返回数据。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值