第2章 本地开发环境搭建与G‑Counter初始实现

本章从零搭建基于Node.js/TypeScript的CRDT实验环境,并实现最基础的G‑Counter(增长计数器)。通过本章,你将掌握:如何初始化TypeScript项目、G‑Counter的CRDT语义(状态为各副本的贡献数组,合并操作为逐元素取最大值),以及如何用30行代码写一个可合并的计数器。完成本章后,你的项目目录中将有一个可运行的gcounter.ts和一个初步的合并函数,这是后续所有CRDT实验的脚手架。

本章目标:
  • 配置Node.js + TypeScript开发环境(tsc编译、ESLint/Prettier可选)
  • 理解G‑Counter的数学原理:state = [c₁, c₂, …, cₙ],merge = max
  • 实现increment()、value()、merge()三个核心方法
  • 验证两个副本合并后的最终值等于各自增量之和
G‑Counter 合并流程 (两个副本) 副本A state [3, 0, 2] (c₀=3, c₁=0, c₂=2) 副本B state [1, 2, 0] (c₀=1, c₁=2, c₂=0) 逐元素取 max 合并后 state [3, 2, 2]

2.1 环境配置

确保已安装 Node.js ≥ 18 和 npm ≥ 9。以下命令在终端中依次执行:

mkdir crdt-workshop && cd crdt-workshop
npm init -y
npm install typescript @types/node --save-dev
npx tsc --init

tsc --init 会生成 tsconfig.json,请确认其中的 target 为 "ES2022" 或更高版本,module 为 "commonjs"(或 "ESNext")。为方便运行,可安装 tsx:

npm install tsx --save-dev
// 之后可用 npx tsx src/gcounter.ts 直接运行
推荐目录结构:
crdt-workshop/src/ 放所有源码,crdt-workshop/tests/ 放测试文件。本章只需一个 gcounter.ts。

2.2 G‑Counter 原理

G‑Counter(Grow‑only Counter)是最简单的CRDT之一。其核心规则:

  • 状态:一个长度为 n(副本数)的整数数组 state = [c₀, c₁, …, cₙ₋₁],每个元素代表对应副本的贡献计数。
  • 查询:value() 返回所有元素之和。
  • 更新:increment(i, delta) 仅增加 state[i](只能加,不能减)。
  • 合并:merge(other) 对两个状态数组的每个位置取 Math.max,保证最终状态单调递增。

合并的交换律、结合律、幂等性由 max 运算天然满足,因此G‑Counter是符合CRDT规范的。下面的表格对比了三种基础操作:

操作签名语义
increment(idx: number, delta?: number) => void只增加指定副本的计数,本地调用
value() => number返回所有副本贡献之和
merge(other: GCounter) => GCounter逐元素取最大值,返回新副本状态

2.3 编写最小合并函数

在 src/gcounter.ts 中实现G‑Counter类。注意合并时需处理 other.state 长度可能不同的情况(容错设计)。

export class GCounter {
  private state: number[];

  constructor(size: number);
  constructor(stateOrSize: number | number[]) {
    if (typeof stateOrSize === 'number') {
      this.state = new Array(stateOrSize).fill(0);
    } else {
      this.state = [...stateOrSize]; // 浅拷贝防止引用篡改
    }
  }

  /** 副本 idx 的计数增加 delta (delta >= 0) */
  increment(idx: number, delta: number = 1): void {
    if (delta < 0) throw new Error('G-Counter只支持增加');
    this.state[idx] += delta;
  }

  /** 所有副本计数之和 */
  value(): number {
    return this.state.reduce((a, b) => a + b, 0);
  }

  /** 与另一个G‑Counter合并,返回新实例 */
  merge(other: GCounter): GCounter {
    const maxLen = Math.max(this.state.length, other.state.length);
    const newState: number[] = new Array(maxLen);
    for (let i = 0; i < maxLen; i++) {
      const a = this.state[i] ?? 0;
      const b = other.state[i] ?? 0;
      newState[i] = Math.max(a, b);
    }
    return new GCounter(newState);
  }
}
常见错误:合并时直接修改 this.state 会破坏幂等性。必须返回新对象(不可变风格),除非你明确需要就地合并(本课程采用新对象返回以保持简单)。

2.4 调试与测试

在同目录下创建 src/test-gcounter.ts 进行快速验证:

import { GCounter } from './gcounter';

const replicaA = new GCounter(3);
replicaA.increment(0, 3);
replicaA.increment(2, 2);
console.log('A value:', replicaA.value()); // 5

const replicaB = new GCounter(3);
replicaB.increment(1, 2);
replicaB.increment(0, 1);
console.log('B value:', replicaB.value()); // 3

const merged = replicaA.merge(replicaB);
console.log('merged value:', merged.value()); // 3 + 2 + 2 = 7

// 并发合并后,任意顺序结果相同
const mergedAlt = replicaB.merge(replicaA);
console.log('mergedAlt value:', mergedAlt.value()); // 7
console.log('一致性验证:', merged.value() === mergedAlt.value()); // true

使用 npx tsx src/test-gcounter.ts 运行。预期输出:5、3、7、7、true。

2.5 性能与稳定性优化提示

  • 避免大数组:当副本数量很大(如 >1000)时,每次合并都遍历全数组可能成为瓶颈。可考虑稀疏数组或增量同步(本课程后续章节会涉及)。
  • 不可变状态:合并返回新对象有助于追踪状态变更历史,但会增加GC压力。对于高频合并,可使用对象池或复用模式,但本课程初级阶段保持不可变更为可溯源。
  • 处理负数增量:G‑Counter天然不支持减法;后续章节会引入PN‑Counter来支持增减。
  • 类型安全:使用TypeScript强类型避免将字符串或负数传入 increment。可在构造函数中加入参数校验,防止NaN或Infinity。