EntityFramework Core进行读写分离最佳实践方式,了解一下(一)?

前言

原本打算写ASP.NET Core MVC基础系列内容,看到有园友提出如何实现读写分离,这个问题提的好,大多数状况下,对于园友在评论中提出的问题,若是是值得深究或者大多数同行比较关注的问题我都会私下去看看,而后进行对应解答,如有叙述不当之处,还请海涵。咱们稍微过一下事务,本文略长,请耐心阅读。数据库

事务

什么是事务呢?有关事务详解可参看我写的SQL Server基础系列,咱们可归结为一句话:多个提交要么所有成功,要么所有失败即同生共死,没有临阵脱逃者。那么问题来了,用了事务有什么做用或者说有什么优势呢?事务容许咱们将相关操做组合打包,以确保应用程序数据的一致性。那么使用事务又有何缺点呢?使用事务虽然确保了数据一致性等等,可是会影响性能,可能会形成死锁。那么问题又来了,既然有其优缺点,那么咱们是否能够手写逻辑实现数据一致性呢?固然能够,咱们能够模拟事务回滚、提交的效果,可是这也没法百分百保证。架构

调用SaveChanges是否在一个事务内?

首先咱们在控制台中进行以下数据添加,而后添加日志打印。负载均衡

            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接口,使得咱们能够在已存在的上下文实例上从新设置链接字符串即更换数据库,可是其前提是必须保证当前上下文链接已关闭,也就是说好比咱们在同一个事务中利用当前上下文进行更改操做,而后更改链接字符串进行更改操做,最后提交事务,由于在此事务内,当前上下文链接还未关闭,因此再更改链接字符串后进行数据库更改操做,将一定会抛出异常。

总结 

花了两天时间研究研究,本文比较详细讲解了对于读写分离后,如何进行数据查询和更改操做最佳实践方式,不知道算不算最好的解决方案,若您有更好的方案,欢迎一块儿探讨或者说还有其余理解和疑问,也欢迎在评论中提出。

相关文章
相关标签/搜索