应用程序开发中有个较常见的场景, 查询某字段的值(该字段一般具有唯一性), 是否存在, 若不存在, 则插入一条记录, 反之, 就更新该记录.
常见的方法是, SELECT... FROM ... FOR UPDATE查询下, 根据SELECT返回情况, 进行相应的操作. 实践中发现, 并发量较大时, 可能会有较多死锁的情况发生,下面利用tb1表演示该问题.
tb1的表结构为:
mysql>SHOW CREATE TABLE tb1 \G
***************************1. row ***************************
Table: tb1
CreateTable: CREATE TABLE `tb1` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`category` varchar(20) NOT NULL DEFAULT ‘‘,
`quantity` bigint(20) NOT NULL DEFAULT ‘0‘,
PRIMARY KEY (`id`),
UNIQUE KEY `uk_category` (`category`)
)ENGINE=InnoDB AUTO_INCREMENT=6 DEFAULT CHARSET=utf8mb4
1 rowin set (0.00 sec)
Session1:
mysql>BEGIN;
QueryOK, 0 rows affected (0.00 sec)
mysql>SELECT * FROM tb1 WHERE category = ‘ko_007‘ FOR UPDATE;
Emptyset (0.00 sec)
Session2:
mysql>BEGIN;
QueryOK, 0 rows affected (0.00 sec)
mysql>SELECT * FROM tb1 WHERE category = ‘ko_007‘ FOR UPDATE;
Emptyset (0.00 sec)
Session1插入category为‘ko_007‘的记录, 出现了锁等待.
mysql>INSERT INTO tb1 (category) VALUES (‘ko_007‘);
…
Session2也插入category为‘ko_007‘的记录, 死锁的情况发生了.
mysql>INSERT INTO tb1 (category) VALUES (‘ko_007‘);
ERROR1213 (40001): Deadlock found when trying to get lock; try restartingtransaction
针对死锁的产生, 从数据库本身来看, 是否还有其它方法完成上面的需求呢 …
MySQL提供了INSERT ... ON DUPLICATE KEY UPDATE语法, 其运行方式是: 若主键(或唯一键)不存在, 就插入一条记录; 若主键存在, 则更新该记录.
对于上述需求, 单个SQL语句即可搞定: 有则更新, 无则插入.
INSERTINTO tb1 (category) VALUES (‘ko_007‘) ON DUPLICATE KEY UPDATE quantity =quantity + 1;
近日, 项目的一个分布式日志产生量统计程序, 从Kafka订阅消息处理后, 将结果写入数据库, 就使用了INSERT ... ON DUPLICATE KEY UPDATE. 其虽不像SELECT ... FROM ... FOR UPDATE容易发生死锁, 却发现了另外一个问题: 与数据表主键相关的AUTO_INCREMENT值非常大, 主键值呈跳跃式增长, 这样持续下去, 主键值要不够用了啊!
最初想到的可能是, innodb_autoinc_lock_mode设置为2引起的, 确认后, 其值是1. 哎, 原因还未找到.
测试下INSERT... ON DUPLICATE KEY UPDATE, 过程如下面所示:
tb1表中存在category 为‘ko_007‘的记录:
mysql>SELECT * FROM tb1 WHERE category = ‘ko_007‘;
+----+----------+----------+
| id| category | quantity |
+----+----------+----------+
| 10| ko_007 | 1 |
+----+----------+----------+
1 rowin set (0.00 sec)
此时查看表结构, AUTO_INCREMENT为12:
mysql>SHOW CREATE TABLE tb1 \G
***************************1. row ***************************
Table: tb1
CreateTable: CREATE TABLE `tb1` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`category` varchar(20) NOT NULL DEFAULT ‘‘,
`quantity` bigint(20) NOT NULL DEFAULT ‘0‘,
PRIMARY KEY (`id`),
UNIQUE KEY `uk_category` (`category`)
)ENGINE=InnoDB AUTO_INCREMENT=12 DEFAULT CHARSET=utf8mb4
1 rowin set (0.00 sec)
执行下面的SQL:
mysql>INSERT INTO tb1 (category) VALUES (‘ko_007‘) ON DUPLICATE KEY UPDATE quantity =quantity + 1;
QueryOK, 2 rows affected (0.00 sec)
查看category 为‘ko_007‘的记录, 和表结构, 可看到quantity 字段更新为2, AUTO_INCREMENT增加了1, 变为13, 如下面所示:
mysql>SELECT * FROM tb1 WHERE category = ‘ko_007‘;
+----+----------+----------+
| id| category | quantity |
+----+----------+----------+
| 10| ko_007 | 2 |
+----+----------+----------+
1 rowin set (0.00 sec)
mysql>SHOW CREATE TABLE tb1 \G
***************************1. row ***************************
Table: tb1
CreateTable: CREATE TABLE `tb1` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`category` varchar(20) NOT NULL DEFAULT ‘‘,
`quantity` bigint(20) NOT NULL DEFAULT ‘0‘,
PRIMARY KEY (`id`),
UNIQUE KEY `uk_category` (`category`)
)ENGINE=InnoDB AUTO_INCREMENT=13 DEFAULT CHARSET=utf8mb4
1 rowin set (0.00 sec)
其实查看MySQL官方文档对于INSERT ... ON DUPLICATE KEY UPDATE的说明时,细心的话可发现文档早已说明了该语法UPDATE唯一记录时, AUTO_INCREMENT会增加的.
大致的原因是, MySQL在执行INSERT ... ON DUPLICATE KEY UPDATE时, 首先要AUTO_INCREMENT+1获取可用的自增值, 继续后面的逻辑时, MySQL发现这是个UPDATE操作, 继而更新相应的记录.
既然整数型的自增主键值有用完的风险, 那使用VARCHAR数据类型的字段作为主键, 就避开该问题呀, 确实是, 调整后的表结构如下:
mysql>SHOW CREATE TABLE tb2 \G
***************************1. row ***************************
Table: tb2
CreateTable: CREATE TABLE `tb2` (
`category` varchar(20) NOT NULL DEFAULT ‘‘,
`quantity` bigint(20) NOT NULL DEFAULT ‘0‘,
PRIMARY KEY (`category`)
)ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
1 rowin set (0.00 sec)
但这也可能带来某些副作用,如数据表存储空间变大, 主从复制效率降低等等…