1 MySql 的三种锁
1.1 表锁
开销小, 加锁快
不会出现死锁
锁定粒度大, 发生锁冲突的概率最高, 并发度最低
1.2 行锁
开销大, 加锁慢
会出现死锁
锁定粒度小, 发生锁冲突的概率最低, 并发度最高
1.3 页锁
开销和加锁时间介于表锁和行锁之间
会出现死锁
锁定粒度介于表锁和行锁之间, 并发度一般
1.4 不同的引擎支持不同的锁机制
MyISAM 和 MEMORY 支持表锁
BDB 支持页锁, 也支持表锁
Innodb 既支持行锁, 也支持表锁, 默认行锁
// 查询表锁争用情况
检查 `table_locks_waited` 和 `table_locks_immediate` 状态变量来分析
- show status like 'table%'
- //table_locks_waited 的值越高, 则说明存在严重的表级锁的争用情况
2 表锁的锁模式
是否兼容 | 请求 none | 请求读锁 | 请求写锁 |
---|---|---|---|
当前处于读锁 | 是 | 是 | 否 |
当前处于写锁 | 是 | 否 | 否 |
session_1 | session_2 |
---|---|
锁定 film_text 的 Write 锁定 lock table fime_text write | |
对当前 seesion 做 select,insert,update... | 对其进行查询操作 select |
释放锁 unlock tables | 等待 |
获得锁,查询返回 |
对 MyISAM 表的读操作, 不会阻塞其他用户对同一张表的读请求, 但会阻塞对同一张表的写请求
session_1 | session_2 |
---|---|
锁定 film_text 的 Write 锁定 lock table fime_text write | |
对当前 seesion 做 select,insert,update... | 对其进行查询操作 select |
释放锁 unlock tables | 等待 |
获得锁,查询返回 |
MyISAM
执行查询语句前, 会自动给涉及的所有表进行表加读锁
执行更新 (update,delete,insert) 会自动给涉及到的表加写锁
这个过程不需要用户干预, 因此不需要用户直接用 lock table 命令
对于给 MyISAM 显示加锁, 一般是为了在一定程度上模拟事务操作, 实现对某一个时间点多个表一致性读取
2.1 实例
订单表 orders
记录各订单的总金额 total
订单明细表 order_detail
记录各订单每一产品的金额小计 subtotal
假设我们需要检查这两个表的金额合计是否相符
- select sum(total) from orders;
- select sum(subtotal) from order_tail;
如果不给表加锁, 可能出现错误, 在第一条执行的过程, 第二张表发生了该表, 正确的方法
- lock tables orders read local,order_detail read local;
- select sum(total) from orders;
- select sum(subtotal) from order_tail;
- unlock tables
2.2 注意点
在用 lock tables 给表显式加表锁时, 必须同时取得所有涉及的表的锁, 并且 MySQL 支持锁升级
即在执行 lock tables 后, 只能访问显式加锁的这些表, 不能访问未加锁的表
如果加的是读锁, 那么只能执行查询, 不能更新
其实, 在自动加锁的情况下也基本如此, MySQL 问题一次获得 SQL 语句所需要的全部锁
这也正是 MyISAM 的表不会出现死锁 (Deadlock Free) 的原因
session_1 | session_2 |
---|---|
获得表 film_textd 写锁 lock table film_text read; | |
可以查询 select * from film_text | 可以查询可以查询 select * from film_text |
不能查询没有锁定的表 select * from film | 可以查询或更新未锁定的表 select * from film |
插入或更新锁定表会提示错误 update...from film_text | 更新锁定表会等待 update...from film_text |
释放锁 unlock tables | 等待 |
获得锁,更新成功 |
2.3 tips
当使用 lock tables 时, 不仅需要一次锁定用到的所有表, 而且
同一个表在 SQL 语句中出现多少次, 就要通过与 SQL 语句中别名锁多少次
lock table actor read
会提示错误
select a.first_name.....
需要对别名分别锁定
lock table actor as a read,actor as b read;
3MyISAM 的并发锁
在一定条件下, MyISAM 也支持并发插入和读取
MyISAM 有一个系统变量 concurrent_insert, 专门用以控制其并发插入的行为, 其值分别可以为 01 或 2
删除操作不会重整整个表, 只是把行标记为删除, 在表中留下 "空洞",MyISAM 倾向于在可能时填满这些空洞, 插入时就会重用 这些空间, 无空洞则把新行插到表尾
0, 不允许并发插入, 所有插入对表加互斥锁
1, 只要表中无空洞, 就允许并发插入. 如果 MyISAM 允许在一个读表的同时, 另一个进程从表尾插入记录这也是 MySQL 的默认设置
2, 无论 MyISAM 表中有无空洞, 都强制在表尾并发插入记录, 若无读线程, 新行插入空洞中
可以利用 MyISAM 的并发插入特性, 来解决应用中对同表查询和插入的锁争用
例如, 将 concurrent_insert 系统变量为 2, 总是允许并发插入
3.1 MyISAM 的锁调度
MyISAM 的读和写锁互斥, 读操作串行的
一个进程请求某个 MyISAM 表的读锁, 同时另一个进程也请求同表的写锁, MySQL 如何处理呢?
答案是写进程先获得锁不仅如此, 即使读进程先请求先到锁等待队列, 写请求后到, 写锁也会插到读请求之前!
这是因为 MySQL 认为写请求一般比读请求重要
这也正是 MyISAM 表不适合有大量更新和查询操作应用的原因
因为, 大量的更新操作会造成查询操作很难获得读锁, 从而可能永远阻塞
幸好我们可以通过一些设置来调节 MyISAM 的调度行为
启动参数 low-priority-updates
给予读请求以优先的权利
执行命令 SET LOW_PRIORITY_UPDATES=1
使该连接发出的更新请求优先级降低
指定 INSERTUPDATEDELETE 语句的 LOW_PRIORITY 属性
降低该语句的优先级
另外, MySQL 也提供了一种折中的办法来调节读写冲突, 即给系统参数
max_write_lock_count
设置一个合适的值, 当一个表的读锁达到这个值后, MySQL 便暂时将写请求的优先级降低, 给读进程一定获得锁的机会
4 InnoDB 锁问题
与 MyISAM 最大不同
支持事务
采用行锁
行锁和表锁本来就有许多不同之处, 另外, 事务的引入也带来了一些新问题
4.1 事务及其 ACID
事务是由一组 SQL 语句组成的逻辑处理单元, 事务具有 ACID 属性
原子性(Actomicity)
事务是一个原子操作单元, 其对数据的修改, 要么全都执行, 要么全都不执行
一致性(Consistent)
在事务开始和完成时, 数据都必须保持一致状态
这意味着所有相关的数据规则都必须应用于事务的修改, 以操持完整性
事务结束时, 所有的内部数据结构 (如 B 树索引或双向链表) 也都必须是正确的
隔离性(Isolation)
一个事务所做的修改在最终提交前对其他事务不可见
持久性(Durability)
一旦事务提交, 它对于数据的修改会持久化到 DB
4.2 事务带来的问题
相对于串行处理来说, 并发事务处理能大大增加数据库资源的利用率, 提高数据库系统的事务吞吐量, 从而可以支持可以支持更多的用户
但并发事务处理也会带来一些问题, 主要包括以下几种情况
更新丢失(Lost Update)
当多个事务选择同一行, 然后基于最初选定值更新该行时, 由于事务隔离性, 最后的更新覆盖了其他事务所做的更新
例如, 两个编辑人员制作了同一文档的电子副本每个编辑人员独立地更改其副本, 然后保存更改后的副本, 这样就覆盖了原始文档最后保存其更改保存其更改副本的编辑人员覆盖另一个编辑人员所做的修改如果在一个编辑人员完成并提交事务之前, 另一个编辑人员不能访问同一文件, 则可避免此问题
脏读(Dirty Reads)
一个事务正在对一条记录做修改, 在该事务提交前, 这条记录的数据就处于不一致状态
这时, 另一个事务也来读取同一条记录, 读取了这些未提交的数据
不可重复读(Non-Repeatable Reads)
一个事务在读取某些数据已经发生了改变或某些记录已经被删除
幻读(Phantom Reads)
一个事务按相同的查询条件重新读取以前检索过的数据, 却发现其他事务插入了满足其查询条件的新数据
4.3 事务隔离级别
在并发事务处理带来的问题中, 更新丢失通常应该是完全避免的但防止更新丢失, 并不能单靠数据库事务控制器来解决, 需要应用程序对要更新的数据加必要的锁来解决, 因此, 防止更新丢失应该是应用的责任
脏读不可重复读和幻读, 其实都是数据库读一致性问题, 必须由数据库提供一定的事务隔离机制来解决数据库实现事务隔离的方式, 基本可以分为以下两种
在读取数据前, 对其加锁, 房主其他事务对数据进行修改
不用加任何锁, 通过一定机制生成一个数据请求时间点的一致性数据快照, 并用这个快照来提供一定级别 (语句级或事务级) 的一致性读取从用户的角度, 好像是数据库可以提供同一数据的多个版本, 因此, 这种技术叫做数据多版本并发控制(MultiVersion Concurrency Control, 简称 MVCC 或 MCC), 也经常称为多版本数据库
数据库的事务隔离级别越严格, 并发副作用越小, 但付出的代价也越大
因为事务隔离实质上就是使事务在一定程度上串行化进行, 这显然与并发矛盾,
不同的应用对读一致性和事务隔离程度的要求也是不同的, 比如许多应用对不可重复读和幻读并不敏感, 可能更关心数据并发访问的能力
为了解决隔离与并发的矛盾, ANSI SQL 定义了4种隔离级别
隔离级别 / 读数据一致性及允许的并发副作用 | 读数据一致性 | 脏读 | 不可重复读 | 幻读 |
---|---|---|---|---|
未提交读(Read uncommitted) | 最低级别,只能保证不读取物理上损坏的数据 | 是 | 是 | 是 |
已提交度(Read committed) | 语句级 | 否 | 是 | 是 |
可重复读(Repeatable read) | 事务级 | 否 | 否 | 是 |
可序列化(Serializable) | 最高级别,事务级 | 否 | 否 | 否 |
- // 查看 Innodb 行锁争用情况
- show status like 'innodb_row_lock%'
- // 如果发现争用比较严重, 如 Innodb_row_lock_waits 和 Innodb_row_lock_time_avg 的值比较高
- // 通过查询 information_schema 相关表来查看锁情况
- select * from innodb_locks
- select * from innodb_locks_waits
- // 或者通过设置 Innodb monitors 来进一步观察发生锁冲突的表, 数据行等, 并分析锁争用的原因
- show ENGINE innodb status
- // 停止监视器
- drop table innodb_monitor;
- // 默认情况每 15 秒回向日志中记录监控的内容, 如果长时间打开会导致. err 文件变得非常巨大, 所以确认原因后, 要删除监控表关闭监视器, 或者通过使用 --console 选项来启动服务器以关闭写日志功能
4.4 InnoDB 的行锁
InnoDB 支持以下两种类型的行锁
共享锁(读锁 S)
允许一个事务去读一行, 阻止其他事务获得相同数据集的排他锁
排他锁(写锁 X)
允许获取排他锁的事务更新数据, 阻止其他事务取得相同的数据集共享读锁和排他写锁
另外, 为了允许行 / 表锁共存, 实现多粒度锁机制, InnoDB 还有两种内部使用的意向锁(Intention Locks), 这两种意向锁都是表锁
意向共享锁(IS)
事务打算给数据行共享锁, 事务在给一个数据行加共享锁前必须先取得该表的 IS 锁
意向排他锁(IX)
事务打算给数据行加排他锁, 事务在给一个数据行加排他锁前必须先取得该表的 IX 锁
当前锁 / 是否兼容 / 请求锁 | X | IX | S | IS |
---|---|---|---|---|
X | 冲突 | 冲突 | 冲突 | 冲突 |
IX | 冲突 | 兼容 | 冲突 | 兼容 |
S | 冲突 | 冲突 | 兼容 | 兼容 |
IS | 冲突 | 兼容 | 兼容 | 兼容 |
如果一个事务的请求锁与当前锁兼容, InnoDB 就请求的锁授予该事务
如果不兼容, 该事务就要等待锁释放
对于 UPDATEDELETE 和 INSERT 语句, InnoDB 会自动给涉及数据集排他锁(X)
对于普通 SELECT 语句, InnoDB 不会任何锁
可以通过以下语句显示地给记录加读 / 写锁
共享锁(S)
select * from table_name where ... lock in share mode
排他锁(X)
select * from table_name where ... FOR UPDATE
用
select * from table_name where ... lock in share mode
获得共享锁, 主要用在需要数据依存关系时确认某行记录是否存在, 并确保没有人对这个记录 UPDATE 或 DELETE
但如果当前事务也需要对该记录进行更新, 则很有可能造成死锁, 对于锁定行记录后需要进行更新操作的应用, 应该使用
select * from table_name where ... FOR UPDATE
方式获取排他锁
4.5 实例
4.5.1 Innodb 共享锁
session_1 | session_2 |
---|---|
set autocommit=0,select * from actor where id =1 | set autocommit=0,select * from actor where id =1 |
当前 seesion 对 id 为 1 的记录加入共享锁 select * from actor where id =1 lock in share mode | |
其他 seesion 仍然可以查询,并对该记录加入 select * from actor where id =1 lock in share mode | |
当前 session 对锁定的记录进行更新,等待锁 update。。。where id=1 | |
当前 session 对锁定记录进行更新,则会导致死锁退出 update。。。where id=1 | |
获得锁,更新成功 |
4.5.2 Innodb 排他锁
session_1 | session_2 |
---|---|
set autocommit=0,select * from actor where id =1 | set autocommit=0,select * from actor where id =1 |
当前 seesion 对 id 为 1 的记录加入 for update 共享锁 select * from actor where id =1 for update | |
可查询该记录 select *from actor where id =1, 但是不能再记录共享锁,会等待获得锁 select *from actor where id =1 for update | |
更新后释放锁 update。。。 commit | |
其他 session,获得所,得到其他 seesion 提交的记录 |
4.6 Innodb 行锁实现
InnoDb 行锁是通过给索引上的索引项加锁来实现
如果没有索引, InnoDB 将通过隐藏的聚簇索引来对记录加锁
Record Locks: 对索引项加锁
Gap lock: 对索引项之的间隙, 第一天记录前的间隙, 或最后一条记录后的间隙, 加锁
Next-key lock: 前两种的组合, 对记录及其前面的间隙加锁
InnoDb 的行锁, 实现特点意味着:
如果不通过索引条件检索数据, 那么 Innodb 将对表的所有记录加锁, 和表锁一样
间隙锁(Next-Key 锁)
- SELECT * FROM emp WHERE empid > 100 FOR UPDATE
- // 是一个范围条件的检索, InnoDB 不仅会对符合条件的 empid 值为 101 的记录加锁, 也会对 empid 大于 101(这些记录并不存在)的间隙加锁
InnoDB 使用间隙锁的目的, 一方面是为了防止幻读, 以满足相关隔离级别的要求, 对于上面的例子, 要是不使用间隙锁, 如果其他事务插入了 empid 大于 100 的任何记录, 那么本事务如果再次执行上述语句, 就会发生幻读; 另一方面, 是为了满足其恢复和复制的需要很显然, 在使用范围条件检索并锁定记录时, InnoDB 这种加锁机制会阻塞符合条件范围内键值的并发插入, 这往往会造成严重的锁等待因此, 在实际开发中, 尤其是并发插入比较多的应用, 我们要尽量优化业务逻辑, 尽量使用相等条件来访问更新数据, 避免使用范围条件
4.7 什么时候使用表锁
对于 InnoDB, 在绝大部分情况下都应该使用行锁
因为事务和行锁往往是我们之所以选择 InnoDB 的理由
但在个别特殊事务中, 也可以考虑使用表锁
事务需要更新大部分数据, 表又较大, 如果使用默认的行锁, 不仅这个事务执行效率低, 而且可能造成其他事务长时间锁等待和锁冲突, 这种情况下可以考虑使用表锁来提高该事务的执行速度
事务涉及多个表, 比较复杂, 很可能引起死锁, 造成大量事务回滚
这种情况也可以考虑一次性锁定事务涉及的表, 从而避免死锁减少数据库因事务回滚带来的开销
当然, 应用中这两种事务不能太多, 否则, 就应该考虑使用MyISAM
在 InnoDB 下 , 使用表锁要注意以下两点
使用 LOCK TALBES 虽然可以给 InnoDB 加表级锁, 但必须说明的是, 表锁不是由 InnoDB 引擎层管理的, 而是由其上一层MySQL Server 负责的
仅当
autocommit=0innodb_table_lock=1(默认设置)
时, InnoDB 层才能知道 MySQL 加的表锁,MySQL Server 才能感知 InnoDB 加的行锁
这种情况下, InnoDB 才能自动识别涉及表锁的死锁
否则, InnoDB 将无法自动检测并处理这种死锁
在用 LOCAK TABLES 对 InnoDB 锁时要注意, 要将 AUTOCOMMIT 设为 0, 否则MySQL 不会给表加锁; 事务结束前, 不要用 UNLOCAK TABLES 释放表锁, 因为 UNLOCK TABLES 会隐含地提交事务; COMMIT 或 ROLLBACK 产不能释放用 LOCAK TABLES 加的表级锁, 必须用 UNLOCK TABLES 释放表锁, 正确的方式见如下语句
- // 例如, 如果需要写表 t1 并从表 t 读, 可以按如下做:
- SET AUTOCOMMIT=0;
- LOCAK TABLES t1 WRITE, t2 READ, ...;
- [do something with tables t1 and here];
- COMMIT;
- UNLOCK TABLES;
关于死锁
MyISAM 表锁是 deadlock free 的, 这是因为MyISAM 总是一次性获得所需的全部锁, 要么全部满足, 要么等待, 因此不会出现死锁但是在 InnoDB 中, 除单个 SQL 组成的事务外, 锁是逐步获得的, 这就决定了 InnoDB 发生死锁是可能的
发生死锁后, InnoDB 一般都能自动检测到, 并使一个事务释放锁并退回, 另一个事务获得锁, 继续完成事务但在涉及外部锁, 或涉及锁的情况下, InnoDB 并不能完全自动检测到死锁, 这需要通过设置锁等待超时参数 innodb_lock_wait_timeout 来解决需要说明的是, 这个参数并不是只用来解决死锁问题, 在并发访问比较高的情况下, 如果大量事务因无法立即获取所需的锁而挂起, 会占用大量计算机资源, 造成严重性能问题, 甚至拖垮数据库我们通过设置合适的锁等待超时阈值, 可以避免这种情况发生
通常来说, 死锁都是应用设计的问题, 通过调整业务流程数据库对象设计事务大小以及访问数据库的 SQL 语句, 绝大部分都可以避免下面就通过实例来介绍几种死锁的常用方法
在应用中, 如果不同的程序会并发存取多个表, 应尽量约定以相同的顺序为访问表, 这样可以大大降低产生死锁的机会如果两个 session 访问两个表的顺序不同, 发生死锁的机会就非常高! 但如果以相同的顺序来访问, 死锁就可能避免
在程序以批量方式处理数据的时候, 如果事先对数据排序, 保证每个线程按固定的顺序来处理记录, 也可以大大降低死锁的可能
在事务中, 如果要更新记录, 应该直接申请足够级别的锁, 即排他锁, 而不应该先申请共享锁, 更新时再申请排他锁, 甚至死锁
在 REPEATEABLE-READ 隔离级别下, 如果两个线程同时对相同条件记录用 SELECT...ROR UPDATE 加排他锁, 在没有符合该记录情况下, 两个线程都会加锁成功程序发现记录尚不存在, 就试图插入一条新记录, 如果两个线程都这么做, 就会出现死锁这种情况下, 将隔离级别改成 READ COMMITTED, 就可以避免问题
当隔离级别为 READ COMMITED 时, 如果两个线程都先执行 SELECT...FOR UPDATE, 判断是否存在符合条件的记录, 如果没有, 就插入记录此时, 只有一个线程能插入成功, 另一个线程会出现锁等待, 当第1个线程提交后, 第2个线程会因主键重出错, 但虽然这个线程出错了, 却会获得一个排他锁! 这时如果有第3个线程又来申请排他锁, 也会出现死锁对于这种情况, 可以直接做插入操作, 然后再捕获主键重异常, 或者在遇到主键重错误时, 总是执行 ROLLBACK 释放获得的排他锁
如果出现死锁, 可以用 SHOW INNODB STATUS 命令来确定最后一个死锁产生的原因和改进措施
锁总结
对于 MyISAM 的表锁, 主要有以下几点
共享读锁 (S) 之间是兼容的, 但共享读锁 (S) 和排他写锁 (X) 之间, 以及排他写锁之间 (X) 是互斥的, 也就是说读和写是串行的
在一定条件下,MyISAM 允许查询和插入并发执行, 我们可以利用这一点来解决应用中对同一表和插入的锁争用问题
MyISAM 默认的锁调度机制是写优先, 这并不一定适合所有应用, 用户可以通过设置 LOW_PRIPORITY_UPDATES 参数, 或在 INSERTUPDATEDELETE 语句中指定 LOW_PRIORITY 选项来调节读写锁的争用
由于表锁的锁定粒度大, 读写之间又是串行的, 因此, 如果更新操作较多,MyISAM 表可能会出现严重的锁等待, 可以考虑采用 InnoDB 表来减少锁冲突
对于 InnoDB 表, 主要有以下几点
InnoDB 的行销是基于索引实现的, 如果不通过索引访问数据, InnoDB 会使用表锁
InnoDB 间隙锁机制, 以及 InnoDB 使用间隙锁的原因
在不同的隔离级别下, InnoDB 的锁机制和一致性读策略不同
MySQL 的恢复和复制对 InnoDB 锁机制和一致性读策略也有较大影响
锁冲突甚至死锁很难完全避免
在了解 InnoDB 的锁特性后, 用户可以通过设计和 SQL 调整等措施减少锁冲突和死锁, 包括:
尽量使用较低的隔离级别
精心设计索引, 并尽量使用索引访问数据, 使加锁更精确, 从而减少锁冲突的机会
选择合理的事务大小, 小事务发生锁冲突的几率也更小
给记录集显示加锁时, 最好一次性请求足够级别的锁比如要修改数据的话, 最好直接申请排他锁, 而不是先申请共享锁, 修改时再请求排他锁, 这样容易产生死锁
不同的程序访问一组表时, 应尽量约定以相同的顺序访问各表, 对一个表而言, 尽可能以固定的顺序存取表中的行这样可以大减少死锁的机会
尽量用相等条件访问数据, 这样可以避免间隙锁对并发插入的影响
不要申请超过实际需要的锁级别; 除非必须, 查询时不要显示加锁
对于一些特定的事务, 可以使用表锁来提高处理速度或减少死锁的可能
来源: http://www.jianshu.com/p/c248e672fe83