优化反射性能的总结(中)
加杉删逼你去检索下读写分离一大堆告诉你写更改删除走主数据库读走从数据库我要说的是这个完全错误的原因其实但从字面来说上面也是很有道理的但是问题就出现在现实上延迟问题主从备份主数据库从数据库我们都知道是往主数据库写入从数据库自动从主数据库复制数据但是这里是有延迟的而且延迟还不低不说多吧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的数据这样就可以实现切换主从了实际中我感觉我上面还是有很多不足的比如如果我是支持用户自己选择数据库的那么就应该改成

相关新闻

优化反射性能的总结(下)

优化反射性能的总结(下)

献廊苑现二、安装方式 推荐通过 NuGet 包管理器进行安装,以下为两种具体安装途径: (一)使用 Package Manager Console 在 Visual Studio 的「Package Manager Console」中执行以下命令: Install-Package ManySpeech.Al…

2026/7/6 11:13:21 阅读更多 →
Fish Li 该如何帮助您呢?

Fish Li 该如何帮助您呢?

埔退鞍促先看一段让人疑惑的代码: // 这段代码在干什么?为什么要这样写? static inline void*& NextObj(void* obj) {return *(void**)obj; } void* memory_block malloc(1024); NextObj(memory_block) another_block; // ??? 如果…

2026/5/17 9:26:04 阅读更多 →
HTTP Live Streaming(HLS)直播技术分析与实现

HTTP Live Streaming(HLS)直播技术分析与实现

帘粤萌显Blazor 是由Microsoft开发的一款基于.NET的开源交互式Web UI框架。Blazor使开发人员能够使用C#和HTML建立全堆栈的单页应用程序,并避免使用JavaScript。Blazor基于组件模型,该模型提供了以具有强类型的符合Razor标准的页面和组件的形式构建用户界…

2026/7/4 23:30:59 阅读更多 →

最新新闻

OceanBase发布“湖库一体”AI数据库:一套引擎替代交易库+数仓+向量库+数据湖

OceanBase发布“湖库一体”AI数据库:一套引擎替代交易库+数仓+向量库+数据湖

6月29日,OceanBase正式发布面向AI时代的湖库一体AI数据库,提出以“湖库一体”为核心架构,将数据湖的开放与海量存储能力、数据库的事务处理与分析能力,以及多模态数据处理能力统一到一套强一致的数据底座上,帮助Agent&…

2026/7/7 6:15:44 阅读更多 →
eHR 五层完整架构解析:分清基础刚需功能与高阶增值能力

eHR 五层完整架构解析:分清基础刚需功能与高阶增值能力

人力资源eHR软件系统(Electronic Human Resources System),是指通过信息化手段将企业HR管理全流程数字化、自动化的软件平台,涵盖招聘、入职、薪酬、绩效、考勤、培训等核心模块,帮助企业告别纸质档案与Excel表格&…

2026/7/7 6:13:44 阅读更多 →
Python处理气象NetCDF数据:读取、裁剪、统计与可视化

Python处理气象NetCDF数据:读取、裁剪、统计与可视化

一、前言 做气象、海洋、能源研究的同学,几乎每天都要和NetCDF(.nc)格式打交道。这种格式虽然跨平台、自带元数据,但对新手来说,用Python读取、裁剪、计算区域平均、画图,每一步都可能踩坑。本文基于公开的…

2026/7/7 6:09:43 阅读更多 →
《我的倒霉蛋宝贝》 泰剧|在线观看|奇幻|浪漫|Unlucky Bae

《我的倒霉蛋宝贝》 泰剧|在线观看|奇幻|浪漫|Unlucky Bae

《我的倒霉蛋宝贝》 泰剧|在线观看|奇幻|浪漫|Unlucky Bae资料可在线播放《我的倒霉蛋宝贝》https://tool.nineya.com/s/1jskahdln English Practice Fantasy Romance Edition 以《我的倒霉蛋宝贝》为主题的英语练习,边追剧边学英语。Part 1 Vocabulary Choose th…

2026/7/7 6:09:43 阅读更多 →
Stable Diffusion本地部署指南:免费无限量AI绘画实战

Stable Diffusion本地部署指南:免费无限量AI绘画实战

🚀 30款热门AI模型一站整合,DeepSeek/GLM/Qwen 随心用,限时 5 折。 👉 点击领海量免费额度 最近在AI绘画领域,不少开发者都遇到了同样的问题:云端AI绘画工具要么收费昂贵,要么生成次数有限制…

2026/7/7 6:09:43 阅读更多 →
YimMenu技术架构深度解析:从游戏逆向工程到现代C++菜单设计

YimMenu技术架构深度解析:从游戏逆向工程到现代C++菜单设计

YimMenu技术架构深度解析:从游戏逆向工程到现代C菜单设计 【免费下载链接】YimMenu YimMenu, a GTA V menu protecting against a wide ranges of the public crashes and improving the overall experience. 项目地址: https://gitcode.com/GitHub_Trending/yi/Y…

2026/7/7 6:07:42 阅读更多 →

日新闻

鸿蒙新特性:图片画廊与轮播导航——构建沉浸式图片浏览体验

鸿蒙新特性:图片画廊与轮播导航——构建沉浸式图片浏览体验

图片浏览是移动应用中最高频的场景之一。从社交应用的照片流到电商平台的商品图集,从旅游应用的景点相册到摄影作品展示——用户对图片浏览的体验要求不断提高:流畅的切换动画、直观的缩略图导航、便捷的收藏操作、自动播放模式。HarmonyOS NEXT ArkUI 虽…

2026/7/7 0:05:16 阅读更多 →
24V DC-DC降压芯片PW2312B/PW2815,SOT23-6到SOP8-EP方案对比

24V DC-DC降压芯片PW2312B/PW2815,SOT23-6到SOP8-EP方案对比

24V稳压芯片完整选型指南 PW8600 PW75XX PW2815 PW2312B LDODC/DC全方案 一、24V稳压方案概述 24V直流电源在工业自动化、门禁系统、电梯控制、汽车电子、LED驱动、监控设备等场景中应用极广,是最常见的中压直流母线电压。要将24V母线稳定降压至下游MCU、传感器…

2026/7/7 0:05:16 阅读更多 →
RAG+知识图谱混合检索与Graph RAG核心对比

RAG+知识图谱混合检索与Graph RAG核心对比

做企业RAG落地的团队,往往容易卡在一容易踩坑的选型难题: 当需求单纯靠向量RAG搞不定、单纯靠知识图谱也搞不定,必须同时依赖「文本语义理解 实体关系推理」时,到底是做「向量图谱混合检索」就够了,还是必须上「Grap…

2026/7/7 0:07:19 阅读更多 →

周新闻

B站视频下载神器BiliTools:5分钟学会轻松保存任何B站内容

B站视频下载神器BiliTools:5分钟学会轻松保存任何B站内容

B站视频下载神器BiliTools:5分钟学会轻松保存任何B站内容 【免费下载链接】BiliTools A cross-platform bilibili toolbox. 跨平台哔哩哔哩工具箱,支持下载视频、番剧等等各类资源 项目地址: https://gitcode.com/GitHub_Trending/bilit/BiliTools …

2026/7/6 8:11:50 阅读更多 →
威胁模型全解析:从新手入门到实战应用,助你构建安全产品!

威胁模型全解析:从新手入门到实战应用,助你构建安全产品!

威胁模型的陌生现状在忙碌疲惫的一天里,参与了关于混合后量子密码学的讨论,应付端点攻击找茬的人,还参与留言板讨论后,发现“威胁模型”对多数人仍是陌生概念,且多被当作时髦用语。有趣的相关画作有一幅由 Embyr 创作的…

2026/7/6 8:11:52 阅读更多 →
渗透测试入门指南:从零基础到实战环境搭建

渗透测试入门指南:从零基础到实战环境搭建

1. 从“看热闹”到“入门”:我理解的渗透测试到底是什么?每次看到新闻里说某个大公司的数据被“黑”了,或者某个网站被攻击导致服务瘫痪,你是不是和我一样,心里会冒出两个念头:一是“这黑客真厉害”&#x…

2026/7/6 6:52:56 阅读更多 →

月新闻