SQL 事务隔离级别可序列化与读取在开发与生产中提交
SQL Transaction Isolation Level Serializable vs Read Committed in Dev vs Production
我目前正在寻找死锁和 SQL 服务器的问题。发生了相当多的死锁。以下死锁图是我关注的(来自现场环境):
<deadlock>
<victim-list>
<victimProcess id="process41fce08"/>
<victimProcess id="process40c9048"/>
</victim-list>
<process-list>
<process id="process41fce08" taskpriority="0" logused="0" waitresource="KEY: 5:72057595257749504 (03e6337489b1)" waittime="2133" ownerId="213210603" transactionname="SELECT" lasttranstarted="2015-09-30T09:06:29.133" XDES="0xe1d9dcb0" lockMode="RangeS-S" schedulerid="4" kpid="3608" status="suspended" spid="113" sbid="0" ecid="0" priority="0" trancount="0" lastbatchstarted="2015-09-30T09:06:29.133" lastbatchcompleted="2015-09-30T09:06:29.133" clientapp=".Net SqlClient Data Provider" hostname="MYSERVER" hostpid="1692" loginname="MYUSER" isolationlevel="serializable (4)" xactid="213210603" currentdb="5" lockTimeout="4294967295" clientoption1="673185824" clientoption2="128056">
<executionStack>
<frame procname="" line="25" stmtstart="1876" stmtend="5346" sqlhandle="0x030005007f7dca111202de0024a400000100000000000000"/>
</executionStack>
<inputbuf> Proc [Database Id = 5 Object Id = 298483071] THIS IS READ STORED PROCEDURE </inputbuf>
</process>
<process id="process40c9048" taskpriority="0" logused="0" waitresource="KEY: 5:72057595257749504 (03e6337489b1)" waittime="2137" ownerId="213210579" transactionname="SELECT" lasttranstarted="2015-09-30T09:06:29.130" XDES="0x8051fcb0" lockMode="RangeS-S" schedulerid="2" kpid="3908" status="suspended" spid="101" sbid="0" ecid="0" priority="0" trancount="0" lastbatchstarted="2015-09-30T09:06:29.130" lastbatchcompleted="2015-09-30T09:06:29.130" clientapp=".Net SqlClient Data Provider" hostname="MYSERVER" hostpid="5328" loginname="MYUSER" isolationlevel="serializable (4)" xactid="213210579" currentdb="5" lockTimeout="4294967295" clientoption1="673185824" clientoption2="128056">
<executionStack>
<frame procname="" line="25" stmtstart="1876" stmtend="5346" sqlhandle="0x030005007f7dca111202de0024a400000100000000000000"/>
</executionStack>
<inputbuf> Proc [Database Id = 5 Object Id = 298483071] THIS IS READ STORED PROCEDURE </inputbuf>
</process>
<process id="process11eca3708" taskpriority="0" logused="247980" waitresource="OBJECT: 5:1745427475:0 " waittime="1913" ownerId="213208999" transactionname="user_transaction" lasttranstarted="2015-09-30T09:06:28.640" XDES="0xc2f3ae90" lockMode="IX" schedulerid="3" kpid="272" status="suspended" spid="72" sbid="0" ecid="0" priority="0" trancount="2" lastbatchstarted="2015-09-30T09:06:29.410" lastbatchcompleted="2015-09-30T09:06:29.410" clientapp=".Net SqlClient Data Provider" hostname="MYSERVER" hostpid="4172" loginname="MYUSER" isolationlevel="serializable (4)" xactid="213208999" currentdb="5" lockTimeout="4294967295" clientoption1="671088672" clientoption2="128056">
<executionStack>
<frame procname="" line="6" stmtstart="232" sqlhandle="0x0300050098df89558d028500b59f00000100000000000000"/>
</executionStack>
<inputbuf> Proc [Database Id = 5 Object Id = 1435099032] THIS IS READ UPDATE PROCEDURE </inputbuf>
</process>
</process-list>
<resource-list>
<keylock hobtid="72057595257749504" dbid="5" objectname="" indexname="" id="lockee29f480" mode="X" associatedObjectId="72057595257749504">
<owner-list/>
<waiter-list>
<waiter id="process41fce08" mode="RangeS-S" requestType="wait"/>
</waiter-list>
</keylock>
<keylock hobtid="72057595257749504" dbid="5" objectname="" indexname="" id="lockee29f480" mode="X" associatedObjectId="72057595257749504">
<owner-list>
<owner id="process11eca3708" mode="X"/>
</owner-list>
<waiter-list>
<waiter id="process40c9048" mode="RangeS-S" requestType="wait"/>
</waiter-list>
</keylock>
<objectlock lockPartition="0" objid="1745427475" subresource="FULL" dbid="5" objectname="" id="lock128d27d80" mode="S" associatedObjectId="1745427475">
<owner-list>
<owner id="process41fce08" mode="S"/>
</owner-list>
<waiter-list>
<waiter id="process11eca3708" mode="IX" requestType="convert"/>
</waiter-list>
</objectlock>
</resource-list>
</deadlock>
查看死锁受害者进程 id = process41fce08,这是一个 select 存储过程,似乎 运行 在可序列化事务下。
但是,查看堆栈跟踪,未使用事务范围。没有创建显式事务。一个简单的 SqlConnection 和 SqlCommand 用于执行存储过程。
查看调用堆栈以在我的本地开发机器上执行相同的代码路径,这显示事务隔离级别 运行ning 在 ReadCommitted 下。使用以下方法检索此信息:
select
CASE transaction_isolation_level
WHEN 0 THEN 'Unspecified'
WHEN 1 THEN 'ReadUncommitted'
WHEN 2 THEN 'ReadCommitted'
WHEN 3 THEN 'Repeatable'
WHEN 4 THEN 'Serializable'
WHEN 5 THEN 'Snapshot'
ELSE 'Unknown'
END
FROM sys.dm_exec_sessions
where session_id = @@SPID
来自存储过程内部。如果我打电话:
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE
在存储过程中,我看到上面的隔离级别发生了变化select。这是我所期望的,也是我在开发环境中看到的:
Default Isolation Level in ADO.NET
鉴于:
- 调用堆栈中未设置 TransactionScope
- 存储过程内部没有设置隔离级别
似乎在实时环境中设置隔离级别?问题是什么设置它或强制它可序列化?
.NET 4 上的 MVC3 应用程序中的代码 运行s。
这似乎是连接池的问题。
我注意到在连接字符串中我们有以下内容:
Pooling=true; Min Pool Size=5; Max Pool Size=100; Connect Timeout=7
我创建了许多单元测试来证明这一点。
SQL 创建脚本
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
CREATE PROCEDURE [dbo].[usp_GetName]
(
@id int,
@TransactionIsolation varchar(30) output
)
AS
BEGIN
-- SET NOCOUNT ON added to prevent extra result sets from
-- interfering with SELECT statements.
SET NOCOUNT ON;
set @TransactionIsolation = dbo.fn_GetTransactionIsolation();
SELECT [Id]
,[Name]
,[Status]
FROM [dbo].[NameTable]
where Id = @id
END
CREATE FUNCTION [dbo].[fn_GetTransactionIsolation]
(
)
RETURNS varchar(30)
AS
BEGIN
-- Declare the return variable here
DECLARE @til varchar(30)
select @til =
CASE transaction_isolation_level
WHEN 0 THEN 'Unspecified'
WHEN 1 THEN 'ReadUncommitted'
WHEN 2 THEN 'ReadCommitted'
WHEN 3 THEN 'Repeatable'
WHEN 4 THEN 'Serializable'
WHEN 5 THEN 'Snapshot'
ELSE 'Unknown'
END
FROM sys.dm_exec_sessions
where session_id = @@SPID
-- Return the result of the function
RETURN @til
END
GO
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
CREATE TABLE [dbo].[NameTable](
[Id] [int] IDENTITY(1,1) NOT NULL,
[Name] [nvarchar](50) NOT NULL,
[Status] [int] NOT NULL,
CONSTRAINT [PK_NameTable] PRIMARY KEY CLUSTERED
(
[Id] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY]
GO
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
CREATE TABLE [dbo].[SPLog](
[Id] [int] IDENTITY(1,1) NOT NULL,
[StoredProcName] [nvarchar](50) NOT NULL,
[LogDate] [datetime] NOT NULL,
CONSTRAINT [PK_SPLog] PRIMARY KEY CLUSTERED
(
[Id] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY]
GO
单元测试
using System;
using System.Data;
using System.Data.SqlClient;
using System.Threading;
using System.Transactions;
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace TransactionTest
{
[TestClass]
public class UnitTest1
{
[TestMethod]
public void SPGet_NoTransaction_ShouldReturn_ReadCommitted()
{
using (var con = new SqlConnection(GetConnection()))
{
con.Open();
using (var cmd = new SqlCommand("usp_GetName", con))
{
cmd.CommandType = CommandType.StoredProcedure;
AddParameter(cmd, "@id", 1, SqlDbType.Int);
AddParameter(cmd, "@TransactionIsolation", 1, SqlDbType.VarChar, 30, ParameterDirection.Output);
SqlDataReader reader = cmd.ExecuteReader();
while (reader.Read())
{
}
reader.Close();
var transactionIsolation = (string)cmd.Parameters["@TransactionIsolation"].Value;
Assert.AreEqual("ReadCommitted", transactionIsolation);
}
}
}
[TestMethod]
public void SPUpdate_NoTransaction_ShouldReturn_ReadCommitted()
{
using (var con = new SqlConnection(GetConnection()))
{
con.Open();
using (var cmd = new SqlCommand("usp_UpdateName", con))
{
cmd.CommandType = CommandType.StoredProcedure;
AddParameter(cmd, "@id", 1, SqlDbType.Int);
AddParameter(cmd, "@name", "test update", SqlDbType.VarChar, 30);
AddParameter(cmd, "@status", 1, SqlDbType.Int);
AddParameter(cmd, "@TransactionIsolation", 1, SqlDbType.VarChar, 30, ParameterDirection.Output);
SqlDataReader reader = cmd.ExecuteReader();
while (reader.Read())
{
}
reader.Close();
var transactionIsolation = (string)cmd.Parameters["@TransactionIsolation"].Value;
Assert.AreEqual("ReadCommitted", transactionIsolation);
}
}
}
[TestMethod]
public void SPGet_TransactionSerializable_ShouldReturn_Serializable()
{
using (var tran = new TransactionScope(
TransactionScopeOption.Required,
new TransactionOptions { IsolationLevel = System.Transactions.IsolationLevel.Serializable }))
{
using (var con = new SqlConnection(GetConnection()))
{
con.Open();
using (var cmd = new SqlCommand("usp_GetName", con))
{
cmd.CommandType = CommandType.StoredProcedure;
AddParameter(cmd, "@id", 1, SqlDbType.Int);
AddParameter(cmd, "@TransactionIsolation", 1, SqlDbType.VarChar, 30, ParameterDirection.Output);
SqlDataReader reader = cmd.ExecuteReader();
while (reader.Read())
{
}
reader.Close();
var transactionIsolation = (string)cmd.Parameters["@TransactionIsolation"].Value;
Assert.AreEqual("Serializable", transactionIsolation);
}
}
tran.Complete();
}
}
private static string GetConnection()
{
var builder = new SqlConnectionStringBuilder();
builder.InitialCatalog = "ACMETransactions";
builder.DataSource = ".";
builder.IntegratedSecurity = true;
builder.Pooling = true;
builder.MaxPoolSize = 100;
return builder.ToString();
}
private static void AddParameter(SqlCommand command, string name, object value, SqlDbType type, int size = -1, ParameterDirection direction = ParameterDirection.Input)
{
command.Parameters.Add(new SqlParameter
{
ParameterName = name,
SqlDbType = type,
Value = value,
Direction = direction
});
if (size != -1)
{
command.Parameters[command.Parameters.Count - 1].Size = size;
}
}
[TestMethod]
public void SPGet__MultiThread_Conflict()
{
string serializedIsolationResult = "";
string normalIsolationResult = "";
var normalThread = new Thread(new ThreadStart(() =>
{
using (var con = new SqlConnection(GetConnection()))
{
con.Open();
using (var cmd = new SqlCommand("usp_GetName", con))
{
cmd.CommandType = CommandType.StoredProcedure;
AddParameter(cmd, "@id", 1, SqlDbType.Int);
AddParameter(cmd, "@TransactionIsolation", 1, SqlDbType.VarChar, 30, ParameterDirection.Output);
SqlDataReader reader = cmd.ExecuteReader();
while (reader.Read()) ;
reader.Close();
normalIsolationResult = (string)cmd.Parameters["@TransactionIsolation"].Value;
}
}
}));
var serializedThread = new Thread(new ThreadStart(() =>
{
using (var tran = new TransactionScope(
TransactionScopeOption.Required,
new TransactionOptions { IsolationLevel = System.Transactions.IsolationLevel.Serializable }))
{
using (var con = new SqlConnection(GetConnection()))
{
con.Open();
using (var cmd = new SqlCommand("usp_GetName", con))
{
cmd.CommandType = CommandType.StoredProcedure;
AddParameter(cmd, "@id", 1, SqlDbType.Int);
AddParameter(cmd, "@TransactionIsolation", 1, SqlDbType.VarChar, 30, ParameterDirection.Output);
SqlDataReader reader = cmd.ExecuteReader();
while (reader.Read()) ;
reader.Close();
serializedIsolationResult = (string)cmd.Parameters["@TransactionIsolation"].Value;
}
normalThread.Join();
Thread.Sleep(1000);
}
tran.Complete();
}
}));
serializedThread.Start();
normalThread.Start();
serializedThread.Join();
Assert.AreEqual("ReadCommitted", normalIsolationResult);
Assert.AreEqual("Serializable", serializedIsolationResult);
}
[TestMethod]
public void SPGet__MultiThread_NoTransactionScope()
{
string serializedIsolationResult = "";
string normalIsolationResult = "";
var normalThread = new Thread(new ThreadStart(() =>
{
using (var con = new SqlConnection(GetConnection()))
{
con.Open();
using (var cmd = new SqlCommand("usp_GetName", con))
{
cmd.CommandType = CommandType.StoredProcedure;
AddParameter(cmd, "@id", 1, SqlDbType.Int);
AddParameter(cmd, "@TransactionIsolation", 1, SqlDbType.VarChar, 30, ParameterDirection.Output);
SqlDataReader reader = cmd.ExecuteReader();
while (reader.Read()) ;
reader.Close();
normalIsolationResult = (string)cmd.Parameters["@TransactionIsolation"].Value;
}
}
}));
var serializedThread = new Thread(new ThreadStart(() =>
{
using (var con = new SqlConnection(GetConnection()))
{
con.Open();
using (var cmd = new SqlCommand("usp_GetName", con))
{
cmd.CommandType = CommandType.StoredProcedure;
AddParameter(cmd, "@id", 1, SqlDbType.Int);
AddParameter(cmd, "@TransactionIsolation", 1, SqlDbType.VarChar, 30, ParameterDirection.Output);
SqlDataReader reader = cmd.ExecuteReader();
while (reader.Read()) ;
reader.Close();
serializedIsolationResult = (string)cmd.Parameters["@TransactionIsolation"].Value;
}
}
}));
serializedThread.Start();
normalThread.Start();
serializedThread.Join();
Assert.AreEqual("ReadCommitted", normalIsolationResult);
Assert.AreEqual("ReadCommitted", serializedIsolationResult);
}
[TestMethod]
public void SPGet__MultiConnection()
{
string normalIsolationResult2 = "";
string normalIsolationResult1 = "";
using (var con = new SqlConnection(GetConnection()))
{
con.Open();
using (var cmd = new SqlCommand("usp_GetName", con))
{
cmd.CommandType = CommandType.StoredProcedure;
AddParameter(cmd, "@id", 1, SqlDbType.Int);
AddParameter(cmd, "@TransactionIsolation", 1, SqlDbType.VarChar, 30, ParameterDirection.Output);
SqlDataReader reader = cmd.ExecuteReader();
while (reader.Read()) ;
reader.Close();
normalIsolationResult1 = (string)cmd.Parameters["@TransactionIsolation"].Value;
}
}
using (var con = new SqlConnection(GetConnection()))
{
con.Open();
using (var cmd = new SqlCommand("usp_GetName", con))
{
cmd.CommandType = CommandType.StoredProcedure;
AddParameter(cmd, "@id", 1, SqlDbType.Int);
AddParameter(cmd, "@TransactionIsolation", 1, SqlDbType.VarChar, 30, ParameterDirection.Output);
SqlDataReader reader = cmd.ExecuteReader();
while (reader.Read()) ;
reader.Close();
normalIsolationResult2 = (string)cmd.Parameters["@TransactionIsolation"].Value;
}
}
Assert.AreEqual("ReadCommitted", normalIsolationResult1);
Assert.AreEqual("ReadCommitted", normalIsolationResult2);
}
[TestMethod]
public void SPGet__SingleConnection()
{
string normalIsolationResult2 = "";
string normalIsolationResult1 = "";
using (var con = new SqlConnection(GetConnection()))
{
con.Open();
using (var cmd = new SqlCommand("usp_GetName", con))
{
cmd.CommandType = CommandType.StoredProcedure;
AddParameter(cmd, "@id", 1, SqlDbType.Int);
AddParameter(cmd, "@TransactionIsolation", 1, SqlDbType.VarChar, 30, ParameterDirection.Output);
SqlDataReader reader = cmd.ExecuteReader();
while (reader.Read()) ;
reader.Close();
normalIsolationResult1 = (string)cmd.Parameters["@TransactionIsolation"].Value;
}
using (var cmd = new SqlCommand("usp_GetName", con))
{
cmd.CommandType = CommandType.StoredProcedure;
AddParameter(cmd, "@id", 1, SqlDbType.Int);
AddParameter(cmd, "@TransactionIsolation", 1, SqlDbType.VarChar, 30, ParameterDirection.Output);
SqlDataReader reader = cmd.ExecuteReader();
while (reader.Read()) ;
reader.Close();
normalIsolationResult2 = (string)cmd.Parameters["@TransactionIsolation"].Value;
}
}
Assert.AreEqual("ReadCommitted", normalIsolationResult1);
Assert.AreEqual("ReadCommitted", normalIsolationResult2);
}
}
}
只需更改 GetConnection 方法以删除 Pooling/MaxPoolSize 即可使测试每次都通过。
使用这些参数,一些测试将失败。
我假设在存在死锁的实时环境中,我们看到连接被重用,事务范围被设置为 Serializable,而代码在明确使用 TransactionScope 的地方执行任何更新。
我目前正在寻找死锁和 SQL 服务器的问题。发生了相当多的死锁。以下死锁图是我关注的(来自现场环境):
<deadlock>
<victim-list>
<victimProcess id="process41fce08"/>
<victimProcess id="process40c9048"/>
</victim-list>
<process-list>
<process id="process41fce08" taskpriority="0" logused="0" waitresource="KEY: 5:72057595257749504 (03e6337489b1)" waittime="2133" ownerId="213210603" transactionname="SELECT" lasttranstarted="2015-09-30T09:06:29.133" XDES="0xe1d9dcb0" lockMode="RangeS-S" schedulerid="4" kpid="3608" status="suspended" spid="113" sbid="0" ecid="0" priority="0" trancount="0" lastbatchstarted="2015-09-30T09:06:29.133" lastbatchcompleted="2015-09-30T09:06:29.133" clientapp=".Net SqlClient Data Provider" hostname="MYSERVER" hostpid="1692" loginname="MYUSER" isolationlevel="serializable (4)" xactid="213210603" currentdb="5" lockTimeout="4294967295" clientoption1="673185824" clientoption2="128056">
<executionStack>
<frame procname="" line="25" stmtstart="1876" stmtend="5346" sqlhandle="0x030005007f7dca111202de0024a400000100000000000000"/>
</executionStack>
<inputbuf> Proc [Database Id = 5 Object Id = 298483071] THIS IS READ STORED PROCEDURE </inputbuf>
</process>
<process id="process40c9048" taskpriority="0" logused="0" waitresource="KEY: 5:72057595257749504 (03e6337489b1)" waittime="2137" ownerId="213210579" transactionname="SELECT" lasttranstarted="2015-09-30T09:06:29.130" XDES="0x8051fcb0" lockMode="RangeS-S" schedulerid="2" kpid="3908" status="suspended" spid="101" sbid="0" ecid="0" priority="0" trancount="0" lastbatchstarted="2015-09-30T09:06:29.130" lastbatchcompleted="2015-09-30T09:06:29.130" clientapp=".Net SqlClient Data Provider" hostname="MYSERVER" hostpid="5328" loginname="MYUSER" isolationlevel="serializable (4)" xactid="213210579" currentdb="5" lockTimeout="4294967295" clientoption1="673185824" clientoption2="128056">
<executionStack>
<frame procname="" line="25" stmtstart="1876" stmtend="5346" sqlhandle="0x030005007f7dca111202de0024a400000100000000000000"/>
</executionStack>
<inputbuf> Proc [Database Id = 5 Object Id = 298483071] THIS IS READ STORED PROCEDURE </inputbuf>
</process>
<process id="process11eca3708" taskpriority="0" logused="247980" waitresource="OBJECT: 5:1745427475:0 " waittime="1913" ownerId="213208999" transactionname="user_transaction" lasttranstarted="2015-09-30T09:06:28.640" XDES="0xc2f3ae90" lockMode="IX" schedulerid="3" kpid="272" status="suspended" spid="72" sbid="0" ecid="0" priority="0" trancount="2" lastbatchstarted="2015-09-30T09:06:29.410" lastbatchcompleted="2015-09-30T09:06:29.410" clientapp=".Net SqlClient Data Provider" hostname="MYSERVER" hostpid="4172" loginname="MYUSER" isolationlevel="serializable (4)" xactid="213208999" currentdb="5" lockTimeout="4294967295" clientoption1="671088672" clientoption2="128056">
<executionStack>
<frame procname="" line="6" stmtstart="232" sqlhandle="0x0300050098df89558d028500b59f00000100000000000000"/>
</executionStack>
<inputbuf> Proc [Database Id = 5 Object Id = 1435099032] THIS IS READ UPDATE PROCEDURE </inputbuf>
</process>
</process-list>
<resource-list>
<keylock hobtid="72057595257749504" dbid="5" objectname="" indexname="" id="lockee29f480" mode="X" associatedObjectId="72057595257749504">
<owner-list/>
<waiter-list>
<waiter id="process41fce08" mode="RangeS-S" requestType="wait"/>
</waiter-list>
</keylock>
<keylock hobtid="72057595257749504" dbid="5" objectname="" indexname="" id="lockee29f480" mode="X" associatedObjectId="72057595257749504">
<owner-list>
<owner id="process11eca3708" mode="X"/>
</owner-list>
<waiter-list>
<waiter id="process40c9048" mode="RangeS-S" requestType="wait"/>
</waiter-list>
</keylock>
<objectlock lockPartition="0" objid="1745427475" subresource="FULL" dbid="5" objectname="" id="lock128d27d80" mode="S" associatedObjectId="1745427475">
<owner-list>
<owner id="process41fce08" mode="S"/>
</owner-list>
<waiter-list>
<waiter id="process11eca3708" mode="IX" requestType="convert"/>
</waiter-list>
</objectlock>
</resource-list>
</deadlock>
查看死锁受害者进程 id = process41fce08,这是一个 select 存储过程,似乎 运行 在可序列化事务下。
但是,查看堆栈跟踪,未使用事务范围。没有创建显式事务。一个简单的 SqlConnection 和 SqlCommand 用于执行存储过程。
查看调用堆栈以在我的本地开发机器上执行相同的代码路径,这显示事务隔离级别 运行ning 在 ReadCommitted 下。使用以下方法检索此信息:
select
CASE transaction_isolation_level
WHEN 0 THEN 'Unspecified'
WHEN 1 THEN 'ReadUncommitted'
WHEN 2 THEN 'ReadCommitted'
WHEN 3 THEN 'Repeatable'
WHEN 4 THEN 'Serializable'
WHEN 5 THEN 'Snapshot'
ELSE 'Unknown'
END
FROM sys.dm_exec_sessions
where session_id = @@SPID
来自存储过程内部。如果我打电话:
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE
在存储过程中,我看到上面的隔离级别发生了变化select。这是我所期望的,也是我在开发环境中看到的:
Default Isolation Level in ADO.NET
鉴于:
- 调用堆栈中未设置 TransactionScope
- 存储过程内部没有设置隔离级别
似乎在实时环境中设置隔离级别?问题是什么设置它或强制它可序列化?
.NET 4 上的 MVC3 应用程序中的代码 运行s。
这似乎是连接池的问题。
我注意到在连接字符串中我们有以下内容:
Pooling=true; Min Pool Size=5; Max Pool Size=100; Connect Timeout=7
我创建了许多单元测试来证明这一点。
SQL 创建脚本
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
CREATE PROCEDURE [dbo].[usp_GetName]
(
@id int,
@TransactionIsolation varchar(30) output
)
AS
BEGIN
-- SET NOCOUNT ON added to prevent extra result sets from
-- interfering with SELECT statements.
SET NOCOUNT ON;
set @TransactionIsolation = dbo.fn_GetTransactionIsolation();
SELECT [Id]
,[Name]
,[Status]
FROM [dbo].[NameTable]
where Id = @id
END
CREATE FUNCTION [dbo].[fn_GetTransactionIsolation]
(
)
RETURNS varchar(30)
AS
BEGIN
-- Declare the return variable here
DECLARE @til varchar(30)
select @til =
CASE transaction_isolation_level
WHEN 0 THEN 'Unspecified'
WHEN 1 THEN 'ReadUncommitted'
WHEN 2 THEN 'ReadCommitted'
WHEN 3 THEN 'Repeatable'
WHEN 4 THEN 'Serializable'
WHEN 5 THEN 'Snapshot'
ELSE 'Unknown'
END
FROM sys.dm_exec_sessions
where session_id = @@SPID
-- Return the result of the function
RETURN @til
END
GO
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
CREATE TABLE [dbo].[NameTable](
[Id] [int] IDENTITY(1,1) NOT NULL,
[Name] [nvarchar](50) NOT NULL,
[Status] [int] NOT NULL,
CONSTRAINT [PK_NameTable] PRIMARY KEY CLUSTERED
(
[Id] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY]
GO
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
CREATE TABLE [dbo].[SPLog](
[Id] [int] IDENTITY(1,1) NOT NULL,
[StoredProcName] [nvarchar](50) NOT NULL,
[LogDate] [datetime] NOT NULL,
CONSTRAINT [PK_SPLog] PRIMARY KEY CLUSTERED
(
[Id] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY]
GO
单元测试
using System;
using System.Data;
using System.Data.SqlClient;
using System.Threading;
using System.Transactions;
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace TransactionTest
{
[TestClass]
public class UnitTest1
{
[TestMethod]
public void SPGet_NoTransaction_ShouldReturn_ReadCommitted()
{
using (var con = new SqlConnection(GetConnection()))
{
con.Open();
using (var cmd = new SqlCommand("usp_GetName", con))
{
cmd.CommandType = CommandType.StoredProcedure;
AddParameter(cmd, "@id", 1, SqlDbType.Int);
AddParameter(cmd, "@TransactionIsolation", 1, SqlDbType.VarChar, 30, ParameterDirection.Output);
SqlDataReader reader = cmd.ExecuteReader();
while (reader.Read())
{
}
reader.Close();
var transactionIsolation = (string)cmd.Parameters["@TransactionIsolation"].Value;
Assert.AreEqual("ReadCommitted", transactionIsolation);
}
}
}
[TestMethod]
public void SPUpdate_NoTransaction_ShouldReturn_ReadCommitted()
{
using (var con = new SqlConnection(GetConnection()))
{
con.Open();
using (var cmd = new SqlCommand("usp_UpdateName", con))
{
cmd.CommandType = CommandType.StoredProcedure;
AddParameter(cmd, "@id", 1, SqlDbType.Int);
AddParameter(cmd, "@name", "test update", SqlDbType.VarChar, 30);
AddParameter(cmd, "@status", 1, SqlDbType.Int);
AddParameter(cmd, "@TransactionIsolation", 1, SqlDbType.VarChar, 30, ParameterDirection.Output);
SqlDataReader reader = cmd.ExecuteReader();
while (reader.Read())
{
}
reader.Close();
var transactionIsolation = (string)cmd.Parameters["@TransactionIsolation"].Value;
Assert.AreEqual("ReadCommitted", transactionIsolation);
}
}
}
[TestMethod]
public void SPGet_TransactionSerializable_ShouldReturn_Serializable()
{
using (var tran = new TransactionScope(
TransactionScopeOption.Required,
new TransactionOptions { IsolationLevel = System.Transactions.IsolationLevel.Serializable }))
{
using (var con = new SqlConnection(GetConnection()))
{
con.Open();
using (var cmd = new SqlCommand("usp_GetName", con))
{
cmd.CommandType = CommandType.StoredProcedure;
AddParameter(cmd, "@id", 1, SqlDbType.Int);
AddParameter(cmd, "@TransactionIsolation", 1, SqlDbType.VarChar, 30, ParameterDirection.Output);
SqlDataReader reader = cmd.ExecuteReader();
while (reader.Read())
{
}
reader.Close();
var transactionIsolation = (string)cmd.Parameters["@TransactionIsolation"].Value;
Assert.AreEqual("Serializable", transactionIsolation);
}
}
tran.Complete();
}
}
private static string GetConnection()
{
var builder = new SqlConnectionStringBuilder();
builder.InitialCatalog = "ACMETransactions";
builder.DataSource = ".";
builder.IntegratedSecurity = true;
builder.Pooling = true;
builder.MaxPoolSize = 100;
return builder.ToString();
}
private static void AddParameter(SqlCommand command, string name, object value, SqlDbType type, int size = -1, ParameterDirection direction = ParameterDirection.Input)
{
command.Parameters.Add(new SqlParameter
{
ParameterName = name,
SqlDbType = type,
Value = value,
Direction = direction
});
if (size != -1)
{
command.Parameters[command.Parameters.Count - 1].Size = size;
}
}
[TestMethod]
public void SPGet__MultiThread_Conflict()
{
string serializedIsolationResult = "";
string normalIsolationResult = "";
var normalThread = new Thread(new ThreadStart(() =>
{
using (var con = new SqlConnection(GetConnection()))
{
con.Open();
using (var cmd = new SqlCommand("usp_GetName", con))
{
cmd.CommandType = CommandType.StoredProcedure;
AddParameter(cmd, "@id", 1, SqlDbType.Int);
AddParameter(cmd, "@TransactionIsolation", 1, SqlDbType.VarChar, 30, ParameterDirection.Output);
SqlDataReader reader = cmd.ExecuteReader();
while (reader.Read()) ;
reader.Close();
normalIsolationResult = (string)cmd.Parameters["@TransactionIsolation"].Value;
}
}
}));
var serializedThread = new Thread(new ThreadStart(() =>
{
using (var tran = new TransactionScope(
TransactionScopeOption.Required,
new TransactionOptions { IsolationLevel = System.Transactions.IsolationLevel.Serializable }))
{
using (var con = new SqlConnection(GetConnection()))
{
con.Open();
using (var cmd = new SqlCommand("usp_GetName", con))
{
cmd.CommandType = CommandType.StoredProcedure;
AddParameter(cmd, "@id", 1, SqlDbType.Int);
AddParameter(cmd, "@TransactionIsolation", 1, SqlDbType.VarChar, 30, ParameterDirection.Output);
SqlDataReader reader = cmd.ExecuteReader();
while (reader.Read()) ;
reader.Close();
serializedIsolationResult = (string)cmd.Parameters["@TransactionIsolation"].Value;
}
normalThread.Join();
Thread.Sleep(1000);
}
tran.Complete();
}
}));
serializedThread.Start();
normalThread.Start();
serializedThread.Join();
Assert.AreEqual("ReadCommitted", normalIsolationResult);
Assert.AreEqual("Serializable", serializedIsolationResult);
}
[TestMethod]
public void SPGet__MultiThread_NoTransactionScope()
{
string serializedIsolationResult = "";
string normalIsolationResult = "";
var normalThread = new Thread(new ThreadStart(() =>
{
using (var con = new SqlConnection(GetConnection()))
{
con.Open();
using (var cmd = new SqlCommand("usp_GetName", con))
{
cmd.CommandType = CommandType.StoredProcedure;
AddParameter(cmd, "@id", 1, SqlDbType.Int);
AddParameter(cmd, "@TransactionIsolation", 1, SqlDbType.VarChar, 30, ParameterDirection.Output);
SqlDataReader reader = cmd.ExecuteReader();
while (reader.Read()) ;
reader.Close();
normalIsolationResult = (string)cmd.Parameters["@TransactionIsolation"].Value;
}
}
}));
var serializedThread = new Thread(new ThreadStart(() =>
{
using (var con = new SqlConnection(GetConnection()))
{
con.Open();
using (var cmd = new SqlCommand("usp_GetName", con))
{
cmd.CommandType = CommandType.StoredProcedure;
AddParameter(cmd, "@id", 1, SqlDbType.Int);
AddParameter(cmd, "@TransactionIsolation", 1, SqlDbType.VarChar, 30, ParameterDirection.Output);
SqlDataReader reader = cmd.ExecuteReader();
while (reader.Read()) ;
reader.Close();
serializedIsolationResult = (string)cmd.Parameters["@TransactionIsolation"].Value;
}
}
}));
serializedThread.Start();
normalThread.Start();
serializedThread.Join();
Assert.AreEqual("ReadCommitted", normalIsolationResult);
Assert.AreEqual("ReadCommitted", serializedIsolationResult);
}
[TestMethod]
public void SPGet__MultiConnection()
{
string normalIsolationResult2 = "";
string normalIsolationResult1 = "";
using (var con = new SqlConnection(GetConnection()))
{
con.Open();
using (var cmd = new SqlCommand("usp_GetName", con))
{
cmd.CommandType = CommandType.StoredProcedure;
AddParameter(cmd, "@id", 1, SqlDbType.Int);
AddParameter(cmd, "@TransactionIsolation", 1, SqlDbType.VarChar, 30, ParameterDirection.Output);
SqlDataReader reader = cmd.ExecuteReader();
while (reader.Read()) ;
reader.Close();
normalIsolationResult1 = (string)cmd.Parameters["@TransactionIsolation"].Value;
}
}
using (var con = new SqlConnection(GetConnection()))
{
con.Open();
using (var cmd = new SqlCommand("usp_GetName", con))
{
cmd.CommandType = CommandType.StoredProcedure;
AddParameter(cmd, "@id", 1, SqlDbType.Int);
AddParameter(cmd, "@TransactionIsolation", 1, SqlDbType.VarChar, 30, ParameterDirection.Output);
SqlDataReader reader = cmd.ExecuteReader();
while (reader.Read()) ;
reader.Close();
normalIsolationResult2 = (string)cmd.Parameters["@TransactionIsolation"].Value;
}
}
Assert.AreEqual("ReadCommitted", normalIsolationResult1);
Assert.AreEqual("ReadCommitted", normalIsolationResult2);
}
[TestMethod]
public void SPGet__SingleConnection()
{
string normalIsolationResult2 = "";
string normalIsolationResult1 = "";
using (var con = new SqlConnection(GetConnection()))
{
con.Open();
using (var cmd = new SqlCommand("usp_GetName", con))
{
cmd.CommandType = CommandType.StoredProcedure;
AddParameter(cmd, "@id", 1, SqlDbType.Int);
AddParameter(cmd, "@TransactionIsolation", 1, SqlDbType.VarChar, 30, ParameterDirection.Output);
SqlDataReader reader = cmd.ExecuteReader();
while (reader.Read()) ;
reader.Close();
normalIsolationResult1 = (string)cmd.Parameters["@TransactionIsolation"].Value;
}
using (var cmd = new SqlCommand("usp_GetName", con))
{
cmd.CommandType = CommandType.StoredProcedure;
AddParameter(cmd, "@id", 1, SqlDbType.Int);
AddParameter(cmd, "@TransactionIsolation", 1, SqlDbType.VarChar, 30, ParameterDirection.Output);
SqlDataReader reader = cmd.ExecuteReader();
while (reader.Read()) ;
reader.Close();
normalIsolationResult2 = (string)cmd.Parameters["@TransactionIsolation"].Value;
}
}
Assert.AreEqual("ReadCommitted", normalIsolationResult1);
Assert.AreEqual("ReadCommitted", normalIsolationResult2);
}
}
}
只需更改 GetConnection 方法以删除 Pooling/MaxPoolSize 即可使测试每次都通过。
使用这些参数,一些测试将失败。
我假设在存在死锁的实时环境中,我们看到连接被重用,事务范围被设置为 Serializable,而代码在明确使用 TransactionScope 的地方执行任何更新。