本文開始前我將按部就班先了解下實現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 6.0版本以上,EF一直保持數據庫鏈接打開,由於要啓動一個transaction必須是在數據庫鏈接打開的前提下,同時這也就意味着咱們執行多個操做在一個transaction的惟一方式是要麼使用 TransactionScope 要麼使用 ObjectContext.Connection 屬性而且啓動調用Open()方法以及BeginTransaction()方法直接返回EntityConnection對象。若是你在底層數據庫鏈接上啓動了transaction,再調用API鏈接數據庫可能會失敗。
在開始學習事務以前咱們先了解兩個概念:
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上下文中的派生類的構造器中關閉自身的鏈接而使用咱們傳入的鏈接。
代碼以下:
public EntityDbContext(DbConnection con) : base(con, contextOwnsConnection: false) { }
using (var con = new SqlConnection("ConnectionString")) { using (var SqlTransaction = con.BeginTransaction()) { using (var ctx = new EntityDbContext(con)) {
} } }
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既不會提交事務也不會回滾現有的事務,除非你清楚這是你想作的 ,不然請謹慎使用。
在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中才有異步方法等等。
咱們知道在查詢上是沒有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隔離。
因爲本人對隔離級別中最熟悉的是 READ_UNCOMMITED 、 READ_COMMITED 、 REPEATABLE_READ 以及 SERIALIZABLE ,而對此Snapshot隔離級別不太熟悉,就詳細敘述下,以備忘。
考慮到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應用程序死鎖歸結於如下:
using (var scope = new TransactionScope(TransactionScopeOption.Required, new TransactionOptions { IsolationLevel = IsolationLevel.Snapshot })) { //You need to do something scope.Complete(); }
貌似寫EF系列以來咱們從未談論過一個東西,而這個東西卻一直是咱們關注的,那就是緩存,難道在EF中沒有緩存嗎,答案是否認的,至少我的以爲在此篇文章談論緩存仍是比較合適宜,由於與事務有關,再加上這原本就是一個須要深刻去學習的地方,因此不能妄自菲薄,如有不妥之處,請指正。
咱們談論的是二級緩存,經過二級緩存來提升查詢性能,因此一語道破天機二級緩存就是一個查詢緩存,經過SQL命令將查詢的結果存儲在緩存中,以致於當咱們下次執行相同的命令時會去緩存中去拿數據而不是一遍又一遍的執行底層的查詢,這將對咱們的應用程序有一個性能上的提高同時也減小了對數據庫的負擔,固然這也就形成了對內存的佔用。
接下來咱們進入實戰,咱們依然借用【異步】中的兩個類來進行查詢。經過以下代碼咱們來進行查詢:
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系列支持的大家,正是有大家的支持我纔會很仔細的一字一句的去斟酌,以避免誤導了別人,因此纔會更加的謹慎的去敘述,同時也感謝對這一系列中有不妥之處或是錯處做出指正的園友們,正是有了大家的支持,使我才能更好的學習且收穫更多!