以下对MySQL更新使用索引合并导致死锁问题进行了问题重现及分析,并说明了查看SQL语句执行时使用的索引、使用的锁、分析死锁的方法,及最后解决死锁问题的方法。
参考“MySQL死锁、锁、索引相关资料整理”( https://blog.csdn.net/a82514921/article/details/104616581)。
在营销活动中,中奖的用户可领取兑换码,保存兑换码信息的数据库表结构示例如下:
create table if not exists table_cdkey ( cdkey_id varchar(20) NOT NULL COMMENT '兑换码 ID', cdkey_type varchar(20) NOT NULL COMMENT '兑换码类型', cdkey varchar(200) NOT NULL COMMENT '兑换码内容', status varchar(2) NOT NULL DEFAULT 'N' COMMENT '领取状态,Y-已领取,N-未领取', biz_seq varchar(32) DEFAULT NULL COMMENT '中奖流水', create_time DATETIME(3) NOT NULL COMMENT '创建时间', update_time DATETIME(3) NOT NULL COMMENT '更新时间', PRIMARY KEY (cdkey_id), INDEX idx_table_cdkey_1(cdkey_type), UNIQUE INDEX uni_table_cdkey(biz_seq) ) ENGINE=InnoDB COMMENT='兑换码信息表'; |
兑换码相关说明如下:
l 存在多种不同类型的兑换码;
l 每个兑换码都存在一个唯一ID(主键);
l 用户需要在前端页面查看兑换码内容,用于在合作机构的页面根据兑换码进行奖品兑换;
l 兑换码由人工导入数据库表中,初始状态为未领取,当某个兑换码被分配给用户后,状态为变为已领取;
l 兑换码初始时关联的中奖流水为NULL,当被分配给用户后,中奖流水字段值会更新为用户对应的中奖记录流水,使用户的中奖记录与兑换码信息能够关联。
需要实现以下功能:
已中奖的用户,每条中奖记录,可领取兑换码,且只能领取一个兑换码,需要尽量提高领取兑换码功能的TPS。
对应的数据库操作为:
用户完成领取兑换码的操作后,在兑换码信息表中找到一条类型满足,且领取状态为未领取,中奖流水为NULL的记录,将该记录的状态修改为已领取,中奖流水修改为对应的中奖流水。
用户查看已领取的兑换码时,根据兑换码类型与中奖流水号从兑换码信息表中查询对应记录,获取兑换码内容。
查找类型满足,且状态为未领取的最小兑换码ID,尝试将该兑换码与中奖流水进行绑定,可实现上述功能。
使用的SQL语句如下所示:
update table_cdkey set status = #{status,jdbcType=VARCHAR}, biz_seq = #{biz_seq,jdbcType=VARCHAR}, update_time = #{update_time,jdbcType=TIMESTAMP} where cdkey_type = #{cdkey_type,jdbcType=VARCHAR} and status = 'N' and cdkey_id = ( select * from (select min(cdkey_id) from table_cdkey where cdkey_type = #{cdkey_type,jdbcType=VARCHAR} and status='N' and biz_seq is null ) as temp ) |
领取兑换码操作并发执行时,查找类型满足,且状态为未领取的最小兑换码ID的操作,可能出现多个线程查找到同一条兑换码记录,导致只有一个线程更新成功,其他线程都更新失败的情况。因此当update操作返回行数为0时,需要进行重试。
以上实现方式的TPS较低,10并发压测时,成功率约95%(update返回受影响行数为0时重试5次),TPS约20;20并发压测时,成功率约90%(重试次数相同),TPS约12。
为了简化领取兑换码的操作,尝试在update时使用limit限制更新限数,实现方式如下所示。
尝试更新兑换码信息表,将类型满足、状态为未领取且中奖流水为NULL的兑换码记录的状态修改为领取,中奖流水修改为对应的中奖流水,通过limit限制仅更新一条记录。使用的SQL语句如下所示:
update table_cdkey set status = #{status,jdbcType=VARCHAR}, biz_seq = #{biz_seq,jdbcType=VARCHAR}, update_time = #{update_time,jdbcType=TIMESTAMP} where cdkey_type = #{cdkey_type,jdbcType=VARCHAR} and status = 'N' and biz_seq is null limit 1 |
进行压测,当线程数大于2时,上述update操作会出现死锁,对应异常信息为“com.mysql.jdbc.exceptions.jdbc4.MySQLTransactionRollbackException: Deadlock found when trying to get lock; try restarting transaction”。
压测时,兑换码信息表总数据量为十几万。
上述实现方式中,使用了MySQL的两个特性:
l update兑换码信息表时,使用了limit 1限制只更新一条记录,MySQL支持对update操作使用limit限制;
l 兑换码信息表中的中奖流水号字段设置了唯一索引,默认值为NULL,MySQL支持唯一索引字段存在多个NULL值。
https://dev.mysql.com/doc/refman/5.6/en/update.html
The LIMIT clause places a limit on the number of rows that can be updated. |
For multiple-table syntax, ORDER BY and LIMIT cannot be used. |
You can use LIMIT row_count to restrict the scope of the UPDATE. A LIMIT clause is a rows-matched restriction. The statement stops as soon as it has found row_count rows that satisfy the WHERE clause, whether or not they actually were changed. |
MySQL支持使用limit限制update的行数,只支持对单表update限制,不支持对多表update限制。
https://dev.mysql.com/doc/refman/5.6/en/create-table.html
A UNIQUE index creates a constraint such that all values in the index must be distinct. An error occurs if you try to add a new row with a key value that matches an existing row. For all engines, a UNIQUE index permits multiple NULL values for columns that can contain NULL. |
MySQL支持唯一索引字段存在多个NULL值。
在本地Windows环境安装MariaDB数据库,用于测试。
在MySQL文档“14.14 InnoDB Startup Options and System Variables”(https://dev.mysql.com/doc/refman/5.6/en/innodb-parameters.html)中,对InnoDB的版本进行了说明。从MySQL 5.6.11开始,InnoDB不再有单独的版本号,而和是MySQL的版本一致。文档说明如下:
innodb_version
The InnoDB version number. Starting in MySQL 5.6.11, separate version numbering for InnoDB is discontinued and this value is the same the version number of the server. |
在MariaDB文档“Changes & Improvements in MariaDB 10.0”(https://mariadb.com/kb/en/library/changes-improvements-in-mariadb-100/)中,说明MariaDB 10.0是以前稳定的MariaDB系列。它基于MariaDB 5.5系列,具有MySQL 5.6移植回来的功能。文档说明如下:
MariaDB 10.0 is a previous stable series of MariaDB. It is built on the MariaDB 5.5 series with backported features from MySQL 5.6 and entirely new features not found anywhere else. |
安装MariaDB 10.0.10版本后,查看InnoDB版本(innodb_version系统变量)为5.6.15-63.0。
安装MariaDB 10.0.36版本后,查看InnoDB版本为5.6.39-83.1。
测试环境使用的数据库InnoDB版本通常为5.6.15,数据库版本通常为MariaDB 10.0.10。
在InnoDB 5.6.15版本及之前,当需要开启InnoDB死锁监控器、锁监控器时,需要在每次MySQL服务器重启后重新创建对应的数据库表,操作比较麻烦;在InnoDB 5.6.15版本之后,当需要开启InnoDB死锁监控器、锁监控器时,可以通过设置系统变量完成,操作比较简单。
为了便于测试,选择InnoDB版本比5.6.15稍高的5.6.39,对应的MariaDB版本为10.0.36。
“2.3.7.7 Starting MySQL as a Windows Service”( https://dev.mysql.com/doc/refman/5.5/en/windows-start-service.html)进行了说明,将MySQL安装为Windows服务的示例脚本如下。
C:\> "C:\Program Files\MySQL\MySQL Server 5.5\bin\mysqld" --install MySQL --defaults-file=C:\my-opts.cnf |
对于MySQL的系统变量,通过SET语法对全局变量或会话变量进行修改时,当重启MySQL服务器后,被修改的变量会恢复,不便于测试。因此需要对MySQL的系统变量进行持久修改,使得MySQL服务器重启后,系统变量可以保持,不需要重新设置。
MySQL对系统变量持久修改的说明如下。
“4.2.6 Using Option Files”( https://dev.mysql.com/doc/refman/5.5/en/option-files.html)说明大多数MySQL程序都可以从选项文件(有时称为配置文件)中读取启动选项。选项文件提供了一种指定常用选项的便捷方式,因此每次运行程序时都无需在命令行中输入这些选项。文档说明如下。
Most MySQL programs can read startup options from option files (sometimes called configuration files). Option files provide a convenient way to specify commonly used options so that they need not be entered on the command line each time you run a program. |
“4.3.1 mysqld — The MySQL Server”( https://dev.mysql.com/doc/refman/5.5/en/mysqld.html)说明mysqld,也称为MySQL Server,是完成MySQL安装中大部分工作的主程序。文档说明如下。
mysqld, also known as MySQL Server, is the main program that does most of the work in a MySQL installation. |
“5.1.6 Server Command Options”( https://dev.mysql.com/doc/refman/5.5/en/server-options.html)说明了以下内容。
l 启动mysqld服务器时,可以指定程序选项。最常用的方法是在选项文件或命令行中提供选项。但是在大多数情况下,最好确保服务器每次运行时都使用相同的选项。确保这一点的最佳方法是将它们列在选项文件中。
l mysqld从[mysqld]和[server]组中读取选项。
l 使用--defaults-file选项可以使MySQL仅读取指定的选项文件。
文档说明如下。
When you start the mysqld server, you can specify program options. The most common methods are to provide options in an option file or on the command line. However, in most cases it is desirable to make sure that the server uses the same options each time it runs. The best way to ensure this is to list them in an option file. |
mysqld reads options from the [mysqld] and [server] groups. |
--defaults-file=file_name Read only the given option file. |
“5.1.8 Using System Variables”(https://dev.mysql.com/doc/refman/5.6/en/using-system-variables.html)说明了在选项文件中设置变量的格式,如下所示。
[mysqld] innodb_log_file_size=16M max_allowed_packet=1G |
在进行测试时,已将MySQL安装为Windows服务,在服务启动脚本中,--defaults-file选项指定了MySQL读取的选项文件路径。
当需要持久修改MySQL系统变量时,需要修改--defaults-file选项对应的选项文件的[mysqld]节点。
持久修改MySQL系统变量后,需要重启MySQL服务器生效。
3.1.5.1 事务隔离级别
InnoDB的默认隔离级别是REPEATABLE READ。
行内TDSQL默认事务隔离级别为READ COMMITTED。
需要将事务隔离级别修改为READ COMMITTED,与测试环境TDSQL保持一致。
修改示例如下。
[mysqld] transaction-isolation = READ-COMMITTED |
3.1.5.2 死锁监控器
死锁监控器默认未开启,开启后可以将所有死锁的信息打印到mysqld错误日志中。
当需要开启死锁监控器时,修改示例如下。
[mysqld] innodb_print_all_deadlocks = ON |
3.1.5.3 锁监控器
锁监控器默认未开启,开启后可以定期将锁信息打印到mysqld错误日志中。
当需要开启锁监控器时,修改示例如下。
[mysqld] innodb_status_output = ON innodb_status_output_locks = ON |
3.1.5.4 优化器
优化器默认启用了index_merge选项。
当需要关闭优化器index_merge选项时,修改示例如下。
[mysqld] optimizer_switch = index_merge=off,index_merge_union=off,index_merge_sort_union=off,index_merge_intersection=off |
3.1.5.5 其他修改
MySQL允许的最大同时客户端连接数由参数max_connections设置,默认值为151。
在测试时,默认值可能无法满足要求,导致应用连接数据库时出现“com.mysql.jdbc.exceptions.jdbc4.MySQLNonTransientConnectionException: Too many connections”异常,需要将该参数值调大,示例如下。
[mysqld] max_connections = 500 |
创建数据库表,为两个字段分别创建索引,其中一个为非唯一索引,另一个为唯一索引,如下所示。
CREATE TABLE test_update_limit2 ( |
创建另一个数据库表,为两个字段分别创建索引,均为非唯一索引,如下所示。
|
使用与前文类似的update语句,以上两个数据库表均可以出现死锁。为了减少干扰项,以下测试时使用两个字段均为非唯一索引的表,即test_update_limit4。
在进行测试时,测试数据库表的总数据量为50万条,测试程序最大线程数为200,数据库连接池最大数量为1000。
在进行测试时,首先按照需要设置MySQL系统变量,再删除测试数据库表的数据,再向其中插入数据,最后更新数据。
3.2.2.1 设置MySQL系统变量
按照需要设置MySQL系统变量,如开启/停止死锁监控器、开启/停止锁监控器等。
3.2.2.2 删除数据
略。
3.2.2.3 插入数据
向测试数据库表插入数据,idx1字段设置为null,idx2字段为'1'、'2'、'3'、'4'、'5'中随机数(即每个值约10万行),status字段值为'0'。
3.2.2.4 更新数据
update语句如下所示。
update test_update_limit4 |
更新条件为status字段值为'0',且idx1字段值为null,且idx2字段值为'1'、'2'、'3'、'4'、'5'中的随机数;将status字段值更新为'1'(不更新idx1或idx2字段值),通过limit 1限制仅更新1行。
在测试程序中,每隔0.1秒查询一次INFORMATION_SCHEMA.INNODB_LOCKS表中的InnoDB锁信息,并在日志中打印。
将MySQL配置文件中的系统变量innodb_print_all_deadlocks设置为“ON”,重启MySQL服务后,会在日志中打印全部的死锁信息。
查看日志中的死锁信息如下。
*** (1) TRANSACTION: TRANSACTION 741197, ACTIVE 0 sec fetching rows mysql tables in use 3, locked 3 LOCK WAIT 5 lock struct(s), heap size 2936, 3 row lock(s) MySQL thread id 16, OS thread handle 0x1a68, query id 399 localhost 127.0.0.1 test updating update test_update_limit4 set status='1' where status='0' and idx1 is null and idx2 = '5' limit 1 *** (1) WAITING FOR THIS LOCK TO BE GRANTED: RECORD LOCKS space id 11 page no 1159 n bits 1144 index `idx_test_update_limit4_1` of table `testdb`.`test_update_limit4` trx table locks 1 total table locks 3 trx id 741197 lock_mode X locks rec but not gap waiting lock hold time 0 wait time before grant 0 *** (2) TRANSACTION: TRANSACTION 741193, ACTIVE 0 sec fetching rows mysql tables in use 3, locked 3 6 lock struct(s), heap size 1184, 7 row lock(s) MySQL thread id 20, OS thread handle 0xd50, query id 395 localhost 127.0.0.1 test updating update test_update_limit4 set status='1' where status='0' and idx1 is null and idx2 = '2' limit 1 *** (2) HOLDS THE LOCK(S): RECORD LOCKS space id 11 page no 1159 n bits 1144 index `idx_test_update_limit4_1` of table `testdb`.`test_update_limit4` trx table locks 1 total table locks 3 trx id 741193 lock_mode X locks rec but not gap lock hold time 0 wait time before grant 0 *** (2) WAITING FOR THIS LOCK TO BE GRANTED: RECORD LOCKS space id 11 page no 4152 n bits 184 index `PRIMARY` of table `testdb`.`test_update_limit4` trx table locks 1 total table locks 3 trx id 741193 lock_mode X locks rec but not gap waiting lock hold time 0 wait time before grant 0 *** WE ROLL BACK TRANSACTION (1) |
在上述死锁日志中,可以看到导致死锁的索引为idx_test_update_limit4_1与PRIMARY。
*** (1) TRANSACTION: TRANSACTION 741199, ACTIVE 0 sec fetching rows mysql tables in use 3, locked 3 LOCK WAIT 6 lock struct(s), heap size 2936, 7 row lock(s) MySQL thread id 17, OS thread handle 0x1e88, query id 400 localhost 127.0.0.1 test updating update test_update_limit4 set status='1' where status='0' and idx1 is null and idx2 = '4' limit 1 *** (1) WAITING FOR THIS LOCK TO BE GRANTED: RECORD LOCKS space id 11 page no 4152 n bits 184 index `PRIMARY` of table `testdb`.`test_update_limit4` trx table locks 1 total table locks 7 trx id 741199 lock_mode X locks rec but not gap waiting lock hold time 0 wait time before grant 0 *** (2) TRANSACTION: TRANSACTION 741196, ACTIVE 0 sec starting index read mysql tables in use 3, locked 3 4 lock struct(s), heap size 1184, 3 row lock(s) MySQL thread id 19, OS thread handle 0x21e4, query id 396 localhost 127.0.0.1 test updating update test_update_limit4 set status='1' where status='0' and idx1 is null and idx2 = '2' limit 1 *** (2) HOLDS THE LOCK(S): RECORD LOCKS space id 11 page no 4152 n bits 184 index `PRIMARY` of table `testdb`.`test_update_limit4` trx table locks 1 total table locks 7 trx id 741196 lock_mode X locks rec but not gap lock hold time 0 wait time before grant 0 *** (2) WAITING FOR THIS LOCK TO BE GRANTED: RECORD LOCKS space id 11 page no 1159 n bits 1144 index `idx_test_update_limit4_1` of table `testdb`.`test_update_limit4` trx table locks 1 total table locks 7 trx id 741196 lock_mode X locks rec but not gap waiting lock hold time 0 wait time before grant 0 *** WE ROLL BACK TRANSACTION (2) |
在上述死锁日志中,可以看到导致死锁的索引也是idx_test_update_limit4_1与PRIMARY。
在死锁日志中,未发现因为索引idx_test_update_limit4_2导致死锁。
查看上述执行的SQL语句的执行计划,idx2字段值可为'1'、'2'、'3'、'4'、'5'中的随机数,如使用'1'。执行以下语句查看执行计划。
explain update test_update_limit4 set status='1' where status='0' and idx1 is null and idx2 = '1' limit 1 \G; |
查看执行计划如下。
id: 1 select_type: SIMPLE table: test_update_limit4 type: index_merge possible_keys: idx_test_update_limit4_1,idx_test_update_limit4_2 key: idx_test_update_limit4_2,idx_test_update_limit4_1 key_len: 99,99 ref: NULL rows: 132924 Extra: Using intersect(idx_test_update_limit4_2,idx_test_update_limit4_1 ); Using where |
type为“index_merge”,说明使用了索引合并。
possible_keys为“idx_test_update_limit4_1,idx_test_update_limit4_2”, 说明可以使用的索引为“idx_test_update_limit4_1,idx_test_update_limit4_2”。
key为“idx_test_update_limit4_2,idx_test_update_limit4_1”,说明实际使用的索引为“idx_test_update_limit4_2,idx_test_update_limit4_1”。
将MySQL配置文件中的系统变量innodb_status_output与innodb_status_output_locks设置为“ON”,重启MySQL服务后,会在日志中定时打印锁信息。
查看日志中的锁信息如下。
RECORD LOCKS space id 11 page no 2194 n bits 968 index `idx_test_update_limit4_2` of table `testdb`.`test_update_limit4` trx table locks 1 total table locks 87 trx id 764405 lock_mode X locks rec but not gap lock hold time 0 wait time before grant 0 RECORD LOCKS space id 11 page no 4156 n bits 264 index `PRIMARY` of table `testdb`.`test_update_limit4` trx table locks 1 total table locks 87 trx id 764405 lock_mode X locks rec but not gap lock hold time 0 wait time before grant 0 RECORD LOCKS space id 11 page no 1160 n bits 1032 index `idx_test_update_limit4_1` of table `testdb`.`test_update_limit4` trx table locks 1 total table locks 87 trx id 764405 lock_mode X locks rec but not gap lock hold time 0 wait time before grant 0 RECORD LOCKS space id 11 page no 4156 n bits 264 index `PRIMARY` of table `testdb`.`test_update_limit4` trx table locks 1 total table locks 87 trx id 764405 lock_mode X locks rec but not gap lock hold time 0 wait time before grant 0 |
可以看到当前事务依次对“idx_test_update_limit4_2”“PRIMARY”“idx_test_update_limit4_1”“PRIMARY”索引持有锁。
在程序中定时查询INFORMATION_SCHEMA.INNODB_LOCKS表的信息并打印,如下所示。
lock_id |
lock_trx_id |
lock_mode |
lock_type |
lock_index |
lock_space |
lock_page |
lock_rec |
lock_data |
772196:11:2067:735 |
772196 |
X |
RECORD |
idx_test_update_limit4_2 |
11 |
2067 |
735 |
'5', '0' |
772196:11:4156:2 |
772196 |
X |
RECORD |
PRIMARY |
11 |
4156 |
2 |
'0' |
776477:11:1160:3 |
776477 |
X |
RECORD |
idx_test_update_limit4_1 |
11 |
1160 |
3 |
NULL, '1' |
776477:11:4156:3 |
776477 |
X |
RECORD |
PRIMARY |
11 |
4156 |
3 |
'1' |
可以看到事务会对“idx_test_update_limit4_2”“PRIMARY”“idx_test_update_limit4_1”索引持有锁。
MySQL的Bug #77209为“Update may use index merge without any reason (increasing chances for deadlock)”( https://bugs.mysql.com/bug.php?id=77209),描述为“On some conditions UPDATE query uses index merge when both indexes expect to retrieve 1 row. This behavior increases chances for deadlock.”,即在某些情况下,当两个索引都预期检索1行时,UPDATE查询使用索引合并,该行为增加了产生死锁的可能性。
建议的修改方法为“Do not use index merge when single index is good enough. Try to avoid using index merge in UPDATE to not provoke deadlocks.”,即当单个索引足够好时,不要使用索引合并。尽量避免在UPDATE中使用索引合并,以防引发死锁。
出现上述死锁时,UPDATE语句使用了limit 1使其仅更新1 行,index merge优化器默认为打开,与该bug的描述相符。
MySQL文档说明“如果在搜索中使用了二级索引,并且要设置的索引记录锁是排它的,InnoDB还会检索相应的聚簇索引记录并对它们设置锁。”
即当更新条件中的字段包含二级索引时,会将对应的主键也锁定。
出现上述死锁时,UPDATE操作对于索引加锁的顺序为“idx_test_update_limit4_2”“PRIMARY”“idx_test_update_limit4_1”“PRIMARY”。
首先对idx_test_update_limit4_2索引加锁,且一次事务中仅对其加一次锁,因此对idx_test_update_limit4_2索引加锁时不会出现死锁。
由于对“PRIMARY”“idx_test_update_limit4_1”索引的加锁顺序为“PRIMARY”“idx_test_update_limit4_1”“PRIMARY”,可能出现循环依赖,可能产生死锁。
通过以下方法,可以避免上述情况产生的死锁。
l 关闭index_merge优化器选项
l 使用索引提示
l 使用联合索引
l 只对一个字段创建索引
在MySQL配置文件中,将系统变量optimizer_switch设置为“index_merge=off,index_merge_union=off,index_merge_sort_union=off,index_merge_intersection=off”,重启MySQL服务后,可以关闭index_merge优化器选项。
以下为关闭index_merge优化器选项,继续执行上述的测试步骤。
查看上述执行的SQL语句的执行计划,idx2字段值可为'1'、'2'、'3'、'4'、'5'中的随机数,如使用'1'。执行以下语句查看执行计划。
explain update test_update_limit4 set status='1' where status='0' and idx1 is null and idx2 = '1' limit 1 \G; |
查看执行计划如下。
id: 1 select_type: SIMPLE table: test_update_limit4 type: range possible_keys: idx_test_update_limit4_1,idx_test_update_limit4_2 key: idx_test_update_limit4_2 key_len: 99 ref: NULL rows: 265848 Extra: Using where |
type为“range”,说明未使用索引合并。
possible_keys为“idx_test_update_limit4_1,idx_test_update_limit4_2”,说明可以使用的索引为“idx_test_update_limit4_1,idx_test_update_limit4_2”。
key为“idx_test_update_limit4_2”,说明实际使用的索引为“idx_test_update_limit4_2”。
与启用index_merge时的锁监控器日志类似,略。
与启用index_merge时的INFORMATION_SCHEMA.INNODB_LOCKS表锁信息类似,略。
在MySQL中使用索引提示,可以指定使用的索引。
使用USE INDEX、FORCE INDEX或IGNORE INDEX可以使用索引提示,指定需要使用的索引,或需要忽略的索引。
测试步骤与之前的类似。
update语句如下所示。
update test_update_limit4 force index (idx_test_update_limit4_1) set status='1' where status='0' and idx1 is null and idx2 = #{idx2,jdbcType=VARCHAR} limit 1 |
更新条件为status字段值为'0',且idx1字段值为null,且idx2字段值为'1'、'2'、'3'、'4'、'5'中的随机数;将status字段值更新为'1'(不更新idx1或idx2字段值),通过limit 1限制仅更新1行。通过“force index”设置强制使用idx_test_update_limit4_1索引,不使用idx_test_update_limit4_2索引。
或使用类似如下语句,通过“ignore index”设置忽略idx_test_update_limit4_1索引,只使用idx_test_update_limit4_2索引。
update test_update_limit4 ignore index (idx_test_update_limit4_1) set status='1' where status='0' and idx1 is null and idx2 = #{idx2,jdbcType=VARCHAR} limit 1 |
查看上述执行的SQL语句的执行计划,idx2字段值可为'1'、'2'、'3'、'4'、'5'中的随机数,如使用'1'。执行以下语句查看执行计划。
explain update test_update_limit4 ignore index (idx_test_update_limit4_1) set status='1' where status='0' and idx1 is null and idx2 = '1' limit 1 \G; |
查看执行计划如下。
id: 1 select_type: SIMPLE table: test_update_limit4 type: range possible_keys: idx_test_update_limit4_2 key: idx_test_update_limit4_2 key_len: 99 ref: NULL rows: 265848 Extra: Using where |
type为“range”,说明未使用索引合并。
possible_keys为“idx_test_update_limit4_2”,说明可以使用的索引为“idx_test_update_limit4_2”。
key为“idx_test_update_limit4_2”,说明实际使用的索引为“idx_test_update_limit4_2”。
使用“force index”时,explain输出结果类似,略。
RECORD LOCKS space id 11 page no 3200 n bits 128 index `idx_test_update_limit4_1` of table `testdb`.`test_update_limit4` trx table locks 1 total table locks 200 trx id 792423 lock_mode X locks rec but not gap lock hold time 0 wait time before grant 0 RECORD LOCKS space id 11 page no 4156 n bits 592 index `PRIMARY` of table `testdb`.`test_update_limit4` trx table locks 1 total table locks 200 trx id 792423 lock_mode X locks rec but not gap lock hold time 0 wait time before grant 0 |
可以看到当前事务依次对“idx_test_update_limit4_1”“PRIMARY”索引持有锁。
lock_id |
lock_trx_id |
lock_mode |
lock_type |
lock_index |
lock_space |
lock_page |
lock_rec |
lock_data |
798947:11:3200:30 |
798947 |
X |
RECORD |
idx_test_update_limit4_1 |
11 |
3200 |
30 |
NULL, '100020' |
798947:11:4156:30 |
798947 |
X |
RECORD |
PRIMARY |
11 |
4156 |
30 |
'100020' |
可以看到事务会对“idx_test_update_limit4_1”“PRIMARY”索引持有锁。
在创建数据库表时,对两个字段设置一个联合索引。
进行测试时,启用index_merge优化器选项。
CREATE TABLE test_update_limit3 ( |
测试步骤与之前的类似。
update语句如下所示。
update test_update_limit3 set seq_no = #{seqNo,jdbcType=VARCHAR}, status='1' where idx = #{idx,jdbcType=VARCHAR} and status='0' and seq_no is null and not_idx = #{notIdx,jdbcType=VARCHAR} limit 1 |
更新条件为status字段值为'0',idx字段值为'1'、'2'、'3'、'4'、'5'中的随机数,seq_no字段值为null,且not_idx字段值为固定值“not_index”(插入时均设置为该值);将seq_no字段值更新为指定值,将status字段值更新为'1'(对索引涉及的字段更新了更新),通过limit 1限制仅更新1行。
查看上述执行的SQL语句的执行计划,idx字段值可为'1'、'2'、'3'、'4'、'5'中的随机数,如使用'1'。执行以下语句查看执行计划。
explain update test_update_limit3 set seq_no = '1', status='1' where idx = '1' and status='0' and seq_no is null and not_idx = 'not_index' limit 1 \G; |
查看执行计划如下。
id: 1 select_type: SIMPLE table: test_update_limit3 type: range possible_keys: idx_test_update_limit3 key: idx_test_update_limit3 key_len: 197 ref: NULL rows: 209494 Extra: Using where; Using buffer |
type为“range”,说明未使用索引合并。
possible_keys为“idx_test_update_limit3”, 说明可以使用的索引为“idx_test_update_limit3”。
key为“idx_test_update_limit3”,说明实际使用的索引为“idx_test_update_limit3”。
在满足业务功能的前提下,建表时只对一个字段创建索引,update语句只指定一个存在索引的字段,在更新时不会出现死锁。
使用以上解决方法时,即使更新数据库时更新了两个包含索引的字段值,也不会出现死锁。