第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()三个核心方法 - 验证两个副本合并后的最终值等于各自增量之和
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。