快捷搜索:

数据库并发一致性案例分析

本文示例源代码或素材下载

本部分内容为《数据库道理》课程中的一个讲堂案例,幻灯片供给的动画演示有助于理解并发节制的本色,本文内容为幻灯片的择要。

假如你对自己并发节制的能力很有自大的话,读完“一、问题提出”后直接可以跳转到“四、看来问题真不简单”处涉猎。

本文着末给出了部分测试用代码的简单解说。

一、问题提出

设某银行存款帐户数据如下表:

现在要求编写一法度榜样,完成两项功能:存款与取款。每次操作完成后向明细表中插入一行记录并更新帐户余额。

二、问题彷佛很简单

办理法子:

① 读取着末一行记录的帐户余额数据

② 根据存、取款金额谋略出新的帐户余额

③ 将新的记录插入表中

真的这么简单?

在不斟酌并发问题的环境下是可行的

假如斟酌并发,问题就多了(导致余额谋略差错!请参考幻灯片与案例代码)

三、让我来想一想

既然存在并发问题,那么办理并发问题的最好法子便是加锁呀!着手试试~~

怎么加锁?加什么锁?

读之前加共享锁?不可!(参考幻灯片)

读之前加排它锁?照样不可!(参考幻灯片)

当然,问题还不止这些!若何读取着末一行记录?你会发明跟着明细记录的增添越来越没效率。

四、看来问题真的不是这么简单

问题出在哪里那?从系统设计一开始我们就走错了!从新设计!

为什么引入冗余数据?

确保帐户余额在独一的地方进行存储

避免了读取帐户余额时造访大年夜量数据并排序

新的问题:

我们无法直接对数据库进行锁操作

必须经由过程合理的事务隔离级别完成并发节制(ReadUnCommitted、ReadCommitted、RepeatableRead、Serializable),哪一种好呢?

五、发急吃不着热豆腐

看来我们必须对各事务隔离级别一一阐发

① ReadUnCommitted

显然不可

在这个事务隔离级别下连脏数据都可能读到,何况“脏”帐户余额数据。

② ReadCommitted

也不可

该隔离级别与二级封锁协议相对应。读数据前加共享锁,读完就开释。前面阐发过,此处不再赘述。

③ RepeatableRead

这个隔离级别对照迷惑人,必要仔细阐发:

RepeatableRead对应第三级封锁协议:读前加共享锁,事务完成才开释。

(历程参考幻灯片,结论:可以避免并发问题,但带来了逝世锁!)

④ Serializable

该事务隔离级别在履行时可以避免幻影读。

但对付本案例履行效果与RepeatableRead一样(效率低下,成功率低,还有憎恶的逝世锁!)。

彷佛走到了绝路

颠末从新设计后仍旧无法让人知足的办理问题!连最高隔离级别都邑在高度并发时由于逝世锁造成很大年夜一部分事务履行掉败!

六、绝处逢生

缘故原由阐发

逝世锁的缘故原由是由于读前加S锁,而写前要将S锁提升为X锁,因为S锁容许共享,导致X锁提升掉败,发存亡锁。

办理法子

假如在读时就加上X锁,就可避免上述问题(从封锁协议角度这彷佛弗成能,但确完全可行!)

着实SQL Server容许在一条敕令中同时完成读、写操作,这就为我们供给了入手点。

1、将存、取款用到的数据经由过程收集发给存储历程。

2、数据加锁、改动、解锁。

3、将结果经由过程收集回传。

将收集延迟放到了事务之外,前进了事务效率。

实验结果

因为在同一台机械上履行数据库与利用法度榜样,实验结果注解存储历程的履行效率不如直接在利用法度榜样中经由过程敕令调用高。

假如能在一个带宽受到限定的收集上将数据库与利用法度榜样分离,然后测试,信托会有令人知足的结果。(有待详细实验证明)

八、思虑

近来园子里面关于O/R Mapping评论争论得很猛烈,想问大年夜家一个问题,便是对付上述问题,O/R Mapping是否供给了办理法子,容许在Mapping的同时加倍精细的节制更新手段呢?

附:代码阐发

本文测试用代码共有5个项目,分手是:

1、SimpleUpdate(最简单的更新,在没有并发时事情得很好)

2、SimpleUpdateInMultiThread(引入并发,10个线程同时事情,结果上面的更新策略呈现了问题)

3、RepeatableReadUpdate(本文第五部分中,应用RepealableRead事务隔离级其余并发更新,随没有差错,但导致了逝世锁)

4、AnotherMethod(本文着末给出的更新要领,高效且没有逝世锁)

5、UseStoredProcedure(应用存储历程完成更新)创建存储历程的代码可以从DataBase目录下找到。

筹备事情

首先在SQL Server 2005中建立一空数据库DBApp,法度榜样履行时会自动在此数据库中创建所必要的表以及记录。

1、SimpleUpdate

public void Operation(double amount)

{

SqlConnection conn = new SqlConnection(ConfigurationManager.AppSettings.Get("ConnectionString"));

SqlCommand cmd = new SqlCommand();

cmd.Connection = conn;

conn.Open();

cmd.CommandText = "SELECT TOP 1 Balance FROM AccountDetail WHERE AccountID = 1 ORDER BY AccountDetailID DESC";

double oldBalance = Convert.ToDouble(cmd.ExecuteScalar());

double newBalance = oldBalance + amount;

cmd.CommandText = "INSERT INTO AccountDetail (AccountID, Amount, Balance) VALUES (1, " +

amount.ToString() + ", " + newBalance.ToString() + ")";

cmd.ExecuteNonQuery();

conn.Close();

}

这段代码没有斟酌任何并发问题,也没有应用事务,仅仅是读取着末一笔记录的余额数据,然后根据余额和存取钱金额算出最新余额,并将数据插入到明细记录中。在没有并发问题时,该法度榜样可以很好的履行。调用该段代码的主法度榜样如下:

该段代码引入了事务,并将事务隔离级别设置为RepeatableRead,法度榜样颠末漫长的履行后,你会发明只管没有呈现任何余额谋略差错,但10个线程中仅有一半阁下履行成功,其它线程履行掉败,这是因为内部逝世锁问题造成的。感兴趣的话可以查看SQL Server中锁的状态。

public void Operation()

{

SqlConnection conn = new SqlConnection(ConfigurationManager.AppSettings.Get("ConnectionString"));

SqlCommand cmd1 = new SqlCommand();

SqlCommand cmd2 = new SqlCommand();

SqlCommand cmd3 = new SqlCommand();

cmd1.Connection = conn;

cmd2.Connection = conn;

cmd3.Connection = conn;

conn.Open();

SqlTransaction tx = conn.BeginTransaction(IsolationLevel.RepeatableRead);

try

{

cmd1.CommandText = "SELECT Balance FROM Account WHERE AccountID = 1";

cmd1.Transaction = tx;

double oldBalance = double.Parse(cmd1.ExecuteScalar().ToString());

double newBalance = oldBalance + amount;

//为了表示随机性,先随机苏息一段光阴。

Thread.Sleep(rand.Next(500));

cmd2.CommandText = "INSERT INTO AccountDetail (AccountID, Amount, Balance) VALUES (1, " +

amount.ToString() + ", " + newBalance.ToString() + ")";

cmd2.Transaction = tx;

cmd2.ExecuteNonQuery();

cmd3.CommandText = "UPDATE Account SET Balance = " + newBalance.ToString() + " WHERE AccountID=1";

cmd3.Transaction = tx;

cmd3.ExecuteNonQuery();

tx.Commit();

}

catch

{

tx.Rollback();

throw new Exception("Transaction Error!");

}

conn.Close();

}

4、AnotherMethod

您可能还会对下面的文章感兴趣: