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

鉴于:

  1. 调用堆栈中未设置 TransactionScope
  2. 存储过程内部没有设置隔离级别

似乎在实时环境中设置隔离级别?问题是什么设置它或强制它可序列化?

.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 的地方执行任何更新。