xingba-coder 📅 发布时间:2026/7/13 3:28:47 👁️ 浏览次数: 凡收辣呐你去检索下读写分离一大堆告诉你写更改删除走主数据库读走从数据库我要说的是这个完全错误的原因其实但从字面来说上面也是很有道理的但是问题就出现在现实上延迟问题主从备份主数据库从数据库我们都知道是往主数据库写入从数据库自动从主数据库复制数据但是这里是有延迟的而且延迟还不低不说多吧100ms是很常见的当然你服务器牛叉可以做到1ms那也不能说他没延迟只是延迟多少的问题最大问题读写分离最大的问题不是读写分离如何写而是如何处理实际情况比如写入了更新了你读取的时候还没有读取到读取到的是旧的总结所以说如果你看到读写分离一股脑的用读只走从数据库的可以直接划走了DeepSeek的说法您举的这个例子太典型了这正好说明了读写分离不能简单粗暴地按照所有读走从库来设计。您说的这个场景的致命问题// 邀约注册 立即登录场景public class InvitationService{// 接口1邀约自动注册写主库public async Task AutoRegisterFromInvitation(string invitationCode){// 验证邀约码创建用户写主库var user new User {Username GenerateUsername(),Password HashPassword(temp123),InvitationCode invitationCode};_dbContext.Users.Add(user);await _dbContext.SaveChangesAsync(); // 写入主库// 返回成功前端自动跳转到登录页return Ok(new { success true, userId user.Id });}// 接口2用户立即登录如果强制走从库就完蛋了public async Task Login(string username, string password){// 问题这里如果强制走从库查询var user await _readOnlyDbContext.Users.FirstOrDefaultAsync(u u.Username username);if (user null){// 用户看到刚注册成功现在说用户不存在return BadRequest(用户不存在);}// 验证密码...return Ok(new { token GenerateToken(user) });}}现实中的连锁问题1. 新用户注册后立即操作// 用户注册后立即完善资料public async Task CompleteProfile(int userId, UserProfile profile){// 如果这里走从库查询用户var user await _readOnlyDbContext.Users.FindAsync(userId);// 可能为null用户刚注册从库还没同步user.Profile profile;await _dbContext.SaveChangesAsync(); // 又写主库// 如果user为null这里直接异常}2. 订单创建后立即查询public async Task CreateOrder(Order order){// 创建订单主库_dbContext.Orders.Add(order);await _dbContext.SaveChangesAsync();// 立即返回订单详情给前端// 如果前端立即调用查询订单接口走从库// 可能查不到刚创建的订单return Ok(new { orderId order.Id });}// 前端代码const createOrder async () {await api.post(/orders, orderData); // 创建成功const result await api.get(/orders/${orderId}); // 立即查询// 可能返回404用户我刚刚创建的订单呢}正确的解决方案方案一基于业务语义的路由public class SmartConnectionRouter{public bool ShouldUseReadOnlyDb(HttpContext context){var endpoint context.GetEndpoint();// 这些场景必须走主库即使它们是读操作var criticalReadOperations new[]{/auth/login, // 登录/users/{id}/profile, // 用户详情/orders/{id}, // 订单详情/payments/status // 支付状态};// 这些场景可以走从库容忍延迟var tolerantReadOperations new[]{/products/list, // 商品列表/articles, // 文章列表/statistics/report // 统计报表};var path context.Request.Path.ToString();return tolerantReadOperations.Any(p path.Contains(p)) !criticalReadOperations.Any(p path.Contains(p));}}方案二用户会话级别的路由public class UserAwareConnectionSelector{public string GetConnectionString(HttpContext context){// 新注册用户短时间内强制主库if (context.User.IsNewlyRegistered()){return Default; // 主库}// 关键操作强制主库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));}}方案三写操作后的读强制主库public class WriteAwareConnectionMiddleware{public async Task InvokeAsync(HttpContext context, IConnectionStringSelector selector){// 如果是写操作设置标记if (context.Request.Method ! GET){context.Items[ForceMasterDb] true;selector.SetConnectionStringName(Default);}else{// 读操作检查是否需要强制主库var forceMaster context.Items[ForceMasterDb] as bool? ?? false;if (forceMaster || IsCriticalReadOperation(context)){selector.SetConnectionStringName(Default);}else{selector.SetConnectionStringName(ReadOnly);}}await _next(context);}}现实中的妥协// 实际项目中我们通常这样做public class PracticalApproach{// 1. 关键业务路径全部主库// - 用户认证相关// - 订单交易相关// - 支付相关// - 库存相关// 2. 非关键业务走从库// - 商品浏览// - 内容展示// - 报表统计// - 搜索建议// 3. 新用户特殊处理// - 注册后30分钟内强制主库// - 关键操作后短时间内强制主库}结论您说得对读写分离不能简单地按照所有读走从库来设计必须理解业务语义 - 哪些读操作对实时性要求高识别数据流 - 写操作后哪些读操作会立即发生接受复杂性 - 路由逻辑会变得复杂但这是必要的监控和调整 - 根据实际运行情况不断优化路由策略那种一刀切的读写分离方案在实际业务中会带来很多您提到的这种数据不一致问题。合理的读写分离是艺术不是简单的技术配置。PasteForm中的读写方案由于最近一个项目要考虑读写分离的问题之前的都比较小都是直接主库操作所以对PasteForm的做了一个改版PasteForm框架介绍PasteForm是一个基于ABP的敏捷开发框架核心思想是通过对Dto进行标注特性让管理端完全交给后端然你体验啥叫敏捷开发原理说明上面说到了读写分离在这个框架中我主要用dbContext的方式实现数据库的相关操作别问为啥不用仓储我感觉仓储的存在很奇怪或者说不够直接不够灵活!思路一和其他文章一样在读取的时候走从数据库在其他操作上走主数据库但是这个想法直接就被毙了因为这个方案完全用不了和业务需求完全冲突思路二既然思路一走不通那就换一个方式其实在实际开发中几乎的项目很多是走主库的很少走从的为啥呢这里说的多少是接口不是说访问次数哈那就换一个思路让开发者主动标记我这个Action走从库还是走主库上面说的走从库的少那么我就默认走主库这个思路我觉得是可行的而且问了AI也是肯定答复那么问题就剩下如何写和测试了!请看PasteFormDbContext的代码/////////[ConnectionStringName(PasteFormDbProperties.ConnectionStringName)]public class PasteFormDbContext : AbpDbContext , IPasteFormDbContext{/* Add DbSet for each Aggregate Root here. Example:* public DbSet Questions { get; set; }*////////////////public PasteFormDbContext(DbContextOptions options): base(options){}//其他代码}发现没有有一个过滤器ConnectionStringName其他没有设置链接串的地方如果你查看这个过滤器的源码你会发觉里面也没有写啥public class ConnectionStringNameAttribute : Attribute{public string Name { get; }public ConnectionStringNameAttribute(string name){Check.NotNull (name, name);Name name;}public static string GetConnStringName (){return GetConnStringName(typeof(T));}public static string GetConnStringName(Type type){ConnectionStringNameAttribute customAttribute type.GetTypeInfo().GetCustomAttribute ();if (customAttribute null){return type.FullName;}return customAttribute.Name;}}也就是说执行数据库链接串写入到dbContext的不是他他只是做一个标记然后我找到了这个DefaultConnectionStringResolverpublic class DefaultConnectionStringResolver : IConnectionStringResolver, ITransientDependency{protected AbpDbConnectionOptions Options { get; }public DefaultConnectionStringResolver(IOptionsMonitor options){Options options.CurrentValue;}[Obsolete(Use ResolveAsync method.)]public virtual string Resolve(string? connectionStringName null){return ResolveInternal(connectionStringName);}public virtual Task 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;}}我们来看看这个AI的解释DefaultConnectionStringResolver 是ABP框架数据访问层的一个基础且关键的组件它优雅地处理了连接字符串的管理问题为应用程序特别是多租户应用程序提供了强大的灵活性。上面的代码意思是什么呢在ABP中链接串还有一个东西叫名称上面的意思就是基于传入的名称返回给调用方链接具体字符串注意看他注入的生命周期是瞬时的那么我们不就可以改变这个让读取的时候基于上下文返回字符串而不是从传入的名称综上从上面信息那么问题就变成了我如何基于上下文给dbContext喂不一样的连接字符串或者说基于上下文给不一样的dbContext问题又来了如果你看一个Action你会发现在Action的过滤器执行前Controller的构造函数已经执行了也就是生命周期的顺序不对都已经执行dbContext的初始化了你才想改他的链接字符串那么我们就换一个换成更早的更底层的中间件/////////public class ConnectionStringMiddleware{private readonly RequestDelegate _next;private readonly IConnectionStringSelector _selector;///////////////public ConnectionStringMiddleware(RequestDelegate next, IConnectionStringSelector selector){_next next;_selector selector;}///////////////public async Task InvokeAsync(HttpContext context){var endpoint context.GetEndpoint();string connectionStringName PasteFormDbProperties.SqliteConnectionStringName;if (endpoint?.Metadata.GetMetadata () ! null){connectionStringName PasteFormDbProperties.SqliteReadOnlyConnectionStringName;}//else if (endpoint?.Metadata.GetMetadata () ! null)//{// connectionStringName Default;//}//else//{// connectionStringName context.Request.Method.Equals(GET, StringComparison.OrdinalIgnoreCase)// ? ReadOnly// : Default;//}_selector.SetConnectionStringName(connectionStringName);await _next(context);}好理解吧上面的意思是如果当前的终结点没有UseReadOnlyConnectionAttribute过滤器则走默认的也就是主库有则走从库然后设置这个信息到IConnectionStringSelectorpublic interface IConnectionStringSelector{string GetConnectionStringName();void SetConnectionStringName(string name);}////// 返回当前上下文的链接串名称注意是名称不是链接字符串///public class ConnectionStringSelector : IConnectionStringSelector{/////////private string _connectionStringName PasteFormDbProperties.SqliteConnectionStringName;////////////public string GetConnectionStringName() _connectionStringName;////////////public void SetConnectionStringName(string name) _connectionStringName name;}这样大致信息就链接起来了对原来的代码几乎没有改动那么生效的就是让刚刚改的代码生效//读写分离支持 如果不需要需要把下面三行给注释掉context.Services.AddScoped ();context.Services.Replace(ServiceDescriptor.Singleton ());// app.UseMiddleware (); 在UseRouting之后上面中DynamicConnectionStringResolver的注入为啥是单例呢因为里面的代码意思就是基于链接名称获取连接字符串这个是一对一的关系不需要做特意的变更因为一个程序启动后这个对应关系是固定的关键点在于ConnectionStringSelector基于访问上下文修改当前的连接名称测试改动后我启动测试下在权限page的Action中做如下只读标记///////////////[HttpGet][UseReadOnlyConnectionAttribute]//关键点在这标识这个接口走只读[TypeFilter(typeof(RoleAttribute), Arguments new object[] { data, view })]public async Task Page([FromQuery] InputQueryRoleInfo input){//具体实现代码}然后我去创建一个新数据image会发现读取列表的时候是没有这个数据的image因为测试阶段我的从数据库没有从主数据库自动同步而测试其他表的新增和读取则正常也就是role的page接口走的是从数据库的读取结语其实关键点在于IConnectionStringSelector所以非接口函数要实现的话我们可以手动修改IConnectionStringSelector的数据这样就可以实现切换主从了实际中我感觉我上面还是有很多不足的比如如果我是支持用户自己选择数据库的那么就应该改成
在Windows 11中用ollama和本地小模型运行claude code 1.下载ollama安装包并安装 wget -c https://github.com/ollama/ollama/releases/download/v0.17.6/OllamaSetup.exe -O Ollama0176.exe然后双击exe文件,按默认安装。再启动Ollama客户端。 2.拉取运行qwen小模型 C:\d>ollama run qwen3.5:0.8b --thinkfalse pulli… 2026/5/17 8:33:13
企业高质量发展的4D层级体系构建:BD→OD→TD→LD 一、序言:企业发展 我们都说企业要发展,发展是硬道理!企业需要在复杂多变的市场环境中,实现持续增长、长效盈利与核心竞争力的沉淀。 那么如何有效发展企业呢?这里笔者提出了四个层次的发展架构:BD… 2026/7/5 20:34:05
【图像加密】基于二维 Logistic 混沌映射+ Liu混沌系统的图像加密 解密及安全性分析信息熵、相邻像素相关性)附matlab代码 ✅作者简介:热爱科研的Matlab仿真开发者,擅长毕业设计辅导、数学建模、数据处理、建模仿真、程序设计、完整代码获取、论文复现及科研仿真。🍎 往期回顾关注个人主页:Matlab科研工作室👇 关注我领取海量matlab电子书和… 2026/7/8 17:54:54
AWS EMR+PySpark大规模EDA实战:从17亿行出租车数据说起 1. 项目概述:为什么用 AWS EMR 做 PySpark 探索性分析,而不是本地笔记本? 你手头有一份纽约出租车的完整历史票价数据集——近200GB的原始CSV,包含2009到2016年超过17亿条行程记录,字段包括上车/下车时间、经纬度、乘… 2026/7/13 3:28:44
分享一些关于svg,制图辅助工具,svg图形的导入(4) 1.在demos里面利用表达式与动点导入贝塞尔曲线,圆,椭圆等 以二次贝塞尔函数为例,你可以设置三个动点,一边手动调节曲线弧度,用公式表示第4个点 横坐标:x(t)(1−t)2⋅x02t(1−t)⋅x1t2⋅x2x(t)(1−t)2⋅x02t(1−t)⋅x1t2⋅x2 纵坐标&… 2026/7/13 3:28:44
直流电机控制:从硬件设计到三闭环算法的工程实践 1. 项目背景与核心需求在工业自动化和嵌入式系统开发领域,直流电机控制一直是个经典而关键的课题。不同于市面上现成的电机驱动模块,我们需要针对特定应用场景定制电机性能参数,这就涉及到从硬件选型到控制算法的全链路设计。最近我在一个AGV… 2026/7/13 3:28:44
Streamlit实战指南:Python数据应用快速交付与生产部署 1. 这不是又一个“Hello World”教程:为什么Streamlit是数据人绕不开的交付终点你手头刚跑完一个效果惊艳的销售预测模型,准确率92%,特征重要性图清晰得像教科书;或者你花两周打磨出一套用户行为漏斗分析看板,SQL和Pyt… 2026/7/13 3:26:43
Google Terminal Assistant:终端原生AI编程助手深度解析 1. 项目概述:这不是新闻标题,而是一次开发者工作流的静默重置 “Google Just Killed $200/Month AI Coding Tools With This Free Terminal Assistant”——看到这个标题,我第一反应不是点开链接,而是把终端窗口最小化,… 2026/7/13 3:26:43
450轴距无人机动力系统配置实战:从电机KV值到电池3S/4S选择 450轴距无人机动力系统配置实战:从电机KV值到电池3S/4S选择1. 动力系统匹配的核心逻辑450轴距无人机作为创客和高校机器人战队的经典选择,其动力系统配置直接影响飞行性能与安全性。与简单堆砌硬件不同,科学匹配需要遵循"推力需求→电机… 2026/7/13 3:24:40
HS2-HF Patch终极指南:如何用3步解决Honey Select 2的70+个痛点 HS2-HF Patch终极指南:如何用3步解决Honey Select 2的70个痛点 【免费下载链接】HS2-HF_Patch Automatically translate, uncensor and update HoneySelect2! 项目地址: https://gitcode.com/gh_mirrors/hs/HS2-HF_Patch 你是否曾经在Honey Select 2中遇到过… 2026/7/13 0:01:19
语音转文字工具AsrTools:让音频整理变得简单高效 语音转文字工具AsrTools:让音频整理变得简单高效 【免费下载链接】AsrTools ✨ AsrTools: Smart Voice-to-Text Tool | Efficient Batch Processing | User-Friendly Interface | No GPU Required | Supports SRT/TXT Output | Turn your audio into accurate text … 2026/7/13 0:03:19
基于深度学习的蘑菇或花卉或动漫人物或中草药货水果蔬菜等识别系统31(设计源文件+万字报告+讲解)(支持资料、图片参考_相关定制)_ 基于深度学习的蘑菇或花卉或动漫人物或中草药货水果蔬菜等识别系统31(设计源文件万字报告讲解)(支持资料、图片参考_相关定制)_ 独家界面!不会重复,此项目属于本人原创,若有雷同,均是盗卖,各位买… 2026/7/13 0:05:20
Git reset 与 revert 深度对比:5个关键差异与 3 种典型应用场景 Git Reset 与 Revert 深度对比:5个关键差异与3种典型应用场景在团队协作开发中,代码版本管理如同行走钢丝——一步失误可能导致整个项目陷入混乱。作为Git进阶用户,你是否曾在深夜面对错误的提交束手无策?是否在强制推送后收到同事… 2026/7/12 0:01:13
GitHub 学生包申请避坑:5个常见失败原因与开发者工具调试方案 GitHub 学生包申请技术排障指南:5个高频失败场景与开发者工具实战方案第一次尝试申请GitHub学生包时,我盯着屏幕上那个不断转圈的加载动画整整15分钟,最终只等来了一行冰冷的错误提示。这可能是许多开发者共同的经历——明明按照教程操作&… 2026/7/12 0:01:13
冒烟测试用例设计规范:5%-10%覆盖率下的3类核心场景与执行标准 冒烟测试用例设计的黄金法则:5%-10%覆盖率下的精准筛选策略在快节奏的敏捷开发环境中,冒烟测试作为质量保障的第一道防线,其重要性不言而喻。当测试资源有限而时间紧迫时,如何从海量测试用例中精准筛选出那关键的5%-10%࿰… 2026/7/13 2:34:55