原本打算寫ASP.NET Core MVC基礎系列內容,看到有園友提出如何實現讀寫分離,這個問題提的好,大多數狀況下,對於園友在評論中提出的問題,若是是值得深究或者大多數同行比較關注的問題我都會私下去看看,而後進行對應解答,如有敘述不當之處,還請海涵。咱們稍微過一下事務,本文略長,請耐心閱讀。數據庫
什麼是事務呢?有關事務詳解可參看我寫的SQL Server基礎系列,咱們可歸結爲一句話:多個提交要麼所有成功,要麼所有失敗即同生共死,沒有臨陣脫逃者。那麼問題來了,用了事務有什麼做用或者說有什麼優勢呢?事務容許咱們將相關操做組合打包,以確保應用程序數據的一致性。那麼使用事務又有何缺點呢?使用事務雖然確保了數據一致性等等,可是會影響性能,可能會形成死鎖。那麼問題又來了,既然有其優缺點,那麼咱們是否能夠手寫邏輯實現數據一致性呢?固然能夠,咱們能夠模擬事務回滾、提交的效果,可是這也沒法百分百保證。架構
首先咱們在控制檯中進行以下數據添加,而後添加日誌打印。負載均衡
using (var context = new EFCoreDbContext()) { var blog = new Blog() { IsDeleted = false, CreatedTime = DateTime.Now, ModifiedTime = DateTime.Now, Name = "demo", Url = "http://www.cnblogs.com/createmyslef" }; context.Add(blog); context.SaveChanges(); }
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { var loggerFactory = new LoggerFactory(); loggerFactory.AddConsole(LogLevel.Debug); optionsBuilder.UseLoggerFactory(loggerFactory); optionsBuilder.UseSqlServer("data source=WANGPENG;User Id=sa;Pwd=sa123;initial catalog=Demo1;integrated security=True;"); }
咱們經過打印日誌得知在調用SaveChanges方法時則包含在事務中進行提交,因此請那些可在項目中用到多表添加擔憂出現問題就加上了以下開啓事務,這很顯然是畫蛇添足。分佈式
using (var context = new EFCoreDbContext()) { using (var transaction = context.Database.BeginTransaction()) { var blog = new Blog() { IsDeleted = false, CreatedTime = DateTime.Now, ModifiedTime = DateTime.Now, Name = "demo", Url = "http://www.cnblogs.com/createmyslef" }; context.Add(blog); context.SaveChanges(); try { transaction.Commit(); } catch (Exception) { //TODO } } }
看到如上日誌信息還不是更加肯定是否是,咱們再來看看在上下文中的 context.Database.AutoTransactionsEnabled 方法,詳細解釋以下:ide
// 摘要: // Gets or sets a value indicating whether or not a transaction will be created // automatically by Microsoft.EntityFrameworkCore.DbContext.SaveChanges if none // of the 'BeginTransaction' or 'UseTransaction' methods have been called. // Setting this value to false will also disable the Microsoft.EntityFrameworkCore.Storage.IExecutionStrategy // for Microsoft.EntityFrameworkCore.DbContext.SaveChanges // The default value is true, meaning that SaveChanges will always use a transaction // when saving changes. // Setting this value to false should only be done with caution since the database // could be left in a corrupted state if SaveChanges fails.
經過AutoTransactionsEnabled方法解釋得知:其默認值爲True,也就意味着當調用SaveChanges方法將使用事務性提交。固然咱們能夠在上下文構造函數中設置是否全局禁用事務,以下:函數
public class EFCoreDbContext : DbContext { public EFCoreDbContext() { Database.AutoTransactionsEnabled = false; } }
在EF Core中咱們何時會用到事務呢?若是是單一上下文,單一數據庫,那麼事務跟咱們沒啥關係,壓根不用管事務。若是是在單一數據庫使用多個上下文(跨上下文)或者多個數據庫,這個時候事務就閃亮登場了。好比對於電商中的商品、購物車、訂單管理、支付、物流,咱們徹底能夠實例化五個不一樣的上下文,此時將涉及到跨上下文操做使用事務保持數據一致性,固然這是針對在同一關係數據庫中。或者是實例化同一上下文屢次來使用事務保持數據一致性。能夠參看官網的介紹《https://docs.microsoft.com/en-us/ef/core/saving/transactions》,沒什麼看頭,都是針對同一數據庫操做,無非仍是我所說的跨上下文、使用上下文結合底層DbConnection來使用事務共享鏈接等等 ,稍微大一點的看點則是在EF Core 2.1中引入了System.Transactions,可指定隔離級別以及使用ambient transactions(查資料做用是存在多個事務,事務之間存在鏈接,如此一來將顯得整個做用域很是冗長,經過使用此事務則在特定範圍內,全部鏈接都將包含在該事務中),在此就不佔用篇幅介紹了,和你們同樣咱們最關心的是分佈式事務,也就是使用不一樣上下文針對多個數據庫,可是遺憾的是直到EF Core 2.1還不支持分佈式事務,由於.NET Core中相關APi也還不完善,繼續等待吧。性能
隨着流量的進入,數據庫將承受不可抗拒的壓力,單一數據庫將再也不適用,這都是隨着項目的演變所帶來架構的迭代改變,這個時候就涉及到分庫,對於查詢的數據單獨做爲一個數據庫,做爲數據的更改也單獨用一個數據庫,再結合那些什麼負載均衡等等,數據庫壓力也就減弱了許多。只做查詢的數據庫咱們稱之爲從數據庫,對於數據庫更改的數據庫稱之爲主數據庫,主-從數據庫(Master-Slave)數據的同步方式也有不少,雖然我也沒接觸過,咱們就利用SQL Server中的複製進行發佈-訂閱來模擬演示仍是能夠的。咱們來看看.NET Core Web應用程序如何實現讀寫分離,額外加一句,項目中我也未用到,都是我私下的研究,方案行不行,合不合理能夠一塊兒探討。咱們建立了兩個Demo數據庫,以下:學習
咱們將Demo1做爲主數據庫,Demo2做爲從數據庫,接下來用一張動態圖演示建立複製發佈-訂閱(每隔10秒發佈一次)。ui
咱們給出Demo1上下文,Demo2和其同樣,按照正常作法接下來咱們應該在.NET Core Web應用程序中注入Demo1和Demo2上下文,以下:this
public class Demo1DbContext : DbContext { public Demo1DbContext(DbContextOptions<Demo1DbContext> options) :base(options) { } public DbSet<Blog> Blogs { get; set; } }
public class Demo2DbContext : DbContext { public Demo2DbContext(DbContextOptions<Demo2DbContext> options) :base(options) { } public DbSet<Blog> Blogs { get; set; } }
services.AddDbContext<Demo1DbContext>(options => { options.UseSqlServer("data source=WANGPENG;User Id=sa;Pwd=sa123;initial catalog=Demo1;integrated security=True;"); }).AddDbContext<Demo2DbContext>(options => { options.UseSqlServer("data source=WANGPENG;User Id=sa;Pwd=sa123;initial catalog=Demo2;integrated security=True;"); });
而後咱們建立Demo控制器,經過Demo1上下文添加數據,Demo2上下文讀取數據,以下:
[Route("[controller]")] public class DemoController : Controller { private readonly Demo1DbContext _demo1DbContext; private readonly Demo2DbContext _demo2DbContext; public DemoController(Demo1DbContext demo1DbContext, Demo2DbContext demo2DbContext) { _demo1DbContext = demo1DbContext; _demo2DbContext = demo2DbContext; } [HttpGet("index")] public IActionResult Index() { var blogs = _demo2DbContext.Blogs.ToList(); return View(blogs); } [HttpGet("create")] public IActionResult CreateDemo1Blog() { var blog = new Blog() { IsDeleted = false, CreatedTime = DateTime.Now, ModifiedTime = DateTime.Now, Name = "demoBlog1", Url = "http://www.cnblogs.com/createmyslef" }; _demo1DbContext.Blogs.Add(blog); _demo1DbContext.SaveChanges(); return RedirectToAction(nameof(Index)); } }
@{ ViewData["Title"] = "Index"; } @model IEnumerable<EFCore.Blog> <div class="panel panel-primary"> <div class="panel-heading panel-head">博客列表</div> <div class="panel-body"> <table class="table" style="margin: 4px"> <tr> <th> @Html.DisplayNameFor(model => model.Id) </th> <th> @Html.DisplayNameFor(model => model.Name) </th> <th> @Html.DisplayNameFor(model => model.Url) </th> </tr> @if (Model != null) { @foreach (var item in Model) { <tr> <td> @Html.DisplayFor(modelItem => item.Id) </td> <td> @Html.DisplayFor(modelItem => item.Name) </td> <td> @Html.DisplayFor(modelItem => item.Url) </td> </tr> } } </table> </div> </div>
咱們看到經過Demo1上下文添加數據後重定向到Demo2上下文查詢到的列表頁面,到了10秒自動同步到Demo2數據庫,經過刷新能夠看到數據顯示。雖然結果如咱們所指望,可是實現的路徑卻令咱們不是那麼如意,由於所用實體都是同樣的,只是說所鏈接數據庫不同而已,可是咱們須要建立兩個不一樣的上下文實例,很顯然這不是最佳實踐方式,那麼咱們如何作纔是最佳實踐方式呢?接下來咱們再來建立一個Demo3數據庫,表結構和Demo一、Demo2一致,以下:
接下來咱們在.NET Core Web應用程序Demo一、Demo2上下文所在的類庫中建立以下擴展方法(方便有同行須要學習,給出Demo項目基本結構)。
public static class ChangeDatabase { public static void ChangeToDemo3Db(this DbContext context) { context.Database.GetDbConnection().ConnectionString = "data source=WANGPENG;User Id=sa;Pwd=sa123;initial catalog=Demo3;integrated security=True;"; } }
咱們暫且不去看爲什麼這樣設置,咱們只是添加上下文擴展方法,更改鏈接爲Demo3的數據庫,而後接下來咱們獲取博客列表時,調用上述擴展方法,請問:是否能夠獲取到Demo3的數據或者說是否會拋出異常呢?咱們依然經過動態圖來進行演示,以下:
一直以來咱們認爲利用 context.Database.GetDbConnection() 方法能夠回到ADO.NET進行查詢,可是咱們經過實際證實,咱們能夠設置其餘數據庫鏈接從而達到讀寫分離最佳實踐方式,免去再實例化一個上下文。因此對於上述咱們配置的Demo1和Demo2上下文,咱們大可只須要Demo1上下文即主數據庫,對於從數據庫進行查詢,咱們只需在Demo1上下文的基礎上更該鏈接字符串便可,以下:
public static class ChangeDatabase { public static void ChangeToDemo2Db(this DbContext context) { context.Database.GetDbConnection().ConnectionString = "data source=WANGPENG;User Id=sa;Pwd=sa123;initial catalog=Demo2;integrated security=True;"; } } [HttpGet("index")] public IActionResult Index() { _demo1DbContext.ChangeToDemo2Db(); var blogs = _demo1DbContext.Blogs.ToList(); return View(blogs); }
接下來問題來了,那麼爲什麼更改Demo1上下文鏈接字符串就能轉移到其餘數據庫查詢呢?就是爲了解決讀寫分離免去實例化上下文即Demo2的狀況,可是內部是如何實現的呢?由於EF Core內部添加了方法實現IRelationalConnection接口,使得咱們能夠在已存在的上下文實例上從新設置鏈接字符串即更換數據庫,可是其前提是必須保證當前上下文鏈接已關閉,也就是說好比咱們在同一個事務中利用當前上下文進行更改操做,而後更改鏈接字符串進行更改操做,最後提交事務,由於在此事務內,當前上下文鏈接還未關閉,因此再更改鏈接字符串後進行數據庫更改操做,將一定會拋出異常。
花了兩天時間研究研究,本文比較詳細講解了對於讀寫分離後,如何進行數據查詢和更改操做最佳實踐方式,不知道算不算最好的解決方案,若您有更好的方案,歡迎一塊兒探討或者說還有其餘理解和疑問,也歡迎在評論中提出。