导读:本期聚焦于周翰文创作的《Riak datatype counter计数器如何在分布式系统中保证最终一致?》,敬请观看详情。在一个跨机房部署的订单系统里,两个节点同时给用户积分加一,最后总数却只多了一而不是二,这种计数丢失让不少团队头疼。Riak的counter数据类型基于CRDT中的PN-Counter模型,把加法与减法拆成独立的增长向量和递减向量,每个副本只更新自己对应的节点分量,合并时按分量取最大值再求和。这样即便网络分区期间多个客户端并发自增,恢复连通后各副本也能无损合并出正确总数。下文将拆解它的底层结构、读写流程以及和传统计数方案的差异,帮助你判断是否适合用Riak counter替代关系库里的自增字段。

在构建高可用分布式存储时,计数类需求几乎无法回避:页面访问量、库存扣减、用户积分都离不开一个能安全累加的数字。Riak作为一款面向可用性的AP系统,提供了名为counter的专用数据类型,它属于CRDT(无冲突复制数据类型)家族,专门解决多副本并发更新计数器时的合并难题。与传统数据库里直接执行UPDATE table SET count=count+1不同,Riak counter从设计上就放弃了强一致,转而保证任何情况下副本融合后结果确定且正确。

Riak datatype counter计数器如何在分布式系统中保证最终一致?

counter的底层结构与PN-Counter原理

Riak的counter实现了CRDT中的PN-Counter(Positive-Negative Counter)。它的核心思想是把一个逻辑计数器拆成两个映射:一个记录各节点发生的递增总量,另一个记录各节点发生的递减总量。每个节点在本地只修改属于自己的那一对分量,例如节点A自增时仅更新P[A],节点B自增时仅更新P[B],彼此不触碰对方的数据。这样并发写操作在物理上就被隔离到不同字段,自然不会产生写写冲突。

当系统需要读取当前计数值时,会把所有节点的P分量求和得到总增加量,再把所有节点的N分量求和得到总减少量,两者相减即为对外展示的数值。因为合并操作只是对各节点分量取最大值(即保留每个节点见过的最大累积值)后再相加,所以满足交换律、结合律与幂等性。即便同一条更新消息被送达多次,或者两个副本以不同顺序收到更新,最终合并出的P和N向量完全一致,计数结果也就必然相同。

下面用一段伪代码展示PN-Counter的本地模型与合并函数,帮助理解Riak在后台大致如何处理。注意代码里仅用普通映射表达,不涉及真实网络调用。

// 每个副本维护的PN-Counter结构
// counter = { p: {nodeA: 3, nodeB: 1}, n: {nodeA: 0, nodeB: 0} }

function local_increment(counter, nodeId, delta) {
  if (!counter.p[nodeId]) counter.p[nodeId] = 0;
  counter.p[nodeId] += delta;
}

function local_decrement(counter, nodeId, delta) {
  if (!counter.n[nodeId]) counter.n[nodeId] = 0;
  counter.n[nodeId] += delta;
}

function merge_counter(c1, c2) {
  var result = { p: {}, n: {} };
  var nodes = new Set([...Object.keys(c1.p), ...Object.keys(c2.p)]);
  nodes.forEach(function(id) {
    result.p[id] = Math.max(c1.p[id] || 0, c2.p[id] || 0);
  });
  var nodesN = new Set([...Object.keys(c1.n), ...Object.keys(c2.n)]);
  nodesN.forEach(function(id) {
    result.n[id] = Math.max(c1.n[id] || 0, c2.n[id] || 0);
  });
  return result;
}

function value(counter) {
  var sumP = 0, sumN = 0;
  Object.keys(counter.p).forEach(function(k) { sumP += counter.p[k]; });
  Object.keys(counter.n).forEach(function(k) { sumN += counter.n[k]; });
  return sumP - sumN;
}

从上述逻辑可以看出,Riak counter并不需要中心化的锁服务来协调自增请求。每个节点独立推进自己的分量,网络恢复后通过无冲突合并得出全局视图。这也是它能在分区期间依然允许写入、且事后不丢数的根本原因。不过代价是,若某个节点分量一直未被其他节点合并,读取时可能暂时看到偏小的数值,这正是最终一致性的直观体现。

Riak中counter的读写接口与使用示例

在Riak KV里,counter被归类为bucket type中的一种数据类型,通常需要在创建bucket时指定数据类型为counter。客户端可以通过专属的fetch与update指令来操作,而不用像操作普通对象那样自己序列化JSON。以官方Erlang客户端为例,更新一个计数器只需要指明增量,Riak会自动路由到负责该对象的vnode并修改对应节点分量。

下面的代码片段展示了如何使用Erlang对名为user_123_score的counter执行加五操作,并读取结果。实际项目中会把这些调用封装到服务层,避免业务代码直接依赖客户端细节。

% 连接Riak并选中counter类型的bucket
{ok, Pid} = riakc_pb_socket:start_link("127.0.0.1", 8087).
Bucket = {<<"counters">>, <<"user_123_score">>}.

% 执行递增五
{ok, C1} = riakc_pb_socket:fetch_type(Pid, Bucket).
{ok, C2} = riakc_counter:increment(C1, 5).
ok = riakc_pb_socket:update_type(Pid, Bucket, riakc_counter:to_op(C2)).

% 重新读取最新值
{ok, C3} = riakc_pb_socket:fetch_type(Pid, Bucket).
Score = riakc_counter:value(C3).

读流程上,Riak默认会向多个副本发起读请求并合并返回。因为counter的合并是确定性的,所以即使部分副本暂时落后,协调者节点也会把各副本的PN向量取最大后算出一个值返回。若业务要求更严格的读后写一致,可以调高读仲裁参数,让读操作等待更多副本响应,但会牺牲部分延迟与可用性。对于纯计数展示类场景,采用默认配置往往就能满足需求。

另外需要提醒,Riak counter只支持整数增减,不支持浮点。若业务里需要统计金额类小数,应改用其他CRDT如rw-set配合外部汇总,或者把单位放大成整数处理。同时counter无法回退到某个历史快照,因为它的设计目标是永远向前累加,历史分量会被持续合并进向量,不会保留时间线。

与传统计数方案及替代CRDT的对比

最常见的传统方案是关系数据库的自增列或带条件的UPDATE count=count+1。在单主架构下这能工作,但主库宕机切换时往往需暂停写入;在多主或分库分表环境,如果没有全局事务,就容易出现本文开头提到的更新丢失。另一种做法是把计数放到Redis并用INCR命令,但Redis默认异步复制,主节点挂掉且未同步的数据会永久消失,且集群跨机房脑裂时两边各增各的,恢复后无法自动和解。

Riak counter通过CRDT属性弥补了上述缺陷:它不依赖单点协调,分区期间两侧都能写,合并算法保证不丢增量。下表简要对比三者在并发写、分区容忍与合并正确性上的差异。

方案并发写冲突网络分区表现恢复后计数
关系库单主自增由主库串行化从库不可写不丢但停写
Redis INCR单线程无冲突脑裂两边独立可能丢失或重复
Riak counter分量隔离无冲突两边均可写自动合并准确

当然Riak counter并非银弹。它要求部署Riak集群本身带来运维复杂度,且只能表达一个可加减的整数,不能附带业务上下文。如果系统已经建立在PostgreSQL上且写入压力不大,引入Riak仅为计数并不划算;此时可考虑PostgreSQL里的触发器加乐观锁,或采用应用层定期校准。只有当系统本就使用Riak、或面临真正的高并发跨机房写计数时,counter数据类型的价值才最明显。

总结来看,Riak datatype counter计数器用PN-Counter模型把并发加法转化为各节点独立分量,再利用CRDT合并规则实现最终一致。它适合对可用性要求极高、能接受短暂读取偏差的计数场景。理解其原理与限制,才能在分布式架构中正确选用,而不是盲目替换现有方案。

RiakcounterCRDT修改时间:2026-08-17 09:02:41

免责声明:​ 已尽一切努力确保本网站所含信息的准确性。网站内容多为原创整理与精心编撰,观点力求客观中立。本站旨在免费分享,内容仅供个人学习、研究或参考使用。若引用了第三方作品,版权归原作者所有。如内容涉及您的权益,请联系我们处理。
内容垂直聚焦
专注技术核心技术栏目,确保每篇文章深度聚焦于实用技能。从代码技巧到架构设计,为用户提供无干扰的纯技术知识沉淀,精准满足专业提升需求。
知识结构清晰
覆盖从开发到部署的全链路。AI、前端、编程、数据库、服务器、建站、系统层层递进,构建清晰学习路径,帮助用户系统化掌握开发与运维所需的核心技术。
深度技术解析
拒绝泛泛而谈,深入技术细节与实践难点。无论是数据库优化还是服务器配置,均结合真实场景与代码示例进行剖析,致力于提供可直接应用于工作的解决方案。
专业领域覆盖
精准对应开发生命周期。从前端界面到后端编程,从数据库操作到服务器运维,形成完整闭环,一站式满足全栈工程师和运维人员的技术需求。
即学即用高效
内容强调实操性,步骤清晰、代码完整。用户可根据教程直接复现和应用于自身项目,显著缩短从学习到实践的距离,快速解决开发中的具体问题。
持续更新保障
专注既定技术方向进行长期、稳定的内容输出。确保各栏目技术文章持续更新迭代,紧跟主流技术发展趋势,为用户提供经久不衰的学习价值。