导读
正所谓有人 (锁) 的地方就有江湖(事务), 人在江湖飘, 怎能一无所知?
今天来细说一下 MySQL 中的三类锁, 分别是全局锁, 表级锁, 行级锁.
全局锁
全局锁简单的说就是锁住整个数据库实例, 命令是
Flush tables with read lock
. 当你需要为整个数据库处于只读的状态的时候, 可以使用这个命令.
一旦使用全局锁, 之后其他线程的以下语句会被阻塞: 数据更新语句 (数据的增删改), 数据定义语句(包括建表, 修改表结构等) 和更新类事务的提交语句.
全局锁的使用场景大部分都是用来数据库备份.
为什么备份要加全局锁?
用户买东西, 首先会从余额里扣除金额, 然后在订单里添加商品. 如果备份数据库, 不加锁, 并且备份顺序为先备份用余额, 再备份订单商品, 有可能备份了用户余额后, 用户下订单买东西提交事务, 然后再备份订单商品表, 此时订单商品已存在. 最后备份出来的数据为. 最后用户余额为买东西前的余额, 没有减少, 但是订单商品却多了. 演示如下图:
这种情况可能用户会觉得赚了, 但是如果备份顺序反过来, 先备份商品表再备份余额表, 用户就会发现我付了钱, 但是商品没有加, 这中结果就会更加的严重.
因此保证备份数据的一致性很重要, 必要的手段就是加锁.
全局锁有什么坏处?
全局锁是个啥? 介绍完了读者心里已经有数了, 让这个库只读? 这是多么可怕的操作, 简单列举几个危险之处:
如果在主库备份, 备份期间不能执行任何更新操作, 会导致整个业务停摆, 高并发情况下更甚.
如果你在从库上备份, 那么备份期间从库不能执行主库同步过来的 binlog, 会导致主从延迟.
全局备份比较好的解决方案
全局锁远瞅不错, 近瞅吓一跳, 陈某在此不推荐使用.
其实 官方自带的逻辑备份工具是 mysqldump. 当 mysqldump 使用参数 **-single-transaction** 的时候, 导数据之前就会启动一个事务, 来确保拿到一致性视图. 而由于 MVCC 的支持, 这个过程中数据是可以正常更新的.
一致性备份是好, 但前提是存储引擎支持事务, 这也是 MyISAM 被 InnoDB 取代的原因之一.
表级锁
MySQL 里面表级别的锁有两种: 一种是表锁, 一种是元数据锁(meta data lock,MDL).
表锁一般是在数据库引擎不支持行锁的时候才会被用到的 .
MDL 会直到事务提交才释放, 在做表结构变更的时候, 你一定要小心不要导致锁住线上查询和更新 .
如何加表锁
显式加表锁和解锁的语句很简单, 如下:
- lock tables tb_name read/write;
- unlock tables;
需要注意, lock tables 语法除了会限制别的线程的读写外, 也限定了本线程接下来的操作对象.
举个例子, 如果在某个线程 A 中执行 lock tables t1 read, t2 write; 这个语句, 则其他线程写 t1, 读写 t2 的语句都会被阻塞. 同时, 线程 A 在执行 unlock tables 之前, 也只能执行读 t1, 读写 t2 的操作. 连写 t1 都不允许, 自然也不能访问其他表.
MDL
MDL 不需要显式使用, 在访问一个表的时候会被自动加上.
当对一个表做增删改查操作的时候, 加 MDL 读锁; 当要对表做结构变更操作的时候, 加 MDL 写锁.
读锁之间不互斥, 因此你可以有多个线程同时对一张表增删改查.
读写锁之间, 写锁之间是互斥的, 用来保证变更表结构操作的安全性. 因此, 如果有两个线程要同时给一个表加字段, 其中一个要等另一个执行完才能开始执行.
查询表级锁争用
查询表级锁的争用可以通过以下参数分析获得:
Table_locks_immediate
: 能够立即获得表级锁的次数
Table_locks_waited: 不能立即获取表级锁而需要等待的次数
查询语句如下:
show status like 'table_locks_waited'
如果 Table_locks_waited 的值比较大, 则说明存在着较严重的表级锁争用情况.
行级锁
MySQL 的行锁是在引擎层由各个引擎自己实现的. 但并不是所有的引擎都支持行锁, 比如 MyISAM 引擎就不支持行锁. 不支持行锁意味着并发控制只能使用表锁, 对于这种引擎的表, 同一张表上任何时刻只能有一个更新在执行, 这就会影响到业务并发度. InnoDB 是支持行锁的, 这也是 MyISAM 被 InnoDB 替代的重要原因之一.
InnoDB 的行锁是针对索引加的锁, 不是针对记录加的锁. 并且该索引不能失效, 否则都会从行锁升级为表锁.
在 InnoDB 事务中, 行锁是在需要的时候才加上的, 但并不是不需要了就立刻释放, 而是要等到事务结束时才释放.
行级锁分为排它锁(写锁), 共享锁(读锁), 间隙锁.
排他锁
排他锁, 也称写锁, 独占锁, 当前写操作没有完成前, 它会阻断其他写锁和读锁.
MySQL 中的更新语句 (update/delete/insert) 会自动加上排它锁.
如上图, 事务 B 中的 update 语句被阻塞了, 直到事务 A 提交才能执行更新操作.
排他锁也可以手动添加, 如下:
select * from user where id=1 for update;
注意以下两点:
行锁是针对索引加锁的, 上述例子中 id 是主键索引.
加了排他锁并不是其他的事务不能读取这行的数据, 而是不能再在这行上面加锁了.
间隙锁
当我们用范围条件检索数据, 并请求共享或排他锁时, InnoDB 会给符合条件的已有数据记录的索引项加锁; 对于键值在条件范围内但并不存在的记录, 叫做 "间隙(GAP)".InnoDB 也会对这个 "间隙" 加锁, 这种锁机制就是所谓的间隙锁(Next-Key 锁).
如上图, 给 id>5 中并不存在的数据加上了间隙锁, 当插入 id=6 的数据时被阻塞了.
这是一个坑: 若执行的条件是范围过大, 则 InnoDB 会将整个范围内所有的索引键值全部锁定, 很容易对性能造成影响
共享锁
共享锁, 也称读锁, 多用于判断数据是否存在, 多个读操作可以同时进行而不会互相影响. 当如果事务对读锁进行修改操作, 很可能会造成死锁. 如下图所示.
分析行锁定
通过检查 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: 系统启动后到现在总共等待的次数; 非常重要的参数. 直接决定优化的方向和策略.
死锁解决方案
1, 直接进入等待, 直到超时. 这个超时时间可以通过参数 innodb_lock_wait_timeout 来设置, 默认 50 秒. 注意超时时间不能设置太短, 如果仅仅是短暂的等待, 一旦设置时间很短, 很快便解锁了, 会出现误伤.
2, 发起死锁检测, 发现死锁后, 主动回滚死锁链条中的某一个事务, 让其他事务得以继续执行. 将参数 innodb_deadlock_detect 设置为 on, 表示开启这个逻辑, 默认开启. 主动死锁检测在发生死锁的时候, 是能够快速发现并进行处理的, 但是它也是有额外负担的. 当并发很高的时候, 检测死锁将会消耗大量的资源, 因此控制并发也是很重要的一种策略.
来源: https://www.cnblogs.com/Chenjiabing/p/12610822.html