You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

C# MySQL事务并发时出现唯一键重复插入异常原因排查

问题根源与解决方案

嘿,这个问题我之前也踩过坑!你的代码核心问题在于**“检查-插入”的操作不是原子性的**——哪怕包裹在事务里也没用。默认情况下,你的SELECT查询是“快照读”,不会锁住任何行或间隙,多个并发事务能同时通过检查步骤,接着一起执行插入,这时候第二个插入就会触发唯一键的Duplicate Entry异常。

举个直观的场景:

  1. 事务A执行SELECT ... WHERE name_col='xxx',发现无匹配记录
  2. 同时事务B执行相同的SELECT,也判定无记录
  3. 事务A先完成INSERT并提交
  4. 事务B再执行INSERT,直接触发唯一键冲突抛出异常

哪怕两个事务都在独立事务中,这个场景也会发生,因为默认隔离级别(比如InnoDB的REPEATABLE READ)下,快照读不会阻止其他事务的写入操作。

两种靠谱的解决办法,按需选择:

1. 用SELECT ... FOR UPDATE加排他锁,让检查+插入变成原子操作

把查询语句改成带FOR UPDATE的版本,这样第一个事务执行查询时,会锁住对应行(如果不存在则锁住对应间隙,防止其他事务插入相同值),其他事务必须等待第一个事务提交/回滚后才能继续。

修改后的核心代码:

var tr = mysql.BeginTransaction();
try
{
    // 加排他锁查询,确保并发下只有一个事务能走到插入步骤
    var checkCmd = new MySqlCommand("SELECT id FROM your_table WHERE name_col = @name FOR UPDATE", mysql, tr);
    checkCmd.Parameters.AddWithValue("@name", new_name); // 必须用参数化!
    int existingId = -1;
    using (var reader = checkCmd.ExecuteReader())
    {
        if (reader.HasRows)
        {
            reader.Read();
            existingId = reader.GetInt32(0);
        }
    }

    if (existingId != -1)
    {
        tr.Rollback();
        return existingId;
    }

    // 执行插入
    var insertCmd = new MySqlCommand("INSERT INTO your_table (name_col) VALUES (@name)", mysql, tr);
    insertCmd.Parameters.AddWithValue("@name", new_name);
    insertCmd.ExecuteNonQuery();
    int newId = (int)insertCmd.LastInsertedId;
    tr.Commit();
    return newId;
}
catch (Exception e)
{
    tr.Rollback();
    throw;
}

⚠️ 注意:这个方法要求表是InnoDB引擎(MyISAM不支持行锁和事务),同时要控制事务时长,避免锁持有太久导致其他请求等待。

2. 直接尝试插入,捕获唯一键异常后查询现有ID

另一种“乐观”思路:直接执行插入,如果碰到Duplicate Entry异常(MySQL错误码1062),再去查询已存在的记录ID。这种方式不需要加锁,并发性能更好,适合高流量场景。

代码示例:

int resultId = -1;
var tr = mysql.BeginTransaction();
try
{
    var insertCmd = new MySqlCommand("INSERT INTO your_table (name_col) VALUES (@name)", mysql, tr);
    insertCmd.Parameters.AddWithValue("@name", new_name);
    insertCmd.ExecuteNonQuery();
    resultId = (int)insertCmd.LastInsertedId;
    tr.Commit();
}
catch (MySqlException ex)
{
    // 判定是否为唯一键冲突异常
    if (ex.Number == 1062)
    {
        tr.Rollback();
        // 查询已存在的ID(读操作无需事务,安全)
        var queryCmd = new MySqlCommand("SELECT id FROM your_table WHERE name_col = @name", mysql);
        queryCmd.Parameters.AddWithValue("@name", new_name);
        using (var reader = queryCmd.ExecuteReader())
        {
            if (reader.HasRows)
            {
                reader.Read();
                resultId = reader.GetInt32(0);
            }
        }
    }
    else
    {
        tr.Rollback();
        throw; // 其他异常直接抛出
    }
}
return resultId;

重中之重:修复SQL注入漏洞!

你的原代码用String.Format拼接SQL,这是极高危操作——如果new_name传入'); DROP TABLE your_table;--这类恶意字符串,数据库直接遭殃。一定要用参数化查询,就像上面例子里的@name参数,既能防注入,还能提升查询性能(MySQL可缓存查询计划)。

内容的提问来源于stack exchange,提问作者jw_

火山引擎 最新活动