MySQL 并发插入竞态问题:原子写入实践攻略

 更新时间:2026年07月09日 15:16:19   作者:不会写DN  
在数据库管理中,尤其是在使用MySQL这类关系型数据库时,并发插入操作可能会引发竞态条件(Race Conditions)和数据不一致的问题,这篇文章给大家介绍MySQL并发插入竞态问题:原子写入实践攻略,感兴趣的朋友跟随小编一起看看吧

在涉及唯一约束的业务场景中,比如用户注册、唯一编号数据写入,“先查询是否存在,不存在再插入”是很容易想到的直觉写法。

单测与低并发下通常表现正常,但随着请求量上升,日志中会不定期出现唯一键冲突报错,很多开发者对此感到困惑——明明已经做了存在性判断,为什么还会重复写入?

问题的根源在于,分步的查询与插入并非原子操作,在并发环境下天然存在竞态条件。本文将拆解这一问题的成因,并介绍基于数据库原生能力的标准解决方案。

一、“先查后插”的并发漏洞

以用户表的邮箱唯一约束为例,典型的分步写代码逻辑如下:

// 检查邮箱是否已注册
var count int64
db.Model(&model.User{}).Where("email = ?", email).Count(&count)
if count == 0 {
    // 不存在则创建用户
    db.Create(&newUser)
}

单线程执行时,这段逻辑完全符合预期。但当两个相同邮箱的请求几乎同时到达时,执行时序会出现交叉:

时间轴:
T1 请求A执行SELECT -> 结果为0,判定邮箱未注册
T2 请求B执行SELECT -> 结果为0,同样判定邮箱未注册
T3 请求A执行INSERT -> 写入成功
T4 请求B执行INSERT -> 触发唯一索引约束,返回 1062 Duplicate entry 错误

两个请求都在对方写入完成前完成了查询,都得到了“数据不存在”的结论,最终后写入的请求必然冲突。

即便将两步操作包裹在同一个数据库事务中,问题依然存在。在 InnoDB 默认的可重复读隔离级别下,普通 SELECT 是快照读,不会对不存在的数据行加锁,因此无法阻塞其他事务的并发写入。事务只能保证自身操作的原子性,无法填补分步判断带来的时间窗口。

二、基于数据库原生能力的原子方案

解决该问题的核心思路,是将“判断 + 写入”合并为单条 SQL 操作,依托数据库唯一索引的锁机制保证执行原子性。MySQL 提供了两种标准语法,对应不同业务语义。

1. INSERT IGNORE:冲突则静默跳过

在 INSERT 语句中加入 IGNORE 关键字,当遇到唯一键冲突时,数据库不会抛出错误,而是直接忽略本次写入,返回受影响行数为 0。

INSERT IGNORE INTO users (email, name, password) 
VALUES ('a@example.com', '张三', 'encrypted_pwd');
  • 无冲突:正常写入,受影响行数为 1
  • 有冲突:静默跳过,无报错,受影响行数为 0

该方案适用于幂等性写入场景,例如批量数据导入、初始化数据去重,重复数据无需处理,仅需通过受影响行数判断是否实际写入即可。其最大价值是避免将正常业务冲突转化为系统错误,减少无效错误日志。

2. ON DUPLICATE KEY UPDATE:冲突则执行更新

若冲突时需要更新部分字段,可使用该语法,即常说的 Upsert(更新或插入)。当唯一键命中时,执行后续的 UPDATE 逻辑;未命中则正常插入。

INSERT INTO users (email, name, last_login_time) 
VALUES ('a@example.com', '张三', NOW())
ON DUPLICATE KEY UPDATE name = VALUES(name), last_login_time = VALUES(last_login_time);
  • 无冲突:执行插入,受影响行数为 1
  • 有冲突:执行更新,受影响行数为 2

典型适用场景如用户登录态刷新、配置项写入,数据不存在则创建、存在则刷新。整条语句在数据库层面原子执行,不存在并发窗口。

三、GORM 中的工程化实现

GORM 提供了 clause.OnConflict 原生支持上述两种冲突处理逻辑,无需手动拼接原生 SQL。

冲突忽略模式

对应 INSERT IGNORE 语义,适合注册、幂等写入场景:

func (r *userRepo) CreateIfNotExist(ctx context.Context, user *model.User) (bool, error) {
    res := r.db.WithContext(ctx).
        Clause(clause.OnConflict{DoNothing: true}).
        Create(user)
    if res.Error != nil {
        return false, res.Error
    }
    return res.RowsAffected > 0, nil
}

调用方通过返回布尔值判断是否实际创建成功,错误分支仅处理真正的系统异常。

冲突更新模式

对应 ON DUPLICATE KEY UPDATE 语义,指定冲突时需要更新的字段:

func (r *userRepo) Upsert(ctx context.Context, user *model.User) error {
    return r.db.WithContext(ctx).
        Clause(clause.OnConflict{
            Columns:   []clause.Column{{Name: "email"}},
            DoUpdates: clause.AssignmentColumns([]string{"name", "avatar", "updated_at"}),
        }).
        Create(user).Error
}

Columns 指定用于判断冲突的唯一键列,DoUpdates 指定冲突发生后需要更新的字段列表,语义清晰且与数据库特性对齐。

误区:SELECT FOR UPDATE并非解法

部分开发者会尝试在事务中使用 SELECT ... FOR UPDATE 加行锁,试图消除竞态。但该方案存在本质缺陷:行锁的前提是数据行存在。

当查询的目标数据尚未写入时,查询结果为空,没有实际的行可以施加行锁。两个并发事务都会查询到空结果,并同时认为可以安全写入,最终依然会触发唯一键冲突。

间隙锁在部分场景下会对写入产生限制,但其生效条件与隔离级别、索引类型、查询范围密切相关,依赖间隙锁保证写入安全并不稳定。相比之下,直接使用数据库原生的冲突处理语法,是成本最低、可靠性最高的方案。

实践总结

  1. 优先放弃分步判断写法。只要存在并发可能,先查后插就存在冲突概率,不应作为生产环境的首选方案。
  2. 仅需去重、无需更新:选择 INSERT IGNORE / OnConflict{DoNothing: true},通过受影响行数区分写入结果。
  3. 存在即需更新字段:选择 ON DUPLICATE KEY UPDATE / OnConflict{DoUpdates},实现原子 Upsert。
  4. 唯一索引是基础前提。冲突处理语法依赖唯一索引生效,建表时约束必须完整,这是数据一致性的最后防线。

本质上,这是一个很典型的工程设计原则:能通过底层组件原子能力解决的问题,就不要在上层通过分步逻辑模拟。数据库本身已经提供了成熟的并发写入处理机制,善用这些特性,既能减少代码复杂度,也能获得更可靠的并发表现。

到此这篇关于MySQL 并发插入竞态问题:原子写入实践指南的文章就介绍到这了,更多相关mysql并发插入内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • mysql的in会不会让索引失效?

    mysql的in会不会让索引失效?

    今天小编就为大家分享一篇关于mysql的in会不会让索引失效?,小编觉得内容挺不错的,现在分享给大家,具有很好的参考价值,需要的朋友一起跟随小编来看看吧
    2019-04-04
  • Mysql中tinyint(1)和tinyint(4)的区别详析

    Mysql中tinyint(1)和tinyint(4)的区别详析

    这篇文章主要给大家介绍了关于Mysql中tinyint(1)和tinyint(4)区别的相关资料,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下
    2022-02-02
  • MySQL中CONCAT()函数拼接出现NULL的问题解决

    MySQL中CONCAT()函数拼接出现NULL的问题解决

    本文主要介绍了MySQL中CONCAT()函数拼接出现NULL的问题解决,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2023-03-03
  • 教你如何让spark sql写mysql的时候支持update操作

    教你如何让spark sql写mysql的时候支持update操作

    spark提供了一个枚举类,用来支撑对接数据源的操作模式,本文重点给大家介绍如何让spark sql写mysql的时候支持update操作,本文通过实例代码给大家介绍的非常详细,需要的朋友参考下吧
    2022-02-02
  • 深入解析半同步与异步的MySQL主从复制配置

    深入解析半同步与异步的MySQL主从复制配置

    这篇文章主要介绍了半同步与异步的MySQL主从复制配置,包括不同的连接方案的讨论,需要的朋友可以参考下
    2015-12-12
  • MySQL延迟问题和数据刷盘策略流程分析

    MySQL延迟问题和数据刷盘策略流程分析

    这篇文章主要介绍了MySQL延迟问题和数据刷盘策略流程分析,本文要给大家提到了mysql复制流程,需要的朋友可以参考下
    2020-02-02
  • 简单了解MySQL数据库优化技巧

    简单了解MySQL数据库优化技巧

    这篇文章主要介绍了简单了解MySQL数据库优化技巧,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2020-07-07
  • mysql 事务处理及表锁定深入简析

    mysql 事务处理及表锁定深入简析

    本文将详细介绍mysql 事务处理及表锁定,需要了解更多的朋友可以参考下
    2012-11-11
  • MySql InnoDB存储引擎之Buffer Pool运行原理讲解

    MySql InnoDB存储引擎之Buffer Pool运行原理讲解

    缓冲池是用于存储InnoDB表,索引和其他辅助缓冲区的缓存数据的内存区域。缓冲池的大小对于系统性能很重要。更大的缓冲池可以减少磁盘I/O来多次访问同一表数据。在专用数据库服务器上,可以将缓冲池大小设置为计算机物理内存大小的百分之80
    2023-01-01
  • MySQ索引操作命令总结(创建、重建、查询和删除索引命令详解)

    MySQ索引操作命令总结(创建、重建、查询和删除索引命令详解)

    本篇文章主要是对MySQL索引操作方法做了一下总结,包括创建索引、重建索引、查询索引、删除索引的操作
    2014-04-04

最新评论