普通视图

发现新文章,点击刷新页面。
昨天 — 2025年9月26日首页

楖览:Vue3 源码研究导读

作者 excel
2025年9月26日 20:50

前言

Vue3 的源码采用 模块化 Monorepo 架构,分布在 packages/ 目录下。每个模块都承担着清晰的职责:有的处理模板编译,有的负责运行时渲染,有的提供响应式引擎。
理解这些模块的关系,是研究源码的第一步。


一、编译层

1. compiler-core

  • 作用:模板编译的核心逻辑。

    • 输入:模板字符串 <div>{{ msg }}</div>
    • 输出:渲染函数源码
  • 特点:平台无关,只管 AST 生成和转换。

  • 示例

    import { baseParse } from '@vue/compiler-core'
    
    const ast = baseParse('<p>{{ hello }}</p>')
    console.log(ast.children[0].tag) // 输出 "p"
    

2. compiler-dom

  • 作用:扩展 compiler-core,加入浏览器平台相关逻辑。

  • 应用场景:处理 DOM 专用指令,例如 v-model、事件绑定。

  • 示例

    import { compile } from '@vue/compiler-dom'
    
    const { code } = compile('<button @click="count++">{{ count }}</button>')
    console.log(code) 
    // 输出渲染函数源码字符串,内部包含 _createElementVNode 等调用
    

3. compiler-sfc

  • 作用:处理单文件组件(.vue),解析 <template><script><style>

  • 示例

    import { parse } from '@vue/compiler-sfc'
    
    const source = `
    <template><div>{{ msg }}</div></template>
    <script>export default { data(){ return { msg: 'hi' } } }</script>
    `
    const { descriptor } = parse(source)
    console.log(descriptor.template.content) // "<div>{{ msg }}</div>"
    

4. compiler-ssr

  • 作用:专用于服务端渲染,输出字符串拼接代码而不是 DOM 操作。

  • 示例

    import { compile } from '@vue/compiler-ssr'
    
    const { code } = compile('<div>{{ msg }}</div>')
    console.log(code) 
    // 输出包含 ctx.msg 的字符串拼接函数
    

二、运行时层

1. runtime-core

  • 作用:Vue 运行时的核心,包含组件系统、虚拟 DOM、生命周期调度。

  • 特点:不依赖任何平台 API,可移植。

  • 示例

    import { h, render } from '@vue/runtime-core'
    
    const vnode = h('h1', null, 'Hello Core')
    render(vnode, document.body)
    

2. runtime-dom

  • 作用:为浏览器环境实现 runtime-core 的渲染逻辑,调用真实的 DOM API。

  • 示例

    import { createApp } from 'vue'
    
    const App = {
      data: () => ({ count: 0 }),
      template: `<button @click="count++">{{ count }}</button>`
    }
    
    createApp(App).mount('#app')
    

3. runtime-test

  • 作用:提供一个测试用渲染器,不依赖真实 DOM。

  • 示例

    import { createApp, h } from '@vue/runtime-test'
    
    const App = { render: () => h('div', 'test') }
    const root = {}
    createApp(App).mount(root)
    
    console.log(root.children[0].type) // "div"
    

4. server-renderer

  • 作用:在 Node.js 环境下生成 HTML 字符串,配合 compiler-ssr 使用。

  • 示例

    import { renderToString } from '@vue/server-renderer'
    import { createSSRApp } from 'vue'
    
    const app = createSSRApp({ template: `<h1>Hello SSR</h1>` })
    const html = await renderToString(app)
    console.log(html) // "<h1>Hello SSR</h1>"
    

三、基础模块

1. reactivity

  • 作用:Vue 响应式系统的核心。

  • 示例

    import { ref, effect } from '@vue/reactivity'
    
    const count = ref(0)
    effect(() => console.log('count changed:', count.value))
    
    count.value++ // 触发 effect
    

2. shared

  • 作用:公共工具函数与常量,整个源码都会用到。

  • 示例

    import { isArray } from '@vue/shared'
    
    console.log(isArray([1, 2, 3])) // true
    

四、整合入口

1. vue

  • 作用:开发者使用的入口包,整合了所有子模块。

  • 示例

    import { createApp, ref } from 'vue'
    
    const App = {
      setup() {
        const msg = ref('Hello Vue3')
        return { msg }
      },
      template: `<p>{{ msg }}</p>`
    }
    
    createApp(App).mount('#app')
    

五、模块关系图

        compiler-core
         /       \
 compiler-dom   compiler-ssr
       |             |
   compiler-sfc   server-renderer
       |
     vue (整合入口)
       |
 runtime-core —— reactivity —— shared
       |
 runtime-dom / runtime-test

六、结语

Vue3 的源码像一座“分层工厂”:

  • 编译层:把模板翻译成渲染函数。
  • 运行时层:执行渲染函数,生成 DOM 或字符串。
  • 基础模块:提供响应式与工具函数。
  • 整合入口:最终打包成 vue,交到开发者手里。

这张导览图与示例,能帮助你快速定位源码,带着问题深入研究细节。

本文部分内容借助 AI 辅助生成,并由作者整理审核。

前端项目中的测试分类与实践 —— 以 Vue 项目为例

作者 excel
2025年9月26日 20:42

在现代前端工程化体系中,测试已经成为保障代码质量和开发效率的关键环节。一个大型框架(如 Vue)通常会设计多种测试命令,来覆盖不同层面的需求。以下将对常见的几类测试命令进行拆解说明,并配合示例代码来帮助理解。


一、单元测试(test-unit

概念

单元测试(Unit Test)关注的是最小逻辑单元,例如一个函数或一个小组件。

示例

// math.ts
export function add(a: number, b: number) {
  return a + b;
}
// math.test.ts
import { describe, it, expect } from 'vitest';
import { add } from './math';

describe('math utils', () => {
  it('add should return correct sum', () => {
    expect(add(1, 2)).toBe(3); // ✅ 测试基本逻辑
  });

  it('add should work with negative numbers', () => {
    expect(add(-1, 5)).toBe(4);
  });
});

👉 说明

  • 粒度小,执行快。
  • 不依赖构建产物,直接跑源码。

二、端到端测试(test-e2e

概念

端到端测试(E2E Test)是模拟用户的实际操作来验证系统行为,常用于验证打包后的产物。

示例

<!-- App.vue -->
<template>
  <button @click="count++">Clicked {{ count }} times</button>
</template>

<script setup lang="ts">
import { ref } from 'vue';
const count = ref(0);
</script>
// app.e2e.test.ts
import { test, expect } from '@playwright/test';

test('button click should increase counter', async ({ page }) => {
  await page.goto('http://localhost:5173'); // 假设启动了 dev server
  const button = page.getByRole('button');
  await button.click();
  await expect(button).toHaveText('Clicked 1 times'); // ✅ 模拟真实点击
});

👉 说明

  • 需要先 build 出产物,然后在浏览器中运行。
  • 粒度大,接近真实用户体验。
  • 执行速度比单测慢。

三、类型声明测试(test-dts / test-dts-only

概念

类型测试的目标是保证生成的 .d.ts 文件能正常工作,让 TypeScript 用户拥有良好的类型提示。

示例

// vue-shim.d.ts (假设库生成的声明文件)
export function createApp(rootComponent: any): {
  mount(selector: string): void;
};
// dts.test.ts
import { createApp } from 'vue';

// ✅ 正确用法
createApp({}).mount('#app');

// ❌ 错误用法:少了 mount 参数
// 这里应该触发 TS 编译错误
// createApp({}).mount();

👉 说明

  • test-dts 会先生成 .d.ts 文件再检查。
  • test-dts-only 直接用现有的 .d.ts 文件进行编译验证。
  • 关键在于保障 API 类型和实际逻辑一致。

四、覆盖率测试(test-coverage

概念

覆盖率测试不仅运行单元测试,还会统计代码哪些部分被执行,输出报告(如语句、分支、函数、行覆盖率)。

示例

// stringUtils.ts
export function greet(name?: string) {
  if (name) {
    return `Hello, ${name}`;
  }
  return 'Hello, guest';
}
// stringUtils.test.ts
import { describe, it, expect } from 'vitest';
import { greet } from './stringUtils';

describe('greet', () => {
  it('should greet with name', () => {
    expect(greet('Alice')).toBe('Hello, Alice');
  });
});

👉 说明

  • 测试只覆盖了有 name 的情况,guest 分支没测到。
  • test-coverage 运行后会提示覆盖率不足,提醒你写额外测试:
it('should greet guest when no name provided', () => {
  expect(greet()).toBe('Hello, guest');
});

五、对比总结

命令 测试范围 示例场景
test-unit 模块逻辑 add(1,2) → 3
test-e2e 打包产物 & 用户行为 点击按钮计数器增加
test-dts 类型声明生成 + 检查 createApp().mount() 类型是否报错
test-dts-only 仅检查现有类型声明 不构建,直接验证
test-coverage 单测 + 覆盖率报告 提示 guest 分支未覆盖
test 全部测试集合 本地一键跑完

六、潜在问题

  1. E2E 测试执行慢:CI/CD 环境可能成为瓶颈。
  2. 覆盖率追求过度:高覆盖率不代表高质量,测试内容比数字更重要。
  3. 类型声明忽视:很多库项目容易忽略 d.ts 测试,导致 TS 用户踩坑。
  4. 依赖构建链路:像 test-e2etest-dts 一旦构建失败,测试链全挂。

结语

不同的测试类型各有侧重:

  • 单元测试 → 保证基础逻辑正确。
  • 端到端测试 → 模拟用户真实场景。
  • 类型测试 → 保证 TypeScript 用户的体验。
  • 覆盖率测试 → 衡量测试充分性。

它们共同构建了一个完整的质量保障体系,帮助项目在开发和交付中保持高可靠性。


本文部分内容借助 AI 辅助生成,并由作者整理审核。

为什么要使用 TypeScript:TS 相比 JavaScript 的优势

作者 excel
2025年9月25日 21:30

一、概念

JavaScript(JS)是动态类型语言,灵活但不安全;TypeScript(TS)是 JS 的超集,提供 静态类型检查智能提示更好的协作性,最终编译为 JS 在任何环境中运行。

一句话:TS 让 JS 更安全、更高效,尤其在多人协作与调用复杂 API 时优势明显。


二、原理

  1. 静态类型系统 —— 提前捕获错误。
  2. 类型声明文件(.d.ts —— 为 API、库提供类型说明,让编辑器能直接提示返回值与参数类型。
  3. 智能提示 (IntelliSense) —— 基于类型定义,IDE 能自动补全、展示 API 返回值结构。

三、对比(TS vs JS)

特性 JavaScript TypeScript
API 调用 只能查文档或运行时打印 console.log IDE 自动提示参数和返回值类型
错误发现 多数在运行时发现 大部分在编译期发现
可维护性 依赖开发者自觉 接口、类型明确,重构安全
开发效率 API 使用需频繁切换到文档 API 类型直接可见,减少错误调用

四、实践

例子 1:调用 API 时返回值结构不明确

JavaScript

fetch("/api/user")
  .then(res => res.json())
  .then(data => {
    console.log(data.id); // 运行时报错:如果 data 里没有 id
  });
  • JS 下,data 的结构完全不明确,开发者只能 console.log 去猜。

TypeScript

interface User {
  id: number;
  name: string;
}

fetch("/api/user")
  .then(res => res.json())
  .then((data: User) => {
    console.log(data.id); // ✅ 提示为 number 类型
    console.log(data.age); 
    // ❌ 编译时报错:Property 'age' does not exist on type 'User'
  });
  • TS 自动告诉你 data 里有哪些字段,减少 API 使用错误。

例子 2:调用第三方库 API

JavaScript

import _ from "lodash";

_.flattenDeep([1, [2, [3, [4]]]]); 
// 开发者必须查文档才能知道 flattenDeep 的返回类型

TypeScript

import _ from "lodash";

const arr = _.flattenDeep([1, [2, [3, [4]]]]);
// IDE 提示 arr: any[] → 立即知道返回值是数组
  • TS 的 .d.ts 类型声明让 IDE 显示 API 的参数与返回值,减少文档查找时间。

五、拓展

  1. 快速掌握陌生库

    • 使用 TS 时,导入一个新库后,光是把鼠标悬停在函数上就能看到参数与返回类型 → 等于内置文档
    • 这在 React、Vue、Node.js 的第三方库开发时非常高效。
  2. 团队协作中的 API 约定

    • 后端接口返回类型定义在 TS 的 interfacetype 中,前端团队成员直接用 → 避免反复询问字段含义。
  3. 与现代框架契合

    • Vue3、React 都内置对 TS 的友好支持。
    • 比如在 React 中,propsstate 有了明确类型后,组件使用错误能立刻发现。

六、潜在问题

  1. 学习成本 —— 类型系统初学者需要适应。
  2. 编译开销 —— 多一步转译,但现在工具链(Vite、Webpack、tsc)优化后影响不大。
  3. 第三方库类型缺失 —— 部分库没有类型定义文件时需要安装 @types/xxx 或手写声明。

总结

TypeScript 相比 JavaScript 最大的优势不仅在于 静态类型安全,更在于 提升开发效率

  • 调用 API 时自动提示参数与返回值,无需频繁查文档;
  • 减少逻辑错误与拼写错误;
  • 在多人协作、大型项目中保证一致性和可维护性。

因此,TS 已成为现代前端工程化开发的主流选择。


本文部分内容借助 AI 辅助生成,并由作者整理审核。

昨天以前首页

基于两台服务器的蓝绿切换实现零下线更新

作者 excel
2025年9月25日 08:36

一、概念

在网站或应用的运维中,“零下线更新”是指在发布新版本时,用户访问不受影响,不会出现中断、502 错误或服务不可达。常见的一种做法是 蓝绿部署(Blue-Green Deployment)

  • 蓝环境(Blue) :当前运行的生产版本,正在处理用户请求;
  • 绿环境(Green) :新版本环境,在后台完成更新与测试;
  • 切换:当绿环境稳定后,将流量从蓝环境切换到绿环境;如果发现问题,再快速切回蓝环境。

二、原理

  1. 两台服务器独立运行

    • 一台服务器作为现网服务;
    • 另一台空闲服务器用于新版本部署。
  2. Nginx 负载均衡控制流量

    • 在前端通过 upstream 管理两台服务器;
    • 切换时修改权重或启用/禁用某台服务器,reload Nginx 配置即可生效。
  3. 平滑切换机制

    • Nginx reload 是平滑的,不会直接杀掉旧进程;
    • 旧的 Worker 会等待当前请求处理完毕后再退出;
    • 新 Worker 会立即接管新请求,从而实现零停机。

三、对比

  • 单机热更新:通过软链切换或直接覆盖文件来更新,简单但风险高,一旦更新失败会影响整个服务。
  • 蓝绿部署:两台服务器互为备份,流量可随时切换,安全性更高,更新过程对用户透明。
  • 灰度发布:在蓝绿基础上进一步扩展,可逐步分配流量,验证稳定性后再全量切换。

四、实践

Nginx 配置示例

upstream backend {
    server 192.168.1.101:8080 weight=1;  # 蓝环境
    # server 192.168.1.102:8080 weight=0;  # 绿环境(未启用)
}

server {
    listen 80;
    server_name example.com;

    location / {
        proxy_pass http://backend;
    }
}

切换流程

  1. 在蓝环境正常运行时,更新绿环境;

  2. 完成新版本部署和测试;

  3. 修改 upstream,把流量切到绿环境:

    upstream backend {
        # server 192.168.1.101:8080 weight=0;
        server 192.168.1.102:8080 weight=1;
    }
    
  4. nginx -t && nginx -s reload 平滑生效;

  5. 观察运行情况,如果有问题,立刻切回蓝环境。


五、拓展

  • 灰度流量分配:先给绿环境分配少量流量(如 10%),逐步放量;
  • 健康检查:在 Nginx 或上层负载均衡中增加健康检查,确保异常节点不接收请求;
  • 自动化部署:结合 CI/CD 工具,实现部署、切换、回滚一体化。

六、潜在问题与不足

  1. 成本问题

    • 至少需要两台服务器,资源利用率偏低;
    • 小型团队可能难以承担额外硬件开销。
  2. 会话保持问题

    • 如果应用依赖本地 Session,切换时可能导致用户登录状态丢失;
    • 通常需要外部存储(Redis、数据库)来做会话共享。
  3. 数据库版本兼容问题

    • 蓝绿服务器共用同一个数据库时,新旧版本可能对数据结构有不同要求;
    • 需要数据库向下兼容,或者提前做 schema 升级。
  4. 切换瞬间的请求不一致

    • 如果有长连接(WebSocket/HTTP2),可能部分连接仍然留在旧环境;
    • 需要在应用层设计容错机制。
  5. DNS 切换滞后(如果用 DNS 方式)

    • DNS 缓存会导致部分用户仍然访问旧环境,影响一致性。

七、总结

两台服务器的蓝绿切换方案,是实现 零下线更新 的经典方式,简单、可靠、回滚迅速。它特别适合对 服务可用性要求高 的业务场景。但在资源成本、会话保持、数据库兼容和长连接管理上仍有不足,需要结合 共享存储、灰度发布、自动化工具 才能做到真正的高可用与稳定。


本文部分内容借助 AI 辅助生成,并由作者整理审核。

深入理解 encodeURI / encodeURIComponent 与 decodeURI / decodeURIComponent 的区别

作者 excel
2025年9月23日 08:04

一、概念

在前端开发中,URL 编码是常见操作。JavaScript 提供了四个相关方法:

  • encodeURI:对整个 URI 进行编码。
  • encodeURIComponent:对 URI 片段(组件)进行编码。
  • decodeURI:对 encodeURI 编码的结果进行解码。
  • decodeURIComponent:对 encodeURIComponent 编码的结果进行解码。

简而言之:

  • encodeURI / decodeURI 用在完整 URL 层级。
  • encodeURIComponent / decodeURIComponent 用在 URL 中的参数或单独片段。

二、原理

  1. URL 编码的目的
    URL 只能使用 ASCII 字符(安全子集),因此像空格、中文、特殊符号(如 ?&=)必须转换为合法形式。
    比如:空格 → %20中 → %E4%B8%AD
  2. encodeURI 的编码规则
    保留了 URL 中合法的结构符号:
  • 不会转义 : / ? # & = 等(这些在 URL 中有语义作用)。
  • 会转义空格、中文、非法符号。
  1. encodeURIComponent 的编码规则
    它比 encodeURI 更严格:
  • 几乎所有非字母数字字符都会被转义,包括 ? & = 等。
  • 因此适合用于参数值的编码,避免与 URL 本身结构冲突。
  1. 解码函数
  • decodeURI 只会解码 encodeURI 编码的字符串。
  • decodeURIComponent 更“彻底”,可以解码参数中的转义内容。

三、对比

方法 适用范围 保留字符 示例
encodeURI 整个 URL : / ? # & = 等结构符号 encodeURI("https://a.com?q=测试") → "https://a.com?q=%E6%B5%8B%E8%AF%95"
encodeURIComponent URL 片段 / 参数 不保留任何结构符号 encodeURIComponent("测试&demo") → "%E6%B5%8B%E8%AF%95%26demo"
decodeURI 解码 encodeURI 的结果 保留结构符号 decodeURI("https://a.com?q=%E6%B5%8B%E8%AF%95") → "https://a.com?q=测试"
decodeURIComponent 解码 encodeURIComponent 的结果 更彻底 decodeURIComponent("%E6%B5%8B%E8%AF%95%26demo") → "测试&demo"

四、实践

1. 错误示例

// 错误:encodeURIComponent 编码整个 URL
let url = "https://example.com/page?q=hello world";
let wrong = encodeURIComponent(url);
console.log(wrong);
// 输出: "https%3A%2F%2Fexample.com%2Fpage%3Fq%3Dhello%20world"
// 问题:整个 URL 都被转义,浏览器无法识别

2. 正确用法

// 正确:encodeURI 用于整个 URL
let url = "https://example.com/page?q=hello world";
let correct = encodeURI(url);
console.log(correct);
// 输出: "https://example.com/page?q=hello%20world"

// 正确:encodeURIComponent 用于参数值
let param = "hello world&测试";
let encodedParam = encodeURIComponent(param);
let finalUrl = `https://example.com/page?q=${encodedParam}`;
console.log(finalUrl);
// 输出: "https://example.com/page?q=hello%20world%26%E6%B5%8B%E8%AF%95"

五、拓展

  1. 为什么要区分 URI 与 URI Component?

    • URI(统一资源标识符)由多个部分组成,如协议、主机名、路径、查询参数。
    • 如果把整个 URL 都用 encodeURIComponent,结构符号也被转义,URL 失效。
    • 所以规范上才有这两个不同层级的方法。
  2. 配合使用场景

    • 构造 API 请求时,encodeURIComponent 是必需的,否则特殊字符会导致参数错位。
    • 解析 URL 时,必须选择和编码阶段对应的解码函数,否则会解码不完整或报错。
  3. 与 escape/unescape 的区别

    • 旧方法 escape / unescape 已弃用,不支持完整的 UTF-8 编码。
    • 应始终使用 encodeURI 系列。

六、潜在问题

  1. 双重编码问题

    • 如果某个参数已经被编码,再次用 encodeURIComponent,会出现 %25(即 % 的转义)。
    • 解决:只在拼接 URL 时编码一次。
  2. 解码不匹配

    • decodeURI 解码 encodeURIComponent 的结果时,可能失败或解码不完整。

    • 必须成对使用:

      • encodeURIdecodeURI
      • encodeURIComponentdecodeURIComponent
  3. 兼容性问题

    • 现代浏览器已统一支持这四个方法,但在部分旧代码库里仍存在 escape / unescape,需要注意迁移。

✅ 总结一句:

  • 整个 URL → encodeURI
  • 参数值 → encodeURIComponent
  • 解码时严格配对使用

本文部分内容借助 AI 辅助生成,并由作者整理审核。

构建百分级并发的 Node.js 应用(Nginx 版本)

作者 excel
2025年9月23日 07:45

在高并发场景下,Node.js 应用往往需要借助网关/负载均衡器来分发流量。相比应用层实现分发逻辑,用 Nginx 处理“百分比分流”更稳定、更高效。本文将介绍一个完整的高并发方案:

  1. Nginx 百分比分发
  2. 消息队列处理并发写
  3. Redis 热点缓存
  4. 数据库读写分离

概念

  • Nginx 百分比分发:通过 upstreamweight 配置,控制不同后端服务接收多少流量(例如 70% 到 A,30% 到 B)。
  • 消息队列:把并发写请求入队,顺序消费,避免数据冲突。
  • Redis 缓存:高频读操作优先读缓存,减少 DB 压力。
  • 读写分离:主库写,从库读,应用层或中间件自动分流。

原理

  • Nginx 负载均衡:基于权重(weight),按照比例把请求分配到不同的 Node.js 服务。
  • MQ 串行化:写请求进入队列 → 单线程消费 → 顺序写入数据库。
  • 缓存:Cache-Aside 模式(读缓存 → DB → 回填)。
  • 数据库:主从复制,应用决定走主库还是从库。

实践

1. Nginx 配置:百分比分发

/etc/nginx/conf.d/node.conf 新建配置:

upstream node_app {
    server 127.0.0.1:4001 weight=70 max_fails=3 fail_timeout=30s;   # 服务 A,70% 流量
    server 127.0.0.1:4002 weight=30max_fails=3 fail_timeout=30s;   # 服务 B,30% 流量
}

server {
    listen 3000;

    location / {
        proxy_pass http://node_app;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    }
}

🔎 说明

  • max_fails=3:30 秒内失败 3 次即认为该节点不可用。
  • fail_timeout=30s:在 30 秒内暂停向该节点转发请求。
  • 到期后 Nginx 会重新尝试,节点恢复后自动重新加入。

👉 启动方式:

nginx -s reload   # 重载配置
curl http://127.0.0.1:3000/api/test   # 请求将按 70/30 分发到两个 Node 服务

2. MQ:并发写请求串行化

生产者

// mq/producer.js
const amqp = require('amqplib');

async function produce(queue, payload) {
  const conn = await amqp.connect('amqp://user:pass@localhost:5672');
  const ch = await conn.createChannel();
  await ch.assertQueue(queue, { durable: true });
  ch.sendToQueue(queue, Buffer.from(JSON.stringify(payload)), { persistent: true });
  await ch.close();
  await conn.close();
}

module.exports = { produce };

消费者

// mq/consumer.js
const amqp = require('amqplib');
const { updateUser } = require('../db');

async function startConsumer(queue) {
  const conn = await amqp.connect('amqp://user:pass@localhost:5672');
  const ch = await conn.createChannel();
  await ch.assertQueue(queue, { durable: true });
  ch.prefetch(1);

  ch.consume(queue, async (msg) => {
    if (!msg) return;
    const payload = JSON.parse(msg.content.toString());
    try {
      await updateUser(payload.userId, payload.changes);
      ch.ack(msg);
    } catch (err) {
      console.error(`[Consumer Error] ${err.message}`);
      ch.nack(msg, false, false);
    }
  });
}

startConsumer('user-update').catch(console.error);

3. Redis 缓存

// cache.js
const Redis = require('ioredis');
const redis = new Redis();
const { getUserById, updateUserInDB } = require('./db');

async function getUser(userId) {
  const key = `user:${userId}`;
  let cached = await redis.get(key);
  if (cached) return JSON.parse(cached);

  const user = await getUserById(userId);
  if (!user) return null;

  await redis.set(key, JSON.stringify(user), 'EX', 60);
  return user;
}

async function updateUser(userId, changes) {
  await updateUserInDB(userId, changes);
  await redis.del(`user:${userId}`); // 删除缓存,下次请求自动回填
}

4. 数据库读写分离

// db.js
const { Pool } = require('pg');

const writePool = new Pool({ host: 'db-primary', user: 'app', password: 'pwd', database: 'mydb' });
const readPools = [
  new Pool({ host: 'db-replica-1', user: 'app', password: 'pwd', database: 'mydb' }),
  new Pool({ host: 'db-replica-2', user: 'app', password: 'pwd', database: 'mydb' }),
];

let rr = 0;
function getReadPool() {
  rr = (rr + 1) % readPools.length;
  return readPools[rr];
}

async function getUserById(id) {
  return (await getReadPool().query('SELECT * FROM users WHERE id=$1', [id])).rows[0];
}

async function updateUserInDB(id, changes) {
  await writePool.query('UPDATE users SET data=$1 WHERE id=$2', [changes, id]);
}

module.exports = { getUserById, updateUserInDB, updateUser: updateUserInDB };

拓展

  • 灰度发布:动态调整 weight 实现平滑升级。
  • 一致性:对强一致场景,写后读要走主库。
  • 缓存雪崩防护:给 TTL 加随机偏移。
  • MQ 幂等处理:写请求必须带唯一 ID,避免重复消费。

潜在问题

  • Nginx 单点:可部署多实例,前面加云 LB。
  • 读写延迟:从库可能存在复制延迟,需在关键场景强制走主库。
  • 消息丢失:需开启 RabbitMQ 持久化,并配置死信队列。
  • 缓存击穿:热点数据需加互斥锁。

总结

通过 Nginx 百分比分发 + RabbitMQ 串行化写 + Redis 热点缓存 + 数据库读写分离,我们就能在 Node.js 系统中搭建一个支撑“百分级并发”的生产级架构。

Nginx 的引入让分发层更高效、更稳定,也能更好地支持灰度发布和回滚。

最后这种是由于购买大型服务器超贵的解决方案,如果金额足够使用一台大型服务器就足够了。


本文部分内容借助 AI 辅助生成,并由作者整理审核。

❌
❌