【问题标题】:SQL Transaction Isolation Level Serializable vs Read Committed in Dev vs ProductionSQL 事务隔离级别 Serializable vs Read Committed in Dev vs Production
【发布时间】:2015-10-05 10:40:34
【问题描述】:

我目前正在寻找我们遇到的死锁和 SQL Server 问题。发生了相当多的死锁。以下死锁图是我关注的(来自现场环境):

<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,这是一个选择存储过程,似乎在 Serializable 事务下运行。

但是,查看堆栈跟踪,未使用事务范围。没有创建显式事务。一个简单的 SqlConnection 和 SqlCommand 用于执行存储过程。

查看调用堆栈以在我的本地开发机器上执行相同的代码路径,这显示了在 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

在存储过程中,我在上面的选择中看到了隔离级别的变化。这是我所期望的,也是我在开发环境中看到的:

Default Isolation Level in ADO.NET

所以考虑到:

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

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

代码在 .NET 4 上的 MVC3 应用程序中运行。

【问题讨论】:

    标签: sql sql-server ado.net transactionscope distributed-transactions


    【解决方案1】:

    这似乎取决于连接池。

    我注意到在连接字符串中我们有以下内容:

    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 的更新。

    【讨论】:

      猜你喜欢
      • 2019-06-18
      • 2010-11-30
      • 2014-09-18
      • 2022-09-23
      • 1970-01-01
      • 1970-01-01
      • 2011-09-30
      • 2012-07-04
      • 2011-06-28
      相关资源
      最近更新 更多