C# & SQL 服务器:并行事务中的死锁

C# & SQL Server : deadlock in parallel transactions

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

我在第一次或第三次完整执行之间出现死锁,在第二次 select 中 class 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);
    }
}

业务逻辑class:

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; }
}

查询执行 class table 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;
    }
}

要创建的脚本 table1:

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

查询执行class2

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;
    }
}

要创建的脚本 table 2

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

控制台class

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