<output id="qn6qe"></output>

    1. <output id="qn6qe"><tt id="qn6qe"></tt></output>
    2. <strike id="qn6qe"></strike>

      亚洲 日本 欧洲 欧美 视频,日韩中文字幕有码av,一本一道av中文字幕无码,国产线播放免费人成视频播放,人妻少妇偷人无码视频,日夜啪啪一区二区三区,国产尤物精品自在拍视频首页,久热这里只有精品12

      我來說說讀寫分離,就是數(shù)據(jù)庫讀寫分離在ORM中是如何實(shí)現(xiàn)的,附上源碼

      現(xiàn)狀

      你去檢索下讀寫分離,一大堆告訴你,寫,更改,刪除,走主數(shù)據(jù)庫,讀走從數(shù)據(jù)庫
      我要說的是,這個(gè)完全錯(cuò)誤的

      原因

      其實(shí)但從字面來說,上面也是很有道理的,但是問題就出現(xiàn)在現(xiàn)實(shí)上

      延遲問題

      主從備份,主數(shù)據(jù)庫,從數(shù)據(jù)庫,我們都知道,是往主數(shù)據(jù)庫寫入,從數(shù)據(jù)庫自動(dòng)從主數(shù)據(jù)庫復(fù)制數(shù)據(jù),但是,這里是有延遲的,而且延遲還不低,不說多吧,100ms是很常見的!當(dāng)然你服務(wù)器牛叉,可以做到1ms,那也不能說他沒延遲,只是延遲多少的問題!

      最大問題

      讀寫分離最大的問題,不是讀寫分離如何寫,而是如何處理實(shí)際情況,比如寫入了,更新了,你讀取的時(shí)候還沒有讀取到,讀取到的是舊的?。。?/p>

      總結(jié)

      所以說,如果你看到讀寫分離,一股腦的用讀只走從數(shù)據(jù)庫的,可以直接劃走了!

      DeepSeek的說法

      您舉的這個(gè)例子太典型了!這正好說明了讀寫分離不能簡(jiǎn)單粗暴地按照"所有讀走從庫"來設(shè)計(jì)

      您說的這個(gè)場(chǎng)景的致命問題

      // 邀約注冊(cè) + 立即登錄場(chǎng)景
      public class InvitationService
      {
          // 接口1:邀約自動(dòng)注冊(cè)(寫主庫)
          public async Task<IActionResult> AutoRegisterFromInvitation(string invitationCode)
          {
              // 驗(yàn)證邀約碼,創(chuàng)建用戶(寫主庫)
              var user = new User { 
                  Username = GenerateUsername(), 
                  Password = HashPassword("temp123"),
                  InvitationCode = invitationCode
              };
              
              _dbContext.Users.Add(user);
              await _dbContext.SaveChangesAsync(); // 寫入主庫
              
              // 返回成功,前端自動(dòng)跳轉(zhuǎn)到登錄頁
              return Ok(new { success = true, userId = user.Id });
          }
      
          // 接口2:用戶立即登錄(如果強(qiáng)制走從庫就完蛋了?。?    public async Task<IActionResult> Login(string username, string password)
          {
              // 問題:這里如果強(qiáng)制走從庫查詢
              var user = await _readOnlyDbContext.Users
                  .FirstOrDefaultAsync(u => u.Username == username);
                  
              if (user == null)
              {
                  // 用戶看到:"剛注冊(cè)成功,現(xiàn)在說用戶不存在?"
                  return BadRequest("用戶不存在!");
              }
              
              // 驗(yàn)證密碼...
              return Ok(new { token = GenerateToken(user) });
          }
      }
      

      現(xiàn)實(shí)中的連鎖問題

      1. 新用戶注冊(cè)后立即操作

      // 用戶注冊(cè)后立即完善資料
      public async Task CompleteProfile(int userId, UserProfile profile)
      {
          // 如果這里走從庫查詢用戶
          var user = await _readOnlyDbContext.Users.FindAsync(userId);
          // 可能為null!用戶剛注冊(cè),從庫還沒同步
          
          user.Profile = profile;
          await _dbContext.SaveChangesAsync(); // 又寫主庫
          // 如果user為null,這里直接異常!
      }
      

      2. 訂單創(chuàng)建后立即查詢

      public async Task<IActionResult> CreateOrder(Order order)
      {
          // 創(chuàng)建訂單(主庫)
          _dbContext.Orders.Add(order);
          await _dbContext.SaveChangesAsync();
          
          // 立即返回訂單詳情給前端
          // 如果前端立即調(diào)用查詢訂單接口(走從庫)
          // 可能查不到剛創(chuàng)建的訂單!
          return Ok(new { orderId = order.Id });
      }
      
      // 前端代碼
      const createOrder = async () => {
          await api.post('/orders', orderData); // 創(chuàng)建成功
          const result = await api.get(`/orders/${orderId}`); // 立即查詢
          // 可能返回404!用戶:"我剛剛創(chuàng)建的訂單呢?"
      }
      

      正確的解決方案

      方案一:基于業(yè)務(wù)語義的路由

      public class SmartConnectionRouter
      {
          public bool ShouldUseReadOnlyDb(HttpContext context)
          {
              var endpoint = context.GetEndpoint();
              
              // 這些場(chǎng)景必須走主庫(即使它們是讀操作?。?        var criticalReadOperations = new[]
              {
                  "/auth/login",           // 登錄
                  "/users/{id}/profile",   // 用戶詳情
                  "/orders/{id}",          // 訂單詳情
                  "/payments/status"       // 支付狀態(tài)
              };
              
              // 這些場(chǎng)景可以走從庫(容忍延遲)
              var tolerantReadOperations = new[]
              {
                  "/products/list",        // 商品列表
                  "/articles",             // 文章列表
                  "/statistics/report"     // 統(tǒng)計(jì)報(bào)表
              };
              
              var path = context.Request.Path.ToString();
              return tolerantReadOperations.Any(p => path.Contains(p)) && 
                     !criticalReadOperations.Any(p => path.Contains(p));
          }
      }
      

      方案二:用戶會(huì)話級(jí)別的路由

      public class UserAwareConnectionSelector
      {
          public string GetConnectionString(HttpContext context)
          {
              // 新注冊(cè)用戶:短時(shí)間內(nèi)強(qiáng)制主庫
              if (context.User.IsNewlyRegistered())
              {
                  return "Default"; // 主庫
              }
              
              // 關(guān)鍵操作:強(qiáng)制主庫
              if (IsCriticalOperation(context))
              {
                  return "Default"; // 主庫
              }
              
              // 其他情況:可以從庫
              return "ReadOnly"; // 從庫
          }
          
          private bool IsCriticalOperation(HttpContext context)
          {
              var criticalPaths = new[]
              {
                  "/auth/", "/users/", "/orders/", "/payments/"
              };
              
              var path = context.Request.Path.ToString();
              return criticalPaths.Any(p => path.StartsWith(p));
          }
      }
      

      方案三:寫操作后的讀強(qiáng)制主庫

      public class WriteAwareConnectionMiddleware
      {
          public async Task InvokeAsync(HttpContext context, IConnectionStringSelector selector)
          {
              // 如果是寫操作,設(shè)置標(biāo)記
              if (context.Request.Method != "GET")
              {
                  context.Items["ForceMasterDb"] = true;
                  selector.SetConnectionStringName("Default");
              }
              else
              {
                  // 讀操作:檢查是否需要強(qiáng)制主庫
                  var forceMaster = context.Items["ForceMasterDb"] as bool? ?? false;
                  if (forceMaster || IsCriticalReadOperation(context))
                  {
                      selector.SetConnectionStringName("Default");
                  }
                  else
                  {
                      selector.SetConnectionStringName("ReadOnly");
                  }
              }
              
              await _next(context);
          }
      }
      

      現(xiàn)實(shí)中的妥協(xié)

      // 實(shí)際項(xiàng)目中,我們通常這樣做:
      public class PracticalApproach
      {
          // 1. 關(guān)鍵業(yè)務(wù)路徑全部主庫
          // - 用戶認(rèn)證相關(guān)
          // - 訂單交易相關(guān)  
          // - 支付相關(guān)
          // - 庫存相關(guān)
          
          // 2. 非關(guān)鍵業(yè)務(wù)走從庫
          // - 商品瀏覽
          // - 內(nèi)容展示
          // - 報(bào)表統(tǒng)計(jì)
          // - 搜索建議
          
          // 3. 新用戶特殊處理
          // - 注冊(cè)后30分鐘內(nèi)強(qiáng)制主庫
          // - 關(guān)鍵操作后短時(shí)間內(nèi)強(qiáng)制主庫
      }
      

      結(jié)論

      您說得對(duì)!讀寫分離不能簡(jiǎn)單地按照"所有讀走從庫"來設(shè)計(jì),必須:

      1. 理解業(yè)務(wù)語義 - 哪些讀操作對(duì)實(shí)時(shí)性要求高
      2. 識(shí)別數(shù)據(jù)流 - 寫操作后哪些讀操作會(huì)立即發(fā)生
      3. 接受復(fù)雜性 - 路由邏輯會(huì)變得復(fù)雜,但這是必要的
      4. 監(jiān)控和調(diào)整 - 根據(jù)實(shí)際運(yùn)行情況不斷優(yōu)化路由策略

      那種"一刀切"的讀寫分離方案,在實(shí)際業(yè)務(wù)中會(huì)帶來很多您提到的這種數(shù)據(jù)不一致問題。合理的讀寫分離是藝術(shù),不是簡(jiǎn)單的技術(shù)配置。

      PasteForm中的讀寫方案

      由于最近一個(gè)項(xiàng)目要考慮讀寫分離的問題,之前的都比較小,都是直接主庫操作,所以對(duì)PasteForm的做了一個(gè)改版

      PasteForm框架介紹

      PasteForm是一個(gè)基于ABP的敏捷開發(fā)框架,核心思想是通過對(duì)Dto進(jìn)行標(biāo)注特性,讓管理端完全交給后端,然你體驗(yàn)啥叫敏捷開發(fā)?。。?/p>

      原理說明

      上面說到了讀寫分離,在這個(gè)框架中,我主要用dbContext的方式實(shí)現(xiàn)數(shù)據(jù)庫的相關(guān)操作,別問為啥不用倉儲(chǔ),我感覺倉儲(chǔ)的存在很奇怪,或者說不夠直接,不夠靈活!

      思路一

      和其他文章一樣,在讀取的時(shí)候走從數(shù)據(jù)庫,在其他操作上走主數(shù)據(jù)庫,但是這個(gè)想法直接就被斃了,因?yàn)檫@個(gè)方案完全用不了,和業(yè)務(wù)需求完全沖突!

      思路二

      既然思路一走不通,那就換一個(gè)方式
      其實(shí)在實(shí)際開發(fā)中,幾乎的項(xiàng)目很多是走主庫的,很少走從的,為啥呢?這里說的多少是接口,不是說訪問次數(shù)哈!
      那就換一個(gè)思路,
      讓開發(fā)者主動(dòng)標(biāo)記,我這個(gè)Action走從庫還是走主庫,上面說的走從庫的少,那么我就默認(rèn)走主庫
      這個(gè)思路我覺得是可行的,而且問了AI,也是肯定答復(fù),那么問題就剩下如何寫和測(cè)試了!

      請(qǐng)看PasteFormDbContext的代碼

          /// <summary>
          /// 
          /// </summary>
          [ConnectionStringName(PasteFormDbProperties.ConnectionStringName)]
          public class PasteFormDbContext : AbpDbContext<PasteFormDbContext>, IPasteFormDbContext
          {
              /* Add DbSet for each Aggregate Root here. Example:
               * public DbSet<Question> Questions { get; set; }
               */
      
              /// <summary>
              /// 
              /// </summary>
              /// <param name="options"></param>
              /// <param name="currentUser"></param>
              public PasteFormDbContext(DbContextOptions<PasteFormDbContext> options)
                  : base(options)
              {
      
              }
      		//其他代碼
      }
      

      發(fā)現(xiàn)沒有,有一個(gè)過濾器

      ConnectionStringName
      

      其他沒有設(shè)置鏈接串的地方,
      如果你查看這個(gè)過濾器的源碼,你會(huì)發(fā)覺里面也沒有寫啥

      public class ConnectionStringNameAttribute : Attribute
      {
          public string Name { get; }
      
          public ConnectionStringNameAttribute(string name)
          {
              Check.NotNull<string>(name, "name");
              Name = name;
          }
      
          public static string GetConnStringName<T>()
          {
              return GetConnStringName(typeof(T));
          }
      
          public static string GetConnStringName(Type type)
          {
              ConnectionStringNameAttribute customAttribute = type.GetTypeInfo().GetCustomAttribute<ConnectionStringNameAttribute>();
              if (customAttribute == null)
              {
                  return type.FullName;
              }
      
              return customAttribute.Name;
          }
      }
      

      也就是說,執(zhí)行數(shù)據(jù)庫鏈接串寫入到dbContext的不是他,他只是做一個(gè)標(biāo)記
      然后我找到了這個(gè)DefaultConnectionStringResolver

      public class DefaultConnectionStringResolver : IConnectionStringResolver, ITransientDependency
      {
          protected AbpDbConnectionOptions Options { get; }
      
          public DefaultConnectionStringResolver(IOptionsMonitor<AbpDbConnectionOptions> options)
          {
              Options = options.CurrentValue;
          }
      
          [Obsolete("Use ResolveAsync method.")]
          public virtual string Resolve(string? connectionStringName = null)
          {
              return ResolveInternal(connectionStringName);
          }
      
          public virtual Task<string> ResolveAsync(string? connectionStringName = null)
          {
              return Task.FromResult(ResolveInternal(connectionStringName));
          }
      
          private string? ResolveInternal(string? connectionStringName)
          {
              if (connectionStringName == null)
              {
                  return Options.ConnectionStrings.Default;
              }
      
              string connectionStringOrNull = Options.GetConnectionStringOrNull(connectionStringName);
              if (!connectionStringOrNull.IsNullOrEmpty())
              {
                  return connectionStringOrNull;
              }
      
              return null;
          }
      }
      

      我們來看看這個(gè)AI的解釋

      DefaultConnectionStringResolver 是ABP框架數(shù)據(jù)訪問層的一個(gè)基礎(chǔ)且關(guān)鍵的組件,它優(yōu)雅地處理了連接字符串的管理問題,為應(yīng)用程序特別是多租戶應(yīng)用程序提供了強(qiáng)大的靈活性。
      

      上面的代碼意思是什么呢?
      在ABP中,鏈接串還有一個(gè)東西叫名稱,上面的意思就是基于傳入的名稱,返回給調(diào)用方鏈接具體字符串!
      注意看他注入的生命周期,是瞬時(shí)的,那么我們不就可以改變這個(gè),讓讀取的時(shí)候,基于上下文返回字符串,而不是從傳入的名稱!

      綜上

      從上面信息,那么問題就變成了,我如何基于上下文,給dbContext喂不一樣的連接字符串,或者說基于上下文給不一樣的dbContext

      問題又來了,
      如果你看一個(gè)Action,你會(huì)發(fā)現(xiàn),在Action的過濾器執(zhí)行前,Controller的構(gòu)造函數(shù)已經(jīng)執(zhí)行了
      也就是生命周期的順序不對(duì),都已經(jīng)執(zhí)行dbContext的初始化了,你才想改他的鏈接字符串
      那么我們就換一個(gè),換成更早的,更底層的中間件

          /// <summary>
          /// 
          /// </summary>
          public class ConnectionStringMiddleware
          {
              private readonly RequestDelegate _next;
              private readonly IConnectionStringSelector _selector;
      
              /// <summary>
              /// 
              /// </summary>
              /// <param name="next"></param>
              /// <param name="selector"></param>
              public ConnectionStringMiddleware(RequestDelegate next, IConnectionStringSelector selector)
              {
                  _next = next;
                  _selector = selector;
              }
      
              /// <summary>
              /// 
              /// </summary>
              /// <param name="context"></param>
              /// <returns></returns>
              public async Task InvokeAsync(HttpContext context)
              {
                  var endpoint = context.GetEndpoint();
      
                  string connectionStringName = PasteFormDbProperties.SqliteConnectionStringName;
                  if (endpoint?.Metadata.GetMetadata<UseReadOnlyConnectionAttribute>() != null)
                  {
                      connectionStringName = PasteFormDbProperties.SqliteReadOnlyConnectionStringName;
                  }
                  //else if (endpoint?.Metadata.GetMetadata<UseWriteConnectionAttribute>() != null)
                  //{
                  //    connectionStringName = "Default";
                  //}
                  //else
                  //{
                  //    connectionStringName = context.Request.Method.Equals("GET", StringComparison.OrdinalIgnoreCase)
                  //        ? "ReadOnly"
                  //        : "Default";
                  //}
      
                  _selector.SetConnectionStringName(connectionStringName);
                  await _next(context);
              }
      

      好理解吧,上面的意思是,如果當(dāng)前的終結(jié)點(diǎn)沒有UseReadOnlyConnectionAttribute過濾器,則走默認(rèn)的,也就是主庫,有則走從庫,然后設(shè)置這個(gè)信息到IConnectionStringSelector

          public interface IConnectionStringSelector
          {
              string GetConnectionStringName();
              void SetConnectionStringName(string name);
          }
      
          /// <summary>
          /// 返回當(dāng)前上下文的鏈接串名稱,注意是名稱,不是鏈接字符串
          /// </summary>
          public class ConnectionStringSelector : IConnectionStringSelector
          {
              /// <summary>
              /// 
              /// </summary>
              private string _connectionStringName = PasteFormDbProperties.SqliteConnectionStringName;
      
              /// <summary>
              /// 
              /// </summary>
              /// <returns></returns>
              public string GetConnectionStringName() => _connectionStringName;
      
              /// <summary>
              /// 
              /// </summary>
              /// <param name="name"></param>
              public void SetConnectionStringName(string name) => _connectionStringName = name;
          }
      

      這樣大致信息就鏈接起來了
      對(duì)原來的代碼幾乎沒有改動(dòng),
      那么生效的就是讓剛剛改的代碼生效

                      //讀寫分離支持 如果不需要,需要把下面三行給注釋掉
                      context.Services.AddScoped<IConnectionStringSelector, ConnectionStringSelector>();
                      context.Services.Replace(ServiceDescriptor.Singleton<IConnectionStringResolver, DynamicConnectionStringResolver>());
                      //  app.UseMiddleware<ConnectionStringMiddleware>(); 在UseRouting之后
      

      上面中DynamicConnectionStringResolver的注入為啥是單例呢
      因?yàn)槔锩娴拇a意思就是基于鏈接名稱獲取連接字符串,這個(gè)是一對(duì)一的關(guān)系,不需要做特意的變更,因?yàn)橐粋€(gè)程序啟動(dòng)后,這個(gè)對(duì)應(yīng)關(guān)系是固定的!
      關(guān)鍵點(diǎn)在于ConnectionStringSelector
      基于訪問上下文,修改當(dāng)前的連接名稱?。。?/p>

      測(cè)試

      改動(dòng)后,我啟動(dòng)測(cè)試下
      在權(quán)限page的Action中做如下只讀標(biāo)記

              /// <summary>
              /// 
              /// </summary>
              /// <param name="input"></param>
              /// <returns></returns>
              [HttpGet]
              [UseReadOnlyConnectionAttribute]//關(guān)鍵點(diǎn)在這,標(biāo)識(shí)這個(gè)接口走只讀
              [TypeFilter(typeof(RoleAttribute), Arguments = new object[] { "data", "view" })]
              public async Task<PagedResultDto<RoleInfoListDto>> Page([FromQuery] InputQueryRoleInfo input)
              {
      		//具體實(shí)現(xiàn)代碼
      		}
      

      然后我去創(chuàng)建一個(gè)新數(shù)據(jù)
      image
      會(huì)發(fā)現(xiàn)讀取列表的時(shí)候,是沒有這個(gè)數(shù)據(jù)的
      image

      因?yàn)闇y(cè)試階段,我的從數(shù)據(jù)庫沒有從主數(shù)據(jù)庫自動(dòng)同步
      而測(cè)試其他表的新增和讀取,則正常!
      也就是role的page接口,走的是從數(shù)據(jù)庫的讀取!

      結(jié)語

      其實(shí)關(guān)鍵點(diǎn)在于IConnectionStringSelector
      所以,非接口函數(shù)要實(shí)現(xiàn)的話,我們可以手動(dòng)修改IConnectionStringSelector的數(shù)據(jù),這樣就可以實(shí)現(xiàn)切換主從了!

      實(shí)際中,我感覺我上面還是有很多不足的,比如如果我是支持用戶自己選擇數(shù)據(jù)庫的,那么就應(yīng)該改成IConnectionStringSelector
      只配置用主還是從
      而下一個(gè)地方,則基于實(shí)際配置,自動(dòng)拆分,比如拆分成sqlite的主庫,還是sqlite得從庫!

      posted @ 2025-10-15 18:36  PasteSpider  閱讀(408)  評(píng)論(1)    收藏  舉報(bào)
      主站蜘蛛池模板: 国产三级精品片| 一区二区三区午夜福利院| 亚洲第四色在线中文字幕| 久久99热只有频精品8| 推特国产午夜福利在线观看| 国产精品美女久久久久久麻豆| 国产一区在线观看不卡| 国内精品伊人久久久久av| 日本一本无道码日韩精品| 国产粉嫩美女一区二区三| 国产中文字幕精品免费| 自偷自拍亚洲综合精品| 国内精品国产三级国产a久久| 日韩av片无码一区二区不卡| 亚洲人成亚洲人成在线观看| 国产成人免费| 久久综合久中文字幕青草| 天干天干夜啦天干天干国产| 午夜精品福利亚洲国产| 人妻少妇精品视频专区| 久久99精品国产麻豆婷婷| 亚洲精品国产一区二区三区在线观看| 国产精品美女久久久久久麻豆| 久久国产精品老女人| 婷婷综合久久中文字幕| 日本高清在线观看WWW色| 黑人玩弄人妻中文在线| 在线中文字幕第一页| 麻豆精品一区二区综合av| 国产成人午夜福利在线播放| 国精品午夜福利视频| 四虎永久免费精品视频| 99久久精品久久久久久婷婷| 久久久精品94久久精品| 国产一区二区三区小说| 精品亚洲无人区一区二区| 玩弄漂亮少妇高潮白浆| 亚洲熟妇自偷自拍另欧美| 国产99久久精品一区二区| 快好爽射给我视频| 国产免费午夜福利在线观看|