深入理解SELECT ... LOCK IN SHARE MODE和SELECT ... FOR UPDATE

本文涉及的产品
RDS MySQL Serverless 基础系列,0.5-2RCU 50GB
云数据库 RDS MySQL,集群系列 2核4GB
推荐场景:
搭建个人博客
云数据库 RDS MySQL,高可用系列 2核4GB
简介: 深入理解SELECT ... LOCK IN SHARE MODE和SELECT ... FOR UPDATE

LOCK IN SHARE MODE

SELECT ... LOCK IN SHARE MODE走的是IS锁(意向共享锁),即在符合条件的rows上都加了共享锁,这样的话,其他session可以读取这些记录,也可以继续添加IS锁,但是无法修改这些记录直到你这个加锁的session执行完成(否则直接锁等待超时)。

SELECT ... FOR UPDATE

SELECT ... FOR UPDATE 走的是IX锁(意向排它锁),即在符合条件的rows上都加了排它锁,其他session也就无法在这些记录上添加任何的S锁或X锁。如果不存在一致性非锁定读的话(读取快照),那么其他session是无法读取和修改这些记录的,但是innodb有非锁定读(快照读并不需要加锁)

for update锁表

InnoDB默认是行级别的锁,当有明确指定的主键时候,是行级锁。否则是表级别。

for update的注意点

  1. for update 仅适用于InnoDB,并且必须开启事务,在begin与commit之间才生效。
  2. for update的加锁方式无非是比lock in share mode的方式多阻塞了select...lock in share mode的查询方式,并不会阻塞快照读(常规的select)

for update 和 for update nowait区别

(前者阻塞其他事务,后者拒绝其他事务)

for update锁住表或者锁住行,只允许当前事务进行操作(读写),其他事务被阻塞,直到当前事务提交或者回滚,被阻塞的事务自动执行

for update nowait 锁住表或者锁住行,只允许当前事务进行操作(读写),其他事务被拒绝,事务占据的statement连接也会被断开

应用场景

在我看来,SELECT ... LOCK IN SHARE MODE的应用场景适合于两张表存在关系时的写操作,拿mysql官方文档的例子来说,一个表是child表,一个是parent表,假设child表的某一列child_id映射到parent表的c_child_id列,那么从业务角度讲,此时我直接insert一条child_id=100记录到child表是存在风险的,因为刚insert的时候可能在parent表里删除了这条c_child_id=100的记录,那么业务数据就存在不一致的风险。正确的方法是再插入时执行select * from parent where c_child_id=100 lock in share mode,锁定了parent表的这条记录,然后执行insert into child(child_id) values (100)就ok了。

但是如果是同一张表的应用场景,举个例子,电商系统中计算一种商品的剩余数量,在产生订单之前需要确认商品数量>=1,产生订单之后应该将商品数量减1。
1 select amount from product where product_name='XX';
2 update product set amount=amount-1 where product_name='XX';

显然1的做法是是有问题,因为如果1查询出amount为1,但是这时正好其他session也买了该商品并产生了订单,那么amount就变成了0,那么这时第二步再执行就有问题。

那么采用lock in share mode可行吗,也是不合理的,因为两个session同时锁定该行记录时,这时两个session再update时必然会产生死锁导致事务回滚。以下是操作范例(按时间顺序)

session1(开启事务)
mysql> begin;
Query OK, 0 rows affected (0.00 sec)
mysql> select * from test_jjj lock in share mode;
+-----+------------+
| id  | name       |
+-----+------------+
| 234 | asdasdy123 |
| 123 | jjj        |
+-----+------------+
2 rows in set (0.00 sec)
session2(开启事务,锁定了相同的行)
mysql> begin;
Query OK, 0 rows affected (0.00 sec)
mysql> select * from test_jjj lock in share mode;
+-----+------------+
| id  | name       |
+-----+------------+
| 234 | asdasdy123 |
| 123 | jjj        |
+-----+------------+
2 rows in set (0.00 sec)
session1(这时session1在update时就会引起锁等待,等待session2)
mysql> update test_jjj set name='jjj1' where name='jjj';
session2(这时session2同样update,引起锁等待,等待session1,接着检测到死锁,回滚session2,注意
执行时间不要超过session1的锁等待超时检测时间,即不要超过innodb_lock_wait_timeout设置的值)
mysql> update test_jjj set name='jjj1' where name='jjj';
ERROR 1213 (40001): Deadlock found when trying to get lock; try restarting transaction
session1(此时session1执行完成)
mysql> update test_jjj set name='jjj1' where name='jjj';
Query OK, 1 row affected (29.20 sec)
Rows matched: 1  Changed: 1  Warnings: 0

session1在update时就会引起锁等待,等待session2

session1在update时就会引起锁等待,等待session2

备注:可以通过以下三个命令查看事务的状态

1:查看当前的事务

SELECT * FROM INFORMATION_SCHEMA.INNODB_TRX;

杀死事务进程id(就是上面命令的trx_mysql_thread_id列)

2:查看当前锁定的事务

SELECT * FROM INFORMATION_SCHEMA.INNODB_LOCKS;

3:查看当前等锁的事务

SELECT * FROM INFORMATION_SCHEMA.INNODB_LOCK_WAITS;

通过该案例可知lock in share mode的方式在这个场景中不适用,我们需要使用for  update的方式直接加X锁,从而短暂地阻塞session2的select...for update操作;以下是操作范例

session1
mysql> begin;
Query OK, 0 rows affected (0.00 sec)
mysql> select * from test_jjj for update;
+-----+------------+
| id  | name       |
+-----+------------+
| 234 | asdasdy123 |
| 123 | jjj1       |
+-----+------------+
2 rows in set (0.00 sec)
session2(此时session2处于锁等待状态,得不到结果)
mysql> begin;
Query OK, 0 rows affected (0.00 sec)
mysql> select * from test_jjj for update;
session1(这时session1 update之后提交,可完成)
mysql> update test_jjj set name='jjj1' where name='jjj';
Query OK, 0 rows affected (0.00 sec)
Rows matched: 0  Changed: 0  Warnings: 0
mysql> commit;
Query OK, 0 rows affected (0.00 sec)
session2(session1提交之后session2刚才的查询结果就出来了,也就可以再次update往下执行了)
mysql> select * from test_jjj for update;
+-----+------------+
| id  | name       |
+-----+------------+
| 234 | asdasdy123 |
| 123 | jjj1       |
+-----+------------+
2 rows in set (37.19 sec)
mysql> select * from test_jjj for update;
+-----+------------+
| id  | name       |
+-----+------------+
| 234 | asdasdy123 |
| 123 | jjj1       |
+-----+------------+
2 rows in set (37.19 sec)
mysql> update test_jjj set name='jjj1' where name='jjj';
Query OK, 0 rows affected (0.00 sec)
Rows matched: 0  Changed: 0  Warnings: 0
mysql> commit;
Query OK, 0 rows affected (0.00 sec)


相关实践学习
如何在云端创建MySQL数据库
开始实验后,系统会自动创建一台自建MySQL的 源数据库 ECS 实例和一台 目标数据库 RDS。
全面了解阿里云能为你做什么
阿里云在全球各地部署高效节能的绿色数据中心,利用清洁计算为万物互联的新世界提供源源不断的能源动力,目前开服的区域包括中国(华北、华东、华南、香港)、新加坡、美国(美东、美西)、欧洲、中东、澳大利亚、日本。目前阿里云的产品涵盖弹性计算、数据库、存储与CDN、分析与搜索、云通信、网络、管理与监控、应用服务、互联网中间件、移动服务、视频服务等。通过本课程,来了解阿里云能够为你的业务带来哪些帮助     相关的阿里云产品:云服务器ECS 云服务器 ECS(Elastic Compute Service)是一种弹性可伸缩的计算服务,助您降低 IT 成本,提升运维效率,使您更专注于核心业务创新。产品详情: https://www.aliyun.com/product/ecs
相关文章
|
3月前
|
SQL 关系型数据库 数据库
在 Postgres 中使用 Update Join
【8月更文挑战第11天】
216 0
在 Postgres 中使用 Update Join
|
3月前
|
SQL 关系型数据库 数据管理
在 Postgres 中使用 Delete Join
【8月更文挑战第11天】
159 0
在 Postgres 中使用 Delete Join
|
关系型数据库 MySQL
报错sql_mode=only_full_group_by
报错sql_mode=only_full_group_by
|
存储 SQL 关系型数据库
mysql大量的waiting for table level lock怎么办
mysql大量的waiting for table level lock怎么办
287 0
|
SQL 关系型数据库 MySQL
mysql Error Code: 1175. You are using safe update mode and you tried to update a table without ....
mysql Error Code: 1175. You are using safe update mode and you tried to update a table without ....
218 0
mysql Error Code: 1175. You are using safe update mode and you tried to update a table without ....
Unsafe query: ‘Update‘ statement without ‘where‘ updates all table rows at once
Unsafe query: ‘Update‘ statement without ‘where‘ updates all table rows at once
688 0
|
关系型数据库 数据库 PostgreSQL
【DB吐槽大会】第70期 - PG 不支持update | delete skip locked, nowait语法
大家好,这里是DB吐槽大会,第70期 - PG 不支持update | delete skip locked, nowait语法
|
SQL Oracle 关系型数据库
1120 11g select for update skip locked
[20171120]11g select for update skip locked.txt --//11G在select for update遇到阻塞时可以通过skipped locked跳过阻塞的记录,测试看看: 1.
1277 0