C# 和 SQL 服务器 : deadlock in parallel transactions

标签 c# sql-server parallel-processing transactions unit-of-work

我正在使用 System.Data.SqlClient 对象测试自定义 UnitOfWork 类,我需要它来管理可能的并发事务和一些简单的 ado.net 操作的数据库连接.

我在第一次或第三次完整执行之间出现死锁,在 Repo1 或 Repo2 类中的第二次选择中,即插入命令之后的一次。

即使使用“Serializable”隔离级别也会发生这种情况。

我希望即使在单独的任务中,程序也可以无冲突地执行所有事务,每个事务都以随机顺序排队,但也许我只是使用了错误的隔离级别。

我的意图如下

  • 在同一个事务中:在插入后对表进行选择,我希望在同一个事务中获得在上一个命令中插入的值。

  • 在此事务外的命令中:我希望选择读取实际提交的值,忽略未提交的值

这是我的代码

public class AdoNetUnitOfWork : IDisposable
{
    SqlConnection _connection;
    bool _ownsConnection;
    SqlTransaction _transaction;

    public AdoNetUnitOfWork(SqlConnection connection, bool ownsConnection)
    {
        _connection = connection;
        _ownsConnection = ownsConnection;
        _transaction = connection.BeginTransaction(System.Data.IsolationLevel.ReadCommitted);
    }

    public SqlCommand CreateCommand()
    {
        var command = _connection.CreateCommand();
        command.Transaction = _transaction;
        return command;
    }

    public void SaveChanges()
    {
        if (_transaction == null)
            throw new InvalidOperationException("Transaction have already been commited. Check your transaction handling.");

        _transaction.Commit();
        _transaction = null;
    }

    public void Dispose()
    {
        if (_transaction != null)
        {
            _transaction.Rollback();
            _transaction = null;
        }

        if (_connection != null && _ownsConnection)
        {
            _connection.Close();
            _connection = null;
        }
    }
}

public class UnitOfWorkFactory
{
    public static AdoNetUnitOfWork Create()
    {
        var connection = new SqlConnection(
            "Data Source=localhost;Initial Catalog=***;Integrated Security=False;User Id=**;Password=***;MultipleActiveResultSets=True");
        connection.Open();

        return new AdoNetUnitOfWork(connection, true);
    }
}

业务逻辑类:

public class Service
{
    public Service()
    {
    }

    public OutputStr MakeCalls()
    {
        using (var uow = UnitOfWorkFactory.Create())
        {
            var repo1 = new Repo1(uow);
            repo1.ExecuteQuery(out int idStart1, out int idEnd1);

            var repo2 = new Repo2(uow);
            repo2.ExecuteQuery(out int idStart2, out int idEnd2);

            OutputStr str = new OutputStr()
                                { 
                                    IdStart1 = idStart1, IdEnd1 = idEnd1,
                                    IdStart2 = idStart2, IdEnd2 = idEnd2
                                };

            uow.SaveChanges();

            return str;
        }
    }
}

public class OutputStr
{
    public int IdStart1 { get; set; }
    public int IdEnd1 { get; set; }
    public int IdStart2 { get; set; }
    public int IdEnd2 { get; set; }
}

表1的查询执行类:

public class Repo1
{
    AdoNetUnitOfWork _uow;

    public Repo1(AdoNetUnitOfWork uow)
    {
        _uow = uow;
    }

    public void ExecuteQuery(out int idStart, out int idEnd)
    {
        SqlCommand cmd = _uow.CreateCommand();

        cmd.CommandText = "SELECT COALESCE(MAX(Id),0) from Table1";
        idStart = int.Parse(cmd.ExecuteScalar().ToString());

        cmd.CommandText = @"
            INSERT INTO Table1
               ([Id])
               SELECT COALESCE(MAX(Id),0)+1 
                from Table1";
        cmd.ExecuteNonQuery();

        cmd.CommandText = "SELECT COALESCE(MAX(Id),0) from Table1";
        idEnd = int.Parse(cmd.ExecuteScalar().ToString());
        //idEnd = 0;
    }
}

创建表 1 的脚本:

CREATE TABLE [dbo].[Table1]
(
    [Id] [INT] NOT NULL
) ON [PRIMARY]
GO

查询执行类2

public class Repo2
{
    AdoNetUnitOfWork _uow;

    public Repo2(AdoNetUnitOfWork uow)
    {
        _uow = uow;
    }

    public void ExecuteQuery(out int idStart, out int idEnd)
    {
        SqlCommand cmd = _uow.CreateCommand();

        cmd.CommandText = "SELECT COALESCE(MAX(Id),0) from Table2";
        idStart = int.Parse(cmd.ExecuteScalar().ToString());

        cmd.CommandText = @"INSERT INTO Table2 ([Id])
                                SELECT COALESCE(MAX(Id),0)+1 
                                FROM Table2";
        cmd.ExecuteNonQuery();

        cmd.CommandText = "SELECT COALESCE(MAX(Id), 0) FROM Table2";
        idEnd = int.Parse(cmd.ExecuteScalar().ToString());
        //idEnd = 0;
    }
}

创建表 2 的脚本

CREATE TABLE [dbo].[Table2]
(
    [Id] [INT] NOT NULL
) ON [PRIMARY]
GO

控制台类

class Program
{
    static void Main(string[] args)
    {
        Service svc = new Service();

        for (int i = 0; i < 20; i++)
        {
            Console.WriteLine($"Current call {i}");
            new Task(() => Printer(svc, i)).Start();
        }

        Console.ReadKey();
    }

    private static void Printer(Service svc, int index)
    {
        OutputStr str = svc.MakeCalls();
        Console.WriteLine($@"
            start1:{str.IdStart1}, end1:{str.IdEnd1}, 
            start2:{str.IdStart2}, end2:{str.IdEnd2}");
    }
}

编辑:在数据库上使用这个命令解决了

SELECT is_read_committed_snapshot_on, snapshot_isolation_state_desc,snapshot_isolation_state 
FROM sys.databases WHERE name='db'

ALTER DATABASE db SET allow_snapshot_isolation ON
ALTER DATABASE db SET SINGLE_USER WITH ROLLBACK IMMEDIATE
ALTER DATABASE db SET read_committed_snapshot ON
ALTER DATABASE db SET MULTI_USER

SELECT is_read_committed_snapshot_on, snapshot_isolation_state_desc,snapshot_isolation_state 
FROM sys.databases WHERE name='db'

并将代码更改为

_transaction = connection.BeginTransaction(IsolationLevel.Snapshot);

最佳答案

My intention is the following:
 -in the same transaction: do a select on the table after an insert and I expect to get the value inserted in the prevous command on the same transaction.
 -in commands outside this transaction: i expect that a select reads the actual committed values ignoring, the uncommitted values

那么您应该在数据库中使用 READ_COMMITTED(默认)隔离级别,并将 READ_COMMITTED_SNAPSHOT 选项设置为 ON。 例如

alter database current set read_committed_snapshot on

参见 Snapshot Isolation In SQL Server

关于C# 和 SQL 服务器 : deadlock in parallel transactions,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/53874480/

相关文章:

python - 重复并行运行一个函数

c# - 当密码或用户名字段包含特殊字符时,System.Uri 无法解析

c# - 需要帮助使用线程来监视指定文件夹中的 txt 文件

sql-server - 为什么我必须设置数据库中每个文本列的最大长度?

sql-server - 更改 AD 登录凭据后返回无效的 susername_sname()/SYSTEM_USER

multithreading - Golang : How to capture return values of massively parallel benchmark (> 1 million tasks)?

c# - 如何正确调整/重新压缩图像

c# - 是否可以从深度位图中仅提取玩家的深度像素?

sql-server - 赋值中带有 ISNULL 的存储过程调用。无效的语法?

haskell - 会使用 seq 代替 pseq 吗?