Open-AutoGLM - 智谱开源的手机端AI Agent框架 | AI工具集

mikel阅读(109)

来源: Open-AutoGLM – 智谱开源的手机端AI Agent框架 | AI工具集

Open-AutoGLM 是智谱开源的手机端智能助理框架,基于 AutoGLM 构建,能通过自然语言指令实现手机操作的自动化。Open-AutoGLM通过 Phone Use 能力框架,将自然语言指令转化为实际操作,如点击、滑动、输入等,可在外卖、社交、客服等场景中自动完成任务。框架借助云手机技术保障隐私安全。Open-AutoGLM 开源目的是推动行业共同进步,保护用户隐私,加速 Agent 技术爆发。目前支持 50 多款主流中文应用,提供完整工具链和详细文档,助力开发者快速上手和二次开发。

Open-AutoGLM

Open-AutoGLM的主要功能

  • 自然语言理解与任务执行:用户能用自然语言描述需求,AI 自动解析意图、执行操作。
  • 多模态界面理解:通过视觉语言模型理解手机屏幕内容,识别界面元素进行操作。
  • 自动化操作:支持点击、滑动、输入文本、长按、双击等多种操作,模拟真实用户行为。
  • 敏感操作确认与人工接管:在涉及隐私或敏感操作时,AI 会请求人工确认或接管,确保安全性。
  • 远程调试与控制:支持通过 WiFi 或网络进行远程 ADB 调试,无需物理连接设备即可操作。
  • 支持多款应用:覆盖 50 多款主流中文应用,包括社交通讯、电商购物、美食外卖、视频娱乐等场景。
  • 云手机环境部署:在云端虚拟设备中运行,保障用户隐私和数据安全。

如何使用Open-AutoGLM

  • 准备工作
    • 安装 Python:确保电脑上安装了 Python(推荐 3.10 及以上版本)。
    • 安装 ADB:下载并安装 ADB https://developer.Android.com/studio/releases/platform-tools。安装完成后,将 ADB 的路径添加到系统环境变量中。
    • 配置安卓设备:确保安卓设备已启用开发者模式和USB调试,并连接到电脑。
  • 下载和安装
    • 下载 Open-AutoGLM
      • 访问 Open-AutoGLM GitHub 仓库 https://github.com/zai-org/Open-AutoGLM。
      • 点击页面右上角的 “Code 按钮,选择 “Download ZIP,下载项目代码。
      • 解压下载的文件到本地一个方便的目录(如 C:\Open-AutoGLM 或 ~/Open-AutoGLM)。
    • 安装依赖
      • 打开命令行工具(Windows 的命令提示符或 PowerShell,Mac/Linux 的终端)。
      • 切换到解压后的项目目录:
      • cd path/to/Open-AutoGLM
      • 输入以下命令安装依赖:
      • pip install -r requirements.txt
    • 启动模型服务
      • 输入以下命令启动模型服务(这是必须的步骤,但不需要理解代码):
      • python3 -m vllm.entrypoints.openai.api_server --model zai-org/AutoGLM-Phone-9B --port 8000
      • 如果看到服务启动成功的提示,就可以进行下一步。
  • 运行任务:打开一个新的命令行窗口,输入以下命令执行任务(只需要替换任务描述即可):python main.py --base-url http://localhost:8000/v1 --model "autoglm-phone-9b" "任务描述"

Open-AutoGLM的项目地址

  • GitHub仓库:https://github.com/zai-org/Open-AutoGLM
  • HuggingFace模型库:https://huggingface.co/zai-org/AutoGLM-Phone-9B

Open-AutoGLM的应用场景

  • 外卖点餐:用户只需用自然语言描述需求,Open-AutoGLM 能自动打开美团应用、搜索肯德基、选择全家桶套餐并完成下单操作。
  • 社交媒体互动:在微信、微博等社交应用中,用户可指令 AI“点赞好友的最新动态”或“评论抖音视频”,AI 将自动识别相关内容,执行点赞、评论等互动操作。
  • 办公自动化:在办公软件(如 WPS、Microsoft Office)中,用户通过语音或文字指令“创建一个名为‘项目计划’的文档并写入会议内容”,AI 能完成文档创建和内容编辑。
  • 智能家居控制:通过智能家居应用(如小米智能家居),Open-AutoGLM 能精准识别、控制相应设备,实现家居场景的自动化切换。
  • 交通出行:在地图或打车应用(如高德地图、滴滴出行)中,AI 将实时规划路线,完成叫车操作,方便快捷。

2025:AI 一人公司技术栈(最新实战版) - 今日头条

mikel阅读(49)

来源: 🚀💻 2025:AI 一人公司技术栈(最新实战版) – 今日头条

2025:AI 一人公司技术栈(最新实战版)

这一个月最大的感悟:

独立开发不是在和别人竞争,而是在和自己的时间赛跑。

能自动化的就自动化,能复用的就复用。

把时间浪费在无谓事情上的代价,远比你想得大。

下面是我筛选后的“真正能提高生产力、减少重复劳动”的技术栈。全部亲测高效

1. 核心生产力(三件套)

Cursor

目前最稳的写代码主力。

日常开发 80% 都靠它完成。

2.0 发布后代理能力更强,多线程并行更猛。

Dify

做 AI 原型、自动化工具、内部系统巨快。

私有化后不用担心安全,是真正的“效率倍增器”。

n8n

自动化神器。把能交给机器干的都交出去:

✔ AI 早报

✔ 数据清洗

✔ 工作流触发

✔ 自动提醒

节省时间是指数级的。

2. 前端开发(快速 + 丝滑为主)

Next.js + Tailwind CSS

目前最顺手、最快的组合,没有之一。

Shadcn UI + Lucide Icons

组件好看、定制简单,基本告别自己手搓 UI。

Vercel AI SDK

接 AI 聊天接口就像装插件,一行代码起飞。

和 Next.js 配合简直无敌。

3. 后端开发(按项目规模选)

FastAPI

轻量、速度快,小中型项目极度舒适。

NestJS

当项目结构复杂,需要工程化的时候,它绝对稳。

Alembic / Prisma

数据库迁移工具必备,不踩坑。

4. 数据库(按场景选)

PostgreSQL

复杂业务场景的首选,可靠性无敌。

MongoDB

迭代快、结构灵活的小项目最佳搭档。

Supabase

自带认证、文件存储、实时订阅——

一个 SDK 带走后端 40% 工作量。

5. 部署 & 运维(稳定 + 省心)

Docker + Compose

所有环境统一管理,跨机器无痛复刻。

GitHub Actions / GitLab CI

提交即部署,非常适合一人团队。

Cloudflare

安全 + 加速 + CDN,一步到位。

DigitalOcean

配置简单、价格友好,独立开发者首选云。

6. 运营 & 工具(一个人就是一个团队)

Figma

原型 + UI 全都能自己搞定。

Notion

文档、规划、素材库一站式管理。

Stripe

海外支付接入最优方案,不折腾。

最后的感悟

做一人公司,你会突然明白:

沉默的成本最高,重复劳动最贵。

把所有可以复用的流程自动化,把所有能交给 AI 的交出去。

不是为了更快,而是为了把你的时间留给真正重要的事。

能简化的简化,能自动化的自动化,能放弃的果断放弃。

这就是一人公司的生存哲学。

AutoGLM开源:每台手机,都可以成为AI手机

mikel阅读(54)

来源: AutoGLM开源:每台手机,都可以成为AI手机

我们想做的

到底是一件什么事

图片

过去的很长时间,我们一直在回答同一个问题:

如果 AI 真的是“助手”,它能不能像人一样,自己把手机拿起来,把一件事从头做到尾?

在我们的想象里,AI 不该只停留在聊天框里,而是可以走进用户每天真正要点的那些 App 里:

  • 帮你在外卖 App 里完成从选择到下单;
  • 帮你在云手机里,批量处理通知、点赞评论;
  • 帮你在销售、客服、出勤等场景里,自动做完一堆“你自己也不想点的重复动作”。

AutoGLM 想做的,就是这件事:让 AI 真正学会“使用手机”。

32个月

我们具体做到了什么?

图片

用一句简单的话来说:

我们希望 AutoGLM,不仅能“说”,还能“做”。

为了这一句话,我们从 2023 年 4 月——一个大多数人还不知道大模型是什么的时候,完全从零去探索每一个细节,到今天,已经 32 个月了。

1.“乱点” -> “可控”

在最开始的版本里,基于大模型所构建的系统只知道“点”、“滑”等操作,偶尔能完成一个很短的流程,更多时候会迷失在不知所谓的操作中,甚至陷入死循环。

为了解决这类问题,我们花了近一年的时间,去梳理每一个可能的问题,尝试把这些“乱点的手”变成一只“可控的手

  • 建了一整套 Phone Use 能力框架;
  • 把点击、滑动、输入、截图、界面理解这些最基础的动作抽象出来;
  • 让模型学会把一句自然语言拆解为一系列稳定、可回放的操作步骤;
  • 处理掉各种真实世界的“脏信息”:网络波动、弹窗打断、广告遮挡……

2024 年 10 月 25 日,我们发布了第一个能够在真机上稳定完成一条完整操作链路的 AutoGLM,

它被业内视为全球首个具备 Phone Use 能力的 AI Agent。

2.第一个由 AI 发出的红包

2024 年 11 月,AutoGLM 发出了人类历史上第一个由 AI 完成的手机红包:不是脚本录制,不是内嵌 API 调用,而是通过在屏幕上“看见”界面、“理解”含义,一步一步完成点击。

对我们来说,这是一个信号:

从此以后,手机上的很多交互,第一次可以完全由 AI 接手。

Image

3.走向云手机:把能力放到一个更安全的空间

2025 年,我们发布了 AutoGLM 2.0,验证了强化学习的规模扩展定律,提出了 MobileRL、ComputerRL 和 AgentRL 算法,让 AutoGLM 在上千个虚拟设备环境里同时强化学习,极大扩展了 Agent 的准确性和泛化能力。

更关键的是,我们不希望 Agent 直接在用户真实手机、真实微信上肆意操作,于是选择把它放进一台和用户真实世界脱离的虚拟手机里:

  • 这台手机跑在云端;
  • 每一个动作都可以回放、审计、干预;
  • 真正敏感的数据可以严格隔离。

这套设计背后的直觉很简单:

让 AI 会用手机之前,我们要先保证,它不会在不该点的地方胡乱伸手。

除了云手机的选择,我们主动放弃了操作微信等这些离用户隐私较近的 APP。

为什么要开源?

我们真正在意的是什么?

图片

从产品的角度,AutoGLM 已经可以支撑起很多真实场景;从工程的角度,AutoGLM的积累足够写成一大摞技术报告。

那为什么,在这个时间点,我们选择把它开源?

我们想清楚的,大概有三件事。

1.这件事只在一家公司做,是不够的

AI 手机已经是趋势,但如果“会用手机的 AI 能力”只掌握在极少数厂商手里,那意味着:

  • 开发者的创新空间,要看平台愿不愿意开放接口;
  • 用户日常生活中最重要的那台设备,越来越像“别人家的入口”。

AutoGLM 开源的第一层初衷,是把这一层能力变成整个行业可以共同拥有、共同打磨的公共底座。

你可以把它当成一块积木,放进你自己的系统里,也可以把它拆开、重写、改造,那一刻起,它就不再只是“智谱的 AutoGLM”,而是你和你的团队做出来的东西的一部分。

2.让隐私和控制权,真正留在使用方这一侧

我们也很清楚,Phone Use 能力,一旦做大,天然会碰到最敏感的那一部分世界:个人微信、支付、聊天记录、照片相册、企业内部系统。

我们不希望也不应该,把这些东西握在自己手里。

通过开源和私有化部署,企业和开发者可以在自己的合规环境中完整掌控数据、日志和权限。

可以让手机,成为专属自己的 AI 手机。

用一句话概括就是:

技术向整个生态开放,

数据与隐私,永远留在使用方这一侧。

3.把我们走过的路,变成大家在Agent爆发时代的起跑线

AutoGLM是一条很陡的路:我们啃过的技术难题、踩过的坑、重写过的框架,

在很多场景里都可以复用,也值得被复用。

但这还不够,“一花独放不是春”。Agent 的爆发,需要所有人一起参与。

我们更乐见的是:

  • 有团队基于 AutoGLM,做出真正意义上的 AI 原生手机;
  • 有研究者把其中的某个模块拆出来,变成一篇论文、一套新算法;
  • 有个人开发者把一个 Demo 改成自己的项目,在某个小众场景里真正跑起来。

我们希望开源社区:

从今天开始,人人都可以拥有自己的手机 Agent。

你现在能拿到什么

图片

我们开源的,是一整套可以「拿来就用」的能力,而不仅仅是一份概念说明。

具体包括:

  • 训练好的核心模型;
  • Phone Use 能力框架与工具链;
  • 可直接跑通的 Demo,覆盖 50+ 高频中文 App;
  • 针对 Android 的适配层与示例工程;
  • 文档、快速上手指南。

模型会以 MIT 开源许可证的形式开放,而所有代码会以 Apache-2.0 开源许可证的形式,托管在 GitHub 仓库(示例:github.com/zai-org/Open-AutoGLM)中。

你可以把它当成一套现成的基础设施,也可以单独拆用其中的某一部分,甚至可以把它改得面目全非——只要它帮助你更接近你心目中的「AI 原生手机」。

接下来

图片

现在的 AutoGLM 不是一个完美的答案。真实世界的手机和应用,它还远远没有全部见过。未来的 AI 手机生态,也一定会有比我们今天想象得更惊喜的形态。

我们做的,只是把对“AI 会不会用手机”这个问题的一次完整回答,诚实地、原原本本地,交到你们手里。

在 2025 年尾的这个时刻,正如 Andrej Karpathy 所说,

从今天开始,人人都可以拥有自己的手机 Agent。我们面对的大概不只是“Agent 元年”,而更可能是“Agent 的十年”。

接下来,AutoGLM 团队会继续努力,让我们一起推动 Agent 开源和研究,那个梦想中的“贾维斯”,才会真的在我们人人身边出现。

浏览器加载html、css、js的顺序 - 幼儿园技术家 - 博客园

mikel阅读(50)

来源: 浏览器加载html、css、js的顺序 – 幼儿园技术家 – 博客园

为什么要了解浏览器加载流程?

前端性能优化、SEO、首屏渲染速度、闪烁问题、Hydration mismatch 这些常见问题,本质上都与浏览器加载顺序有关,特别是排查的时候会心态爆炸。

例如实际上用 nuxtjs3 写项目的时候,首页加载老是会出现 Hydration completed but contains mismatches.,diu查了好久,把页面渲染顺序都进行调式,最后发现不是页面问题,而是在 nuxt.config.ts 多配置了首页缓存的,导致每次两端渲染不同:

    nitro.prerender.routes: ['/']

    routeRules: {
        "/": { prerender: true },
    },

虽说不是加载流程问题,但是好歹还是能排查一些问题的,掌握的话查起来还是很快的。


浏览器加载网页的完整过程

下面是简化但准确的流程:

HTML 下载 → 解析 HTML → 下载/解析 CSS → 构建渲染树 → Layout → Paint

JavaScript 则会在关键步骤中插入“阻塞点”,它会影响解析顺序与渲染顺序。


一、浏览器加载 HTML(构建 DOM)

浏览器首先从服务器下载 HTML 文件,然后:

  • 从 上到下 解析
  • 一边读一边构建 DOM 树

在解析 HTML 的过程中,如果遇到外部资源,就会触发对应行为:

  • 浏览器会并行下载 CSS
  • 不会阻塞 HTML 解析
  • 但 CSS 解析未完成前无法渲染页面(渲染阻塞)

遇到 <script>(无 async/defr)

  • 阻塞 HTML 解析
  • 下载 JS
  • 执行 JS
  • 继续解析 HTML

也就是 普通 script 会暂停 DOM 的构建


二、CSS 加载与解析(构建 CSSOM)

CSS 文件下载后需要被解析成 CSSOM(CSS 对象模型)。

CSS 是渲染阻塞的

CSSOM 未构建完成 → 浏览器无法进行首次渲染

但 CSS 不会阻塞 HTML 的解析,只会阻塞渲染阶段。


三、DOM + CSSOM → Render Tree(渲染树)

当 DOM 和 CSSOM 都准备好后,浏览器把它们合成 Render Tree:

  • DOM 决定页面结构
  • CSS 决定页面样式

两者合成渲染树后,才可以进行 Layout & Paint。


四、Layout(布局计算)

浏览器计算每个节点的:

  • 大小
  • 位置
  • 盒模型

五、Paint(绘制)

将渲染树绘制到屏幕像素上。

此时用户才看到页面。


六、JavaScript 对加载顺序的影响

JavaScript 的三种加载方式:

1. 普通 <script>(最慢)

阻塞 HTML 解析 → 下载 JS → 执行 JS → 继续解析 HTML

2. <script defer>(推荐)

并行下载 JS
HTML 完成后按顺序执行
不阻塞解析
在 DOMContentLoaded 之前执行

3. <script async>(最快,但不稳定)

并行下载
下载完立即执行(可能打断 HTML 解析)
执行顺序无法保证

浏览器加载流程

UserJS EngineCSS ParserHTML ParserBrowserUserJS EngineCSS ParserHTML ParserBrowser下载并解析 HTML遇到 <link> 开始下载 CSS(不阻塞 HTML)解析 CSS(阻塞渲染)遇到普通 <script> 下载并执行(阻塞 HTML)DOM Tree 构建完毕CSSOM 构建完毕DOM + CSSOM → Render TreeLayout(布局计算)Paint(绘制)页面显示

最终总结(记住这 7 句话就够了)

  1. HTML 自上而下解析,构建 DOM
  2. CSS 加载不阻塞 HTML,但阻塞渲染
  3. 普通 script 会阻塞 HTML 解析
  4. async 脱离顺序执行
  5. defer 按顺序、在 HTML 完成后执行
  6. DOM + CSSOM 才能进行渲染
  7. Layout → Paint 才真正显示页面

如何使用 PostMan 进行并发测试?_postman 并发测试-CSDN博客

mikel阅读(102)

来源: 如何使用 PostMan 进行并发测试?_postman 并发测试-CSDN博客

01、POST篇
要在Postman中对POST请求进行压力测试,可以按照以下步骤进行操作:

打开Postman应用程序并创建一个新的请求集合(Collection)。

在请求集合中创建一个新的请求,并选择HTTP方法为POST。

在请求URL字段中输入要测试的目标URL地址。

在请求主体(Body)部分中输入POST请求的有效载荷(Payload)数据。

配置请求头(Headers)和其他必要的参数。

在Postman界面的右上角,找到“Runner”按钮并点击它。

进入运行器(Runner)界面,选择之前创建的请求集合,并配置运行参数。

可以设置运行器的迭代次数、并发请求数量、延迟时间等选项,根据需要进行调整。

点击“Start Run”按钮开始运行压力测试。

运行结束后,可以查看每个请求的响应结果和性能指标,如响应时间、吞吐量等。

请注意

进行压力测试时要确保目标服务器能够承受相应的负载。此外,压力测试可能会对目标服务器产生一定的负担,请谨慎操作,避免对生产环境或敏感系统造成不良影响。

流程

 

 

 

 

id 和 procureId 均是唯一的,不能插入重复,因此要在Pre-req中设置变量

现在我也找了很多测试的朋友,做了一个分享技术的交流群,共享了很多我们收集的技术文档和视频教程。
如果你不想再体验自学时找不到资源,没人解答问题,坚持几天便放弃的感受
可以加入我们一起交流。而且还有很多在自动化,性能,安全,测试开发等等方面有一定建树的技术大牛
分享他们的经验,还会分享很多直播讲座和技术沙龙
可以免费学习!划重点!开源的!!!
qq群号:691998057【暗号:csdn999】
AI构建项目

Pre-req 脚本 js

在这里(View -> Show Postman Console)可以看到log的内容

 

生成的id: a54dc69c8ba94dffb2a0813fcf88c069

生成的procureId: P20231201132925285867

并发操作步骤

 

结果

 

02、GET篇
添加环境变量

 

 

引用变量

 

下面是配套资料,对于做【软件测试】的朋友来说应该是最全面最完整的备战仓库,这个仓库也陪伴我走过了最艰难的路程,希望也能帮助到你!

 

最后: 可以在公众号:自动化测试老司机 ! 免费领取一份216页软件测试工程师面试宝典文档资料。以及相对应的视频学习教程免费分享!,其中包括了有基础知识、Linux必备、Shell、互联网程序原理、MySQL数据库、抓包工具专题、接口测试工具、测试进阶-Python编程、Web自动化测试、APP自动化测试、接口自动化测试、测试高级持续集成、测试架构开发测试框架、性能测试、安全测试等。

如果我的博客对你有帮助、如果你喜欢我的博客内容,请 “点赞” “评论” “收藏” 一键三连哦!
————————————————
版权声明:本文为CSDN博主「自动化测试老司机」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/m0_47485438/article/details/138800928

AI面试官:Asp.Net 中使用Log4Net (一) - Ysの陈憨憨 - 博客园

mikel阅读(91)

log4net.config”

来源: AI面试官:Asp.Net 中使用Log4Net (一) – Ysの陈憨憨 – 博客园

1. 先新建一个ASP.NET Core空项目

 

 

1. 什么是log4net?它的作用是什么?

解答:log4net是一个开源的日志记录框架,用于在.NET应用程序中记录日志信息。它可以帮助开发人员将不同级别的日志信息输出到不同的目标(如文件、数据库、控制台等),以便进行应用程序的调试、监控和错误追踪。

案例和代码:假设有一个.NET Core Web API应用程序,我们可以通过集成log4net来记录API请求和异常信息,并将日志信息输出到文件中。

// 首先,通过NuGet安装log4net包

// 在Startup.cs中添加log4net配置
public void ConfigureLogging(IServiceCollection services)
{
    services.AddLogging(builder =>
    {
        builder.AddLog4Net(); // 添加log4net
    });
}

2. log4net日志级别有哪些?如何设置日志级别?

解答:log4net定义了几个日志级别,包括DebugINFOWARNERRORFATAL。可以通过在配置文件或代码中设置<root>或特定<logger>节点的level属性来指定日志级别。

案例和代码:在log4net的配置文件中设置日志级别,例如输出INFO级别及以上的日志信息:

<log4net>
    <root>
        <level value="INFO" />
        <appender-ref ref="FileAppender" />
    </root>
    <!-- 其他appender配置 -->
</log4net>

3. 如何在.NET Core应用程序中使用log4net记录日志?

解答:在.NET Core应用程序中使用log4net需要使用第三方库log4net.Extensions.Logging来进行集成。通过添加log4net的配置,然后在代码中使用ILogger接口来记录日志。

案例和代码:在.NET Core控制台应用程序中使用log4net记录日志:

class Program
{
    private static readonly ILog log = LogManager.GetLogger(typeof(Program));

    static void Main(string[] args)
    {
        // 初始化log4net
        var logRepository = LogManager.GetRepository(Assembly.GetEntryAssembly());
        XmlConfigurator.Configure(logRepository, new FileInfo("log4net.config"));

        log.Info("Application started");

        try
        {
            // 业务逻辑代码
            log.Debug("Some debug information");
        }
        catch (Exception ex)
        {
            log.Error("An error occurred", ex);
        }

        log.Info("Application ended");
    }
}

4. log4net如何输出日志到文件?

解答:可以使用RollingFileAppenderFileAppender来将日志输出到文件。FileAppender每次启动时创建一个新的日志文件,而RollingFileAppender会根据配置的文件大小或日期来生成不同的日志文件。

案例和代码:在log4net的配置文件中添加FileAppender,将日志输出到文件:

<log4net>
    <appender name="FileAppender" type="log4net.Appender.FileAppender">
        <file value="logs/application.log" />
        <appendToFile value="true" />
        <layout type="log4net.Layout.PatternLayout">
            <conversionPattern value="%date %level %logger - %message%newline" />
        </layout>
    </appender>

    <root>
        <level value="INFO" />
        <appender-ref ref="FileAppender" />
    </root>
</log4net>

5. log4net如何将日志信息输出到数据库?

解答:可以使用AdoNetAppender将日志信息输出到数据库。需要配置数据库连接字符串、日志表的结构和相应的SQL语句。

案例和代码:在log4net的配置文件中添加AdoNetAppender,将日志输出到数据库:

<log4net>
    <appender name="AdoNetAppender" type="log4net.Appender.AdoNetAppender">
        <!-- 数据库连接字符串 -->
        <connectionType value="System.Data.SqlClient.SqlConnection, System.Data, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" />
        <connectionString value="Data Source=ServerName;Initial Catalog=DatabaseName;Integrated Security=True;" />

        <!-- 日志表的结构 -->
        <commandText value="INSERT INTO LogTable (Date, Level, Logger, Message) VALUES (@log_date, @log_level, @logger, @message)" />
        <parameter>
            <parameterName value="@log_date" />
            <dbType value="DateTime" />
            <layout type="log4net.Layout.RawTimeStampLayout" />
        </parameter>
        <parameter>
            <parameterName value="@log_level" />
            <dbType value="String" />
            <size value="50" />
            <layout type="log4net.Layout.PatternLayout">
                <conversionPattern value="%level" />
            </layout>
        </parameter>
        <parameter>
            <parameterName value="@logger" />
            <dbType value="String" />
            <size value="255" />
            <layout type="log4net.Layout.PatternLayout">
                <conversionPattern value="%logger" />
            </layout>
        </parameter>
        <parameter>
            <parameterName value="@message" />
            <dbType value="String" />
            <size value="4000" />
            <layout type="log4net.Layout.PatternLayout">
                <conversionPattern value="%message" />
            </layout>
        </parameter>
    </appender>

    <root>
        <level value="INFO" />
        <appender-ref ref="AdoNetAppender" />
    </root>
</log4net>

windows下PC端小程序抓包--Fiddler&Charles-腾讯云开发者社区-腾讯云

mikel阅读(251)

来源: windows下PC端小程序抓包–Fiddler&Charles-腾讯云开发者社区-腾讯云

【背景说明】

当压测小程序没有原码和小程序开发者权限时,我们压测脚本中的header和入参需要通过抓包工具进行抓包,本文讲解在windows下通过pc端微信小程序抓包拿取需要的参数,如需通过手机端抓包和工具下载安装,请参考以下文章:

Fiddler工具下载和安装:https://cloud.tencent.com/developer/article/1810615

Charles工具下载和安装:https://cloud.tencent.com/developer/article/1833591

【操作说明】

Fiddler抓包:

  1. 打开Fiddler工具,开启抓包

2、打开微信pc端小程序

发现fiddler请求数据很多,这时我们可以通过请求过滤来过滤掉没用的请求,操作如下:

3、查看抓到的请求,拿去需要的header和入参信息

4、拿取到需要的参数后复制粘贴到压测工具中即可

Charles抓包:

1、打开安装好的Charles工具,开启抓包

2、这里着重说下windows下证书的安装(比较坑的地方)

点击Help -> SSL Proxying -> Install Charies Root Certificate

这里一定要选择第二个安装地址,将证书安装到受信任的根证书下,不要使用第一个自动选择证书存储

3、证书安装完成后重新请求小程序,抓取到的内容如图,这里我们使用的是Structure模式,根据URL来查看

4、把抓包拿取到的数据放入压测工具中即可

【总结】

总的来说通过抓包工具实现header和入参的拿取还是很简单的,需要注意的就是证书安装问题

CLodop打印控件谷歌浏览器新版本禁用跨域访问解决方法

mikel阅读(194)

最近项目的CLodop打印控制在谷歌浏览器自动升级到V140新版本后,打印提示禁止跨域访问本地的CLodop服务的端口的问题,过去老版本还可以用禁用跨域检测来解决Lodop谷歌浏览器无法打印,解决谷歌浏览器最新chrome94版本CORS跨域问题 – 知乎》,现在没有这个设置项了

网上提供了三种方法,分别是设置调试模式

Chrome 临时关闭 CORS跨域报错

在快捷方式的属性 后面添加

–disable-web-security –user-data-dir=C:\Users\你的用户名\Desktop\Google Chrome Dev

另一个就是在服务器上配置Ngnix代理方式:

nginx

# 在xxxx.gnway.vip的Nginx配置中添加
location /lodop/ {
proxy_pass https://localhost.lodop.net:8443/;
proxy_ssl_verify off;
proxy_set_header Host localhost.lodop.net;
add_header Access-Control-Allow-Origin "*";
add_header Access-Control-Allow-Methods "GET, POST, OPTIONS";
add_header Access-Control-Allow-Headers "*";
}

第三个方案是将项目改成https的访问,我看了下CLodop的打印控件服务它开通了https本地访问地址

我将调取Lodop的脚本链接地址改成了https也不行还是提示跨域错误

于是想到既然不允许跨域,我在服务器安装Lodop打印控件,然后运行后,通过域名解析到打印控件的端口18000,然后同域名下不就可以访问了吗,说干就干

首先服务器安装打印控件

运行打印控件后,查看端口设置

在域名服务器上坐下映射解析

访问一下打印控件的脚本文件,可以正常访问,那应该就没有问题了

下面修改下加载打印脚本的地址

测试下项目的打印功能,可以正常加载Lodop打印控件的脚本了,打印正常了,跨域问题其实没有根本解决,只是折中了下改成了同域名下,就不算跨域了。

C# 任务队列还在轮询?300%性能提升的智能调度方案来了!

mikel阅读(167)

来源: C# 任务队列还在轮询?300%性能提升的智能调度方案来了!

前言

后端系统开发中,异步任务处理几乎是绕不开的环节。然而,传统的ConcurrentQueue<T> + 轮询方案存在诸多弊端,如CPU空转严重、内存占用高、响应延迟大等,严重影响系统性能。

对于.NET开发而言,实现一个高效的任务处理机制至关重要,它能让系统性能得到显著提升。本文将介绍如何用C#完成一个真正高效的任务处理器,告别性能瓶颈。

系统效果

图片

传统方案的三大问题

问题一:CPU空转浪费

传统轮询方式如同不停转圈的陀螺,即便没有任务,也会不断检查队列状态,造成CPU资源的严重浪费。

问题二:响应延迟

100ms的轮询间隔在高并发场景下,延迟会被无限放大,直接影响用户体验。

问题三:扩展性差

单线程处理限制了并发能力,面对突发流量,系统极易成为性能瓶颈。

解决方案

WinForm可视化任务处理器

为解决上述问题,我们将构建一个完整的WinForm任务处理器,其核心设计思路如下:

  • 可视化监控:实时呈现队列状态、处理进度、CPU使用率,让开发者对系统运行情况一目了然。

  • 异常处理:模拟真实场景的错误恢复机制,增强系统的稳定性和可靠性。

  • 优雅停机:确保任务处理的安全性,避免因突然停止导致的任务丢失或数据损坏。

代码实现

主窗体实现

主窗体代码负责处理用户交互和界面更新,通过事件绑定与任务处理器进行通信。

例如,在启动按钮点击事件中,启动任务处理器并更新日志信息;

在添加任务按钮点击事件中,创建新任务并加入队列。

以下是部分关键代码:

namespace AppTaskProcessorDemo
{  
    publicpartialclassForm1 : Form
    {  
        privatereadonly TraditionalTaskProcessor _taskProcessor;  
        privatereadonly System.Windows.Forms.Timer _uiTimer;  
        privateint _taskCounter = 1;  
        public Form1()
        {  
            InitializeComponent();  
            _taskProcessor = new TraditionalTaskProcessor();  

            // 定时器用于更新UI显示  
            _uiTimer = new System.Windows.Forms.Timer();  
            _uiTimer.Interval = 100;  
            _uiTimer.Tick += UpdateUI;  
            _uiTimer.Start();  

            // 绑定事件  
            _taskProcessor.TaskProcessed += OnTaskProcessed;  
            _taskProcessor.ProcessorStatusChanged += OnProcessorStatusChanged;  
            _taskProcessor.ErrorOccurred += OnErrorOccurred;  
        }  

        private void btnStart_Click(object sender, EventArgs e)
        {  
            btnStart.Enabled = false;  
            btnStop.Enabled = true;  
            btnAddTask.Enabled = true;  

            _ = _taskProcessor.StartProcessing();  
            LogMessage("任务处理器已启动");  
        }  

        private void btnStop_Click(object sender, EventArgs e)
        {  
            _taskProcessor.Stop();  
            btnStart.Enabled = true;  
            btnStop.Enabled = false;  
            btnAddTask.Enabled = false;  
            LogMessage("任务处理器已停止");  
        }  

        private void btnAddTask_Click(object sender, EventArgs e)
        {  
            var taskItem = new TaskItem  
            {  
                Id = _taskCounter++,  
                Name = $"任务-{_taskCounter - 1}",  
                Data = $"这是第{_taskCounter - 1}个任务的数据",  
                CreatedTime = DateTime.Now  
            };  

            _taskProcessor.EnqueueTask(taskItem);  
            LogMessage($"已添加任务: {taskItem.Name}");  
        }  

        private void btnClear_Click(object sender, EventArgs e)
        {  
            txtLog.Clear();  
        }  

        private void OnTaskProcessed(object sender, TaskProcessedEventArgs e)
        {  
            if (InvokeRequired)  
            {  
                Invoke(new Action(() => OnTaskProcessed(sender, e)));  
                return;  
            }  

            LogMessage($"✅ 任务完成: {e.Task.Name} (耗时: {e.ProcessTime}ms)");  
        }  

        private void OnProcessorStatusChanged(object sender, ProcessorStatusEventArgs e)
        {  
            if (InvokeRequired)  
            {  
                Invoke(new Action(() => OnProcessorStatusChanged(sender, e)));  
                return;  
            }  

            lblStatusValue.Text = e.IsRunning ? "运行中" : "已停止";  
            lblStatusValue.ForeColor = e.IsRunning ? System.Drawing.Color.Green : System.Drawing.Color.Red;  
        }  

        private void OnErrorOccurred(object sender, TaskErrorEventArgs e)
        {  
            if (InvokeRequired)  
            {  
                Invoke(new Action(() => OnErrorOccurred(sender, e)));  
                return;  
            }  

            LogMessage($"❌ 错误: 任务 {e.Task.Name} - {e.Exception.Message}");  
        }  

        private void UpdateUI(object sender, EventArgs e)
        {  
            lblQueueCountValue.Text = _taskProcessor.QueueCount.ToString();  
            lblProcessedCountValue.Text = _taskProcessor.ProcessedCount.ToString();  
            lblErrorCountValue.Text = _taskProcessor.ErrorCount.ToString();  

            // 更新CPU使用率(模拟)  
            lblCpuUsageValue.Text = $"{_taskProcessor.CpuUsagePercentage:F1}%";  
        }  

        private void LogMessage(string message)
        {  
            if (InvokeRequired)  
            {  
                Invoke(new Action(() => LogMessage(message)));  
                return;  
            }  

            string logEntry = $"[{DateTime.Now:HH:mm:ss}] {message}";  
            txtLog.AppendText(logEntry + Environment.NewLine);  
            txtLog.SelectionStart = txtLog.Text.Length;  
            txtLog.ScrollToCaret();  
        }  

        protected override void OnFormClosing(FormClosingEventArgs e)
        {  
            _taskProcessor?.Stop();  
            _uiTimer?.Stop();  
            base.OnFormClosing(e);  
        }  
    }  
}  

核心处理器实现

核心处理器负责任务的获取、处理和状态管理。它使用ConcurrentQueue<TaskItem>存储任务,通过循环检查队列状态来处理任务,并在处理过程中处理异常和更新任务计数。

以下是部分关键代码:

public classTraditionalTaskProcessor
{  
    privatereadonly ConcurrentQueue<TaskItem> _queue = new();  
    privatereadonly CancellationTokenSource _cts = new();  
    privatevolatilebool _isRunning = false;  
    privateint _processedCount = 0;  
    privateint _errorCount = 0;  

    public async Task StartProcessing()
    {  
        if (_isRunning) return;  

        _isRunning = true;  
        OnProcessorStatusChanged(true);  

        await Task.Run(async () =>  
        {  
            while (_isRunning && !_cts.Token.IsCancellationRequested)  
            {  
                if (_queue.TryDequeue(outvar task))  
                {  
                    try
                    {  
                        var sw = Stopwatch.StartNew();  
                        await ProcessTask(task);  
                        sw.Stop();  

                        Interlocked.Increment(ref _processedCount);  
                        OnTaskProcessed(task, sw.ElapsedMilliseconds);  
                    }  
                    catch (Exception ex)  
                    {  
                        Interlocked.Increment(ref _errorCount);  
                        OnErrorOccurred(task, ex);  
                    }  
                }  
                else
                {  
                    // ⚠️ 关键问题:CPU空转  
                    await Task.Delay(100, _cts.Token);  
                }  
            }  
        }, _cts.Token);  
    }  

    private async Task ProcessTask(TaskItem task)
    {  
        // 模拟不同复杂度的任务处理  
        var processingTime = new Random().Next(500, 2000);  
        await Task.Delay(processingTime);  

        // 模拟偶发异常  
        if (new Random().Next(1, 20) == 1)  
        {  
            thrownew InvalidOperationException($"任务 {task.Name} 处理失败");  
        }  
    }  
}

UI设计器

UI设计器代码定义了窗体的布局和控件属性,包括控制面板、状态监控面板和日志面板等。

通过设置控件的属性,如文本框的背景色、字体等,提升了界面的美观性和可读性。

应用场景

1、数据处理场景

日志分析系统:处理海量日志文件,通过异步任务处理器提高处理效率,减少系统响应时间。

报表生成:异步生成复杂统计报表,避免阻塞主线程,提升用户体验。

数据同步:定时同步不同系统间的数据,确保数据的一致性和及时性。

2、通知系统场景

邮件队列:批量发送营销邮件,通过任务处理器控制发送速度,避免邮件服务器压力过大。

消息推送:移动端消息推送队列,确保消息能够及时、准确地推送给用户。

短信服务:验证码和通知短信发送,提高短信发送的可靠性和效率。

常见问题

问题1:UI线程安全

在更新UI时,若直接跨线程操作UI控件,会引发异常。正确的做法是使用Invoke方法将操作委托给UI线程执行,确保线程安全。

例如:

// ❌ 错误做法  
private void OnTaskProcessed(TaskProcessedEventArgs e)
{  
    txtLog.AppendText($"任务完成: {e.Task.Name}"); // 跨线程操作异常  
}  

// ✅ 正确做法  
private void OnTaskProcessed(TaskProcessedEventArgs e)
{  
    if (InvokeRequired)  
    {  
        Invoke(new Action(() => OnTaskProcessed(e)));  
        return;  
    }  
    txtLog.AppendText($"任务完成: {e.Task.Name}");  
}

问题2:资源释放

在窗体关闭时,应确保任务处理器停止运行,定时器停止工作,避免资源泄漏。

例如:

protected override void OnFormClosing(FormClosingEventArgs e)  
{  
    _taskProcessor?.Stop(); // 确保优雅停机  
    _uiTimer?.Stop();       // 停止定时器  
    base.OnFormClosing(e);  
}

问题3:异常处理

在处理任务时,应区分不同类型的异常,对正常取消操作不记录为错误,对真实业务异常进行详细记录。

例如:

// ✅ 完善的异常处理  
try
{  
    await ProcessTask(task);  
}  
catch (OperationCanceledException)  
{  
    // 正常取消操作,不记录为错误  
    return;  
}  
catch (Exception ex)  
{  
    // 记录真实业务异常  
    LogError(ex, task);  
}

性能测试数据

经过实际测试,传统轮询方案与优化后方案在各项指标上存在显著差异:

指标
传统轮询
优化后方案
CPU占用率
15 – 25%
2 – 5%
内存使用
120MB
45MB
响应延迟
50 – 150ms
1 – 5ms
并发处理
单线程
多线程池

说明

“好的架构不是设计出来的,而是演进出来的”:从轮询到事件驱动,每一步优化都是为了更好的用户体验。

“性能优化的本质是资源的合理分配”:CPU不应该浪费在无意义的空转上。

“可视化是调试的最佳伙伴”:眼见为实的监控面板让问题无所遁形。

高级进阶方向

Channel模式:探索.NET 中的高性能队列解决方案,进一步提升任务处理效率。

Producer – Consumer模式:研究更高效的生产者消费者实现,优化任务的生产和消费流程。

背压机制:引入背压机制,防止队列溢出,确保系统的稳定性。

分布式队列:集成Redis、RabbitMQ等中间件,实现分布式任务队列,处理大规模任务。

总结

通过这个实战项目,我们直观地看到了传统任务队列方案的局限性。可视化的监控界面让我们能够实时观察系统的运行状态,为性能调优和问题排查提供支持。

回顾三个核心要点:

传统轮询方案存在CPU空转、响应延迟、扩展性差的问题

通过事件驱动和可视化监控可以显著提升开发效率

异常处理和优雅停机是生产环境的必备特性

这只是任务队列优化之旅的第一步。在后续文章中,我们将为大家带来更高效的Channel模式和分布式队列解决方案,帮助大家开发更强大的系统。

《手把手教你用 .NET Core 搭建高并发、可扩展的 CQRS 与 DDD 架构》

mikel阅读(144)

来源: 《手把手教你用 .NET Core 搭建高并发、可扩展的 CQRS 与 DDD 架构》

趣事:

最近有人留言,说讲解一下CQRS和DDD架构,其实DDD有分开讲,在历史文章中有体现,比较好理解。

这几天单独重新回顾一次。

直接开干!

01

第一章:DDD 核心概念与 .NET Core 中的分层架构

1.1 什么是 DDD?

领域驱动设计(Domain-Driven Design)是一种软件开发方法论,强调以业务领域为核心,通过与领域专家的深度合作,构建出能够准确反映业务逻辑的软件模型。其核心思想是将复杂的业务逻辑集中在“领域层”,并通过清晰的边界(如聚合、限界上下文)来管理复杂性。

DDD 适用于业务复杂、规则多变、核心竞争力在于业务逻辑的系统,例如金融、电商、ERP、医疗等系统。

02

1.2 DDD 的核心概念

在 .NET Core 项目中,我们通常会将 DDD 分为以下几个核心概念:

  • 实体(Entity)
    :具有唯一标识的对象,其状态会随时间变化。例如 Order(订单)、Customer(客户)。
  • 值对象(Value Object)
    :没有唯一标识,通过属性值来定义的对象。例如 Address(地址)、Money(金额)。
  • 聚合(Aggregate)
    :一组相关对象的集合,由一个**聚合根(Aggregate Root)**统一管理。聚合根负责维护聚合内部的一致性。例如 Order 是聚合根,包含 OrderItem 等子实体。
  • 领域服务(Domain Service)
    :当某个操作不属于任何实体或值对象时,使用领域服务。它封装了跨多个实体的业务逻辑。
  • 仓储(Repository)
    :提供对聚合的持久化访问,屏蔽底层数据访问细节。在 .NET Core 中,通常通过接口定义仓储,由 Entity Framework Core 实现。
  • 领域事件(Domain Event)
    :表示领域中发生的重要事件,用于解耦和通知。例如 OrderPlacedEvent
  • 限界上下文(Bounded Context)
    :一个明确的业务边界,在此边界内,术语、模型和规则具有一致性。在微服务架构中,一个限界上下文通常对应一个微服务。

03

1.3 .NET Core 中的典型分层架构

在 .NET Core 项目中,我们通常采用六边形架构洋葱架构来实现 DDD。以下是常见的分层结构:

MyApp.Solution├── MyApp.Domain         // 领域层:实体、值对象、聚合、领域服务、仓储接口├── MyApp.Application    // 应用层:应用服务、DTO、CQRS 命令/查询、中介处理├── MyApp.Infrastructure // 基础设施层:EF Core 实现仓储、事件总线、外部服务调用├── MyApp.WebApi         // 表现层:ASP.NET Core Web API,接收请求,返回响应└── MyApp.UnitTests      // 单元测试
  • Domain 层
    :纯业务逻辑,不依赖任何外部框架或基础设施。
  • Application 层
    :协调领域逻辑与基础设施,定义用例。
  • Infrastructure 层
    :实现持久化、消息队列、缓存等。
  • WebApi 层
    :处理 HTTP 请求,调用应用服务。

关键点:依赖关系只能从外向内,即 WebApi → Application → DomainInfrastructure 实现 Domain 的接口。

04

1.4 实战示例:订单聚合

我们以电商系统中的 Order 聚合为例:

// Domain/Entities/Order.cspublic class Order : AggregateRoot{    public Guid CustomerId { getprivate set; }    public decimal TotalAmount { getprivate set; }    public OrderStatus Status { getprivate set; }    private readonly List<OrderItem> _items = new();    public IReadOnlyCollection<OrderItem> Items => _items.AsReadOnly();    // 工厂方法    public static Order Create(Guid customerId, List<OrderItemDto> items)    {        var order = new Order        {            Id = Guid.NewGuid(),            CustomerId = customerId,            Status = OrderStatus.Pending        };        foreach (var item in items)        {            order.AddItem(item.ProductId, item.Quantity, item.UnitPrice);        }        // 添加领域事件        order.AddDomainEvent(new OrderCreatedEvent(order.Id));
        return order;    }    private void AddItem(Guid productId, int quantity, decimal unitPrice)    {        // 业务规则校验        if (quantity <= 0throw new DomainException("数量必须大于0");        var item = new OrderItem(productId, quantity, unitPrice);        _items.Add(item);        TotalAmount += item.Amount;    }}
// Domain/Entities/OrderItem.cspublic class OrderItem : Entity{    public Guid ProductId { getprivate set; }    public int Quantity { getprivate set; }    public decimal UnitPrice { getprivate set; }    public decimal Amount => Quantity * UnitPrice;    public OrderItem(Guid productId, int quantity, decimal unitPrice)    {        ProductId = productId;        Quantity = quantity;        UnitPrice = unitPrice;    }}
// Domain/Repositories/IOrderRepository.cspublic interface IOrderRepository : IRepository<OrderGuid>{    Task<Order> GetByOrderNumberAsync(string orderNumber);    Task<List<Order>> GetByCustomerIdAsync(Guid customerId);}
这一章我们建立了 DDD 的基础模型和分层结构。Order 作为聚合根,封装了创建订单的业务规则,并通过领域事件解耦后续操作。

05

第二章:CQRS 模式详解与在 .NET Core 中的实现

2.1 什么是 CQRS?

CQRS(Command Query Responsibility Segregation,命令查询职责分离)是一种架构模式,其核心思想是将**写操作(命令)读操作(查询)**分离,使用不同的模型来处理。

  • 命令(Command)
    :用于修改系统状态的操作,如创建订单、更新用户信息。它不返回数据,只返回操作结果(成功/失败)。
  • 查询(Query)
    :用于读取数据的操作,如获取订单详情、查询用户列表。它不修改系统状态。

CQRS 的本质:一个系统可以有多个“读模型”,但只有一个“写模型”。

06

2.2 为什么需要 CQRS?

虽然 CRUD 模式简单直接,但在复杂系统中会遇到以下问题:

  1. 性能瓶颈
    :读写共用同一数据库和模型,高并发读或写时互相影响。
  2. 模型复杂性
    :同一个模型既要满足写操作的业务校验,又要满足前端多样化的查询需求,导致模型臃肿。
  3. 扩展性差
    :读写无法独立扩展。
  4. 数据一致性延迟容忍
    :某些场景下,查询数据可以容忍短暂延迟(最终一致性),从而提升读性能。

CQRS 正是为了解决这些问题而生

07

2.3 CQRS 的基本结构

在 .NET Core 中,CQRS 通常与 MediatR 库结合使用,实现请求的中介模式。

客户端   ↓Web API Controller   ↓MediatR (中介者)   ↙       ↘Command Handler          Query Handler   ↓                       ↓Domain Layer (业务逻辑)     Read Model (DTO/ViewModel)   ↓                       ↓Write Database (EF Core)   Read Database (SQL View / NoSQL / Cache)
  • 命令处理器(Command Handler)
    :处理写操作,调用领域模型,执行业务逻辑,持久化聚合。
  • 查询处理器(Query Handler)
    :处理读操作,直接从优化的读模型(如视图、缓存、Elasticsearch)中获取数据,不经过领域层。

08

2.4 在 .NET Core 中集成 MediatR

在 Application 层中安装 MediatR 和 MediatR.Extensions.Microsoft.DependencyInjection 包。

<PackageReference Include="MediatR" Version="12.2.0" /><PackageReference Include="MediatR.Extensions.Microsoft.DependencyInjection" Version="12.1.0" />
在 Program.cs 中注册 MediatR:

// WebApi/Program.csbuilder.Services.AddMediatR(cfg => cfg.RegisterServicesFromAssembly(typeof(CreateOrderCommand).Assembly));

09

2.5 实现 CQRS:创建订单命令

我们以“创建订单”为例,展示命令的定义与处理。

// Application/Commands/CreateOrderCommand.cspublic record CreateOrderCommand(    Guid CustomerId,    List<OrderItemDto> Items) : IRequest<Guid>; // 返回订单ID// DTO 用于传输public record OrderItemDto(Guid ProductId, int Quantity, decimal UnitPrice);
// Application/Commands/CreateOrderCommandHandler.cspublic class CreateOrderCommandHandler : IRequestHandler<CreateOrderCommandGuid>{    private readonly IOrderRepository _orderRepository;    private readonly IUnitOfWork _unitOfWork;    public CreateOrderCommandHandler(IOrderRepository orderRepository, IUnitOfWork unitOfWork)    {        _orderRepository = orderRepository;        _unitOfWork = unitOfWork;    }    public async Task<Guid> Handle(CreateOrderCommand request, CancellationToken ct)    {        // 1. 使用领域模型创建订单        var order = Order.Create(request.CustomerId, request.Items);        // 2. 持久化        await _orderRepository.AddAsync(order, ct);        await _unitOfWork.CommitAsync(ct);        // 3. 可选:发布领域事件(用于后续处理,如发送邮件、库存扣减)        // 事件处理将在后续章节讲解        return order.Id;    }}

10

2.6 实现 CQRS:查询订单详情

查询操作不经过领域模型,直接从优化的读模型中获取。

// Application/Queries/GetOrderQuery.cspublic record GetOrderQuery(Guid OrderId) : IRequest<OrderDto>;// Application/Queries/GetOrderQueryHandler.cspublic class GetOrderQueryHandler : IRequestHandler<GetOrderQueryOrderDto>{    private readonly DapperContext _dapperContext; // 使用 Dapper 高效查询    public GetOrderQueryHandler(DapperContext dapperContext)    {        _dapperContext = dapperContext;    }    public async Task<OrderDto> Handle(GetOrderQuery request, CancellationToken ct)    {        const string sql = @"            SELECT o.Id, o.OrderNumber, o.TotalAmount, o.Status,                   c.Name as CustomerName,                   oi.ProductId, oi.Quantity, oi.UnitPrice            FROM Orders o            JOIN Customers c ON o.CustomerId = c.Id            LEFT JOIN OrderItems oi ON o.Id = oi.OrderId            WHERE o.Id = @OrderId";        using var connection = _dapperContext.CreateConnection();        var lookup = new Dictionary<Guid, OrderDto>();        await connection.QueryAsync<OrderDto, OrderItemDto, OrderDto>(            sql,            (order, item) =>            {                if (!lookup.TryGetValue(order.Id, out var existingOrder))                {                    existingOrder = order;                    existingOrder.Items = new List<OrderItemDto>();                    lookup.Add(order.Id, existingOrder);                }                if (item != null) existingOrder.Items.Add(item);                return existingOrder;            },            request,            splitOn: "ProductId"        );        return lookup.Values.FirstOrDefault();    }}
// DTOspublic record OrderDto(    Guid Id,    string OrderNumber,    decimal TotalAmount,    OrderStatus Status,    string CustomerName,    List<OrderItemDto> Items);

11

广告

12

2.7 控制器中使用 CQRS

// WebApi/Controllers/OrdersController.cs[ApiController][Route("api/[controller]")]public class OrdersController : ControllerBase{    private readonly IMediator _mediator;    public OrdersController(IMediator mediator)    {        _mediator = mediator;    }    [HttpPost]    public async Task<IActionResult> CreateOrder([FromBody] CreateOrderCommand command)    {        var orderId = await _mediator.Send(command);        return CreatedAtAction(nameof(GetOrder), new { id = orderId }, orderId);    }    [HttpGet("{id}")]    public async Task<IActionResult> GetOrder(Guid id)    {        var query = new GetOrderQuery(id);        var order = await _mediator.Send(query);        return Ok(order);    }}

13

2.8 CQRS 的优势总结

  • ✅ 职责分离:写和读逻辑清晰分离。
  • ✅ 性能优化:读模型可独立优化(视图、缓存、NoSQL)。
  • ✅ 可扩展性:读写数据库可独立部署和扩展。
  • ✅ 灵活性:前端查询需求变化不影响写模型。

注意:CQRS 增加了系统复杂性,不是所有项目都需要 CQRS。建议在业务复杂、读写负载差异大的系统中使用。

14

第三章:领域事件与事件溯源(Event Sourcing)

3.1 什么是领域事件?

领域事件(Domain Event)是在领域中发生的重要事情,一旦发生就不可变。它代表了系统状态的改变,例如:

  • OrderPlacedEvent
    (订单已创建)
  • PaymentCompletedEvent
    (支付已完成)
  • InventoryDeductedEvent
    (库存已扣减)

领域事件是实现业务解耦最终一致性的关键。

15

3.2 领域事件的价值

  1. 解耦业务逻辑
    :将主流程与后续操作分离。例如,创建订单后自动发送邮件、更新积分,这些都可以通过事件触发,而无需在订单服务中硬编码。
  2. 实现最终一致性
    :在分布式系统中,通过事件通知其他服务更新状态。
  3. 审计与追溯
    :所有事件可持久化,用于审计或重建状态。
  4. 支持事件溯源
    :为更高级的架构模式打下基础。

又到一年毕业季  到了说珍重的时候  总说  时光不老,我们不散  毕业遥遥无期 转眼间就各奔东西  毕业,有着说不完的话题  因为那是懵懂的结束  成熟的开始。

16

3.3 在 .NET Core 中实现领域事件

我们通过 MediatR 来发布和处理领域事件。

17

3.3.1 定义领域事件
// Domain/Events/OrderCreatedEvent.cspublic record OrderCreatedEvent(Guid OrderId) : INotification;
实现 INotification 接口,表示这是一个广播事件,可以有多个处理器。

18

3.3.2 在聚合根中发布事件
// Domain/Entities/Order.cspublic class Order : AggregateRoot{    // ... 其他代码    public static Order Create(Guid customerId, List<OrderItemDto> items)    {        var order = new Order        {            Id = Guid.NewGuid(),            CustomerId = customerId,            Status = OrderStatus.Pending        };        foreach (var item in items)        {            order.AddItem(item.ProductId, item.Quantity, item.UnitPrice);        }        // 发布事件        order.AddDomainEvent(new OrderCreatedEvent(order.Id));
        return order;    }}

19

3.3.3 在仓储中发布事件

事件的发布通常在事务提交后进行。我们可以在 UnitOfWork 提交后,遍历所有聚合的领域事件并发布。

// Infrastructure/Data/UnitOfWork.cspublic class UnitOfWork : IUnitOfWork{    private readonly AppDbContext _context;    private readonly IPublisher _mediator; // MediatR 的发布者    public UnitOfWork(AppDbContext context, IPublisher mediator)    {        _context = context;        _mediator = mediator;    }    public async Task<boolCommitAsync(CancellationToken ct)    {        // 1. 保存实体变更        var result = await _context.SaveChangesAsync(ct);        // 2. 发布所有聚合根的领域事件        await PublishDomainEvents(ct);        return result > 0;    }    private async Task PublishDomainEvents(CancellationToken ct)    {        var aggregates = _context.ChangeTracker            .Entries<IAggregateRoot>()            .Where(x => x.Entity.DomainEvents.Any())            .Select(x => x.Entity)            .ToList();        foreach (var aggregate in aggregates)        {            var events = aggregate.DomainEvents.ToList();            aggregate.ClearDomainEvents(); // 清空已发布的事件            foreach (var @event in events)            {                await _mediator.Publish(@event, ct);            }        }    }}

20

3.3.4 事件处理器(Event Handler)
// Application/Handlers/OrderCreatedEventHandler.cspublic class OrderCreatedEventHandler : INotificationHandler<OrderCreatedEvent>{    private readonly ILogger<OrderCreatedEventHandler> _logger;    private readonly IEmailService _emailService;    private readonly IInventoryService _inventoryService; // 可能是 gRPC 或 HTTP 客户端    public OrderCreatedEventHandler(        ILogger<OrderCreatedEventHandler> logger,        IEmailService emailService,        IInventoryService inventoryService)    {        _logger = logger;        _emailService = emailService;        _inventoryService = inventoryService;    }    public async Task Handle(OrderCreatedEvent notification, CancellationToken ct)    {        _logger.LogInformation("订单 {OrderId} 已创建,正在处理后续操作...", notification.OrderId);        // 1. 发送订单确认邮件        await _emailService.SendOrderConfirmationAsync(notification.OrderId, ct);        // 2. 调用库存服务扣减库存(可能是异步消息)        await _inventoryService.DeductInventoryAsync(notification.OrderId, ct);        // 3. 更新用户积分(领域服务)        // await _pointsService.AddPointsAsync(...);    }}
事件处理器可以有多个,每个关注不同的业务。

21

3.4 事件溯源(Event Sourcing)

事件溯源是一种更激进的持久化模式:不保存实体的当前状态,而是保存导致状态变化的所有事件。实体的状态是通过重放事件来重建的。

22

3.4.1 事件溯源的核心思想
  • 状态是事件的投影
    CurrentState = Apply(Events...)
  • 事件是唯一真相源
    (Source of Truth)

23

3.4.2 事件溯源的结构
Command → [Aggregate] → Events → Event Store → (Replay) → Current State                                 ↓                             Projections → Read Models

24

3.4.3 何时使用事件溯源?
  • 需要完整审计日志
  • 需要时间旅行(查看历史状态)
  • 系统由事件驱动,状态变化频繁
  • 与 CQRS 天然结合

25

26

3.4.4 简单示例:事件存储
// Domain/Events/OrderPlacedEvent.cspublic record OrderPlacedEvent(    Guid OrderId,    Guid CustomerId,    List<OrderItem> Items,    DateTime PlacedAt) : IEvent;// Infrastructure/EventStore/IEventStore.cspublic interface IEventStore{    Task SaveEventsAsync<T>(Guid aggregateId, IEnumerable<IEvent> events, int expectedVersion);    Task<List<IEvent>> GetEventsAsync(Guid aggregateId);}// 简单实现(实际可用 EventStoreDB、Cosmos DB 等)public class SqlEventStore : IEventStore{    public async Task SaveEventsAsync<T>(Guid aggregateId, IEnumerable<IEvent> events, int expectedVersion)    {        // 将事件保存到数据库表        // 包含 AggregateId, AggregateType, Version, EventType, Data (JSON)    }    public async Task<List<IEvent>> GetEventsAsync(Guid aggregateId)    {        // 从数据库加载所有事件并反序列化    }}
事件溯源会显著增加复杂性,建议在有明确需求时再引入。

27

3.5 领域事件 vs 事件溯源

特性
领域事件
事件溯源
持久化
事件可选持久化
事件是唯一持久化形式
状态存储
保存当前状态
不保存状态,通过事件重建
复杂度
中等
适用场景
解耦、通知
审计、历史追溯、CQRS 组合

28

第四章:CQRS 与 DDD 的深度整合与实战模式

在前几章中,我们分别介绍了 DDD 的分层、CQRS 的分离以及领域事件的使用。本章将聚焦于如何将 CQRS 与 DDD 深度整合,并介绍一些在 .NET Core 项目中常见的实战模式和最佳实践。

29

4.1 CQRS + DDD 的典型数据流

理解完整的请求生命周期是掌握架构的关键:

HTTP Request (WebApi)       ↓MediatR Request (Command / Query)       ↙                    ↘[Command Handler]        [Query Handler]       ↓                         ↓Domain Layer:              Read-Optimized:- Load Aggregate           - Direct SQL/Dapper- Execute Business Logic   - Cache (Redis)- Validate Rules           - Elasticsearch- Emit Domain Events       - Return DTO       ↓Persist via Repository & UnitOfWork       ↓Publish Domain Events → Event Handlers (Async)

关键点

  • 写路径
    :必须经过领域模型,确保业务规则被强制执行。
  • 读路径
    :绕过领域模型,直接访问优化的数据源,追求性能。

30

4.2 实战模式一:异步最终一致性

在分布式系统中,强一致性往往带来性能瓶颈。通过领域事件实现最终一致性是常见做法。

场景:用户下单后,需要扣减库存。

传统做法(同步强一致)

// 在命令处理器中直接调用库存服务await _inventoryService.DeductAsync(orderId); // 失败则订单创建失败
问题:库存服务不可用会导致订单无法创建。

改进做法(异步最终一致)

// OrderCreatedEventHandler.cspublic async Task Handle(OrderCreatedEvent notification, CancellationToken ct){    // 发布一个集成事件到消息队列(如 RabbitMQ/Kafka)    await _messageBus.PublishAsync(new InventoryDeductionRequestedEvent(        OrderId: notification.OrderId,        Items: orderItems // 可以从仓储加载    ), ct);}

库存服务监听 InventoryDeductionRequestedEvent,执行扣减。如果失败,可重试或进入死信队列人工处理。

✅ 优势:订单服务不再依赖库存服务,系统更健壮。 ❌ 代价:短暂时间内数据不一致(订单已创建但库存未扣)。

31

4.3 实战模式二:查询模型的优化策略

CQRS 的查询侧有多种优化方式:

策略
描述
适用场景
数据库视图
创建 SQL 视图,预计算关联数据
查询结构稳定,数据量不大
独立读库
主库写,从库读(读写分离)
读远多于写
缓存(Redis)
将查询结果缓存
高频访问、低频更新的数据
物化视图/Projection
用事件驱动的方式维护一个专用的读表
复杂聚合查询
Elasticsearch
全文搜索、复杂过滤
商品搜索、日志分析

示例:使用 Redis 缓存订单详情

// GetOrderQueryHandler.cspublic async Task<OrderDto> Handle(GetOrderQuery request, CancellationToken ct){    var cacheKey = $"order:{request.OrderId}";
    // 先查缓存    var cached = await _cache.GetStringAsync(cacheKey, ct);    if (!string.IsNullOrEmpty(cached))    {        return JsonSerializer.Deserialize<OrderDto>(cached);    }    // 缓存未命中,查数据库    var order = await LoadFromDatabase(request.OrderId, ct);
    // 写入缓存(设置过期时间)    await _cache.SetStringAsync(        cacheKey,         JsonSerializer.Serialize(order),         TimeSpan.FromMinutes(10),         ct);    return order;}
注意:当订单状态更新时,需清除或更新缓存,保证一致性。

32

4.4 实战模式三:Saga 分布式事务管理

当一个业务操作跨越多个限界上下文(微服务)时,需要用 Saga 模式来管理分布式事务。

场景:下单流程涉及 订单服务 → 支付服务 → 库存服务 → 物流服务。

Saga 流程

  1. Order Service
    : 创建订单(初始状态为 “PendingPayment”)
  2. 发布 OrderCreatedEvent
  3. Payment Service
    : 监听事件,发起支付
  4. 支付成功,发布 PaymentCompletedEvent
  5. Inventory Service
    : 扣减库存,发布 InventoryDeductedEvent
  6. Shipping Service
    : 创建发货单

补偿机制:如果任一环节失败,触发补偿事务(Compensating Transaction):

  • 支付失败 → 订单取消
  • 库存不足 → 支付退款

Saga 可以是编排式(Orchestration)或协同式(Choreography)。DDD 中常用协同式(通过事件驱动)。

33

4.5 实战模式四:限界上下文与微服务划分

DDD 的“限界上下文”是划分微服务的理想依据。

电商系统的限界上下文示例

限界上下文
聚合根
职责
订单上下文
Order, OrderItem
订单生命周期管理
支付上下文
Payment, Refund
支付、退款
库存上下文
ProductStock
库存扣减、回滚
客户上下文
Customer, Address
客户信息管理
营销上下文
Coupon, Promotion
优惠券、促销活动

每个上下文可以独立部署为微服务,通过 API 或事件进行通信。

34

4.6 最佳实践总结

  1. 不要过度设计
    :简单 CRUD 场景无需 CQRS 和事件溯源。
  2. 先做 CQRS,再考虑事件溯源
    :事件溯源复杂度高,慎用。
  3. 领域事件命名
    :使用过去时态,如 OrderShippedEvent
  4. 事件幂等性
    :确保事件处理器可安全重试。
  5. 监控与重试
    :对事件总线、消息队列进行监控,实现失败重试机制。
  6. 文档化上下文映射
    :明确各限界上下文之间的关系(合作关系、防腐层等)。

35

本章小结:我们探讨了 CQRS 与 DDD 在实际项目中的整合方式,包括最终一致性、查询优化、Saga 事务和微服务划分。这些模式帮助我们在保持业务清晰的同时,提升系统的可扩展性和健壮性。

36

第五章:项目结构优化、测试策略与部署考量

在掌握了 DDD 和 CQRS 的核心概念与实战模式后,本章将关注项目的可维护性、可测试性生产部署的实际考量。一个成功的架构不仅要在设计上合理,更要在工程实践中可持续。

37

5.1 项目结构优化与模块化

随着业务增长,项目可能变得庞大。合理的模块化能提升代码可维护性。

38

5.1.1 按限界上下文组织项目
eShop.Solution├── eShop.Ordering                # 订单上下文(微服务)│   ├── Ordering.Domain│   ├── Ordering.Application│   ├── Ordering.Infrastructure│   └── Ordering.WebApi├── eShop.Payment                 # 支付上下文│   ├── Payment.Domain│   ├── Payment.Application│   └── ...├── eShop.SharedKernel            # 共享内核(ID、时间、异常基类)├── eShop.EventBus                # 事件总线抽象与实现└── eShop.UnitTests               # 共享测试基类
优点:每个上下文独立开发、部署、扩展。

39

5.1.2 使用功能切片(Vertical Slice Architecture)

对于单体应用,可采用“功能切片”替代传统分层,减少层间依赖。

// Features/Orders/CreateOrder/├── CreateOrderCommand.cs├── CreateOrderCommandHandler.cs├── CreateOrderValidator.cs      // FluentValidation└── CreateOrderResponse.cs// Features/Orders/GetOrder/├── GetOrderQuery.cs├── GetOrderQueryHandler.cs└── GetOrderValidator.cs
优点:功能高度内聚,新增功能无需跨多层修改。

40

5.2 测试策略

DDD + CQRS 架构需要分层测试:

测试类型
范围
工具
说明
单元测试
领域模型、服务
xUnit, Moq
测试实体行为、业务规则
集成测试
命令/查询处理器
xUnit, TestServer
测试应用层与基础设施集成
端到端测试
API 接口
xUnit, SpecFlow
模拟用户场景
契约测试
微服务接口
Pact
确保服务间兼容

41

5.2.1 领域模型单元测试示例
// Ordering.Domain.Tests/OrderTests.cspublic class OrderTests{    [Fact]    public void CreateOrder_WithValidItems_ShouldSucceed()    {        // Arrange        var customerId = Guid.NewGuid();        var items = new List<OrderItemDto>        {            new(Guid.NewGuid(), 2100m)        };        // Act        var order = Order.Create(customerId, items);        // Assert        Assert.Equal(200m, order.TotalAmount);        Assert.Single(order.Items);        Assert.Contains(order.DomainEvents, e => e is OrderCreatedEvent);    }    [Fact]    public void AddItem_WithInvalidQuantity_ShouldThrow()    {        // Arrange        var order = Order.Create(Guid.NewGuid(), new List<OrderItemDto>());        // Act & Assert        Assert.Throws<DomainException>(() =>             order.AddItem(Guid.NewGuid(), 0100m));    }}

42

5.2.2 命令处理器集成测试
// Ordering.Application.Tests/CreateOrderCommandHandlerTests.cspublic class CreateOrderCommandHandlerTests : IClassFixture<DatabaseFixture>{    private readonly DatabaseFixture _fixture;    public CreateOrderCommandHandlerTests(DatabaseFixture fixture)    {        _fixture = fixture;    }    [Fact]    public async Task Handle_ValidCommand_ShouldPersistOrder()    {        // Arrange        var handler = _fixture.GetService<IRequestHandler<CreateOrderCommand, Guid>>();        var command = new CreateOrderCommand(            CustomerId: Guid.NewGuid(),            Items: new List<OrderItemDto> { /* ... */ }        );        // Act        var orderId = await handler.Handle(command, CancellationToken.None);        // Assert        var order = await _fixture.OrderRepository.GetByIdAsync(orderId);        Assert.NotNull(order);        Assert.Equal(OrderStatus.Pending, order.Status);    }}
DatabaseFixture 使用 Testcontainers 或内存数据库(如 SQLite)隔离测试。

43

5.3 生产部署考量

5.3.1 数据库策略
  • 写库
    :使用高性能关系型数据库(如 PostgreSQL, SQL Server),确保 ACID。
  • 读库:
    • 使用物化视图CQRS 读表,通过事件驱动更新。
    • 高频查询使用 Redis 缓存。
    • 复杂搜索使用 Elasticsearch

44

45

5.3.2 事件总线选型
方案
适用场景
In-Memory

 (MediatR)
单体应用,事件处理器在同一进程
RabbitMQ
微服务,需要可靠传递、重试
Kafka
高吞吐、事件溯源、流处理
Azure Service Bus
Azure 生态

建议:初期使用 RabbitMQ,成熟后根据需求迁移。

46

5.3.3 监控与可观测性
  • 日志
    :使用 Serilog 结构化日志,记录命令、事件、错误。
  • 追踪
    :集成 OpenTelemetry,追踪请求链路。
  • 指标
    :暴露 Prometheus 指标(如命令处理时间、事件发布延迟)。
  • 告警
    :对事件积压、失败任务设置告警。

47

5.3.4 部署模式
  • 蓝绿部署 / 金丝雀发布
    :降低发布风险。
  • 健康检查
    :实现 /health 端点,检查数据库、事件总线连接。
  • 配置管理
    :使用 IConfiguration + 配置中心(如 Azure App Configuration)。

48

5.4 常见陷阱与规避

  1. 过度工程
    :不是所有项目都需要 CQRS 和事件溯源。从简单开始。
  2. 事件风暴滥用
    :领域事件应代表业务关键决策,而非所有状态变更。
  3. 事务边界不清晰
    :确保 UnitOfWork 正确管理事务,避免部分提交。
  4. 循环依赖
    :避免服务间循环发布事件。
  5. 缺乏文档
    :用 上下文映射图(Context Map)记录限界上下文关系。

49

结语

DDD 与 CQRS 是强大的架构工具,但它们不是银弹。成功的关键在于:

  • 以业务为核心
    :模型必须准确反映领域知识。
  • 渐进式演进
    :从单体开始,逐步拆分限界上下文。
  • 团队共识
    :统一术语(通用语言),确保开发、产品、领域专家理解一致。
  • 持续重构
    :架构随业务发展而演进。

50

第六章:应对分布式挑战——一致性、性能与弹性

当我们将 DDD 和 CQRS 应用于生产级系统,尤其是微服务架构时,会面临一系列分布式系统特有的挑战。本章将聚焦于如何在保持业务清晰的同时,确保系统的数据一致性、高性能和高可用性

51

6.1 数据一致性:最终一致性 vs 强一致性

在 CQRS 架构中,写模型读模型是分离的。这意味着,当一个命令成功执行后,查询结果可能不会立即反映最新状态。

图片

问题:在 t2 到 t4 之间,客户端查询可能看到旧数据或“未找到”。

解决方案
  1. 接受最终一致性
    • 说明
      :这是 CQRS 的默认模式。向客户端明确说明数据是“最终一致”的。
    • 适用场景
      :大多数业务场景(如社交动态、商品列表)可以容忍短暂延迟。
  2. 读写后读取(Read-Your-Writes Consistency)
    • 方案 A(简单)
      :命令成功后,不立即让客户端查询,而是直接返回完整的创建结果(DTO)。
    • 说明
      :确保用户能立即看到自己刚刚写入的数据。
    • 实现:
// Command Handler 返回完整 OrderDtopublic async Task<OrderDtoHandle(CreateOrderCommand request, ...){    var order = Order.Create(...);    await _repo.AddAsync(order);    await _uow.CommitAsync();    // 直接返回,避免查询延迟    return _mapper.Map<OrderDto>(order);}
  1. 方案 B(复杂)

    :在查询时,如果请求的是“自己刚创建的资源”,则回退到从写库查询(牺牲一点性能换取一致性)。
  2. 会话一致性(Session Consistency)
    • 说明
      :确保同一个用户在一次会话中看到的数据是单调递增的。
    • 实现
      :使用时间戳版本号。查询时带上上次操作的时间戳,只返回该时间戳之后的数据。

52

6.2 性能优化:CQRS 的读写优化策略

CQRS 的核心优势之一就是可以独立优化读写路径。

53

6.2.1 写路径优化
  • 批量处理
    :对于高频写操作(如日志、指标),使用批量提交减少数据库压力。
  • 异步持久化
    :将事件先写入高速队列(如 Kafka),再由后台消费者持久化到数据库。
  • 聚合设计
    :避免大聚合。过大的聚合会导致并发冲突(乐观锁失败率高)。合理拆分聚合根。

54

6.2.2 读路径优化(核心)
技术
描述
适用场景
数据库读写分离
主库写,多个从库读
读远多于写的场景
Redis 缓存
将热点数据(如商品详情)缓存
高频访问、低频更新
物化视图 (Materialized View)
预计算复杂查询结果,存为物理表
报表、聚合统计
Elasticsearch
全文检索、复杂过滤、高亮
商品搜索、日志分析
CDN
缓存静态资源或 API 响应
全球用户访问

示例:用事件驱动更新物化视图

// 当 OrderCreatedEvent 发生时,更新 OrderSummary 表
public class OrderCreatedProjection : INotificationHandler<OrderCreatedEvent>{    private readonly AppDbContext _readContext; // 专用的读库上下文    public async Task Handle(OrderCreatedEvent e, CancellationToken ct)    {        var summary = new OrderSummary        {            OrderId = e.OrderId,            Status = "Pending",            CreatedAt = DateTime.UtcNow,            // ... 其他预计算字段        };        await _readContext.OrderSummaries.AddAsync(summary, ct);        await _readContext.SaveChangesAsync(ct);    }}
关键:投影(Projection)是幂等的,可以安全重放。

55

6.3 弹性与容错:处理失败与重试

在分布式系统中,失败是常态。我们必须设计有弹性的系统。

56

6.3.1 命令处理的幂等性

确保同一个命令被多次处理时,结果一致。

// CreateOrderCommand.cs
public record CreateOrderCommand(
    Guid CommandId, // 唯一标识
    Guid CustomerId,
    List<OrderItemDto> Items) : IRequest<Guid>;
// Command Handler
public async Task<GuidHandle(CreateOrderCommand request, CancellationToken ct){
    // 1. 检查该 CommandId 是否已处理
    if (await _commandLog.IsProcessedAsync(request.CommandId, ct))    {        // 返回上次的结果(幂等)        return await _commandLog.GetResultAsync<Guid>(request.CommandId, ct);    }    // 2. 正常处理逻辑    var order = Order.Create(request.CustomerId, request.Items);    await _repo.AddAsync(order);    await _uow.CommitAsync();    
// 3. 记录命令已处理
    await _commandLog.LogProcessedAsync(request.CommandId, order.Id, ct);
    return order.Id;}

57

6.3.2 事件处理的重试机制

领域事件或集成事件发布失败时,必须重试。

  • 使用消息队列
    :RabbitMQ、Kafka 天然支持消息持久化和重试。
  • 死信队列 (DLQ)
    :处理多次重试仍失败的消息,便于人工干预。
  • 指数退避
    :重试间隔逐渐增加,避免雪崩。
// RabbitMQ Consumer 示例
try{
    await HandleEvent(event);
    await model.BasicAck(deliveryTag, false);
 // 确认}
catch (Exception ex){
    
// 记录日志    
// 消息将自动重回队列或进入DLQ
    throw// 不确认,触发重试}

58

59

6.4 分布式事务与 Saga 模式的进阶

在第五章我们提到了 Saga 模式。这里深入其**编排式(Orchestration)**实现。

场景:订单流程(创建 → 支付 → 发货)

图片

Orchestrator (编排器) 的职责

  1. 定义 Saga 的执行流程。
  2. 发送命令给各个服务。
  3. 监听各步骤的完成事件。
  4. 处理失败,触发补偿命令。

优点:流程清晰,易于调试和监控。缺点:编排器可能成为单点。

选择建议

  • 流程简单、服务少:用协同式(事件驱动)。
  • 流程复杂、需要精确控制:用编排式

60

6.5 监控、追踪与调试

复杂的分布式系统必须具备强大的可观测性。

  • 结构化日志
    :使用 Serilog + Seq 或 ELK,记录命令、事件、错误。
  • 分布式追踪
    :集成 OpenTelemetry,追踪一个请求从 API 到数据库再到其他服务的完整链路。
  • 指标监控
    :使用 Prometheus + Grafana 监控:
    • 命令处理延迟
    • 事件发布/消费速率
    • 数据库连接数
    • 缓存命中率
  • 健康检查
    :实现 /health 端点,检查数据库、Redis、消息队列等依赖。

61

本章小结:我们探讨了在真实生产环境中,如何应对 CQRS 和 DDD 带来的分布式挑战。核心思想是:接受最终一致性、通过独立优化读写路径提升性能、设计幂等和重试机制保证弹性

62

第七章:演进式架构——从单体到微服务的平滑过渡

在前几章中,我们构建了一个基于 DDD 和 CQRS 的强大单体应用。然而,随着业务规模扩大、团队增多,单体架构可能成为瓶颈。本章将探讨如何以领域驱动的方式,将单体应用平滑演进为微服务架构,避免“大爆炸式”重写。

63

7.1 何时拆分微服务?

并非所有系统都需要微服务。以下信号表明可能是时候考虑拆分了:

  1. 团队协作困难
    :多个团队频繁修改同一代码库,导致合并冲突和发布阻塞。
  2. 技术栈异构需求
    :某个功能需要特定技术(如 AI 服务用 Python,核心交易用 .NET)。
  3. 伸缩性需求差异
    :订单服务需要 10 台服务器,而客服系统只需 2 台。
  4. 发布频率不同
    :营销活动需要每日发布,而财务系统每月发布一次。
  5. DDD 限界上下文清晰
    :业务边界明确,上下文映射图已定义。

关键以业务能力(Bounded Context)为单位拆分,而非技术分层。

64

7.2 演进策略:Strangler Fig 模式

“绞杀者模式”(Strangler Fig Pattern)是一种安全的演进策略:新建的微服务逐步“绞杀”旧的单体功能,直到单体被完全替代

65

7.2.1 阶段一:识别并隔离限界上下文

在单体应用中,首先通过命名空间或项目来物理隔离不同的限界上下文。

// 单体应用中的模块化

MyApp.Solution

├── MyApp.Ordering          // 订单上下文├── MyApp.Customer          // 客户上下文├── MyApp.Catalog           // 商品目录上下文├── MyApp.SharedKernel

└── MyApp.WebApi            // API 网关入口

  • 目标:
    减少上下文间的耦合,明确依赖方向(如 Ordering 依赖 Catalog)。
  • 实践:
    • 使用 InternalsVisibleTo 限制程序集访问。
    • 通过 领域事件 耦合,而非直接调用服务。

66

7.2.2 阶段二:暴露 API 与防腐层(ACL)

为即将拆分的上下文设计稳定的 API,并为外部依赖创建防腐层(Anti-Corruption Layer)

//Ordering/Infrastructure/CatalogAcl.cs

public class CatalogAcl : ICatalogService{    private readonly HttpClient _client;    public async Task<ProductDto> GetProductAsync(GuidproductId)    {

        // 调用 Catalog 微服务的 HTTP API        var response = await _client.GetAsync($"/api/products/{productId}");        // 将外部模型转换为内部模型        var external = await response.Content.ReadFromJsonAsync<ExternalProductDto>();        return _mapper.Map<ProductDto>(external);    }}

防腐层的作用:防止外部服务的模型变化污染内部领域模型。

67

7.2.3 阶段三:逐步迁移流量
  1. 部署新微服务
    :将 Catalog 上下文部署为独立的微服务 catalog-service
  2. 双写或双读:
    • 双写
      :在单体和新服务中同时写入数据(确保数据同步)。
    • 双读
      :新服务先从单体数据库读取,验证数据一致性。
  3. 切换流量
    :通过 API 网关(如 Ocelot、YARP)将 /api/products/* 的流量逐步导向新服务。
  4. 移除旧代码
    :确认新服务稳定后,移除单体中的 Catalog 模块和双写逻辑。
图片

68

7.3 微服务间的通信模式

拆分后,服务间通信至关重要。

模式
描述
工具
适用场景
同步 HTTP/REST
请求-响应模式
HttpClient, Refit
简单查询、强一致性要求
gRPC
高性能 RPC,强类型
gRPC .NET
高频调用、低延迟
异步消息
通过消息队列通信
RabbitMQ, Kafka
解耦、最终一致性、事件驱动

建议:优先使用异步消息保持松耦合,必要时使用同步调用。

69

7.4 数据管理:避免分布式事务

微服务中应尽量避免跨服务的分布式事务(如两阶段提交),因其性能差且复杂。

替代方案

  1. Saga 模式
    :如第六章所述,用补偿事务管理长流程。
  2. CQRS + 事件溯源
    :通过事件流保证数据一致性。
  3. API 组合器
    :在查询时,由网关或前端服务聚合多个微服务的数据。

70

7.5 运维与治理

微服务带来运维复杂性,需建立配套体系:

  • 服务发现
    :Consul, Eureka, Kubernetes DNS。
  • 配置中心
    :Azure App Configuration, Consul KV。
  • API 网关
    :路由、认证、限流、熔断。
  • CI/CD
    :每个服务独立的流水线。
  • 日志与监控
    :集中式日志(ELK)、分布式追踪(Jaeger)、指标(Prometheus)。

71

7.6 案例:电商系统拆分路径

  1. 第一阶段
    :拆分 Catalog(商品目录),因其独立性强、访问量大。
  2. 第二阶段
    :拆分 Ordering(订单),因其业务复杂,需独立伸缩。
  3. 第三阶段
    :拆分 Payment(支付),因其涉及第三方集成和安全要求。
  4. 第四阶段
    Customer(客户)和 Marketing(营销)作为独立服务。

每一步都小步迭代,确保系统始终可用

72

本章小结:我们学习了如何以 Strangler Fig 模式,将一个 DDD + CQRS 的单体应用,安全、可控地演进为微服务架构。关键在于以限界上下文为拆分单元,使用防腐层隔离变化,并通过 API 网关逐步切换流量

这种演进式方法最大限度地降低了风险,是大型系统架构演进的推荐实践。

希望这些内容能帮助你在 .NET Core 项目中成功应用 DDD 和 CQRS。如需深入某个主题,请随时提问!

73