Mysql InnoDB行锁及表锁分享

一. 背景知识

[事务(Transaction)、隔离级别、传播机制]

二. 步入正题:表锁和行锁

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;

总结:

上述文档是自己及其团队小伙伴们一起讨论的结果,自己做了一下总结,很感谢团队成员刘昌力、刘明远、朱文彬、祁世松、桑萌萌、石伟男以及彭绍翔等他们的讨论支持。

此文档是对行锁和表锁的一个粗略的认识,存在一定的不足、知识点的缺失、不完善等问题。希望大家能够一起再完善一下,共同学习进步,掌握知识技能,更好的正确高效的运用到平时的工作当中去,才是我们分享技术文档的意思所在。

参考链接:https://zhuanlan.zhihu.com/p/48269420

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,980评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,178评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,868评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,498评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,492评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,521评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,910评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,569评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,793评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,559评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,639评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,342评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,931评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,904评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,144评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,833评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,350评论 2 342