.Net/C#分庫(kù)分表高性能O(1)瀑布流分頁(yè)

框架介紹

依照慣例首先介紹本期主角:ShardingCore 一款ef-core下高性能、輕量級(jí)針對(duì)分表分庫(kù)讀寫(xiě)分離的解決方案,具有零依賴、零學(xué)習(xí)成本掌实、零業(yè)務(wù)代碼入侵

dotnet下唯一一款全自動(dòng)分表,多字段分表框架,擁有高性能,零依賴艾少、零學(xué)習(xí)成本卧蜓、零業(yè)務(wù)代碼入侵,并且支持讀寫(xiě)分離動(dòng)態(tài)分表分庫(kù),同一種路由可以完全自定義的新星組件框架

你的star和點(diǎn)贊是我堅(jiān)持下去的最大動(dòng)力,一起為.net生態(tài)提供更好的解決方案

項(xiàng)目地址

背景

在大數(shù)據(jù)量下針對(duì)app端的瀑布流頁(yè)面分頁(yè)的優(yōu)化實(shí)戰(zhàn),有大量的數(shù)據(jù),前端需要以瀑布流的形式展示出來(lái),我們最簡(jiǎn)單的就是以用戶發(fā)布的文章為例,假設(shè)我們有大量的文章帖子被,需求需要按帖子的發(fā)布時(shí)間倒序展示給用戶看,那么在手機(jī)端我們一般都是以下拉刷新,上拉加載的形式去展示,那么我們一般會(huì)有以下集中寫(xiě)法徙鱼。

常規(guī)分頁(yè)操作

select count(*) from article
select * from article order by publish_time desc limit 0,20

這個(gè)操作是一般我們的常規(guī)分頁(yè)操作,先進(jìn)行total然后進(jìn)行分頁(yè)獲取,這種做法的好處是支持任意規(guī)則的分頁(yè),缺點(diǎn)就是需要查詢兩次,一次count一次limit當(dāng)然后期數(shù)據(jù)量實(shí)在太大可以只需要第一次count,但是也有一個(gè)問(wèn)題就是如果數(shù)據(jù)量一直在變化會(huì)出現(xiàn)下一次分頁(yè)中還會(huì)有上一次的部分?jǐn)?shù)據(jù),因?yàn)閿?shù)據(jù)在不斷地新增,你的分頁(yè)沒(méi)跟上發(fā)布的速度那么就會(huì)有這個(gè)情況發(fā)送.

瀑布流分頁(yè)

除了上述常規(guī)分頁(yè)操作外,我們針對(duì)特定順序的分頁(yè)也可以進(jìn)行特定的分頁(yè)方式來(lái)實(shí)現(xiàn)高性能,因?yàn)榛诖笄疤嵛覀兪谴髷?shù)量下的瀑布流,我們的文章假設(shè)是以雪花id作為主鍵,那么我們的分頁(yè)可以這么寫(xiě)

select * from article where id<last_id order by publish_time desc limit 0,20

首先我們來(lái)分析一下,這個(gè)語(yǔ)句是利用了插入的數(shù)據(jù)分布是順序和你需要查詢的排序一直來(lái)實(shí)現(xiàn)的,又因?yàn)閕d不會(huì)重復(fù)并且雪花id的順序和時(shí)間是一致的都是同向的所以可以利用這種方式來(lái)進(jìn)行排序,limit每次不需要跳過(guò)任何數(shù)目,直接獲取需要的數(shù)目即可,只需要傳遞上一次的查詢結(jié)果的id即可府喳,這個(gè)方式彌補(bǔ)了上述常規(guī)分頁(yè)帶來(lái)的問(wèn)題,并且擁有非常高的性能,但是缺點(diǎn)也顯而易見(jiàn),不支持跳頁(yè),不支持任意排序,所以這個(gè)方式目前來(lái)說(shuō)非常適合前端app的瀑布流排序脖卖。

分片下的實(shí)現(xiàn)

首先分片下需要實(shí)現(xiàn)這個(gè)功能我們需要有id支持分片,并且publish_time按時(shí)間分表,兩者缺一不可乒省。

原理

假設(shè)文章表article我們是以publish_time作為分片字段,假設(shè)按天分表,那么我們會(huì)擁有如下的表

article_20220101畦木、article_20220102袖扛、article_20220103、article_20220104馋劈、article_20220105攻锰、article_20220106......

雪花id輔助分片

因?yàn)?code>雪花id可以反解析出時(shí)間,所以我們對(duì)雪花id的=,>=,>,<=,<,contains的操作都是可以進(jìn)行輔助分片進(jìn)行縮小分片范圍
假設(shè)我們的雪花id解析出來(lái)是2021-01-05 11:11:11,那么針對(duì)這個(gè)雪花id<小于操作我們可以等價(jià)于x < 2021-01-05 11:11:11,那么如果我問(wèn)你這下我們需要查詢的表有哪些,很明顯 [article_20220101、article_20220102妓雾、article_20220103娶吞、article_20220104、article_20220105]械姻,除了20220106外我們都需要查詢妒蛇。

union all分片模式

如果你使用union all的分片模式那么通常會(huì)將20220101-20220105的所有的表進(jìn)行union all然后機(jī)械能過(guò)濾,那么優(yōu)點(diǎn)可想而知:簡(jiǎn)單,連接數(shù)消耗僅1個(gè),sql語(yǔ)句支持的多,缺點(diǎn)也顯而易見(jiàn),優(yōu)化起來(lái)后期是個(gè)很大的問(wèn)題,并且跨庫(kù)下的使用有問(wèn)題

select * from 
(select * from article_20220101 union all select * from article_20220102 union all select * from article_20220103....) t
 where id<last_id order by publish_time desc limit 0,20

流式分片,順序查詢

如果你是流式分片模式進(jìn)行聚合通常我們會(huì)將20220101-20220105的所有的表進(jìn)行并行的分別查詢,然后針對(duì)每個(gè)查詢的結(jié)果集進(jìn)行優(yōu)先級(jí)隊(duì)列的排序后獲取,優(yōu)點(diǎn):語(yǔ)句簡(jiǎn)單便于優(yōu)化,性能可控,支持分庫(kù),缺點(diǎn):實(shí)現(xiàn)復(fù)雜,連接數(shù)消耗多

select * from article_20220101 where id<last_id order by publish_time desc limit 0,20
select * from article_20220102where id<last_id order by publish_time desc limit 0,20
select * from article_20220103 where id<last_id order by publish_time desc limit 0,20
......

流式分片下的優(yōu)化

目前 ShardingCore采用的是流式聚合+union all,當(dāng)且僅當(dāng)用戶手動(dòng)3調(diào)用UseUnionAllMerge時(shí)會(huì)將分片sql轉(zhuǎn)成union all 聚合楷拳。

針對(duì)上述瀑布流的分頁(yè)ShardingCore是這么操作的

  • 確定分片表的順序,也就是因?yàn)榉制侄问?code>publish_time,又因?yàn)榕判蜃侄问?code>publish_time所以分片表其實(shí)是有順序的,也就是[article_20220105绣夺、article_20220104、article_20220103欢揖、article_20220102陶耍、article_20220101],
    因?yàn)槲覀兪情_(kāi)啟n個(gè)并發(fā)線程所以這個(gè)排序可能沒(méi)有意義,但是如果我們是僅開(kāi)啟設(shè)置單個(gè)連接并發(fā)的時(shí)候,程序?qū)F(xiàn)在通過(guò)id<last_id進(jìn)行表篩選,之后依次從大到小進(jìn)行獲取直到滿足skip+take也就是0+20=20條數(shù)據(jù)后,進(jìn)行直接拋棄剩余查詢返回結(jié)果,那么本次查詢基本上就是和單表查詢一樣,因?yàn)榛旧献疃嗫鐑蓮埍砘究梢詽M足要求(具體場(chǎng)景不一定)
  • 說(shuō)明:假設(shè)last_id反解析出來(lái)的結(jié)果是2022-01-04 05:05:05那么可以基本上排除article_20220105,判斷并發(fā)連接數(shù)如果是1那么直接查詢article_20220104,如果不滿足繼續(xù)查詢article_20220103,直到查詢結(jié)果為20條如果并發(fā)連接數(shù)是2那么查詢[article_20220104她混、article_20220103]如果不滿足繼續(xù)下面兩張表直到獲取到結(jié)果為20條數(shù)據(jù),所以我們可以很清晰的了解其工作原理并且來(lái)優(yōu)化

說(shuō)明

  • 通過(guò)上述優(yōu)化可以保證流式聚合查詢?cè)陧樞虿樵兿碌母咝阅躉(1)
  • 通過(guò)上述優(yōu)化可以保證客戶端分片擁有最小化連接數(shù)控制
  • 設(shè)置合理的主鍵可以有效的解決我們?cè)诖髷?shù)據(jù)分片下的性能優(yōu)化

實(shí)踐

ShardingCore目前針對(duì)分片查詢進(jìn)行了不斷地優(yōu)化和盡可能的無(wú)業(yè)務(wù)代碼入侵來(lái)實(shí)現(xiàn)高性能分片查詢聚合烈钞。

接下來(lái)我將為大家展示一款dotnet下唯一一款全自動(dòng)路由、多字段分片坤按、無(wú)代碼入侵毯欣、高性能順序查詢的框架在傳統(tǒng)數(shù)據(jù)庫(kù)領(lǐng)域下的分片功能,如果你使用過(guò)我相信你一定會(huì)愛(ài)上他。

第一步:安裝依賴

# ShardingCore核心框架 版本6.4.2.4+
PM> Install-Package ShardingCore
# 數(shù)據(jù)庫(kù)驅(qū)動(dòng)這邊選擇的是mysql的社區(qū)驅(qū)動(dòng) efcore6最新版本即可
PM> Install-Package Pomelo.EntityFrameworkCore.MySql

第二步添加對(duì)象和上下文

有很多朋友問(wèn)我一定需要使用fluentapi來(lái)使用ShardingCore嗎,只是個(gè)人喜好,這邊我才用dbset+attribute來(lái)實(shí)現(xiàn)

//文章表
    [Table(nameof(Article))]
    public class Article
    {
        [MaxLength(128)]
        [Key]
        public string Id { get; set; }
        [MaxLength(128)]
        [Required]
        public string Title { get; set; }
        [MaxLength(256)]
        [Required]
        public string Content { get; set; }
        
        public DateTime PublishTime { get; set; }
    }
//dbcontext
    public class MyDbContext:AbstractShardingDbContext,IShardingTableDbContext
    {
        public MyDbContext(DbContextOptions<MyDbContext> options) : base(options)
        {
//請(qǐng)勿添加會(huì)導(dǎo)致efcore 的model提前加載的方法如Database.xxxx
        }

        public IRouteTail RouteTail { get; set; }
        
        public DbSet<Article> Articles { get; set; }
    }

第三步:添加文章路由


    public class ArticleRoute:AbstractSimpleShardingDayKeyDateTimeVirtualTableRoute<Article>
    {
        public override void Configure(EntityMetadataTableBuilder<Article> builder)
        {
            builder.ShardingProperty(o => o.PublishTime);
        }

        public override bool AutoCreateTableByTime()
        {
            return true;
        }

        public override DateTime GetBeginTime()
        {
            return new DateTime(2022, 3, 1);
        }
    }

到目前為止基本上Article已經(jīng)支持了按天分表

第四步:添加查詢配置,讓框架知道我們是順序分表且定義分表的順序


    public class TailDayReverseComparer : IComparer<string>
    {
        public int Compare(string? x, string? y)
        {
            //程序默認(rèn)使用的是正序也就是按時(shí)間正序排序我們需要使用倒序所以直接調(diào)用原生的比較器然后乘以負(fù)一即可
            return Comparer<string>.Default.Compare(x, y) * -1;
        }
    }
    //當(dāng)前查詢滿足的復(fù)核條件必須是單個(gè)分片對(duì)象的查詢,可以join普通非分片表
    public class ArticleEntityQueryConfiguration:IEntityQueryConfiguration<Article>
    {
        public void Configure(EntityQueryBuilder<Article> builder)
        {
            //設(shè)置默認(rèn)的框架針對(duì)Article的排序順序,這邊設(shè)置的是倒序
            builder.ShardingTailComparer(new TailDayReverseComparer());
            ////如下設(shè)置和上述是一樣的效果讓框架真對(duì)Article的后綴排序使用倒序
            //builder.ShardingTailComparer(Comparer<string>.Default, false);
            
            //簡(jiǎn)單解釋一下下面這個(gè)配置的意思
            //第一個(gè)參數(shù)表名Article的哪個(gè)屬性是順序排序和Tail按天排序是一樣的這邊使用了PublishTime
            //第二個(gè)參數(shù)表示對(duì)屬性PublishTime asc時(shí)是否和上述配置的ShardingTailComparer一致,true表示一致,很明顯這邊是相反的因?yàn)槟J(rèn)已經(jīng)設(shè)置了tail排序是倒序
            //第三個(gè)參數(shù)表示是否是Article屬性才可以,這邊設(shè)置的是名稱一樣也可以,因?yàn)榭紤]到匿名對(duì)象的select
            builder.AddOrder(o => o.PublishTime, false,SeqOrderMatchEnum.Owner|SeqOrderMatchEnum.Named);
            //這邊為了演示使用的id是簡(jiǎn)單的時(shí)間格式化所以和時(shí)間的配置一樣
            builder.AddOrder(o => o.Id, false,SeqOrderMatchEnum.Owner|SeqOrderMatchEnum.Named);
            //這邊設(shè)置如果本次查詢默認(rèn)沒(méi)有帶上述配置的order的時(shí)候才用何種排序手段
            //第一個(gè)參數(shù)表示是否和ShardingTailComparer配置的一樣,目前配置的是倒序,也就是從最近時(shí)間開(kāi)始查詢,如果是false就是從最早的時(shí)間開(kāi)始查詢
            //后面配置的是熔斷器,也就是復(fù)核熔斷條件的比如FirstOrDefault只需要滿足一個(gè)就可以熔斷
            builder.AddDefaultSequenceQueryTrip(true, CircuitBreakerMethodNameEnum.Enumerator, CircuitBreakerMethodNameEnum.FirstOrDefault);

            //這邊配置的是當(dāng)使用順序查詢配置的時(shí)候默認(rèn)開(kāi)啟的連接數(shù)限制是多少,startup一開(kāi)始可以設(shè)置一個(gè)默認(rèn)是當(dāng)前cpu的線程數(shù),這邊優(yōu)化到只需要一個(gè)線程即可,當(dāng)然如果跨表那么就是串行執(zhí)行
            builder.AddConnectionsLimit(1, LimitMethodNameEnum.Enumerator, LimitMethodNameEnum.FirstOrDefault);
        }
    }

第五步:添加配置到路由


    public class ArticleRoute:AbstractSimpleShardingDayKeyDateTimeVirtualTableRoute<Article>
    {
        //省略.....
        public override IEntityQueryConfiguration<Article> CreateEntityQueryConfiguration()
        {
            return new ArticleEntityQueryConfiguration();
        }
    }

第六步:startup配置


var builder = WebApplication.CreateBuilder(args);

// Add services to the container.
ILoggerFactory efLogger = LoggerFactory.Create(builder =>
{
    builder.AddFilter((category, level) => category == DbLoggerCategory.Database.Command.Name && level == LogLevel.Information).AddConsole();
});
builder.Services.AddControllers();
builder.Services.AddShardingDbContext<MyDbContext>()
    .AddEntityConfig(o =>
    {
        o.CreateShardingTableOnStart = true;
        o.EnsureCreatedWithOutShardingTable = true;
        o.AddShardingTableRoute<ArticleRoute>();
    })
    .AddConfig(o =>
    {
        o.ConfigId = "c1";
        o.UseShardingQuery((conStr, b) =>
        {
            b.UseMySql(conStr, new MySqlServerVersion(new Version())).UseLoggerFactory(efLogger);
        });
        o.UseShardingTransaction((conn, b) =>
        {
            b.UseMySql(conn, new MySqlServerVersion(new Version())).UseLoggerFactory(efLogger);
        });
        o.AddDefaultDataSource("ds0", "server=127.0.0.1;port=3306;database=ShardingWaterfallDB;userid=root;password=root;");
        o.ReplaceTableEnsureManager(sp => new MySqlTableEnsureManager<MyDbContext>());
    }).EnsureConfig();

var app = builder.Build();

app.Services.GetRequiredService<IShardingBootstrapper>().Start();
using (var scope = app.Services.CreateScope())
{
    var myDbContext = scope.ServiceProvider.GetRequiredService<MyDbContext>();
    if (!myDbContext.Articles.Any())
    {
        List<Article> articles = new List<Article>();
        var beginTime = new DateTime(2022, 3, 1, 1, 1,1);
        for (int i = 0; i < 70; i++)
        {
            var article = new Article();
            article.Id = beginTime.ToString("yyyyMMddHHmmss");
            article.Title = "標(biāo)題" + i;
            article.Content = "內(nèi)容" + i;
            article.PublishTime = beginTime;
            articles.Add(article);
            beginTime= beginTime.AddHours(2).AddMinutes(3).AddSeconds(4);
        }
        myDbContext.AddRange(articles);
        myDbContext.SaveChanges();
    }
}
app.MapControllers();

app.Run();

第七步編寫(xiě)查詢表達(dá)式


    public async Task<IActionResult> Waterfall([FromQuery] string lastId,[FromQuery]int take)
    {
        Console.WriteLine($"-----------開(kāi)始查詢,lastId:[{lastId}],take:[{take}]-----------");
        var list = await _myDbContext.Articles.WhereIf(o => String.Compare(o.Id, lastId) < 0,!string.IsNullOrWhiteSpace(lastId)).Take(take)..OrderByDescending(o => o.PublishTime)ToListAsync();
        return Ok(list);
    }

運(yùn)行程序


image.png

因?yàn)?7表是沒(méi)有的所以這次查詢會(huì)查詢07和06表,之后我們進(jìn)行下一次分頁(yè)傳入上次id


image.png

因?yàn)闆](méi)有對(duì)Article.Id進(jìn)行分片路由的規(guī)則編寫(xiě)所以沒(méi)辦法進(jìn)行對(duì)id的過(guò)濾,那么接下來(lái)我們配置Id的分片規(guī)則

首先針對(duì)ArticleRoute進(jìn)行代碼編寫(xiě)


    public class ArticleRoute:AbstractSimpleShardingDayKeyDateTimeVirtualTableRoute<Article>
    {
        public override void Configure(EntityMetadataTableBuilder<Article> builder)
        {
            builder.ShardingProperty(o => o.PublishTime);
            builder.ShardingExtraProperty(o => o.Id);
        }

        public override bool AutoCreateTableByTime()
        {
            return true;
        }

        public override DateTime GetBeginTime()
        {
            return new DateTime(2022, 3, 1);
        }

        public override IEntityQueryConfiguration<Article> CreateEntityQueryConfiguration()
        {
            return new ArticleEntityQueryConfiguration();
        }

        public override Expression<Func<string, bool>> GetExtraRouteFilter(object shardingKey, ShardingOperatorEnum shardingOperator, string shardingPropertyName)
        {
            switch (shardingPropertyName)
            {
                case nameof(Article.Id): return GetArticleIdRouteFilter(shardingKey, shardingOperator);
            }

          return base.GetExtraRouteFilter(shardingKey, shardingOperator, shardingPropertyName);
        }
        /// <summary>
        /// 文章id的路由
        /// </summary>
        /// <param name="shardingKey"></param>
        /// <param name="shardingOperator"></param>
        /// <returns></returns>
        private Expression<Func<string, bool>> GetArticleIdRouteFilter(object shardingKey,
            ShardingOperatorEnum shardingOperator)
        {
            //將分表字段轉(zhuǎn)成訂單編號(hào)
            var id = shardingKey?.ToString() ?? string.Empty;
            //判斷訂單編號(hào)是否是我們符合的格式
            if (!CheckArticleId(id, out var orderTime))
            {
                //如果格式不一樣就直接返回false那么本次查詢因?yàn)槭莂nd鏈接的所以本次查詢不會(huì)經(jīng)過(guò)任何路由,可以有效的防止惡意攻擊
                return tail => false;
            }

            //當(dāng)前時(shí)間的tail
            var currentTail = TimeFormatToTail(orderTime);
            //因?yàn)槭前丛路直硭垣@取下個(gè)月的時(shí)間判斷id是否是在臨界點(diǎn)創(chuàng)建的
            //var nextMonthFirstDay = ShardingCoreHelper.GetNextMonthFirstDay(DateTime.Now);//這個(gè)是錯(cuò)誤的
            var nextMonthFirstDay = ShardingCoreHelper.GetNextMonthFirstDay(orderTime);
            if (orderTime.AddSeconds(10) > nextMonthFirstDay)
            {
                var nextTail = TimeFormatToTail(nextMonthFirstDay);
                return DoArticleIdFilter(shardingOperator, orderTime, currentTail, nextTail);
            }
            //因?yàn)槭前丛路直硭垣@取這個(gè)月月初的時(shí)間判斷id是否是在臨界點(diǎn)創(chuàng)建的
            //if (orderTime.AddSeconds(-10) < ShardingCoreHelper.GetCurrentMonthFirstDay(DateTime.Now))//這個(gè)是錯(cuò)誤的
            if (orderTime.AddSeconds(-10) < ShardingCoreHelper.GetCurrentMonthFirstDay(orderTime))
            {
                //上個(gè)月tail
                var previewTail = TimeFormatToTail(orderTime.AddSeconds(-10));

                return DoArticleIdFilter(shardingOperator, orderTime, previewTail, currentTail);
            }

            return DoArticleIdFilter(shardingOperator, orderTime, currentTail, currentTail);

        }

        private Expression<Func<string, bool>> DoArticleIdFilter(ShardingOperatorEnum shardingOperator, DateTime shardingKey, string minTail, string maxTail)
        {
            switch (shardingOperator)
            {
                case ShardingOperatorEnum.GreaterThan:
                case ShardingOperatorEnum.GreaterThanOrEqual:
                    {
                        return tail => String.Compare(tail, minTail, StringComparison.Ordinal) >= 0;
                    }

                case ShardingOperatorEnum.LessThan:
                    {
                        var currentMonth = ShardingCoreHelper.GetCurrentMonthFirstDay(shardingKey);
                        //處于臨界值 o=>o.time < [2021-01-01 00:00:00] 尾巴20210101不應(yīng)該被返回
                        if (currentMonth == shardingKey)
                            return tail => String.Compare(tail, maxTail, StringComparison.Ordinal) < 0;
                        return tail => String.Compare(tail, maxTail, StringComparison.Ordinal) <= 0;
                    }
                case ShardingOperatorEnum.LessThanOrEqual:
                    return tail => String.Compare(tail, maxTail, StringComparison.Ordinal) <= 0;
                case ShardingOperatorEnum.Equal:
                    {
                        var isSame = minTail == maxTail;
                        if (isSame)
                        {
                            return tail => tail == minTail;
                        }
                        else
                        {
                            return tail => tail == minTail || tail == maxTail;
                        }
                    }
                default:
                    {
                        return tail => true;
                    }
            }
        }

        private bool CheckArticleId(string orderNo, out DateTime orderTime)
        {
            //yyyyMMddHHmmss
            if (orderNo.Length == 14)
            {
                if (DateTime.TryParseExact(orderNo, "yyyyMMddHHmmss", CultureInfo.InvariantCulture,
                        DateTimeStyles.None, out var parseDateTime))
                {
                    orderTime = parseDateTime;
                    return true;
                }
            }

            orderTime = DateTime.MinValue;
            return false;
        }
    }

完整路由:針對(duì)Id進(jìn)行多字段分片并且支持大于小于排序

以上是多字段分片的優(yōu)化,詳情博客可以點(diǎn)擊這邊 .Net下你不得不看的分表分庫(kù)解決方案-多字段分片

然后我們繼續(xù)查詢看看結(jié)果


image.png

第三頁(yè)也是如此


image.png

demo

DEMO

總結(jié)

當(dāng)前框架雖然是一個(gè)很年輕的框架,但是我相信我對(duì)其在分片領(lǐng)域的性能優(yōu)化應(yīng)該在.net現(xiàn)有的所有框架下找不出第二個(gè),并且框架整個(gè)也支持union all聚合,可以滿足列入group+first的特殊語(yǔ)句的查詢,又有很高的性能,一個(gè)不但是全自動(dòng)分片而且還是高性能框架擁有非常多的特性性能,目標(biāo)是榨干客戶端分片的最后一點(diǎn)性能臭脓。

MAKE DOTNET GREAT AGAIN

最后的最后

身位一個(gè)dotnet程序員我相信在之前我們的分片選擇方案除了mycatshardingsphere-proxy外沒(méi)有一個(gè)很好的分片選擇,但是我相信通過(guò)ShardingCore 的原理解析,你不但可以了解到大數(shù)據(jù)下分片的知識(shí)點(diǎn),更加可以參與到其中或者自行實(shí)現(xiàn)一個(gè),我相信只有了解了分片的原理dotnet才會(huì)有更好的人才和未來(lái),我們不但需要優(yōu)雅的封裝,更需要原理的是對(duì)原理了解酗钞。

我相信未來(lái)dotnet的生態(tài)會(huì)慢慢起來(lái)配上這近乎完美的語(yǔ)法

您的支持是開(kāi)源作者能堅(jiān)持下去的最大動(dòng)力


博客

QQ群:771630778

個(gè)人QQ:326308290(歡迎技術(shù)支持提供您寶貴的意見(jiàn))

個(gè)人郵箱:326308290@qq.com

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市来累,隨后出現(xiàn)的幾起案子砚作,更是在濱河造成了極大的恐慌,老刑警劉巖嘹锁,帶你破解...
    沈念sama閱讀 222,681評(píng)論 6 517
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件偎巢,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡兼耀,警方通過(guò)查閱死者的電腦和手機(jī)压昼,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 95,205評(píng)論 3 399
  • 文/潘曉璐 我一進(jìn)店門求冷,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人窍霞,你說(shuō)我怎么就攤上這事匠题。” “怎么了但金?”我有些...
    開(kāi)封第一講書(shū)人閱讀 169,421評(píng)論 0 362
  • 文/不壞的土叔 我叫張陵韭山,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我冷溃,道長(zhǎng)钱磅,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 60,114評(píng)論 1 300
  • 正文 為了忘掉前任似枕,我火速辦了婚禮盖淡,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘凿歼。我一直安慰自己褪迟,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 69,116評(píng)論 6 398
  • 文/花漫 我一把揭開(kāi)白布答憔。 她就那樣靜靜地躺著味赃,像睡著了一般。 火紅的嫁衣襯著肌膚如雪虐拓。 梳的紋絲不亂的頭發(fā)上心俗,一...
    開(kāi)封第一講書(shū)人閱讀 52,713評(píng)論 1 312
  • 那天,我揣著相機(jī)與錄音蓉驹,去河邊找鬼另凌。 笑死,一個(gè)胖子當(dāng)著我的面吹牛戒幔,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播土童,決...
    沈念sama閱讀 41,170評(píng)論 3 422
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼诗茎,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了献汗?” 一聲冷哼從身側(cè)響起敢订,我...
    開(kāi)封第一講書(shū)人閱讀 40,116評(píng)論 0 277
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎罢吃,沒(méi)想到半個(gè)月后楚午,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,651評(píng)論 1 320
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡尿招,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,714評(píng)論 3 342
  • 正文 我和宋清朗相戀三年矾柜,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了阱驾。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,865評(píng)論 1 353
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡怪蔑,死狀恐怖里覆,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情缆瓣,我是刑警寧澤喧枷,帶...
    沈念sama閱讀 36,527評(píng)論 5 351
  • 正文 年R本政府宣布,位于F島的核電站弓坞,受9級(jí)特大地震影響隧甚,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜渡冻,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 42,211評(píng)論 3 336
  • 文/蒙蒙 一戚扳、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧菩帝,春花似錦咖城、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 32,699評(píng)論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至握础,卻和暖如春辐董,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背禀综。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,814評(píng)論 1 274
  • 我被黑心中介騙來(lái)泰國(guó)打工简烘, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人定枷。 一個(gè)月前我還...
    沈念sama閱讀 49,299評(píng)論 3 379
  • 正文 我出身青樓孤澎,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親欠窒。 傳聞我的和親對(duì)象是個(gè)殘疾皇子覆旭,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,870評(píng)論 2 361

推薦閱讀更多精彩內(nèi)容