直接insert并捕获1062错误是最稳的无锁插入路径,因select then insert在高并发下必然因非原子性导致重复插入;on duplicate key update需依赖唯一索引才生效,否则仍报1062。

直接 INSERT + 捕获 1062 错误是 MySQL 最稳的无锁插入路径
查再插(SELECT THEN INSERT)在高并发下必然失效,不是代码写得不够细,而是两次语句之间没有原子性保障。两个请求几乎同时 SELECT 返回空,接着都执行 INSERT,第二个一定触发 Duplicate entry 'xxx' for key 'yyy',错误码固定为 1062。
真正可行的做法是跳过预查,直接发 INSERT,在应用层捕获冲突并走业务分支:
- Java:用
SQLException.getSQLState()判断是否等于"23000",或getErrorCode() == 1062 - Python(pymysql):捕获
IntegrityError,检查err.args[0]是否为1062 - Go(mysql driver):用
errors.Is(err, mysql.ErrDupKey),需驱动支持该错误类型 - 捕获后不 panic,而是返回“已存在”、重试更新、或丢弃——这比加锁、Redis 防重、前端按钮置灰都更贴近数据库真实能力
ON DUPLICATE KEY UPDATE 不是银弹,没唯一索引就等于没写
ON DUPLICATE KEY UPDATE 只在冲突字段上有 PRIMARY KEY 或 UNIQUE KEY 时才生效。没建索引就写这个语句,MySQL 会当普通 INSERT 执行,冲突时照样报 1062,不会自动切到 UPDATE。
常见疏漏点:
- 联合唯一索引如
(user_id, event_type),必须在ON DUPLICATE KEY UPDATE中写全列,或显式指定约束名:ON DUPLICATE KEY UPDATE ...后不能只写ON CONFLICT (user_id)(那是 PostgreSQL 写法) -
VALUES(column)引用的是本次INSERT子句里的值,不是当前行旧值;别误写成count = count + 1,那需要先SELECT再计算,破坏了原子性 - 批量插入时,每行独立触发冲突判断,但整个语句仍是一次网络往返,性能远优于逐条处理
PostgreSQL 必须用 ON CONFLICT,且必须明确指定冲突目标
PostgreSQL 不支持标准 SQL 的 MERGE,也不接受 ON DUPLICATE KEY UPDATE。唯一合规的 UPSERT 是 ON CONFLICT 语法,且必须明确指定冲突目标——要么是索引名,要么是列名组合。
例如唯一索引叫 un_phone,就得写:
INSERT INTO users (phone, name) VALUES ('138xxxx','Alice')
ON CONFLICT ON CONSTRAINT un_phone
DO UPDATE SET name = EXCLUDED.name, updated_at = NOW();
注意:EXCLUDED 是伪表,代表本次想插入但被拒绝的那行数据,不是原记录。如果唯一约束是联合索引 (date, type),不能只写 ON CONFLICT (date),否则可能匹配错行。
写入阻塞主因常是二级索引分裂,不是锁本身
MySQL 在高并发写入时卡住,很多人归因为“行锁争用”,但 InnoDB 表上真正拖慢写入的,往往是二级索引的 B+ 树分裂和缓冲池刷脏压力。主键写入顺序好(如自增 ID),但二级索引字段值随机(如 create_time、user_id),会导致大量页分裂和随机 I/O。
实操建议:
- 用
SHOW INDEX FROM table_name查看Cardinality和Seq_in_index,确认高频写入字段是否真有必要单独建索引 - 把多个低选择性字段合并成联合索引,避免重复维护多棵 B+ 树
- 对写多读少的字段(如 JSON 类型的
extra_info),直接删掉对应索引;宁可用WHERE LIKE '%%'或应用层过滤,也别让它拖累写入 - 删索引前务必用
EXPLAIN FORMAT=TREE看关键写入语句执行路径,确认没被隐式依赖(如子查询、窗口函数、DISTINCT)
无锁不等于无约束,而是在数据库能力边界内做最轻量的协作。真正容易被忽略的是:唯一索引的设计粒度、二级索引的实际维护成本、以及不同数据库对“冲突”的语法定义差异——这些细节一旦出错,所谓“无锁”方案就会退化成更隐蔽的瓶颈。










