一. 背景知识
二. 步入正题:表锁和行锁
1.1. 表锁 vs 行锁
在 MySQL 中锁的种类有很多,但是最基本的还是表锁和行锁:表锁指的是对一整张表加锁,一般是 DDL 处理时使用,也可以自己在 SQL 中指定;而行锁指的是锁定某一行数据或某几行,或行和行之间的间隙。行锁的加锁方法比较复杂,但是由于只锁住有限的数据,对于其它数据不加限制,所以并发能力强,通常都是用行锁来处理并发事务。表锁由 MySQL 服务器实现,行锁由存储引擎实现,常见的就是InnoDb,所以通常我们在讨论行锁时,隐含的一层意义就是数据库的存储引擎为 InnoDb ,而 MyISAM 存储引擎只能使用表锁。
1.2. 表锁
表锁由 MySQL 服务器实现,所以无论你的存储引擎是什么,都可以使用。一般在执行 DDL 语句时,譬 如 ALTER TABLE 就会对整个表进行加锁。在执行 SQL 语句时,也可以明确对某个表加锁。
1.2.1 操作演示(user_test创建表脚本见2.4.1)
-- 事物A中 显示对 user_test 加上读锁locktableuser_test read;select*fromuser_testwhereid=3; -- 此时事物B中对其进行update或者write操作start TRANSACTION;-- 手动开启事物insertintouser_test(age,name)values(18,'Tom');
结论:此时事物B一致处于等待事物A释放锁的状态,最终会回去锁超时
2.1. 行锁(以下验证是在默认的隔离级别(可重复读的事务隔离级)下操作)
InnoDB NEXT-KEY Locks,解决了在可重复读的事务隔离级别下出现幻读的问题。
什么是幻读?
幻读是在可重复读的事务隔离级别下会出现的一种问题,简单来说,可重复读保证了当前事务不会读取到其他事 务已提交的 UPDATE 操作。但同时,也会导致当前事务无法感知到来自其他事务中的 INSERT 或 DELETE 操 作,这就是幻读。
2.2. 关于行锁我们要知道的
行锁在 InnoDB 中是基于索引实现的,所以一旦某个加锁操作没有使用索引,那么该锁就会退化为表锁。
2.3 行锁分类
2.3.1 从加锁范围划分
a)记录锁(Record Locks):存在与唯一索引包括主键索引 顾名思义,记录锁就是为某行记录加锁,它封锁该行的索引记录:
b)间隙锁(Gap Locks):
存在与非唯一索引中,锁定开区间范围内的一段间隔,它是基于临键锁实现的。
间隙锁基于非唯一索引,它锁定一段范围内的索引记录。间隙锁基于下面将会提到的Next-Key Locking 算法,请务必牢记:使用间隙锁锁住的是一个区间,而不仅仅是这个区间中的每一条数据。
是一种加在两个索引之间的锁,或者加在第一个索引之前,或最后一个索引之后的间隙。有时候又称为范围(Range Locks),这个范围可以跨一个索引记录,多个索引记录,甚至是空的。使用间隙锁可以防止其他事务在这个范围内插入或修改记录,保证两次读取这个范围内的记录不会变,从而不会出现幻读现象。很显然,间隙锁会增加数据库的开销,虽然解决了幻读问题,但是数据库的并发性一样受到了影响,所以在选择数据库的隔离级别时,要注意权衡性能和并发性,根据实际情况考虑是否需要使用间隙锁,大多数情况下使用 read committed 隔离级别就足够了,对很多应用程序来说,幻读也不是什么大问题。
产生间隙锁的条件(RR事务隔离级别下):
3.1. 使普通索引锁定;
3.2. 使用多列唯一索引;
3.3. 使用唯一索引锁定多行记录。
c)临键锁(Next-Key Locks)临键锁存在于非唯一索引中(主键中不存在临键锁),该类型的每条记录的索引上都存在这种锁,它是一种特殊的间隙锁,锁定一段左开右闭的索引区间。临键锁,是记录锁与间隙锁的组合,它的封锁范围,既包含索引记录,又包含索引区间。
注意: Next-Key 可以理解为一种特殊的间隙锁,也可以理解为一种特殊的算法。通过临建锁可以解决幻读的问题。 每个数据行上的非唯一索引列上都会存在一把临键锁,当某个事务持有该数据行的临键锁时,会锁住一段左开右闭区间的数据。需要强调的一点是,InnoDB 中行级锁是基于索引实现的,临键锁只与非唯一索引列有关,在唯一索引列(包括主键列)上不存在临键锁 , 但是存在间隙锁。临键锁的主要目的,也是为了避免幻读(Phantom Read)。如果把事务的隔离级别降级为RC,临键锁则也会失效。
2.3.2 从兼容性和存在形态划分
2.4. 操作演示
2.4.1 创建测试表并初始化数据
CREATETABLE`user_test`(`id`bigint(20)unsignedNOTNULLAUTO_INCREMENT COMMENT'自增id',`age`int(11)unsignedNOTNULLCOMMENT'年龄',`name`varchar(16)NOTNULLCOMMENT'姓名',PRIMARY KEY(`id`)COMMENT'主键')ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='行锁测试表';INSERTINTO`user_test`(`id`,`age`,`name`)VALUES(1,10,'Lee');INSERTINTO`user_test`(`id`,`age`,`name`)VALUES(2,24,'Ted');INSERTINTO`user_test`(`id`,`age`,`name`)VALUES(3,32,'Zed');INSERTINTO`user_test`(`id`,`age`,`name`)VALUES(4,45,'Talon');
2.4.2Record lock 记录锁验证
执行一下sql
-- 事物A 中更新数据 sql如下:-- 手动开启事物或者 beginSTART TRANSACTION;-- 根据非唯一索引列 UPDATE 某条记录 (只会产生记录锁,不会产生间隙锁)UPDATEuser_testSETname='LISHI'WHEREid=2;-- 等观看完下面事物B在开启事物commit;rollback;-- 事物B 也同时更新统一条记录START TRANSACTION;UPDATEuser_testSETname='ZHANGSAN'WHEREid=2;commit;rollback;
此时事物B会出现如图结果
事物A commit 或者 rollback 此时事物B会拿到锁执行成功;事物B更新其他id记录不受影响:
START TRANSACTION;UPDATEuser_testSETname='ZHANGSAN'WHEREid=1;commit;
2.4.2Next-Key Locks 临键锁验证
首先在age字段上创建普通索引
ALTERTABLE`user_test` ADD INDEX `index_age`(`age`);
此时该表中 age 列潜在的临键锁有:
(-∞, 10],
(10, 24],
(24, 32],
(32, 45],
(45, +∞],
事物A 中更新数据 sql如下:
START TRANSACTION;-- 根据非唯一索引列 UPDATE 某条记录select*fromuser_testwhereage=10forupdate-- 事物B此时插入或者更新age小于10大于1的一条记录-- 插入一条记录为age=7的记录start TRANSACTION;insertintouser_test(age,name)values(7,'Tom');COMMIT;
-- 事物B更新其中一条记录age为8也会被阻塞(虽然条件id是主键索引,更新的字段是普通索引,因此也会加上间隙锁)start TRANSACTION;UPDATEuser_testSETage=8WHEREid=2;COMMIT;
此时事物B会出现如图结果
事物A commit 或者 rollback 此时事物B会拿到锁执行成功
2.4.3. Gap Locks 间隙锁验证
打开间隙锁设置 首先查看 innodb_locks_unsafe_for_binlog 是否禁用:
show variableslike'innodb_locks_unsafe_for_binlog';
innodb_locks_unsafe_for_binlog:默认值为OFF,即启用间隙锁。因为此参数是只读模式,如果想要禁用间隙锁,需要修改 my.cnf(windows是my.ini) 重新启动才行。
默认mac是没有my.cnf文件的,因此要在 etc文件下创建 my.cnf文件(etc/my.cnf)
my.cnf内容如下(主要添加:)
innodb_locks_unsafe_for_binlog=1
3.my.cnf内容如下:
# Example MySQL config file formediumsystems. # # Thisisfor a system with little memory(32M-64M)whereMySQL plays# an important part,orsystems up to128MwhereMySQLisused together with# other programs(suchasa web server) # # MySQL programs look for option filesinasetof# locations which dependonthe deployment platform. # You can copy this option file to one of those # locations. For information about these locations,see:# http://dev.mysql.com/doc/mysql/en/option-files.html # #Inthis file,you can use alllongoptions that a program supports.# If you want to know which options a program supports,run the program# with the"--help"option. # The following options will be passed to all MySQL clients [client]default-character-set=utf8#password=your_passwordport=3306socket=/tmp/mysql.sock # Here follows entries for some specific programs # The MySQL server [mysqld]character-set-server=utf8init_connect='SET NAMES utf8 port = 3306 socket = /tmp/mysql.sock skip-external-locking key_buffer_size = 16M max_allowed_packet = 1M table_open_cache = 64 sort_buffer_size = 512K net_buffer_length = 8K read_buffer_size = 256K read_rnd_buffer_size = 512K myisam_sort_buffer_size = 8M character-set-server=utf8 init_connect='SETNAMES utf8' innodb_locks_unsafe_for_binlog = 1# Don't listenona TCP/IP port at all. This can be a security enhancement,# if all processes that need to connect to mysqld runonthe same host.# All interaction with mysqld must be made via Unix socketsornamed pipes.# Note that using this option without enabling named pipesonWindows#(via the"enable-named-pipe"option)will render mysqld useless!# #skip-networking # Replication Master Server(default)# binary loggingisrequired for replicationlog-bin=mysql-bin # binary logging format-mixed recommendedbinlog_format=mixed # required unique idbetween1and2^32-1# defaults to1if master-hostisnotset# but willnotfunctionasa master if omittedserver-id=1 # Replication Slave(comment out master section to use this) # # To configure this hostasa replication slave,you can choosebetween# two methods: # #1)Use the CHANGE MASTER TO command(fully describedinour manual)-# the syntaxis: # # CHANGE MASTER TO MASTER_HOST=<host>,MASTER_PORT=<port>,# MASTER_USER=<user>,MASTER_PASSWORD=<password>; # #whereyou replace<host>,<user>,<password>byquoted stringsand#<port>bythe master's port number (3306 by default). # # Example: # # CHANGE MASTER TO MASTER_HOST='125.564.12.1', MASTER_PORT=3306, # MASTER_USER='joe', MASTER_PASSWORD='secret'; # # OR # # 2) Set the variables below. However, in case you choose this method, then # start replication for the first time (even unsuccessfully, for example # if you mistyped the password in master-password and the slave fails to # connect), the slave will create a master.info file, and any later # change in this file to the variables'valuesbelow will be ignoredand# overriddenbythe content of the master.infofile,unless you shutdown# the slave server,deletemaster.infoandrestart the slaver server.# For that reason,you may want to leave the lines below untouched#(commented)andinstead use CHANGE MASTER TO(see above) # # required unique idbetween2and2^32-1#(anddifferentfromthe master)# defaults to2if master-hostisset# but willnotfunctionasa slave if omitted#server-id=2 # # The replication master for this slave-required#master-host=<hostname> # # The username the slave will use for authentication when connecting # to the master-required#master-user=<username> # # The password the slave will authenticate with when connecting to # the master-required#master-password=<password> # mysqld# The port the masterislisteningon.# optional-defaults to3306#master-port=<port> # # binary logging-notrequired for slaves,but recommended#log-bin=mysql-bin # Uncomment the following if you are using InnoDB tables #innodb_data_home_dir=/usr/local/mysql/data#innodb_data_file_path=ibdata1:10M:autoextend#innodb_log_group_home_dir=/usr/local/mysql/data# You canset.._buffer_pool_size up to50-80% # of RAM but beware of setting memory usage too high #innodb_buffer_pool_size=16M#innodb_additional_mem_pool_size=2M#Set.._log_file_size to25%of buffer pool size#innodb_log_file_size=5M#innodb_log_buffer_size=8M#innodb_flush_log_at_trx_commit=1#innodb_lock_wait_timeout=50 [mysqldump] quick max_allowed_packet=16M [mysql]no-auto-rehash# Remove the next comment character if you arenotfamiliar with SQL#safe-updatesdefault-character-set=utf8 [myisamchk]key_buffer_size=20Msort_buffer_size=20Mread_buffer=2Mwrite_buffer=2M [mysqlhotcopy]interactive-timeout
此时再次查看间隙锁是否开启
唯一索引的间隙锁
-- 创建test表并插入一些数据CREATETABLE`test`(`id`int(1)NOTNULLAUTO_INCREMENT,`name`varchar(8)DEFAULTNULL,PRIMARY KEY(`id`))ENGINE=InnoDB DEFAULT CHARSET=utf8;INSERTINTO`test`VALUES('1','小罗');INSERTINTO`test`VALUES('5','小黄');INSERTINTO`test`VALUES('7','小明');INSERTINTO`test`VALUES('11','小红');
在进行测试之前,我们先来看看test表中存在的隐藏间隙:
(-∞, 1]
(1, 5]
(5, 7]
(7, 11]
(11, +∞]
-- 执行事物1如下START TRANSACTION;-- 查询 id 在 5 - 11 范围的数据并加记录锁 SELECT*FROM`test`WHERE`id`BETWEEN5AND11FORUPDATE;-- 事务B进行插入数据操作 -- 注意:以下的语句不是放在一个事务中执行,而是分开多次执行,每次事务中只有一条添加语句START TRANSACTION;-- 事务2插入一条 id = 3,name = '小张1' 的数据 INSERTINTO`test`(`id`,`name`)VALUES(3,'小张1');# 正常执行-- 事务3插入一条 id = 4,name = '小白' 的数据INSERTINTO`test`(`id`,`name`)VALUES(4,'小白');# 正常执行-- 事务4插入一条 id = 6,name = '小东' 的数据INSERTINTO`test`(`id`,`name`)VALUES(6,'小东');# 阻塞-- 事务5插入一条 id = 8, name = '大罗' 的数据INSERTINTO`test`(`id`,`name`)VALUES(8,'大罗');# 阻塞-- 事务6插入一条 id = 9, name = '大东' 的数据INSERTINTO`test`(`id`,`name`)VALUES(9,'大东');# 阻塞-- 事务7插入一条 id = 11, name = '李西' 的数据INSERTINTO`test`(`id`,`name`)VALUES(11,'李西');# 阻塞-- 事务8插入一条 id = 12, name = '张三' 的数据INSERTINTO`test`(`id`,`name`)VALUES(12,'张三');# 正常执行-- 提交事务1,释放事务1的锁 COMMIT;
从上面我们可以看到,(5, 7]、(7, 11] 这两个区间,都不可插入数据,其它区间,都可以正常插入数据。所以我们可以得出结论:当我们给 (5, 7] 这个区间加锁的时候,会锁住 (5, 7]、(7, 11] 这两个区间。
我们再来测试如果我们锁住不存在的数据时,会怎样:
-- 开启事务1 START TRANSACTION;-- 查询 id = 3 这一条不存在的数据并加记录锁 SELECT*FROM`test`WHERE`id`=3FORUPDATE;-- 延迟30秒执行,防止锁释放 SELECTSLEEP(30);-- # 注意:以下的语句不是放在一个事务中执行,而是分开多次执行,每次事务中只有一条添加语句-- 事务2插入一条 id = 3,name = '小张1' 的数据 INSERTINTO`test`(`id`,`name`)VALUES(2,'小张1');# 阻塞-- 事务3插入一条 id = 4,name = '小白' 的数据 INSERTINTO`test`(`id`,`name`)VALUES(4,'小白');# 阻塞-- 事务4插入一条 id = 6,name = '小东' 的数据 INSERTINTO`test`(`id`,`name`)VALUES(6,'小东');# 正常执行-- 事务5插入一条 id = 8, name = '大罗' 的数据 INSERTINTO`test`(`id`,`name`)VALUES(8,'大罗');# 正常执行-- 提交事务1,释放事务1的锁 COMMIT;
我们可以看出,指定查询某一条记录时,如果这条记录不存在,会产生间隙锁。
结论
对于指定查询某一条记录的加锁语句,如果该记录不存在,会产生记录锁和间隙锁,如果记录存在,则只会产生记录锁,如:WHERE `id` = 5 FOR UPDATE;
对于查找某一范围内的查询语句,会产生间隙锁,如:WHERE `id` BETWEEN 5 AND 7 FOR UPDATE;
总结:
上述文档是自己及其团队小伙伴们一起讨论的结果,自己做了一下总结,很感谢团队成员刘昌力、刘明远、朱文彬、祁世松、桑萌萌、石伟男以及彭绍翔等他们的讨论支持。
此文档是对行锁和表锁的一个粗略的认识,存在一定的不足、知识点的缺失、不完善等问题。希望大家能够一起再完善一下,共同学习进步,掌握知识技能,更好的正确高效的运用到平时的工作当中去,才是我们分享技术文档的意思所在。