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。。。。。。

相关文章
相关标签/搜索