什么是锁
在数据库中,除传统的计算机资源(CPU
、RAM
、I/O
)的争用之外,数据也是一种供许多用户共享的资源。
锁是计算机协调多个进程或纯线程并发访问某一资源的机制,保证数据的完整性和一致性。
锁冲突是影响数据库并发访问性能的一个重要因素。
粒度锁
不同的存储引擎所针对的应用场景的特点不同,锁的粒度也不同。
表级锁
开销小,加锁快;不会出现死锁;锁定粒度大,发生锁冲突的概率最高,并发度最低;
使用表级锁的主要是
MyISAM
,MEMORY
,CSV
等一些非事务性存储引擎行级锁
开销大,加锁慢;会出现死锁;锁定粒度最小,发生锁冲突的概率最低,并发度也最高;
使用行级锁的主要是
InnoDB
存储引擎。页级锁
开销和加锁时间界于表锁和行锁之间;会出现死锁;锁定粒度界于表锁和行锁之间,并发度一般。
使用页级锁的主要是
BerkeleyDB
(MySQL 早已不支持此存储引擎) 存储引擎。
MyISAM 表级锁
MyISAM 表级锁的类型
读锁:允许持有该锁的线程读取表数据
写锁:允许持有该锁的线程读取和更新数据
MyISAM 获取表锁的方式
MyISAM
在执行查询语句前,会自动(没有冲突的前提下)获得涉及的所有表的读锁,在执行更新操作(UPDATE、DELETE、INSERT
等)前,会自动(没有冲突的前提下)获得涉及的所有表的写锁,这个过程并不需要用户干预。
MyISAM 表级锁的兼容性
读锁 | 写锁 | |
---|---|---|
读锁 | 兼容 | 冲突 |
写锁 | 冲突 | 冲突 |
如果请求的锁与现有锁兼容,则授予该锁,如果与现有锁冲突,则不授予该锁:
读锁允许将同一表的读锁授予其他客户端会话,阻止将同一表的写锁授予其他客户端会话
写锁阻止将同一表的读锁和写锁授予其他客户端会话
例如:
#session1 使用 sleep 函数人为减慢 select 执行时间,使读锁持续时间更久
select *,sleep(1) from tab_name;
#session2 在 session1 的查询未完成前,以下语句将会被阻塞
update tab_name set column='string';
MyISAM 表级锁的优先级
默认情况下,写锁比读锁具有更高的优先级:当一个锁释放时,会优先授予写锁队列中等候的获取锁请求,最后再授予读锁队列中等候的获取锁请求。如果有大量的更新操作,则会造成查询操作很难获得读锁,从而可能永远阻塞,因此 MyISAM
表不太适合于有大量更新操作和查询操作的应用。
MyISAM 并发插入
MyISAM
存储引擎支持并发插入,以减少给定表的读和写操作之间的争用,并发插入的行为受系统变量 concurrent_insert
的影响:
值为 0 时
不允许并发插入
值为 1 时(默认值)
如果
MyISAM
表中没有空闲块时(空闲块是从表格中间删除行而产生的),MyISAM
允许在一个线程读表的同时,另一个线程插入记录到数据文件的末尾。如果有空闲块,则不允许并发插入
值为 2 时
无论
MyISAM
表中有没有空闲块,都允许在数据文件的末尾并发插入记录。
使用 LOCK TABLES
显式获取表锁时,如果请求的是 READ LOCAL
锁而不是 READ
锁,其他线程也可以使用并发插入。
查询表级锁争用情况
可以通过检查 table_locks_waited
和 table_locks_immediate
状态变量来分析系统上的表锁的争夺,如果 Table_locks_waited
的值比较高,则说明存在着较严重的表级锁争用情况
mysql> SHOW STATUS LIKE 'Table%';
+-----------------------+---------+
| Variable_name | Value |
+-----------------------+---------+
| Table_locks_immediate | 1151552 |
| Table_locks_waited | 15324 |
+-----------------------+---------+
LOCK TABLES 表级锁
LOCK TABLES
和 UNLOCK TABLES
的语法:
LOCK TABLES
tbl_name [[AS] alias] lock_type
[, tbl_name [[AS] alias] lock_type] ...
lock_type: {
READ [LOCAL]
| [LOW_PRIORITY] WRITE
}
UNLOCK TABLES
LOCK TABLES ... READ [LOCAL]
的行为与 MyISAM 的读锁类似,LOCK TABLES ... WRITE
的行为与 MyISAM 的写锁类似。
LOCK TABLES
锁可用于模拟事务或在更新表时获得更快的速度(比如在 MyISAM 表上进行批量操作时,通过显式锁表可以使 MySQL 在所有更新完成前暂不更新缓存、索引等,而是等待所有更新完成后进行一次缓存、索引的更新)
获取锁
LOCK TABLES
显式地为当前客户端会话获取指定的基表或视图的表锁。
如果锁定的是视图,则将视图中使用的所有基表添加到要锁定的表集中并自动锁定它们。
如果锁定的表有外键约束,则任何与外键约束相关的表都将被隐式锁定。
释放表锁
LOCK TABLES
在获取新锁之前,隐式释放当前客户端会话持有的所有表锁UNLOCK TABLES
显式地释放当前客户端会话持有的所有表锁。当与服务器的连接被关闭时,所有由当前线程锁定的表被隐式地解锁
与事务的交互
LOCK TABLES
在锁定表之前会隐式地提交事务UNLOCK TABLES
在释放锁的同时会隐式地提交事务(前提是已通过LOCK TABLES
获取了表锁)START TRANSACTION
开启新的事务,自动结束前一个未结束的事务,并释放获得的表级锁COMMIT
或ROLLBACK
或其他的隐式提交,都不能释放用LOCK TABLES
获得的表级锁,必须用UNLOCK TABLES
释放表锁。
InnoDB 行级锁
InnoDB 行级锁的类型
共享锁(Share S):允许获得该锁的事务读取行数据
独占锁/排他锁(Exclusive X):允许获得该锁的事务读取和更新行数据
InnoDB 行级锁的兼容性
共享锁 | 排它锁 | |
---|---|---|
共享锁 | 兼容 | 冲突 |
排它锁 | 冲突 | 冲突 |
如果请求的锁与现有锁兼容,则授予该锁,如果与现有锁冲突,则不授予该锁:
共享锁允许将相同数据集的共享锁授予其他客户端会话,阻止将相同数据集的排他锁授予其他客户端会话
排它锁阻止将相同数据集的共享锁和排它锁授予其他客户端会话
意向锁
InnoDB 支持多种粒度锁定,允许行锁和表锁并存,例如 LOCK TABLES
语句可以在指定表上获取表锁。为了允许行锁和表锁共存,实现多粒度锁机制,InnoDB
还有两种内部使用的意向锁(Intention Locks
),这两种意向锁都是表锁:
意向共享锁(
IS
):事务想要获得一个表中某几行的共享锁意向排他锁(
IX
):事务想要获得一个表中某几行的排他锁
意向锁存在的意义:当需要获得表锁时,可以直接检查与目标表的意向锁是否冲突,而不需要遍历该表的所有行锁。
例如:事务 A 获得了表中某几行的共享锁。
之后,事务 B 申请整个表的写锁。
如果事务 B 申请成功,那么理论上它就能修改表中的任意一行,这与 A 持有的行锁是冲突的。数据库需要避免这种冲突,就要让 B 的申请被阻塞,直到 A 释放了行锁。
数据库要怎么判断这个冲突呢?
第一步、判断表是否已被其他事务用表锁锁表;
第二步、判断表中的每一行是否已被行锁锁住(这样的判断方法效率实在不高,因为需要遍历整个表)
于是就有了意向锁。在意向锁存在的情况下,事务 A 必须先申请表的意向共享锁,成功后再申请行的共享锁。
在意向锁存在的情况下,第二步发现表上有意向共享锁,说明表中有些行被共享行锁锁住了,因此,事务 B 申请表的写锁会被阻塞。
意向锁是 InnoDB 自动获得的, 不需用户干预。
表级锁的兼容性如下:
X | IX | S | IS | |
---|---|---|---|---|
X | 冲突 | 冲突 | 冲突 | 冲突 |
IX | 冲突 | 兼容 | 冲突 | 兼容 |
S | 冲突 | 冲突 | 兼容 | 兼容 |
IS | 冲突 | 兼容 | 兼容 | 兼容 |
InnoDB 获取锁的方式
隐式获取:
- 对于 UPDATE、 DELETE 和 INSERT 语句, InnoDB 会自动获取数据集的排他锁(X);
显式获取:
SELECT ... LOCK IN SHARE MODE
获取数据集的共享锁SELECT ... FOR SHARE
获取数据集的共享锁SELECT ... FOR UPDATE
获取数据集的排他锁
特别的:对于普通 SELECT 语句,InnoDB 不会获取任何锁,也就不存在锁冲突问题。
行锁与索引
InnoDB 只有通过索引条件检索数据,InnoDB 才使用行级锁,否则,InnoDB 将进行全表扫描,使用表锁。
InnoDB 锁的算法
InnoDB 有三种锁的算法设计:
Record Lock
单个行记录上的锁Gap Lock
间隙锁,锁定一个范围,但不包括记录本身Next-Key Lock
Gap Lock
+Record Lock
锁定一个范围,包括记录本身
死锁(Deadlock Free)
死锁产生
死锁是指两个或多个事务在同一资源上相互占用,并请求锁定对方占用的资源,从而导致恶性循环。
检测死锁
数据库系统实现了各种死锁检测和死锁超时的机制。InnoDB存储引擎能检测到死锁的循环依赖并立即返回一个错误。
死锁恢复
死锁发生以后,只有部分或完全回滚其中一个事务,才能打破死锁,InnoDB目前处理死锁的方法是,将持有最少行级排他锁的事务进行回滚。所以事务型应用程序在设计时必须考虑如何处理死锁,多数情况下只需要重新执行因死锁回滚的事务即可。
外部锁的死锁检测
发生死锁后,InnoDB 一般都能自动检测到,并使一个事务释放锁并回退,另一个事务获得锁,继续完成事务。但在涉及外部锁,或涉及表锁的情况下,InnoDB 并不能完全自动检测到死锁, 这需要通过设置锁等待超时参数 innodb_lock_wait_timeout
来解决
死锁影响性能
死锁会影响性能而不是会产生严重错误,因为InnoDB会自动检测死锁状况并回滚其中一个受影响的事务。在高并发系统上,当许多线程等待同一个锁时,死锁检测可能导致速度变慢。 有时当发生死锁时,禁用死锁检测(使用innodb_deadlock_detect配置选项)可能会更有效,这时可以依赖 innodb_lock_wait_timeout
设置进行事务回滚。
MyISAM 避免死锁:
MyISAM 总是一次性自动获得 SQL 语句所需要的全部锁,所以 MyISAM 表不会出现死锁。
乐观锁、悲观锁的概念
乐观锁(Optimistic Lock)
假设不会发生并发冲突,只在提交操作时检查是否违反数据完整性。
乐观锁, 顾名思义,就是很乐观,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号等机制。乐观锁适用于多读的应用类型,这样可以提高吞吐量,像数据库如果提供类似于write_condition 机制的其实都是提供的乐观锁。
悲观锁(Pessimistic Lock)
假定会发生并发冲突,屏蔽一切可能违反数据完整性的操作。
悲观锁,顾名思义,就是很悲观,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会block直到它拿到锁。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。
锁升级
锁升级是指将当前锁的粒度降低。比如数据库可以把一个表的 1000 个行锁升级为一个页锁,或者将页锁升级为表锁。