回答
单纯的 select 语句是不会加锁的,但是使用 select ... for update
查询时,MySQL 会加排他锁,至于是行锁还是表锁,需要根据实际情况来确认:没用索引/主键的话就是表锁,否则就是是行锁。
分析
环境准备
- 新建如下一张表结构
CREATE TABLE user (
id INT(5) NOT NULL AUTO_INCREMENT,
name VARCHAR(64) DEFAULT NULL,
age INT(3) DEFAULT NULL,
code VARCHAR(255) DEFAULT NULL,
PRIMARY KEY ( id),
KEY idx_name (name) USING BTREE
) ENGINE = INNODB AUTO_INCREMENT = 10001 DEFAULT CHARSET = utf8
id 为主键,name 为普通索引。
- 准备如下数据
INSERT INTO user (name, age, code) VALUES ('大一', 15, '10002');
INSERT INTO user (name, age, code) VALUES ('小二', 14, '10001');
INSERT INTO user (name, age, code) VALUES ('张三', 15, '10002');
INSERT INTO user (name, age, code) VALUES ('李四', 13, '10001');
INSERT INTO user (name, age, code) VALUES ('王五', 16, '10003');
INSERT INTO user (name, age, code) VALUES ('赵六', 15, '10001');
INSERT INTO user (name, age, code) VALUES ('刘七', 14, '10002');
INSERT INTO user (name, age, code) VALUES ('李四', 12, '10003');
INSERT INTO user (name, age, code) VALUES ('小李', 15, '10001');
- 关闭自动提交
为了更好的演示,我们需要关闭自动提交。
select @@autocommit;
如果结果为 1 ,则修改为 0
set @@autocommit = 0;
查询条件为主键
当 where 条件为主键 id 时。
事务 1 使用 select ...for update
查询 id = 10001 的数据,注意,它不提交事务:
begin;
select * from user where id = 10001 for update ;
事务 2 使用 update 语句更新 id = 10001 的这条记录:
begin ;
update user set name = '小熊' where id = 10001;
你会发现,这条 update 语句一直都在转圈圈,也是它在一直等待事务释放锁,如果事务 1 一种都不释放锁,事务 2 会报错:
这个报错说明了事务 2 等待锁的时间超时了,即 id = 10001 这条记录不锁住了但是一直都没有释放。
事务 3 使用 update 语句更新 id = 10002 的记录:
begin ;
update user set name = '小熊' where id = 10003;
commit;
执行成功。
select ...for update
where 条件为主键时,锁住的是主键的对应那条数据,其他数据无影响。所以,为行锁!
查询条件为普通索引
当 where 条件为普通索引时。事务 4 使用 for update 查询 name = '大一'
:
begin;
select * from user where name = '大一' for update ;
事务 5 更新 name = '大一'
:
begin ;
update user set age = 11 where name = '大一';
执行结果与上面一样,报同样的错误:
由于 name = '大一'
只有一条记录,如果我们使用 name= '李四'
会是什么情况呢?
begin;
select * from user where name = '李四' for update ;
你会发现无论是 id = 10004
还是 id = 10008
都是报获取锁超时的错误,但是 id = 10005 可以执行成功:
select ...for update
where 条件为普通索引,也是行锁,锁住的是查询出来的数据。
查询条件为主键范围
利用主键查询 id >= 10001 and id <= 10004
的数据:
begin;
select * from user where id >= 10001 and id <= 10004 for update ;
我们一次 update id 为10001、10002、10003、10004、10005 、1006,我们发现只有 10006 的数据执行成了,其余的全部都为获取锁超时:
select ...for update
where 条件为主键范围时,为行锁。
查询条件明明是 id >= 10001 and id <= 10004
为什么会所 10005 呢?这个是 MySQL 的 "next-key lock
" 机制,以后再来分析。
查询条件为普通字段
我们查询条件调整为 age = 11
:
begin;
select * from user where age = 11 for update ;
这里你会发现无论是 update 哪条数据都会报获取锁失败。
select ...for update
where 条件为普通字段,加的是表锁,而并非行锁。
查询条件为空
那如果不加查询条件呢?其实到这里小伙伴们应该知道,这里加的也是表锁。
查询结果为空
如果 where 条件查询出来的数据是空的呢?
begin;
select * from user where name = '999' for update ;
or
begin;
select * from user where id = 100010 for update ;
这里可以 update 语句都可以执行成功,但是这两条语句条件分别为主键和索引。如果我们切换为普通字段呢:
begin;
select * from user where age = 999 for update ;
这里锁住的依然是整个表。
最后
为小伙伴们总结下 select ...for update
加锁情况:
- 主键字段:行锁
- 普通索引:行锁
- 主键范围:行锁
- 普通字段:表锁
- 空条件:表所
- 条件结果为:使用索引或者主键,不加锁;使用普通字段,表锁
最后,在实际生产环境下,不建议使用 select ...for update
,如果一定要使用则建议使用主键,严禁使用其他字段,锁全表不是开玩笑的,所不定服务就被你这个 for update
搞死了。
Java 面试宝典是大明哥全力打造的 Java 精品面试题,它是一份靠谱、强大、详细、经典的 Java 后端面试宝典。它不仅仅只是一道道面试题,而是一套完整的 Java 知识体系,一套你 Java 知识点的扫盲贴。
它的内容包括:
- 大厂真题:Java 面试宝典里面的题目都是最近几年的高频的大厂面试真题。
- 原创内容:Java 面试宝典内容全部都是大明哥原创,内容全面且通俗易懂,回答部分可以直接作为面试回答内容。
- 持续更新:一次购买,永久有效。大明哥会持续更新 3+ 年,累计更新 1000+,宝典会不断迭代更新,保证最新、最全面。
- 覆盖全面:本宝典累计更新 1000+,从 Java 入门到 Java 架构的高频面试题,实现 360° 全覆盖。
- 不止面试:内容包含面试题解析、内容详解、知识扩展,它不仅仅只是一份面试题,更是一套完整的 Java 知识体系。
- 宝典详情:https://www.yuque.com/chenssy/sike-java/xvlo920axlp7sf4k
- 宝典总览:https://www.yuque.com/chenssy/sike-java/yogsehzntzgp4ly1
- 宝典进展:https://www.yuque.com/chenssy/sike-java/en9ned7loo47z5aw
目前 Java 面试宝典累计更新 400+ 道,总字数 42w+。大明哥还在持续更新中,下图是大明哥在 2024-12 月份的更新情况:
想了解详情的小伙伴,扫描下面二维码加大明哥微信【daming091】咨询
同时,大明哥也整理一套目前市面最常见的热点面试题。微信搜[大明哥聊 Java]或扫描下方二维码关注大明哥的原创公众号[大明哥聊 Java] ,回复【面试题】 即可免费领取。