【C#】事务(进程 ID 64)与另一个进程被死锁在锁资源上,并且已被选作死锁牺牲品。请重新运行该事务。不能在具有唯一索引“XXX_Index”的对象“dbo.Test”中插入重复键的行。

🌹欢迎来到《小5讲堂》🌹
🌹这是《C#》系列文章,每篇文章将以博主理解的角度展开讲解。🌹
🌹温馨提示:博主能力有限,理解水平有限,若有不对之处望指正!🌹

在这里插入图片描述

前言

最近博主在开发一个项目时,用到的都是基本框架功能,有些造轮子的意思。
如果不留意,那么很容易犯一些代码逻辑上的错误。
自己封装框架和使用现有成熟框架最大区别就是容易踩坑,除非你有比较多的框架开发经验。

锁定分析

Lock锁对象

这里博主犯了个小细节错误,锁的对象和范围逻辑不对,导致重复数据出现,大事务里存在同表查询和添加,即使在小量并发下,出现争抢资源,导致锁。

实例化锁对象
下面是错误写法,有发现那里不对吗?

namespace MvcMyNameSpace
{
    public class GetClass
    {
    	private object lockObj = new object();
		
		public string GetNo(string tableName, string prefix)
        {
            lock (lockObj)
            {
            	// 查询表
            	string sql = "select top 1 * from Test"
            }
        }
    }

静态实例化
正确写法,确保锁对象是当前唯一

namespace MvcMyNameSpace
{
    public class GetClass
    {
    	private static object lockObj = new object();
		
		public string GetNo(string tableName, string prefix)
        {
            lock (lockObj)
            {
            	// 查询表
            	string sql = "select top 1 * from Test"
            }
        }
    }

锁范围
博主在上面独立方法代码块加上锁后,运行一段时间任务后,发现还是会出现重复索引。
后来排查发现,是并发会在最外层接口方法没有锁定。
之后在接口API整个方法加了锁后,就再也没有出现了重复索引。

事务锁

博主这里是在一个大事务内进行了表查询,在高并发下就会出现,多个事务可能同时尝试更新或读取相同的数据行或表,导致资源竞争。

重复键

对于sql server数据库而言,可对表的字段设置唯一索引,确保数据库表唯一性。
如果表设置了唯一索引,那么还会出现重复,说明程序端代码逻辑不对,写法有问题。
博主此处出现的错误就是在并发情况,至少有两个并发同一时间获取到同一条记录。

DeepSeek解决方案

从你提供的代码和错误信息来看,程序在执行 SQL 命令时遇到了死锁问题。死锁通常发生在多个事务同时竞争相同的资源,并且每个事务都在等待其他事务释放锁时发生。SQL Server 检测到死锁后,会选择其中一个事务作为“牺牲品”并终止它,以便其他事务可以继续执行。

死锁的原因

在你的代码中,死锁可能由以下原因引起:

  1. 并发事务竞争相同的资源:多个事务可能同时尝试更新或读取相同的数据行或表,导致资源竞争。
  2. 事务范围过大:如果事务涉及多个表或大量数据,死锁的风险会增加。
  3. 锁的顺序不一致:如果不同的事务以不同的顺序锁定资源,可能会导致死锁。

解决方案

以下是几种可能的解决方案:

1. 减少事务范围

  • 尽量减少事务的持续时间,确保事务只包含必要的操作。
  • 如果可能,将事务拆分为多个较小的事务,减少锁的持有时间。

2. 优化 SQL 语句

  • 确保 SQL 语句尽可能高效,减少锁的持有时间。
  • 使用 WITH (NOLOCK) 提示来避免读取操作时的锁竞争(但要注意脏读的风险)。
  • 确保更新操作只锁定必要的行,而不是整个表。

3. 使用锁提示

  • 在 SQL 语句中使用锁提示来控制锁的行为。例如,使用 UPDLOCKROWLOCK 来减少锁的粒度。
  • 例如:
    SELECT * FROM TableName WITH (UPDLOCK) WHERE ...
    

4. 重试机制

  • 在捕获到死锁异常后,实现重试机制。死锁通常是暂时的,重试后事务可能会成功执行。
  • 例如:
    int retryCount = 3;
    while (retryCount > 0)
    {
        try
        {
            // 执行 SQL 命令
            cmd.ExecuteNonQuery();
            break;
        }
        catch (SqlException ex)
        {
            if (ex.Number == 1205) // 1205 是死锁错误代码
            {
                retryCount--;
                if (retryCount == 0)
                    throw; // 重试次数用尽,抛出异常
                Thread.Sleep(1000); // 等待一段时间后重试
            }
            else
            {
                throw; // 其他异常直接抛出
            }
        }
    }
    

5. 检查索引

  • 确保表上有适当的索引,以减少锁的竞争。缺少索引可能导致全表扫描,增加锁的冲突。

6. 分析死锁图

  • 使用 SQL Server 的死锁图(Deadlock Graph)来分析死锁的原因。你可以通过 SQL Server Profiler 或 Extended Events 来捕获死锁图,了解哪些资源和事务导致了死锁。

示例代码改进

以下是一个简单的重试机制示例,你可以在代码中添加:

int retryCount = 3;
while (retryCount > 0)
{
    try
    {
        using (SqlConnection connection = new SqlConnection(connectionString))
        {
            connection.Open();
            using (SqlCommand cmd = new SqlCommand())
            {
                cmd.Connection = connection;

                foreach (String str in List)
                {
                    sqlText = "";

                    // 添加参数
                    ArrayList paramsArray = listParams[i];
                    foreach (Dictionary<string, object> paramsItem in paramsArray)
                    {
                        foreach (var keyItem in paramsItem)
                        {
                            if (string.IsNullOrEmpty($"{keyItem.Value}"))
                            {
                                cmd.Parameters.AddWithValue(keyItem.Key, DBNull.Value);
                            }
                            else
                            {
                                cmd.Parameters.AddWithValue(keyItem.Key, $"{keyItem.Value}");
                            }

                            if (str.Contains("update"))
                            {
                                sqlText += $"{keyItem.Key.Replace("@", "")}='{keyItem.Value}',";
                            }
                            else
                            {
                                sqlText += $"'{keyItem.Value}',";
                            }
                        }
                    }

                    allSql = $"{str}";

                    // 在这里执行你的 SQL 命令
                    cmd.CommandText = str;
                    cmd.ExecuteNonQuery();
                    cmd.Parameters.Clear();

                    i++;
                }

                rows += 1;
            }
        }
        break; // 执行成功,退出循环
    }
    catch (SqlException ex)
    {
        if (ex.Number == 1205) // 1205 是死锁错误代码
        {
            retryCount--;
            if (retryCount == 0)
                throw; // 重试次数用尽,抛出异常
            Thread.Sleep(1000); // 等待一段时间后重试
        }
        else
        {
            throw; // 其他异常直接抛出
        }
    }
}

总结

死锁是数据库系统中常见的问题,通常通过优化事务范围、SQL 语句、锁策略以及实现重试机制来解决。通过分析死锁图和调整代码逻辑,可以有效减少死锁的发生。

相关文章

【C#】事务(进程 ID 64)与另一个进程被死锁在锁资源上,并且已被选作死锁牺牲品。请重新运行该事务。不能在具有唯一索引“XXX_Index”的对象“dbo.Test”中插入重复键的行。

【C#】使用DeepSeek帮助评估数据库性能问题,C# 使用定时任务,每隔一分钟移除一次表,再重新创建表,和往新创建的表追加5万多条记录

【C#】合理使用DeepSeek相关AI应用为我们提供强有力的开发工具,在.net core 6.0框架下使用JsonNode动态解析json字符串,如何正确使用单问号和双问号做好空值处理

【C#】已经实体类和动态实体类的反射使用方法,两分钟回顾,码上就懂

【C#】使用vue3的axios发起get和post请求.net framework部署的API显示跨域

【C#】.net core 6.0 webapi 使用core版本的NPOI的Excel读取数据以及保存数据

【C#】pdf按页分割文件,以及分页合并,效果还不错,你值得拥有

【C#】未能加载文件或程序集“CefSharp.Core.Runtime.dll”或它的某一个依赖项。找不到指定的模块。

【C#】.net core 6.0 在program时间格式统一json格式化,并列举program默认写法和简化写法

【C#】.net core 6.0 ApiController,API控制器方法,API接口以实体类作为接收参数应该注意的点

【C#】 SortedDictionary,查找字典中是否存在给定的关键字

【C#】.net core 6.0 MVC返回JsonResult显示API接口返回值不可被JSON反序列化

【C#】.net core 6.0 使用第三方日志插件Log4net,配置文件详细说明

【C#】使用代码实现龙年春晚扑克牌魔术(守岁共此时),代码实现篇

【C#】使用代码实现龙年春晚扑克牌魔术(守岁共此时),流程描述篇