什么是锁
在数据库中,除传统的计算机资源(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 TABLEStbl_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 LockGap 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 个行锁升级为一个页锁,或者将页锁升级为表锁。