• [设计模式] 静态代理居然能解决这种问题,我惊讶了!


    解决问题

    这篇文章借用 FreeSql.Cloud 开源项目的代码,讲解代理模式的实际应用和收益,以及需要注意的地方。

    FreeSql 是 c#.NET 功能强大的 ORM 框架,定义了 IFreeSql 接口,主要针对单个 ConnectionString 生产 ORM 操作对象。

    跨多数据库的时候,不同的 ConnectionString 需要生成多个 IFreeSql 原始对象,如果是多租户场景每个租户 ConnectionString 都不相同的情况下,就需要创建 N个 IFreeSql 原始对象。

    FreeSql.Cloud 正是为了跨多数据库的问题而产生,它可以解决:

    1、FreeSqlCloud 实现多库版 IFreeSql 接口,从使用习惯上保持与单库版 IFreeSql 一致;

    2、运行时,FreeSqlCloud 可动态添加或删除多个 ConnectionString 对应的 IFreeSql;

    3、FreeSqlCloud 存储多租户 IFreeSql,最后活跃时间 > 10分钟的租户,释放对应 IFreeSql 减少内存开销;

    4、FreeSqlCloud 支持随时 Change 切换到对应的 IFreeSql 进行操作;


    代理模式实战(一)Scoped FreeSqlCloud 多库版本

    IFreeSql 是一个极为严格、简单,且功能强大的接口,我们一直在严格控制 API 泛滥增长,泛滥的 API 在后续改造时非常痛苦。

    正因为它的简单定义,让我们有机会使用到代理模式实现新的 IFreeSql 实现类 FreeSqlCloud。

    public class FreeSqlCloud : IFreeSql
    {
        IFreeSql CurrentOrm => ...; //请看后面
    
        public IAdo Ado => CurrentOrm.Ado;
        public IAop Aop => CurrentOrm.Aop;
        public ICodeFirst CodeFirst => CurrentOrm.CodeFirst;
        public IDbFirst DbFirst => CurrentOrm.DbFirst;
        public GlobalFilter GlobalFilter => CurrentOrm.GlobalFilter;
    
        public void Transaction(Action handler) => CurrentOrm.Transaction(handler);
        public void Transaction(IsolationLevel isolationLevel, Action handler) => CurrentOrm.Transaction(isolationLevel, handler);
        public ISelect<T1> Select<T1>() where T1 : class => CurrentOrm.Select();
        public ISelect<T1> Select<T1>(object dywhere) where T1 : class => Select().WhereDynamic(dywhere);
        public IDelete<T1> Delete<T1>() where T1 : class => CurrentOrm.Delete();
        public IDelete<T1> Delete<T1>(object dywhere) where T1 : class => Delete().WhereDynamic(dywhere);
        public IUpdate<T1> Update<T1>() where T1 : class => CurrentOrm.Update();
        public IUpdate<T1> Update<T1>(object dywhere) where T1 : class => Update().WhereDynamic(dywhere);
        public IInsert<T1> Insert<T1>() where T1 : class => CurrentOrm.Insert();
        public IInsert<T1> Insert<T1>(T1 source) where T1 : class => Insert().AppendData(source);
        public IInsert<T1> Insert<T1>(T1[] source) where T1 : class => Insert().AppendData(source);
        public IInsert<T1> Insert<T1>(List source) where T1 : class => Insert().AppendData(source);
        public IInsert<T1> Insert<T1>(IEnumerable source) where T1 : class => Insert().AppendData(source);
        public IInsertOrUpdate<T1> InsertOrUpdate<T1>() where T1 : class => CurrentOrm.InsertOrUpdate();
    }
    

    如上代码,若 CurrentOrm 返回值是原始 IFreeSql 对象,即会代理调用原始数据库 ORM 操作方法。方法不多,功能却强大,代理模式应用起来就会很轻松。

    由于多个 ConnectionString 的原因,我们需要定义一个字典保存多个原始 IFreeSql 对象:

        readonly Dictionary<string, IFreeSql> _orms = new Dictionary<string, IFreeSql>();
    

    我们已经有了 _orms,还缺什么??还缺一个当前 string _dbkey,有了它我们的 CurrentOrm 方法才知道怎么获取对应的 IFreeSql:

        string _dbkey;
        IFreeSql CurrentOrm => _orms[_dbkey]; //测试不纠结代码安全
    
        //切换数据库
        IFreeSql Change(string db)
        {
            _dbkey = db;
            return _orms[_dbkey];
        }
        //添加 IFreeSql
        FreeSqlCloud Add(string db, IFreeSql orm)
        {
            if (_dbkey == null) _dbkey = db;
            _orms.Add(db, orm);
            return this;
        }
    

    至此我们基于 Scoped 生命周期的 FreeSqlCloud 就完成了,DI 代码大概如下:

    public void ConfigureServices(IServiceCollection services)
    {
        var db1 = new FreeSqlBuilder().UseConnectionString(DataType.Sqlite, "data source=db1.db").Build();
        var db2 = new FreeSqlBuilder().UseConnectionString(DataType.Sqlite, "data source=db2.db").Build();
        services.AddScoped(provider =>
        {
            var cloud = new FreeSqlCloud();
            cloud.Add("db1", db1);
            cloud.Add("db2", db2);
            return cloud;
        });
    }
    

    代理模式实战(二)Singleton FreeSqlCloud 多库版本

    实战(一)我们实现了 Scoped 版本,可是其实项目中 Singleton 单例才是高性能的保证,特别是多租户场景,每次 new FreeSqlCloud 不止还要循环 Add 那么多次,实属浪费!!!

    其实单例并非难事,只需要将 _dbkey 类型修改成 AsyncLocal,这个类型多线程是安全的,有关它的原理请看资料:https://www.cnblogs.com/eventhorizon/p/12240767.html

        AsyncLocal<string> _dbkey;
        IFreeSql CurrentOrm => _orms[_dbkey.Value];
    
        IFreeSql Change(string db)
        {
            _dbkey.Value = db;
            return _orms[_dbkey];
        }
        FreeSqlCloud Add(string db, IFreeSql orm)
        {
            if (_dbkey.Value == null) _dbkey.Value = db;
            _orms.Add(db, orm);
            return this;
        }
    

    至此我们就完成了一个多线程安全的代理模式实现,因此我们只需要在 Ioc 注入之前 Register 好原始 IFreeSql 对象即可:

    public void ConfigureServices(IServiceCollection services)
    {
        var db1 = new FreeSqlBuilder().UseConnectionString(DataType.Sqlite, "data source=db1.db").Build();
        var db2 = new FreeSqlBuilder().UseConnectionString(DataType.Sqlite, "data source=db2.db").Build();
        var cloud = new FreeSqlCloud();
        cloud.Add("db1", db1);
        cloud.Add("db2", db2);
    
        services.AddSingleton(cloud);
    }
    

    代理模式实现(三)Singleton FreeSqlCloud 多库多租户版本

    如上,我们使用字典存储多个 IFreeSql 原始对象,在数量不多的情况下是可行的。

    但是如果我们做的是多租户系统,那么数量很可能达到几百,甚至上千个 IFreeSql 对象,并且这些租户不全都是活跃状态。

    因此我们需要一种释放机制,当租户最后活跃时间 > 10分钟,释放 IFreeSql 资源,减少内存开销;

    我们可以引用 IdleBus 组件解决该问题,IdleBus 空闲对象管理容器,有效组织对象重复利用,自动创建、销毁,解决【实例】过多且长时间占用的问题。有时候想做一个单例对象重复使用提升性能,但是定义多了,有的又可能一直空闲着占用资源。

    IdleBus 专门解决:又想重复利用,又想少占资源的场景。

    此时我们只需要修改内部实现部分代码如下:

    public class FreeSqlCloud : IFreeSql
    {
        IdleBus _orms = new IdleBus<string, IFreeSql>();
        AsyncLocal<string> _dbkey;
        IFreeSql CurrentOrm => _orms.Get(_dbkey.Value);
    
        IFreeSql Change(string db)
        {
            _dbkey.Value = db;
            return this;
        }
        FreeSqlCloud Register(string db, Func create) //注意 create 类型是 Func
        {
            if (_dbkey.Value == null) _dbkey.Value = db;
            _orms.Register(db, create);
            return this;
        }
        //...
    }
    
    public void ConfigureServices(IServiceCollection services)
    {
        var cloud = new FreeSqlCloud();
        cloud.Add("db1", () => new FreeSqlBuilder().UseConnectionString(DataType.Sqlite, "data source=db1.db").Build());
        cloud.Add("db2", () => new FreeSqlBuilder().UseConnectionString(DataType.Sqlite, "data source=db1.db").Build());
    
        services.AddSingleton(cloud);
    }
    

    代理模式实现(四)跟随切换数据库的仓储对象

    1、静态仓储对象

    FreeSql.Repository 对象创建时固定了原始 IFreeSql,因此无法跟随 FreeSqlCloud Change 切换数据库。

    注意:是同一个对象实例创建之后,无法跟随切换,创建新对象实例不受影响。

    因为要在 Repository 创建之前,先调用 fsql.Change 切换好数据库。

    2、动态仓储对象

    但是。。。仍然有一种特殊需求,Repository 在创建之后,仍然能跟随 fsql.Change 切换数据库。

    实战中 Scoped 生命同期可能有多个 Repository 对象,因此切换 cloud 即改变所有 Repository 对象状态,才算方便。

    var repo1 = cloud.GetCloudRepository();
    var repo2 = cloud.GetCloudRepository();
    cloud.Change("db2");
    Console.WriteLine(repo1.Orm.Ado.ConnectionString); //repo -> db2
    Console.WriteLine(repo2.Orm.Ado.ConnectionString); //repo -> db2
    cloud.Change("db1");
    Console.WriteLine(repo1.Orm.Ado.ConnectionString); //repo -> db1
    Console.WriteLine(repo2.Orm.Ado.ConnectionString); //repo -> db1
    

    我们仍然使用了代理模式,IBaseRepository 接口定义也足够简单:

    提示:关键看 CurrentRepo 的获取

    class RepositoryCloud<TEntity> : IBaseRepository<TEntity> where TEntity : class
    {
        readonly FreeSqlCloud _cloud;
        public RepositoryCloud(FreeSqlCloud cloud)
        {
            _cloud = cloud;
        }
    
        public IBaseRepository CurrentRepo => ...; //请看后面
        public IUnitOfWork UnitOfWork
        {
            get => CurrentRepo.UnitOfWork;
            set => CurrentRepo.UnitOfWork = value;
        }
    
        public IFreeSql Orm => CurrentRepo.Orm;
        public Type EntityType => CurrentRepo.EntityType;
        public IDataFilter DataFilter => CurrentRepo.DataFilter;
        public ISelect Select => CurrentRepo.Select;
        public IUpdate UpdateDiy => CurrentRepo.UpdateDiy;
        public ISelect Where(Expressionbool>> exp) => CurrentRepo.Where(exp);
        public ISelect WhereIf(bool condition, Expressionbool>> exp) => CurrentRepo.WhereIf(condition, exp);
    
        public void Attach(TEntity entity) => CurrentRepo.Attach(entity);
        public void Attach(IEnumerable entity) => CurrentRepo.Attach(entity);
        public IBaseRepository AttachOnlyPrimary(TEntity data) => CurrentRepo.AttachOnlyPrimary(data);
        public Dictionary<string, object[]> CompareState(TEntity newdata) => CurrentRepo.CompareState(newdata);
        public void FlushState() => CurrentRepo.FlushState();
    
        public void BeginEdit(List data) => CurrentRepo.BeginEdit(data);
        public int EndEdit(List data = null) => CurrentRepo.EndEdit(data);
    
        public TEntity Insert(TEntity entity) => CurrentRepo.Insert(entity);
        public List Insert(IEnumerable entitys) => CurrentRepo.Insert(entitys);
        public TEntity InsertOrUpdate(TEntity entity) => CurrentRepo.InsertOrUpdate(entity);
        public void SaveMany(TEntity entity, string propertyName) => CurrentRepo.SaveMany(entity, propertyName);
    
        public int Update(TEntity entity) => CurrentRepo.Update(entity);
        public int Update(IEnumerable entitys) => CurrentRepo.Update(entitys);
    
        public int Delete(TEntity entity) => CurrentRepo.Delete(entity);
        public int Delete(IEnumerable entitys) => CurrentRepo.Delete(entitys);
        public int Delete(Expressionbool>> predicate) => CurrentRepo.Delete(predicate);
        public List<object> DeleteCascadeByDatabase(Expressionbool>> predicate) => CurrentRepo.DeleteCascadeByDatabase(predicate);
    }
    

    如上代码关键实现部分 CurrentRepo,我们定义了字典存储多个 IBaseRepository 原始仓储对象:

    因为一个 CloudRepository 对象会创建 1-N 个 IBaseRepository 原始对象,在不使用 cloud.Change(..) 方法的时候只会创建 1 个,最多创建 cloud.Registers 数量,真实场景中不会有人在同一个业务把所有 db 都切换个遍。

        readonly Dictionary<string, IBaseRepository> _repos = new Dictionary<string, IBaseRepository>();
        protected void ForEachRepos(Action> action)
        {
            foreach (var repo in _repos.Values) action(repo);
        }
        public void Dispose()
        {
            ForEachRepos(repo => repo.Dispose());
            _repos.Clear();
        }
        
        protected IBaseRepository CurrentRepo
        {
            get
            {
                var dbkey = _cloud._dbkey.Value;
                if (_repos.TryGetValue(dbkey, out var repo) == false)
                {
                    _repos.Add(dbkey, repo = _cloud.Use(dbkey).GetRepository());
                    if (_dbContextOptions == null) _dbContextOptions = repo.DbContextOptions;
                    else
                    {
                        repo.DbContextOptions = _dbContextOptions;
                        if (_asTypeEntityType != null) repo.AsType(_asTypeEntityType);
                        if (_asTableRule != null) repo.AsTable(_asTableRule);
                    }
                }
                return repo;
            }
        }
    
        Type _dbContextOptions;
        public DbContextOptions DbContextOptions
        {
            get => CurrentRepo.DbContextOptions;
            set => ForEachRepos(repo => repo.DbContextOptions = value);
        }
        Type _asTypeEntityType;
        public void AsType(Type entityType)
        {
            _asTypeEntityType = entityType;
            ForEachRepos(repo => repo.AsType(entityType));
        }
        Func<string, string> _asTableRule;
        public void AsTable(Func<string, string> rule)
        {
            _asTableRule = rule;
            ForEachRepos(repo => repo.AsTable(rule));
        }
    

    由于 DbContextOptions、AsType、AsTable 比较特殊,需要将多个原始仓储对象传播设置,代码如上。

    最终还要为 CloudRepository 创建扩展方法:

    public static IBaseRepository<TEntity> GetCloudRepository<TEntity>(this FreeSqlCloud cloud)
        where TEntity : class
    {
        return new RepositoryCloud(cloud);
    }
    

    结语

    Repository 是一种非常方便做设计的模式,FreeSql 还有很多一些其他设计模式的应用,如果有兴趣以后找机会再写文章。


    作者是什么人?

    作者是一个入行 18年的老批,他目前写的.net 开源项目有:

    开源项目 描述 开源地址 开源协议
    FreeIM 聊天系统架构 https://github.com/2881099/FreeIM MIT
    FreeRedis Redis SDK https://github.com/2881099/FreeRedis MIT
    csredis https://github.com/2881099/csredis MIT
    FightLandlord 斗DI主网络版 https://github.com/2881099/FightLandlord 学习用途
    FreeScheduler 定时任务 https://github.com/2881099/FreeScheduler MIT
    IdleBus 空闲容器 https://github.com/2881099/IdleBus MIT
    FreeSql ORM https://github.com/dotnetcore/FreeSql MIT
    FreeSql.Cloud 分布式tcc/saga https://github.com/2881099/FreeSql.Cloud MIT
    FreeSql.AdminLTE 低代码后台生成 https://github.com/2881099/FreeSql.AdminLTE MIT
    FreeSql.DynamicProxy 动态代理 https://github.com/2881099/FreeSql.DynamicProxy 学习用途

    需要的请拿走,这些都是最近几年的开源作品,以前更早写的就不发了。

  • 相关阅读:
    TTKEFU在线客服系统:实时交流,更好地理解客户需求
    PostgreSql pgAgent
    图解LeetCode——1773. 统计匹配检索规则的物品数量(难度:简单)
    不一样的VR全景购物,赋能商超和店铺购物升级
    idea一些不太常用但是能提升编码效率的快捷键
    java集合
    2Gcsv文件打不开怎么处理,使用byzer工具
    Jira使用浅谈篇一
    jenkins关联github
    计算机毕业设计Java宠物托管系统(源码+系统+mysql数据库+lw文档)
  • 原文地址:https://www.cnblogs.com/FreeSql/p/16624438.html