为什么这个事务管理器不起作用?

时间:2012-07-24 08:46:39

标签: c# sql-server transactions transactionscope

我第一次玩交易我以为我会得到以下代码:

namespace database
{
    class Program
    {
        static string connString = "Server=ServerName;Database=Demo;Trusted_Connection=True;";
        SqlConnection connection = new SqlConnection(connString);
        static Random r = new Random();


        static void Add()
        {
            try
            {
                Thread.Sleep(r.Next(0, 10));
                using (var trans = new TransactionScope())
                {
                    using (var conn = new SqlConnection(connString))
                    {
                        conn.Open();

                        var count = (int)new SqlCommand("select balance from bank WITH (UPDLOCK) where owner like '%Jan%'", conn).ExecuteScalar();
                        Thread.Sleep(r.Next(0, 10));
                        SqlCommand cmd = new SqlCommand("update bank set balance = " + ++count + "where owner like '%Jan%'", conn);
                        cmd.ExecuteNonQuery();
                    }
                    trans.Complete();
                }
            }
            catch (Exception ex)
            {
                Console.WriteLine(ex.Message);
            }
        }

        static void Remove()
        {
            try
            {
                Thread.Sleep(r.Next(0, 10));
                using (var trans = new TransactionScope())
                {
                    using (var conn = new SqlConnection(connString))
                    {
                        conn.Open();

                        var count = (int)new SqlCommand("select balance from bank WITH (UPDLOCK) where owner like '%Jan%'", conn).ExecuteScalar();
                        Thread.Sleep(r.Next(0, 10));
                        SqlCommand cmd = new SqlCommand("update bank set balance = " + --count + "where owner like '%Jan%'", conn);
                        cmd.ExecuteNonQuery();

                    }
                    trans.Complete();
                }
            }
            catch (Exception ex)
            {
                Console.WriteLine(ex.Message);
            }
        }


        static void Main(string[] args)
        {
            for (int i = 0; i < 5; i++)
            {
                Thread t = new Thread(new ThreadStart(Add));
                t.Start();
            }
            for (int i = 0; i < 5; i++)
            {
                Thread t = new Thread(new ThreadStart(Remove));
                t.Start();
            }
            Console.ReadLine();
        }
    }
}

我认为在最后100次加法和100次减法后,我的balane将与我的起点相同 - 100,但每次运行脚本时它都会不断变化。即使使用isolationlevel可序列化。谁能告诉我为什么? O_O

编辑:将连接打开和关闭移动到事务范围内。 现在的问题是我得到“事务(进程ID XX)在锁资源上与另一个进程死锁,并被选为死锁牺牲品。重新运行事务”


像Marc Gravell说的那样: 将连接放在事务范围内并将UPDLOCK添加到select查询并结合将isolationlevel更改为repeatableRead就可以了:)

        static void Add()
        {
            try
            {
                Thread.Sleep(r.Next(0, 10));
                using (var trans = new TransactionScope(TransactionScopeOption.Required, new TransactionOptions() { IsolationLevel = IsolationLevel.RepeatableRead }))
                {
                    using (var conn = new SqlConnection(connString))
                    {
                        conn.Open();

                        var count = (int)new SqlCommand("select balance from bank WITH (UPDLOCK) where owner like '%Jan%'", conn).ExecuteScalar();
                        Thread.Sleep(r.Next(0, 10));
                        SqlCommand cmd = new SqlCommand("update bank set balance = " + ++count + "where owner like '%Jan%'", conn);
                        cmd.ExecuteNonQuery();
                    }
                    trans.Complete();
                }
            }
            catch (Exception ex)
            {
                Console.WriteLine(ex.Message);
            }
        }

2 个答案:

答案 0 :(得分:7)

1:目前TransactionScope可能是冗余且未使用的;尝试更改事务以包装连接,而不是相反(哦,并使用using):

using (var trans = new TransactionScope(TransactionScopeOption.Required,
      new TransactionOptions() { IsolationLevel = IsolationLevel.Serializable }))
using (var conn = new SqlConnection(connString))
{
    conn.Open();
    //...
    trans.Complete();
}

这样,连接应该在事务中正确登记(并且如果发生错误,可以正确清理)

我认为以上是主要问题;即没有参与交易。这意味着可能会丢失更改,因为读/写操作实际上并未提升到更高的隔离级别。

2:但是,如果你自己这样做,我希望你会看到死锁。要避免死锁,如果您知道要更新,可能需要在(UPDLOCK)上使用select - 这将在开始时采用写锁定,因此如果存在竞争线程,则会遇到阻塞而不是死锁。

要明确,此死锁情况是由以下原因引起的:

  • 线程A读取行,获取读锁定
  • 线程B读取行,获得读锁定
  • 线程A尝试更新该行,并被B
  • 阻止
  • 线程B尝试更新该行,并被A
  • 阻止

添加UPDLOCK,这将成为:

  • 线程A读取行,获得写锁定
  • 线程B尝试读取该行,并被A
  • 阻止
  • 主题A更新行
  • 主题A完成交易
  • 线程B能够继续,读取行,获得写锁定
  • 主题B更新行
  • 主题B完成交易

3:但查询做一个微不足道的更新是愚蠢的;更好的方法是在不选择的情况下发布就地更新,即update bank set balance = balance + 1 where ...

答案 1 :(得分:3)

您必须在<{1}}块中打开连接。

而不是

TransactionScope

像这样使用

var conn = new SqlConnection(connString);
conn.Open();
using (var trans = new TransactionScope(TransactionScopeOption.Required, new TransactionOptions() { IsolationLevel = IsolationLevel.Serializable }))
{ 
    // do stuff
}

这样打开连接会自动将其作为轻量级事务在using (var trans = new TransactionScope(TransactionScopeOption.Required, new TransactionOptions() { IsolationLevel = IsolationLevel.Serializable })) using (var conn = new SqlConnection(connString)) { conn.Open(); // do stuff } 中登记。

您始终可以查看examples