EntityFramework之異步、事務及性能優化(九)

前言

本文開始前我將按部就班先了解下實現EF中的異步,並將重點主要是放在EF中的事務以及性能優化上,但願經過此文可以幫助到你。html

異步

既然是異步咱們就得知道咱們知道在什麼狀況下須要使用異步編程,當等待一個比較耗時的操做時,能夠用異步來釋放當前的託管線程而無需等待,從而在管理線程中不須要花費額外的時間,也就是不會阻塞當前線程的運行。sql

在客戶端如:Windows Form以及WPF應用程序中,當執行異步操做時,則當前線程可以保持用戶界面持續響應。在服務器端如:ASP.NET應用程序中,執行異步操做能夠用來處理多個請求,能夠提升服務器的吞吐量等等。數據庫

在大部分應用程序中,對於比較耗時的操做用異步來實現可能會有一些改善,可是若你很少加考慮,動不動就用異步反而會獲得相反的效果以及對應用程序也是致命的。編程

鑑於上述描述,咱們接下來經過EF實現異步來加深理解。(想一想仍是把所用類及映射給出來,以避免沒看過前面的文章的同仁不知所云。)緩存

Student(學生)類:性能優化

    public class Student
    {
        public int Id { get; set; }

        public string Name { get; set; }

        public int FlowerId { get; set; }

        public virtual Flower Flower { get; set; }
    }

Flower(小紅花)類服務器

    public class Flower
    {
        public int Id { get; set; }

        public string Remark { get; set; }

        public virtual ICollection<Student> Students { get; set; }
    }

相關映射:併發

    public class StudentMap : EntityTypeConfiguration<Student>
    {
        public StudentMap()
        {
            ToTable("Student");
            HasKey(key => key.Id);
            HasRequired(p => p.Flower).WithMany(p => p.Students).HasForeignKey(p => p.FlowerId);

        }

    }


    public class FlowerMap:EntityTypeConfiguration<Flower>
    {
        public FlowerMap()
        {
            ToTable("Flower");
            HasKey(p => p.Id);
        }
    }

接下來咱們添加相關數據並實現異步:框架

        static async Task AsycOperation()
        {
            using (var ctx = new EntityDbContext())
            {

                ctx.Set<Student>().FirstOrDefault(d => d.Name == "xpy0928");


                Console.WriteLine("準備添加數據,當前線程Id爲{0}", Thread.CurrentThread.ManagedThreadId); (3)

                Thread.Sleep(3000);

                ctx.Set<Student>().Add(new Student()
                {
                    Flower = new Flower() { Remark = "so bad" },
                    Name = "xpy0928"
                });

                await ctx.SaveChangesAsync();

                Console.WriteLine("數據保存完成,當前線程Id爲{0}", Thread.CurrentThread.ManagedThreadId); (4)
            }
        }

接下來就是在控制檯進行調用以及輸出:異步

            Console.WriteLine("執行異步操做以前,當前線程Id爲{0}", Thread.CurrentThread.ManagedThreadId); (1) 
AsycOperation();

Console.WriteLine(
"執行異步操做後,當前線程Id爲{0}", Thread.CurrentThread.ManagedThreadId); (2)
Console.ReadKey();

這段代碼不難理解,基於咱們對於異步的理解,輸出順序應該是(1)(3)(2)(4),結果如咱們預期同樣,以下:

咱們知道await關鍵字的做用是:在線程池中新起一個將被執行的工做線程Task,當要執行IO操做時則會將工做線程歸還給線程池,所以await所在的方法不會被阻塞。當此任務完成後將會執行該關鍵字以後代碼

因此當執行到await關鍵字時,會在狀態機(async/await經過狀態機實現原理)中執行異步方法並等待執行結果,當異步執行完成後,此時再在線程池中新開一個Id爲11的工做線程,繼續await以後的代碼執行。此時要執行添加數據,因此此時將線程歸還給主線程,不阻塞主線程的運行因此就出現先執行(2)而不是先執行(4)。

接下來看一個稍微在上述基礎上通過改造的方法。以下:

        static async Task AsycOperation()
        {
            using (var ctx = new EntityDbContext())
            {

                ctx.Set<Student>().FirstOrDefault(d => d.Name == "xpy0928");

                Console.WriteLine("準備添加數據,當前線程Id爲{0}", Thread.CurrentThread.ManagedThreadId);

                Thread.Sleep(3000);

                ctx.Set<Student>().Add(new Student()
                {
                    Flower = new Flower() { Remark = "so bad" },
                    Name = "xpy09284"
                });

                await ctx.SaveChangesAsync();

                Console.WriteLine("數據保存完成,當前線程Id爲{0}", Thread.CurrentThread.ManagedThreadId);

                Console.WriteLine("開始執行查詢,當前線程Id爲{0}", Thread.CurrentThread.ManagedThreadId);

                var students = await (from stu in ctx.Set<Student>() select stu).ToListAsync();

                Console.WriteLine("遍歷得到全部學生的姓名,當前線程Id爲{0}", Thread.CurrentThread.ManagedThreadId);

                foreach (var stu in students)
                {
                    Console.WriteLine("學生姓名爲:{0}", stu.Name);
                }
            }
        }

接下來在控制檯中進行以下調用:

            Console.WriteLine("執行異步操做以前,當前線程Id爲{0}", Thread.CurrentThread.ManagedThreadId);
            
            var task = AsycOperation(); ;
            
            task.Wait();
            
            Console.WriteLine("執行異步操做後,當前線程Id爲{0}", Thread.CurrentThread.ManagedThreadId);
            
            Console.ReadKey();

接下咱們進行打印以下:

上述至於爲何不是執行【執行異步操做後,當前線程Id爲10】而後執行【遍歷得到全部學生的姓名,當前線程Id爲12】,想必你們能清楚的明白,是執行上述 task.Wait() 的緣故,必須進行等待當前任務執行完再執行主線程後面的輸出。

對於處理EF中的異步沒有太多去探索的東西,基本就是調用EF中對應的異步方法便可,重點是EF中的事務,請繼續往下看:

*事務 

默認狀況下

  • 可能咱們不曾注意到,其實在EF的全部版本中,當咱們調用SaveChanges方法來執行增、刪、改時其操做內部都用一個transaction包裹着。不信,以下圖,當添加數據時:

  • 對於上下文中的 ExecuteSqlCommand() 方法默認狀況下也是用transaction包裹着命令(Command),其有重載咱們能夠顯示指定執行事務仍是不肯定執行事務。
  • 在此上兩種狀況下,事務的隔離級別是數據庫提供者認爲的默認設置的任何隔離級別,例如在SQL Server上默認是READ COMMITED(讀提交)。
  • EF對於任何查詢都不會用transaction來進行包裹。

在EF 6.0版本以上,EF一直保持數據庫鏈接打開,由於要啓動一個transaction必須是在數據庫鏈接打開的前提下,同時這也就意味着咱們執行多個操做在一個transaction的惟一方式是要麼使用 TransactionScope 要麼使用 ObjectContext.Connection 屬性而且啓動調用Open()方法以及BeginTransaction()方法直接返回EntityConnection對象。若是你在底層數據庫鏈接上啓動了transaction,再調用API鏈接數據庫可能會失敗。

概念

在開始學習事務以前咱們先了解兩個概念:

  • Database.BeginTransaction():它是在一個已存在的DbContext上下文中對於咱們去啓動和完成transactions的一種簡單方式,它容許多個操做組合存在在相同的transaction中,因此要麼提交要麼所有做爲一體回滾,同時它也容許咱們更加容易的去顯示指定transaction的隔離級別。
  • Dtabase.UseTransaction():它容許DbContext上下文使用一個在EF實體框架以外啓動的transaction。

在相同上下文中組合幾個操做到一個transaction 

Database.BeginTransaction有兩種重載——一種是顯示指定隔離級別,一種是無參數使用來自於底層數據庫提供的默認隔離級別,兩種都是返回一個DbContextTransaction對象,該對象提供了事務提交(Commint)以及回滾(RollBack)方法直接表如今底層數據庫上的事務提交以及事務回滾上。

DbContextTransaction一旦被提交或者回滾就會被Disposed,因此咱們使用它的簡單的方式就是使用using(){}語法,當using構造塊完成時會自動調用Dispose()方法。

根據上述咱們如今經過兩個步驟來對學生進行操做,並在同一transaction上提交。以下:

            using (var ctx = new EntityDbContext())
            {

                using (var ctxTransaction = ctx.Database.BeginTransaction())
                {

                    try
                    {
                        ctx.Database.Log = Console.WriteLine;

                        ctx.Database.ExecuteSqlCommand("update student set name='xpy0929'");

                        var list = ctx.Set<Student>().Where(p => p.Name == "xpy0929").ToList();

                        list.ForEach(d =>
                        {

                            d.Name = "xpy0928";

                        });

                        ctx.SaveChanges();

                        ctxTransaction.Commit();
                    }
                    catch (Exception)
                    {
                        ctxTransaction.Rollback();
                    }

                }
            }

咱們經過控制檯輸出SQL日誌查看提交事務成功以下:

【注意】 要開始一個事務必須保持底層數據庫鏈接是打開的,若是數據庫不老是打開的咱們能夠經過 BeginTransaction() 方法將打開數據庫鏈接,若是 DbContextTransaction 打開了數據庫,當調用Disposed()方法時將會關閉數據庫鏈接。

注意事項

當用EF上下文中的 Database.ExecuteSqlCommand 方法來對數據庫進行以下操做時

            using (var ctx = new EntityDbContext())
            {
             
                var sqlCommand = String.Format("ALTER DATABASE {0} SET SINGLE_USER WITH ROLLBACK IMMEDIATE", "DBConnectionString");
                ctx.Database.ExecuteSqlCommand(sqlCommand);
               
            }

此時將會報錯以下:

上述已經講過此方法會被Transaction包裹着,因此致使出錯,可是此方法有重載,咱們進行以下設置便可

 ctx.Database.ExecuteSqlCommand(TransactionalBehavior.DoNotEnsureTransaction,sqlCommand);

將一個已存在的事務添加到上下文中

有時候咱們可能須要事務的做用域更加廣一點,固然是在同一數據庫上可是是在EF以外徹底進行操做。基於此,此時咱們必須手動打開底層的數據庫鏈接來啓動事務,同時通知EF使用咱們手動打開的鏈接來使現有的事務鏈接在此鏈接上,這樣就達到了在EF以外使用事務的目的。

爲了實現上述在EF以外使用事務咱們必須在DbContext上下文中的派生類的構造器中關閉自身的鏈接而使用咱們傳入的鏈接。

第一步

上下文中關閉EF鏈接使用底層鏈接。

代碼以下:

  public EntityDbContext(DbConnection con)
            : base(con, contextOwnsConnection: false)
        { }

第二步

啓動Transcation(若是咱們想避免默認設置咱們能夠手動設置隔離級別),通知EF一個已存在的Transaction已經在咱們手動的設置的底層鏈接上啓動。

            using (var con = new SqlConnection("ConnectionString"))
            {
                using (var SqlTransaction = con.BeginTransaction())
                {
                      using (var ctx = new EntityDbContext(con))
                      {
} } }

第三步

由於此時是在EF實體框架外部執行事務,此時則須要用到上述所講的 Database.UseTransaction 將咱們的事務對象傳遞進去。

 ctx.Database.UseTransaction(SqlTransaction);

此時咱們將能經過SqlConnection實例來自由執行數據庫操做或者說是在上下文中,執行的全部操做都是在一個Transaction上,而咱們只負責提交和回滾事務並調用Dispose方法以及關閉數據庫鏈接便可。

至此給出完整代碼以下:

            using (var con = new SqlConnection("ConnectionString"))
            {
con.Open();
using (var SqlTransaction = con.BeginTransaction()) { try { var sqlCommand = new SqlCommand(); sqlCommand.Connection = con; sqlCommand.Transaction = SqlTransaction; sqlCommand.CommandText = @"update student set name = 'xpy0929'"; sqlCommand.ExecuteNonQuery(); using (var ctx = new EntityDbContext(con)) { ctx.Database.UseTransaction(SqlTransaction); var list = ctx.Set<Student>().Where(d => d.Name == "xpy0929").ToList(); list.ForEach(d => { d.Name = "xpy0928"; }); ctx.SaveChanges(); } SqlTransaction.Commit(); } catch (Exception) { SqlTransaction.Rollback(); } } }

【注意】你能夠設置  ctx.Database.UseTransaction(null); 爲空來清除當前EF中的事務,若是你這樣作了,那麼此時EF既不會提交事務也不會回滾現有的事務,除非你清楚這是你想作的 ,不然請謹慎使用。

TransactionScope Transactions

在msdn上對 TransactionScope 類定義爲是:類中的代碼稱爲事務性代碼。

咱們將上述代碼包含在以下代碼中,則此做用域裏的代碼爲事務性代碼

            using ( var scope = new TransactionScope(TransactionScopeOption.Required))
            {
                
            }

【注意】此時SqlConnection和EF實體框架都使用 TransactionScope  ,所以此時將被會一塊兒提交。

 在.NET 4.5.1中 TransactionScope  可以和異步方法一塊兒使用經過TransactionScopeAsyncFlowOption的枚舉來啓動。

經過以下實現:

using (var scope = new TransactionScope(TransactionScopeAsyncFlowOption.Enabled)) 
{}

接着就是將數據庫鏈接的打開方法(Open)、查詢方法(ExecuteNonQuery)、以及上下文中保存的方法(SaveChanges)都換爲對應的異步方法(OpenAsync)、(ExecuteNonQueryAsync)以及(SaveChangesAsync)便可

使用TransactionScope異步有幾點限制,例如上述的必須是在.NET 4.5.1中才有異步方法等等。 

在EF應用程序中避免死鎖建議

事務隔離級別

咱們知道在查詢上是沒有transaction的,EF只有在SaveChanges上的本地transaction(除非外界系統的transaction即System.Transaction被檢測到,在此種狀況下才會被用到)。

在SQL Server上默認的隔離級別是READ COMMITTED,而且READ COMMITED默認狀況下是共享鎖的,儘管當每條語句完成時鎖會釋放可是這種狀況下仍是極易致使鎖爭用。 那是可能的咱們配置數據庫經過設置 READ_COMMITTED_SNAPSHOT  的選項爲ON來避免徹底讀取甚至是READ COMMITTED隔離級別上。SQL Servert採起了Row Version以及Snapshot(Snapshot和Row Version以及Set Transaction Isolation Level)而不是共享鎖的方式來提供了一樣的保障爲READ COMMITED隔離。

Snapshot Isolation Level(從字面意思將其理解爲快照式隔離級別)

因爲本人對隔離級別中最熟悉的是 READ_UNCOMMITED 、 READ_COMMITED 、 REPEATABLE_READ 以及 SERIALIZABLE ,而對此Snapshot隔離級別不太熟悉,就詳細敘述下,以備忘。

  • 在SQL Server 2005版本中引入此隔離級別,此隔離級別依賴於加強行版本(Row Version)旨在經過避免讀寫阻塞來提升性能,經過非阻塞行爲來顯著下降復瑣事務死鎖的可能性。

  • 啓動該隔離級別將激活臨時數據庫上的臨時表存儲Row Version(行版本)的機制,此時臨時表將更新每一個行版本,用事務序列號來標識每一個事務,同時每一個行版本的序列號也將被記錄下來,此隔離級別的事務適用於在此事務序列號以前有一個序列號的最新行版本,在事務已經開始後建立的新的行版本會被事務所忽略。

  • 該隔離級別使用樂觀併發模式,若是一個Snapshot事務試圖提交已經發生了修改的數據,由於此時事務已經啓動,因此事務將會回滾並拋出一個錯誤。

  • 在事務開始時,在事務中指定要讀取的數據與已存在的數據是事務一致性版本,該事務只知道在該事務啓動以前被提交的修改的數據而經過其餘事務執行當前事務語句對數據作出的更改在當前事務啓動以後是不可見的。這個做用就是好像事務中的語句得到了已經提交數據的快照,由於它存在於事務的開始。

  • 當一個數據庫正在恢復時,當Snapshot事務讀取數據時不會要求鎖定。Snapshot事務不會阻塞其餘事務對其執行寫的操做,同時事務也不會阻塞Snapshot對其指定讀的操做。

  • 在啓動一個事務爲Snapshot隔離級別時以前必須將ALLOW_SNAPSHOT_ISOLATION設置爲ON,當使用Snapshot隔離級別在不一樣數據庫間訪問數據必須保證每一個數據庫上的ALLOW_SNAPSHOT_ISOLATION爲ON。

考慮到SQL Server的默認值以及EF的相關行爲,大部分狀況下每一個EF執行查詢是在它本身被自動調用以及SaveChanges運行在用READ COMMITED隔離的本地事務裏。也就是說EF被設計的能很好和System.Transactions.Transaction一塊兒工做。對於 System.Transactions.Transaction 的默認隔離級別是 SERIALIZABLE ,咱們知道此隔離級別是最嚴格的隔離級別能同時解決髒讀、不可重複讀以及幻影讀取的問題,固然這也就意味着默認狀況下使用 TransactionScope  或者 CommitableTransaction 的話,咱們應該選擇最爲嚴格的隔離級別,同時裏面也要添加許多鎖。

可是幸運的是,這種默認的狀況咱們能垂手可得的進行覆蓋, 例如,爲了配置Snapshot,咱們能夠經過使用TransactionSope來實現。

     using (var scope = new TransactionScope(TransactionScopeOption.Required, new TransactionOptions { IsolationLevel = IsolationLevel.Snapshot }))
     {

        //Do  Something
        scope.Complete();
     }

上述建議經過封裝此構造的方法來簡化使用。 

建議 

鑑於上述對快照式隔離級別(Snapshot Isolation Level)以及EF相關描述,咱們能夠將避免EF應用程序死鎖歸結於如下:

  • 使用快照式事務隔離級別(Snapshot Transaction Isolation Level)或者快照式 Read Committed(Snapshot Read Committed)同時也推薦利用TransactionScope來使用事務。經過使用以下代碼:

     using (var scope = new TransactionScope(TransactionScopeOption.Required, new TransactionOptions { IsolationLevel = IsolationLevel.Snapshot }))
     {

        //You need to do something
        scope.Complete();
     }
  • 固然EF版本更高更好。

  • 當在Transaction裏查詢相同的表時,儘可能使用相同的順序。

 性能優化

貌似寫EF系列以來咱們從未談論過一個東西,而這個東西卻一直是咱們關注的,那就是緩存,難道在EF中沒有緩存嗎,答案是否認的,至少我的以爲在此篇文章談論緩存仍是比較合適宜,由於與事務有關,再加上這原本就是一個須要深刻去學習的地方,因此不能妄自菲薄,如有不妥之處,請指正。

咱們談論的是二級緩存,經過二級緩存來提升查詢性能,因此一語道破天機二級緩存就是一個查詢緩存,經過SQL命令將查詢的結果存儲在緩存中,以致於當咱們下次執行相同的命令時會去緩存中去拿數據而不是一遍又一遍的執行底層的查詢,這將對咱們的應用程序有一個性能上的提高同時也減小了對數據庫的負擔,固然這也就形成了對內存的佔用。

EF 6.1二級緩存

接下來咱們進入實戰,咱們依然借用【異步】中的兩個類來進行查詢。經過以下代碼咱們來進行查詢:

            using (var ctx = new EntityDbContext())
            {
                ctx.Set<Student>().FirstOrDefault(d => d.Name == "xpy0928");
            }

咱們同時刷新一次,此時咱們經過Sql  Profiler進行監控,毫無疑問此時會執行兩次查詢對於相同的查詢

接下來咱們經過EF來實現二級緩存試試看,首先咱們添加的在EF 6.1中沒有二級緩存,此時咱們須要經過NuGet手動安裝最新版本的二級緩存以下:

EF對於相關的配置是在 DbConfiguraion 中,因此確定是在此配置中的構造函數中進行。經過如下步驟進行:

第一步

首先要得到二級緩存實例,以下:

 var transactionHandler = new CacheTransactionHandler(new InMemoryCache());

第二步

由於是對於查詢結果的緩存因此咱們將其註冊到監聽,以下:

 AddInterceptor(transactionHandler);

第三步

由於其緩存服務確定是在在EF初始化過程當中進行加載,也就是將緩存服務添加到DbConfiguration中的Loaded事件中便可。以下:

            Loaded +=
             (sender, args) => args.ReplaceService<DbProviderServices>(
             (s, _) => new CachingProviderServices(s, transactionHandler,
              cachingPolicy));

以上是咱們整個實現二級緩存的大概思路,完整代碼以下【參考官方Second Level Cace for EntityFramework

    public class EFConfiguration : DbConfiguration
    {
        public EFConfiguration()
        {
            var transactionHandler = new CacheTransactionHandler(new InMemoryCache());

            AddInterceptor(transactionHandler);

            var cachingPolicy = new CachingPolicy();

            Loaded +=
             (sender, args) => args.ReplaceService<DbProviderServices>(
             (s, _) => new CachingProviderServices(s, transactionHandler,
              cachingPolicy));

        }

    }

此時咱們再來執行上述查詢並多刷新幾回,此時將執行一次查詢,說明是在緩存中獲取數據,因此二級緩存設置成功,以下:

感謝

關於EF 6.0或者6.1大概就已介紹完,固然裏面可能還有許多更深層次的知識未涉及到,可是本人也就只有這點能力了,作不到面面俱到,望諒解!很是感謝一直以來對我EF系列支持的大家,正是有大家的支持我纔會很仔細的一字一句的去斟酌,以避免誤導了別人,因此纔會更加的謹慎的去敘述,同時也感謝對這一系列中有不妥之處或是錯處做出指正的園友們,正是有了大家的支持,使我才能更好的學習且收穫更多!

 

敬請期待Entity Framework 7.0。。。。。。

相關文章
相關標籤/搜索