应用程序开发中有个较常见的场景, 查询某字段的值(该字段一般具有唯一性), 是否存在, 若不存在, 则插入一条记录, 反之, 就更新该记录.

 

常见的方法是, 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_INCREMENT12:

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)

 

但这也可能带来某些副作用,如数据表存储空间变大, 主从复制效率降低等等