不管是如今流行的微服务,仍是之前的SOA,仍是DDD中都有模块化思想.模块化也是面向对象松耦合的思想,跟类和类之间关系相似,模块是一组类内聚造成一个组,组和组处理各自的业务.html
物理上解决方案中咱们把包或者dll看做一个模块.算法
这类模块主要负责装配(ioc注册,配置加载)等初始化等操做sql
DDD一般咱们把全部的业务逻辑放到领域层,而领域层中的实体,聚合根等都须要持久化,因此领域中的模块有其特殊的持久化需求.数据库
基于上述分析,在一般说的Module中抽象出一个子概念DataModule,继承自Module.主要负责组织ORM的元数据数据,对应EF的话就是的数据上下文的概念,设计出一个数据模块对应一个数据上下文的概念.而EF的数据上下文对应的就是一个数据库,继而演化成一个数据模块对应一个数据库.从物理上表现就是一个dll对应一个数据库;逻辑上表现为一个数据模块对应一个数据库.c#
数据的存放的方式和位置最终依赖于数据库的链接,落到代码中就是一个数据库链接字符.
大而全的架构中须要在单个进程中访问多个数据库资源,若是不加以管理势必会混乱.
而最终管理的实际是数据库链接字符串.
数据工厂的目的就是将全部的数据库链接统一的加载,须要使用的时候统一的地方获取.
采用原始的数据库链接配置的方式在应用启动的时候所有加载,这只能知足静态运行的要求.
而不能根据运行时的状态进行动态的分配,因此数据库工厂实际的加载分为两部分;缓存
最终DbFactory的加载时委托给IDbConfigLoader 来完成的,这样姐能够实现的时候加载配置文件,也能够经过运行时根据动态路由按照必定规则生成数据库链接字符串,更加灵活.架构
根据实体类型定位到数据模块->根据数据模块的名字获取对应的配置.此时的配置能够按照(类型-配置)进行缓存,提升获取效率.app
动态配置:首先要定义动态配置的几个要素:数据模块-编号-路由因子-数据库链接字符串.框架
动态配置是基于静态的,因此获取动态配置的收首先要定位到数据模块.分布式
新增数据->获取数据的路由因子->从配置列表中获取(此时生成的id中包含编号这一要素)
根据id获取/修改/删除->解析id中的编号->从配置列表获取
查询->根据查询条件获取路由因子->从配置列表中获取
若是定位不到单条配置则根据有限的条件获取配置集合进行遍历操做
动态配置获取这部分须要结合动态仓储理解
结合Repository模式的理解将Repository分离为两类ICommandRepository和IQueryRepository.
可是Repisotory的读写分离并不是一个必须选项,因此IRepository继承自ICommandRepository和IQueryRepository.
实际上写部分的逻辑对不一样的ORM,不一样的数据库技术有所不一样 ,因此不管是增删改只能定义接口实现必须关联到具体的技术.
而读却不一样.基于Expression的支持,只要对不一样的ORM实现相似EntityFramework中LinqProvider的功能便可实现跨ORM和数据库技术的查询.
因此抽象层级以下:
这里的实现只是一个思路,具体要集合静态和动态有不一样的命名和实现
在前面进行了读写分离和仓储的设计以后这里只需继续对以前的层级继续向下延伸,不过这里由于了区别于动态仓储,这里实际的类名都增长Static.
这里以EF为例实现接口和对象层级以下:
DbModel 单独读的状况下模式固定为 DbMode.Read 单独写状况下固定为 DbMode.Write,当实现同时存在的时候根据Repository的目的来根据不一样的方法来区分.
静态仓储的实现部分跟如今流行的框架并无区别,最终的区别是在UnitOfWork的注入和建立DbSet背后的逻辑,在后面会进行分析.
在纵向分库的基础上,若是单个库数据量持续增大同样会带来数据过大响应过慢的问题,这时须要对纵向切割库进行横向的切割.具体须要从如下几个点分析
Id生成的要求
生成方式 | 知足 | 不知足 |
---|---|---|
数据库自增 | 1,2,5 | 3,4 |
Guid | 1,3,4 | 2,5 |
时间戳 | 2,3,4,5, | 1 |
统一的服务 | 1,2,3,5 | 4 |
Guid+时间混编 -> 字符串拼接 -> 二进制拼接
将bigint类型的数字转换成64位二进制数据,而后将须要的信息隐藏到id中
具体实现: {AppId:7} + {AppNode:4} + {Time:32} + {Count:14} + {DataNode:6}
仓储自己是对数据访问的一个封装.在静态仓储的基础上,一个类型对应到一个纵向切割的模块.
那么横向切割后如何定位到库进行访问就是个难题.
通常都是对id或者某个字段进行hash,可是在读取的时候却须要扫描多个库来获取结果,后续带来的合并,排序等问题会难以解决.
因为利用仓储模式,那么咱们假设仓储的每个方法均可以定位到一个横向切割的库既能够解决传统方式带来的多库扫描的问题.那么咱们对仓储的方法进行分类(依据参数,也就是数据)
若是对于同一个库的上面三种访问能够创建一样的路由到同一个库便可解决纵向分库的问题.
这种方式我称之为动态路由(IDynamicRouter),依据运行时调用仓储的参数来肯定访问的数据库.下面分析三种路由
IDynamicRouter 实际只有一个String属性Coden.
选取实体和Specification中的若干字段根据算法生成一个字符串,而后根据此字符串便可定位到一个数据库.
基于前面的Id生成算法,在插入时候根据路由能够找到惟一的DataNode,当在有id的状况下便可反向定位到一个数据库.
原始的仓储可能见的最多的是 IRepository
如是抽象层架以下:
有好几个抽象的维度和层级在里面,因此这里面致使类的层级较多.不管是静态仍是动态,base和base以上的都属于框架的内容属于抽象类,如下的都是关联具体技术实现的属于实现类.
不管是静态仓储仍是动态仓储的层级都较多,主要是集成了读写分离,而且仍是可选致使.
最终的使用上都是继承自CommandRepository,QueryRepository,Repository,SeparateRepository.
具体的使用场景是
这里注意3中同时读写的状况,因为不管采用何种技术,写库和读库的同步并非实时的(实际有几秒的延迟).因此这里代码虽然集成到一块儿使用方便,可是在使用时要注意避免在一个请求中写完当即读.
工做单元的好处在于如何能够对数据库的多个操做一次性提交,对事务比较友好.可是我在设计的时候考虑到静态状况下的事务不管是否读写分离,其实只是对单个库进行操做(读不包含在事务中).而动态状况下却有对多库进行操做的状况(实际在使用中极少出现多库操做).因此分为动态和静态两种,实际上就是单个和多个,只是为了保持以前命名的一致性.
从动态路由的概念中,操做定位到哪一个库实际是由请求Repository的参数决定.
若是利用大部分其余架构的IOC注入UnitOfWork到仓储中,此时将会在请求到达Controller决定你的UnitOfWork实例.
而最终数据库即便在简单的静态仓储中都是由到达那个Repository(TEntity的类型能够肯定)肯定的,因此这里用组合的方式,
仓储的实际注入中只注入DbFactory dbFactory, ContextFactory contextFactory这两个对象.
具体dbfacotry以前已经介绍过,主要管理数据库链接的配置.而contextfacotry这个下文会有说明.
UnitOfWork中有几个关键点.
为了不分布式事务和重复提交,那么若是一个请求访问多个不一样仓储,而且多个仓储的对应的同一个数据库,那么建立出来的DbSet必须是同一个,继而UnitOfWork管理的DbContext也是同一个.此时实际的Context的惟一性和生命周期是由ContextFactory来管理.
实际的工做流程是,
上下文特指EF的DbContext,根据数据初始化上下文获取DbContext,初始化数据库的上下文定义以下
public class DbInitContext { public DbInitContext(DbConfig config, DbModule module, DbMode mode) { Mode = mode; Config = config; Module = module; } public DbMode Mode { get; set; } public DbConfig Config { get; set; } public DbModule Module { get; set; } public string ConnectiongString { get { if (Mode == DbMode.Write) return Config.WriteConnectionString; if (Mode == DbMode.Read) return Config.ReadConnectionString; return Config.NameOrConnectionString; } } public List<Type> Types => Module.EntityTypes; public string GetIdentity() { return $"{Config.StaticCoden} {ConnectiongString}"; } public static Func<Type, bool> IsEntity { get { return item => item.IsSubclassOf(typeof(Entity)); } } }
上下文工厂定义以下
using System; using System.Collections.Concurrent; using System.Data; using System.Data.Entity; using Coralcode.EntityFramework.Extension; using Coralcode.Framework.Aspect; using Coralcode.Framework.Data; using Coralcode.Framework.Exceptions; using System.Collections.Generic; namespace Coralcode.EntityFramework.UnitOfWork { [Inject(RegisterType = typeof(ContextFactory), LifetimeManagerType = LifetimeManagerType.PerResolve)] public class ContextFactory : IDisposable { private ConcurrentDictionary<string, CoralDbContext> _contexts = new ConcurrentDictionary<string, CoralDbContext>(); private List<IDbConnection> connnections = new List<IDbConnection>(); private static Func<string, DbInitContext, CoralDbContext> _creator; private bool _isDispose; public ContextFactory() { if (_creator == null) _creator = (item, context) => { var dbContext = new CoralDbContext(context); return dbContext; }; } public static void SetContextCreator(Func<string, DbInitContext, CoralDbContext> creator) { _creator = creator; } /// <summary> /// 建立数据库上下文 /// </summary> /// <param name="context"></param> /// <returns></returns> public virtual CoralDbContext Create(DbInitContext context) { if (context == null) throw CoralException.ThrowException<DbErrorCode>(item => item.InvalideDbCoden, "上下文为空"); if (context.Config == null) throw CoralException.ThrowException<DbErrorCode>(item => item.InvalideDbCoden, "上下文配置为空"); if (string.IsNullOrEmpty(context.Config.NameOrConnectionString)) throw CoralException.ThrowException<DbErrorCode>(item => item.InvalideDbCoden, "链接字符串为空"); return _contexts.GetOrAdd(context.GetIdentity(), item => _creator(item, context)); } /// <summary> /// 获取数据库链接 /// </summary> /// <param name="context"></param> /// <returns></returns> public virtual IDbConnection GetConnection(DbInitContext context) { var connection = Database.DEFaultConnectionFactory.CreateConnection(context.ConnectiongString); connnections.Add(connection); return connection; } /// <summary> /// 获取执行sql的接口 /// </summary> /// <param name="connection"></param> /// <returns></returns> public virtual ISql GetSqlExetuator(IDbConnection connection) { return new DapperSql(connection); } public void DisposeDbContext(string dbIdentity) { if (string.IsNullOrEmpty(dbIdentity)) return; if (_contexts == null) return; CoralDbContext context; if (_contexts.TryRemove(dbIdentity, out context)) context.Dispose(); } public void Dispose() { if (_contexts != null) { foreach (var context in _contexts) { context.Value?.Dispose(); } } _contexts?.Clear(); _contexts = null; connnections?.ForEach(item => { item.Dispose(); }); connnections?.Clear(); connnections = null; } } }
其中SetContextCreator 方法提供能够自定义上下文的扩展,例如Sqlce的,后面再业务组件文章中介绍会提到.
另外dbIdentity 是Dbcontext的一个细节,自带的Dbcontext是根据Dbcontext类型的静态缓存类和表的映射关系.
Dbcontext继承IDbModelCacheKeyProvider以后就能够用dbIdentity来隔离不一样数据模块的元数据缓存.
主要是在单体架构中,多库时无需继承框架的Dbcontext便可实现多个上下文元数据管理(具体可查看前面CRUD的数据层设计)
大部分状况下数据是能够路由的,可是也免不了不能路由的状况
在路由意外的状况中,以分页最难处理,由于分页涉及到排序合并等.
这里咱们根据实际状况分析,知足大部分请求快速响应的原则;
基于以上规则设计以下算法
/// <summary> ///获取分页数据 /// </summary> /// <param name="pageIndex">页码</param> /// <param name="pageCount">页大小</param> /// <param name="specification">条件</param> /// <param name="orderByExpressions">是否排序</param> /// <returns>实体的分页数据</returns> public PagedList<TEntity> GetPaged(int pageIndex, int pageCount, IDynamicSpecification<TEntity> specification, SortExpression<TEntity> orderByExpressions = null) { if (orderByExpressions == null || !orderByExpressions.IsNeedSort()) orderByExpressions = new SortExpression<TEntity>(new List<EditableKeyValuePair<Expression<Func<TEntity, dynamic>>, bool>> { new EditableKeyValuePair<Expression<Func<TEntity, dynamic>>, bool>(item=>item.Id,false), }); if (pageIndex == 0) { pageIndex = 1; } //若是动态路由可用则为单库 if (!string.IsNullOrEmpty(specification.Coden)) { var set = DynamicGetAll(specification); //若是找到了单库 if (set != null) { var queryable = set.Where(specification.SatisfiedBy()); int totel = queryable.Count(); IEnumerable<TEntity> items = orderByExpressions.BuildSort(queryable).Skip(pageCount * (pageIndex - 1)).Take(pageCount); return new PagedList<TEntity>(totel, pageCount, pageIndex, items.ToList()); } } //若是找不到单库 int sum = 0; List<IQueryable<TEntity>> entities = new List<IQueryable<TEntity>>(); foreach (var tmp in DbFactory.GetDynamicDbConfigs(typeof(TEntity))) { var queryable = DynamicGetAll(new SampleRouter(tmp.DynamicCoden)).Where(specification.SatisfiedBy()); sum += queryable.Count(); entities.Add(queryable); } int newDataIndex = (pageIndex + 1) * pageCount; //若是在中值以后则反转排序 if (sum < pageIndex * pageCount * 2 && pageIndex * pageCount > sum) { orderByExpressions.Reverse(); //反转页码 newDataIndex = sum - pageIndex * pageCount; var datas = entities.SelectMany(item => orderByExpressions.BuildSort(item).Take(newDataIndex)).ToList(); orderByExpressions.Reverse(); datas = orderByExpressions.BuildSort(datas).Skip(0).Take(pageCount).ToList(); return new PagedList<TEntity>(sum, pageCount, pageIndex, datas.ToList()); } else { var datas = entities.SelectMany(item => orderByExpressions.BuildSort(item).Take(newDataIndex)) .Skip(pageCount * (pageIndex - 1)).Take(pageCount).ToList(); return new PagedList<TEntity>(sum, pageCount, pageIndex, datas.ToList()); } }
在这个算法中,中值部分最慢,两端较快:如图
举例:
假设有10个数据库的某个表都存放1w数据.
因此最后的性能图以下(!!!!手绘意思下):
动态分库能够归结到数据模型C=f(x),其中C为数据库链接字符串,x为Entity,Specification的字段,甚至是当前请求中应用的某个状态,f为经过X生成Coden的函数
这个典型的应用场景在美团,58等地域性较强的业务中根据省份或者城市分库较为常见.这时候f可能就是一个映射关系(经过key获取value,字典便可).
另外在多租户的状况下,作数据隔离也是比较理想的解决思路.
在更为复杂的状况下C=f(x,y,......),其中C为数据库链接字符串,x,y为Entity或者Specification中某几字段,或者请求的某个状态,f为经过X生成Coden的函数.
另外还可能出现 Cs=f(x,y,......)(少了其中某个参数或几个参数),其中Cs为一组数据库链接字符串
这几个函数须要对业务了解比较清楚,才能实现.
这种分库的方式我总结为多维数据库,其中X,Y,Z等就是不一样的维度,每一个数据库是多维空间中的点.
经过f定位到一个数据库实际就是多维空间的一个点.而以前的Cs,多是比多维少一些维度好比三维空间上落在二维平面上的点.
这种思路在阿里mycat中间件,和阿里maxcomputer计算平台的hash分片中都有所体现.
不过相对于来讲个人这种实现基于应用程序的改造比较简单,可是通用性会有所不足.
最后: 这些设计也并不是一簇而就,在过去两年通过两轮大的重构以后才造成.其中我以为最重要的是想象力.后面多维数据库的概念更多的是想一想的空间. 有兴趣能够留言咱们讨论