C# & SQL 服务器:并行事务中的死锁
C# & SQL Server : deadlock in parallel transactions
我正在测试带有 System.Data.SqlClient
对象的自定义 UnitOfWork
class,我需要它来管理可能的并发事务和一些简单的 ado.net 操作的数据库连接。
我在第一次或第三次完整执行之间出现死锁,在第二次 select 中 class Repo1 或 Repo2,即插入命令之后的那个。
即使使用 "Serializable" 隔离级别也会发生这种情况。
我希望即使在单独的任务中,程序也可以无冲突地执行所有事务,每个事务都以随机顺序排队,但也许我只是使用了错误的隔离级别。
我的本意如下
在同一事务中:在插入后对 table 执行 select,我希望在同一事务中获得在上一个命令中插入的值。
在此事务外的命令中:我希望 select 读取实际提交的值,忽略未提交的值
这是我的代码
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
我正在测试带有 System.Data.SqlClient
对象的自定义 UnitOfWork
class,我需要它来管理可能的并发事务和一些简单的 ado.net 操作的数据库连接。
我在第一次或第三次完整执行之间出现死锁,在第二次 select 中 class Repo1 或 Repo2,即插入命令之后的那个。
即使使用 "Serializable" 隔离级别也会发生这种情况。
我希望即使在单独的任务中,程序也可以无冲突地执行所有事务,每个事务都以随机顺序排队,但也许我只是使用了错误的隔离级别。
我的本意如下
在同一事务中:在插入后对 table 执行 select,我希望在同一事务中获得在上一个命令中插入的值。
在此事务外的命令中:我希望 select 读取实际提交的值,忽略未提交的值
这是我的代码
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