你的位置:首页 > ASP.net教程

[ASP.net教程]基于 EntityFramework 的数据库主从读写分离架构(2)


    回到目录,完整代码请查看(https://github.com/cjw0511/NDF.Infrastructure)中的目录:
     src\ NDF.Data.EntityFramework\MasterSlaves

 
    上一回中(http://www.cnblogs.com/cjw0511/p/4398267.html),我们简单讲述了基于 EF 来实现数据库读写分离的原理。当然,这只是一个 demo 级别的简单实现,实际上,在我们工作环境中,碰到的情况远比这复杂多了,例如数据库连接的配置是通过 config 文件来存储、在进行数据库操作时还需要附带很多事务操作功能等等。今天我们就来聊聊如何处理这些问题。
    
首先,我们来解决数据库连接字符串存储与配置文件的问题
     代码如下:
   
 1 public class DbMasterSlaveCommandInterceptor : DbCommandInterceptor 2   { 3     private Lazy<string> masterConnectionString = new Lazy<string>(() => ConfigurationManager.AppSettings["masterConnectionString"]); 4     private Lazy<string> slaveConnectionString = new Lazy<string>(() => ConfigurationManager.AppSettings["slaveConnectionString"]); 5  6     public string MasterConnectionString 7     { 8       get { return this.masterConnectionString.Value; } 9     }10 11     public string SlaveConnectionString12     {13       get { return this.slaveConnectionString.Value; }14     }15 16 17     public override void ReaderExecuting(DbCommand command, DbCommandInterceptionContext<DbDataReader> interceptionContext)18     {19       this.UpdateConnectionStringIfNeed(interceptionContext, this.SlaveConnectionString);20     }21 22     public override void ScalarExecuting(DbCommand command, DbCommandInterceptionContext<object> interceptionContext)23     {24       this.UpdateConnectionStringIfNeed(interceptionContext, this.SlaveConnectionString);25     }26 27     public override void NonQueryExecuting(DbCommand command, DbCommandInterceptionContext<int> interceptionContext)28     {29       this.UpdateConnectionStringIfNeed(interceptionContext, this.MasterConnectionString);30     }31 32 33     private void UpdateConnectionStringIfNeed(DbInterceptionContext interceptionContext, string connectionString)34     {35       foreach (var context in interceptionContext.DbContexts)36       {37         this.UpdateConnectionStringIfNeed(context.Database.Connection, connectionString);38       }39     }40 41     /// <summary>42     /// 此处改进了对连接字符串的修改判断机制,确认只在 <paramref name="conn"/> 所使用的连接字符串不等效于 <paramref name="connectionString"/> 的情况下才需要修改。43     /// </summary>44     /// <param name="conn"></param>45     /// <param name="connectionString"></param>46     private void UpdateConnectionStringIfNeed(DbConnection conn, string connectionString)47     {48       if (this.ConnectionStringCompare(conn, connectionString))49       {50         ConnectionState state = conn.State;51         if (state == ConnectionState.Open)52           conn.Close();53 54         conn.ConnectionString = connectionString;55 56         if (state == ConnectionState.Open)57           conn.Open();58       }59     }60 61     private bool ConnectionStringCompare(DbConnection conn, string connectionString)62     {63       DbProviderFactory factory = DbProviderFactories.GetFactory(conn);64 65       DbConnectionStringBuilder a = factory.CreateConnectionStringBuilder();66       a.ConnectionString = conn.ConnectionString;67 68       DbConnectionStringBuilder b = factory.CreateConnectionStringBuilder();69       b.ConnectionString = connectionString;70 71       return a.EquivalentTo(b);72     }73   }

 


再者,我们来聊聊数据库操作中的事务处理。

    我们都知道,数据库操作中的事务处理重要包括两大类:
    1、普通数据库操作事务处理,该类型由 DbTransaction 事务基类来控制;
    2、分布式事务,这类操作主要由组件 System.Transactions 来控制,最常用的类型包括 Transaction 和 TransactionScope。
    
    具体涉及到普通数据库事务和分布式事务的意义和区别、普通事务如何会提升为分布式事务等知识点,这里就不赘述了,有兴趣的同学可以另行补课。
    这里需要说明的是,在数据库的事务操作中,很多 dbms 是不支持同一个事务操作不同的数据库或服务器的。另外某些 dbms 支持同一个事务操作多个数据库或服务器(自动提升为分布式事务),但是需要 msdtc 的支持。
    
    所以在这里,我改进的方案是,凡是所有的事务操作,不管是普通数据库事务,还是分布式事务,都“禁用”读写分离,即将所有的在事务内的数据库操作(不管是读还是写,虽然这一定程度上不符合“完全的读写分离”的本意,但是解决了数据库事务兼容性的问题,而且大多数项目开发中,包含事务的操作不占多数),都指向 Master 服务器。实际上基于我们前面对数据库服务器连接字符串的封装,要实现这一点,只需要改动少量代码,如下:
 1 public class DbMasterSlaveCommandInterceptor : DbCommandInterceptor 2   { 3     private Lazy<string> masterConnectionString = new Lazy<string>(() => ConfigurationManager.AppSettings["masterConnectionString"]); 4     private Lazy<string> slaveConnectionString = new Lazy<string>(() => ConfigurationManager.AppSettings["slaveConnectionString"]); 5  6     public string MasterConnectionString 7     { 8       get { return this.masterConnectionString.Value; } 9     }10 11     public string SlaveConnectionString12     {13       get { return this.slaveConnectionString.Value; }14     }15 16 17     public override void ReaderExecuting(DbCommand command, DbCommandInterceptionContext<DbDataReader> interceptionContext)18     {19       this.UpdateToSlave(interceptionContext);20     }21 22     public override void ScalarExecuting(DbCommand command, DbCommandInterceptionContext<object> interceptionContext)23     {24       this.UpdateToSlave(interceptionContext);25     }26 27     public override void NonQueryExecuting(DbCommand command, DbCommandInterceptionContext<int> interceptionContext)28     {29       this.UpdateToMaster(interceptionContext);30     }31 32 33     private void UpdateToMaster(DbInterceptionContext interceptionContext)34     {35       foreach (var context in interceptionContext.DbContexts)36       {37         this.UpdateConnectionStringIfNeed(context.Database.Connection, this.MasterConnectionString);38       }39     }40 41     private void UpdateToSlave(DbInterceptionContext interceptionContext)42     {43       // 判断当前会话是否处于分布式事务中44       bool isDistributedTran = Transaction.Current != null && Transaction.Current.TransactionInformation.Status != TransactionStatus.Committed;45       foreach (var context in interceptionContext.DbContexts)46       {47         // 判断该 context 是否处于普通数据库事务中48         bool isDbTran = context.Database.CurrentTransaction != null;49 50         // 如果处于分布式事务或普通事务中,则“禁用”读写分离,处于事务中的所有读写操作都指向 Master51         string connectionString = isDistributedTran || isDbTran ? this.MasterConnectionString : this.SlaveConnectionString;52 53         this.UpdateConnectionStringIfNeed(context.Database.Connection, connectionString);54       }55     }56 57 58     /// <summary>59     /// 此处改进了对连接字符串的修改判断机制,确认只在 <paramref name="conn"/> 所使用的连接字符串不等效于 <paramref name="connectionString"/> 的情况下才需要修改。60     /// <para>同时,在必要的情况下才会连接进行 Open 和 Close 操作以及修改 ConnectionString 处理,减少了性能的消耗。</para>61     /// </summary>62     /// <param name="conn"></param>63     /// <param name="connectionString"></param>64     private void UpdateConnectionStringIfNeed(DbConnection conn, string connectionString)65     {66       if (this.ConnectionStringCompare(conn, connectionString))67       {68         this.UpdateConnectionString(conn, connectionString);69       }70     }71 72     private void UpdateConnectionString(DbConnection conn, string connectionString)73     {74       ConnectionState state = conn.State;75       if (state == ConnectionState.Open)76         conn.Close();77 78       conn.ConnectionString = connectionString;79 80       if (state == ConnectionState.Open)81         conn.Open();82     }83 84     private bool ConnectionStringCompare(DbConnection conn, string connectionString)85     {86       DbProviderFactory factory = DbProviderFactories.GetFactory(conn);87 88       DbConnectionStringBuilder a = factory.CreateConnectionStringBuilder();89       a.ConnectionString = conn.ConnectionString;90 91       DbConnectionStringBuilder b = factory.CreateConnectionStringBuilder();92       b.ConnectionString = connectionString;93 94       return a.EquivalentTo(b);95     }96   }

 


    关于上面的代码,需要说明的一点是,因为要获取 EF DbContext 的普通数据库事务状态,必须得拿到 DbContext.Database.CurrentTransaction 属性,所以将 UpdateConnectionString 方法拆分成 UpdateToMaster 和 UpdateToSlave 了。