集群和分布式的区别

集群 Cluster 是多个相同节点协作处理同一任务以提高并发和可用性,分布式是将业务拆分为多个独立子系统在不同节点协作完成整体任务。

集群指将相同的系统或服务部署在多个节点上,通过负载均衡共同处理同一业务,关注节点的冗余和并发能力,以提升系统的并发能力和高可用性。集群节点功能一致,节点间通常不需要复杂通信协调,主要解决 量 的问题,如高并发访问或单点故障容错

分布式(Distributed)则是将一个大业务拆分为多个独立的子业务或子系统,分别部署在不同节点上,各节点协作完成整体任务。分布式系统强调节点间的通信与协调,每个节点可能承担不同功能,解决系统复杂性和迭代扩展问题,即“质”的问题

所以说,在redis中,需要单独的看待。

Redis 3.0后便多了一个 REDIS CLUSTER 配置,即Redis集群,他可以突破3.0之前只能一主多从的限制,实现多主多从,这种集群没有中心节点,可水平扩展,据官方文档称可以线性扩展到上万个节点,当前配置性能和高可用都优与哨兵模式,且配置非常简单

img

Redis 集群

Redis三大集群模式

Redis集群是一种通过将多个 Redis 节点连接在一起以实现高可用性、数据分片和负载均衡的技术。它允许 Redis 在不同节点上同时提供服务,提高整体性能和可靠性。

根据搭建的方式和集群的特性,Redis集群主要有三种模式:主从复制模式(Master-Slave)、哨兵模式(Sentinel)和Cluster模式

主从复制

主从复制是Redis的一种基本集群模式,它通过将一个Redis节点(主节点)的数据复制到一个或多个其他Redis节点(从节点)来实现数据的冗余和备份。

主节点负责处理客户端的写操作,同时从节点会实时同步主节点的数据。客户端可以从从节点读取数据,实现读写分离,提高系统性能。

image

配置主从复制

  • 配置主节点:在主节点的 redis.conf 配置文件中,无需进行特殊配置,主节点默认监听所有客户端请求。

    1
    2
    # 主节点默认端口号6379
    port 6379
  • 配置从节点:在从节点的 redis.conf 配置文件中,添加如下配置,指定主节点的地址和端口:

    1
    2
    3
    4
    5
    # 从节点设置端口号6380
    port 6380

    # replicaof 主节点IP 主节点端口
    replicaof 127.0.0.1 6379

    或者,通过Redis命令行在从节点上执行如下命令:

    1
    redis> replicaof 127.0.0.1 6379
  • 验证:在主节点上执行写操作,然后在从节点上进行读操作,检查数据是否一致。

主从复制的优缺点

优点:

  1. 配置简单,易于实现。
  2. 实现数据冗余,提高数据可靠性。
  3. 读写分离,提高系统性能。

缺点:

  1. 主节点故障时,需要手动切换到从节点,故障恢复时间较长。
  2. 主节点承担所有写操作,可能成为性能瓶颈。
  3. 无法实现数据分片,受单节点内存限制

所以说,一般情况下,主从复制模式适用于数据备份和容灾恢复,或者在线升级和扩展

总之,主从复制模式适合数据备份、读写分离和在线升级等场景,但在主节点故障时需要手动切换,不能自动实现故障转移。如果对高可用性要求较高,可以考虑使用哨兵模式或Cluster模式。

哨兵模式

哨兵模式就是所谓的 Sentinel 模式,哨兵模式是在主从复制基础上加入了哨兵节点,实现了自动故障转移。

哨兵节点是一种特殊的 Redis 节点,它会监控主节点和从节点的运行状态。当主节点发生故障时,哨兵节点会自动从从节点中选举出一个新的主节点,并通知其他从节点和客户端,实现故障转移。

image

哨兵模式的配置

  • 配置主从复制:首先按照主从复制模式的配置方法,搭建一个主从复制集群。

  • 配置哨兵节点:在哨兵节点上创建一个新的哨兵配置文件(如:sentinel.conf),并添加如下配置:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    # sentinel节点端口号
    port 26379

    # sentinel monitor 被监控主节点名称 主节点IP 主节点端口 quorum
    sentinel monitor mymaster 127.0.0.1 6379 2

    # sentinel down-after-milliseconds 被监控主节点名称 毫秒数
    sentinel down-after-milliseconds mymaster 60000

    # sentinel failover-timeout 被监控主节点名称 毫秒数
    sentinel failover-timeout mymaster 180000

    quorum是指触发故障转移所需的最小哨兵节点数。down-after-milliseconds表示主节点被判断为失效的时间。failover-timeout是故障转移超时时间。

  • 启动哨兵节点:使用如下命令启动哨兵节点:

    1
    redis> redis-sentinel /path/to/sentinel.conf
  • 验证哨兵模式:手动停止主节点,观察哨兵节点是否自动选举出新的主节点,并通知其他从节点和客户端。

优点:

  1. 自动故障转移,提高系统的高可用性。
  2. 具有主从复制模式的所有优点,如数据冗余和读写分离。

缺点:

  1. 配置和管理相对复杂。
  2. 依然无法实现数据分片,受单节点内存限制。

Cluster模式

一般我们认为前两种模式是集群模式,Cluster 才是真正的分布式方案。它把数据分片到多个主节点上,每个主节点只负责一部分数据,从而同时解决容量和写吞吐的扩展问题。在 Cluster 中,每个主节点可以带从节点,故障时从节点自动升主,兼顾了高可用。

Cluster 模式是 Redis 的一种高级集群模式,它通过数据分片和分布式存储实现了负载均衡和高可用性。在 Cluster 模式下,Redis 将所有的键值对数据分散在多个节点上。每个节点负责一部分数据,称为槽位。通过对数据的分片,Cluster 模式可以突破单节点的内存限制,实现更大规模的数据存储。

image

Redis Cluster 将数据分为16384个槽位,每个节点负责管理一部分槽位。当客户端向 Redis Cluster 发送请求时,Cluster会根据键的哈希值将请求路由到相应的节点。具体来说,Redis Cluster 使用CRC16算法计算键的哈希值,然后对16384取模,得到槽位编号。

Cluster模式配置和实现

  • 配置Redis节点:为每个节点创建一个 redis.conf 配置文件,并添加如下配置:

    如果做一个做一个三主三从的集群,一共需要创建6个像这样的配置

    1
    2
    3
    4
    5
    6
    7
    8
    # cluster节点端口号
    port 7001

    # 开启集群模式
    cluster-enabled yes

    # 节点超时时间
    cluster-node-timeout 15000
  • 启动Redis节点:使用如下命令启动6个节点

    1
    redis> redis-server redis_7001.conf
  • 创建Redis Cluster:使用Redis命令行工具执行如下命令创建Cluster

    1
    redis> redis-cli --cluster create 127.0.0.1:7001 127.0.0.1:7002 127.0.0.1:7003 127.0.0.1:7004 127.0.0.1:7005 127.0.0.1:7006 --cluster-replicas 1

    cluster-replicas 表示从节点的数量,1代表每个主节点都有一个从节点。

  • 验证Cluster模式:向Cluster发送请求,观察请求是否正确路由到相应的节点。

优点:

  1. 数据分片,实现大规模数据存储。
  2. 负载均衡,提高系统性能。
  3. 自动故障转移,提高高可用性。

缺点:

  1. 配置和管理较复杂。
  2. 一些复杂的多键操作可能受到限制。

Cluster模式适用于以下场景:大规模数据存储,因为能通过数据分片,突破单节点内存限制;高性能要求场景,通过负载均衡,提高系统性能;高可用,自动故障转移

Redis三大集群模式的演示

下面在 Ubuntu 26.04.1 中,使用 Redis 8.0.5 实际搭建三种模式。每个节点都是独立 Redis 进程,使用不同端口、配置和数据目录。

实验 Redis 数据端口 哨兵或总线端口 观察重点
一主两从 7100、7101、7102 无 数据复制、从节点只读、手动提升
哨兵 7200、7201、7202 27200、27201、27202 多哨兵判断故障、自动选主、旧主回归
三主三从 Cluster 7300—7305 17300—17305 16384 槽、MOVED、CROSSSLOT、自动接管

主从复制:同步数据,但不会自动选主

配置模板见 7100.conf、7101.conf、7102.conf。脚本将路径和凭据占位符替换后,在运行目录生成实际配置。关键项如下:

1
2
3
4
5
6
7
8
9
10
11
# 主节点 7100:所有节点使用独立 dir / pidfile / logfile
port 7100
bind 127.0.0.1
user default off
user ErgouTree on >zjm10086 ~* &* +@all
masteruser ErgouTree
masterauth zjm10086
appendonly yes

# 7101 和 7102 的配置再分别添加:
replicaof 127.0.0.1 7100

单独运行:

1
.\lab\redis-topologies\lab.cmd demo replication

实验先在 7100 执行 SET lab:replication:message replicated,并在同一连接执行 WAIT 2 5000,本次返回 2;随后从 7101 读到了相同值。向 7101 写入时返回 READONLY。

再停止主节点 7100,7101 仍然报告 role:slave、master_link_status:down,没有自行提升。最后在 7101 执行 REPLICAOF NO ONE,让 7102 和重启后的 7100 跟随 7101,完成手动切换。

主从复制实测:副本同步、只读拒绝与手动升主

结论是:主从复制提供数据副本,自动选主需要额外的协调机制,所以说 有两个从节点 直接等同于 主节点挂了会自动恢复写入 是不对的。

哨兵:主节点故障后自动切换

这里使用 3 个数据节点和 3 个 Sentinel。以 27200.conf 为例:

1
2
3
4
5
6
7
8
9
port 27200
sentinel monitor labmaster 127.0.0.1 7200 2
sentinel auth-user labmaster ErgouTree
sentinel auth-pass labmaster zjm10086
sentinel sentinel-user ErgouTree
sentinel sentinel-pass zjm10086
sentinel down-after-milliseconds labmaster 3000
sentinel failover-timeout labmaster 15000
sentinel parallel-syncs labmaster 1

其他哨兵使用 27201、27202;完整文件还包含回环监听、ACL 与独立日志目录。这里的 3 秒故障判断用于缩短本机实验等待时间,不是生产环境的通用推荐。

1
.\lab\redis-topologies\lab.cmd demo sentinel

启动后,SENTINEL CKQUORUM labmaster 返回 3 个可用哨兵。脚本在写入并等待两份副本确认后停止 7200*没有主动执行 SENTINEL FAILOVER。

本次约 5.35 秒后,哨兵返回的主节点地址从 127.0.0.1:7200 变为 127.0.0.1:7201,原来的值 survives-failover 可以继续读取。旧主 7200 重启后自动成为 7201 的从节点,复制链路恢复为 up。

Sentinel 实测:自动选主、数据保留与旧主回归

其中 quorum=2 用于形成客观下线判断。本次恰好是 3 个哨兵,所以多数也为 2。原始哨兵日志中的 +odown、+switch-master 可以对应到这次切换。Redis Sentinel 官方说明。

Cluster:数据分片、重定向与故障接管

六个配置模板位于 config/cluster。以 7300 为例,关键配置为:

1
2
3
4
5
6
7
8
port 7300
cluster-enabled yes
cluster-config-file nodes.conf
cluster-node-timeout 3000
cluster-require-full-coverage yes
cluster-announce-ip 127.0.0.1
cluster-announce-port 7300
cluster-announce-bus-port 17300

脚本对每个节点设置独立运行目录,再调用下面的真实建群命令;认证密码由 REDISCLI_AUTH 环境变量提供:

1
2
3
4
redis-cli --user ErgouTree --cluster create \
127.0.0.1:7300 127.0.0.1:7301 127.0.0.1:7302 \
127.0.0.1:7303 127.0.0.1:7304 127.0.0.1:7305 \
--cluster-replicas 1 --cluster-yes

复现入口:

1
.\lab\redis-topologies\lab.cmd demo cluster

本次建群后,CLUSTER INFO 显示 cluster_state:ok、6 个节点、3 个主节点,全部 16384 槽已经分配。初始三个主节点分别负责 0—5460、5461—10922、10923—16383。

观察 MOVED:键 lab:route:1001 的槽号为 11324,初始归 7302。故意连接 7300 读取,得到 MOVED 11324 127.0.0.1:7302;换用 redis-cli -c 后可以读到 cluster-routing-ok。这也说明,MOVED 可以只是客户端连错了节点,并不一定意味着刚发生过迁槽。

Cluster 实测:槽位覆盖和 MOVED 自动路由

观察多键约束:MSET lab:plain:a 1 lab:plain:b 2 返回 CROSSSLOT。为三个业务键添加相同 Hash Tag:

1
2
3
lab:stock:{sku-1001}
lab:orders:{sku-1001}
lab:requests:{sku-1001}

它们的 CLUSTER KEYSLOT 都返回 4864,同槽 MSET 成功。SELECT 1 则返回 Cluster 不允许切换数据库的错误。

Cluster 实测:CROSSSLOT 与 Hash Tag 修正

观察自动接管:再停止持有测试键的主节点 7302。本次约 5.79 秒后,副本 7303 提升为主节点;集群恢复 ok,原值仍可读取。重启旧主后,六节点重新恢复健康,旧主成为从节点。

Cluster 实测:主节点故障后的槽位接管

这些端口、节点角色和耗时来自本次记录;重新执行实验时,选举结果、节点 ID 和耗时可能变化。Redis 异步复制并不承诺所有故障下都零丢失。Redis Cluster 官方说明。

深入 Cluster

Redis Cluster 的核心设计围绕 去中心化 与 数据分片 展开,它没有使用一致性哈希,而是通过哈希槽来管理数据分布,并依靠客户端重定向机制实现透明访问。

哈希槽

Redis Cluster 中的哈希槽,本质上是介于 键 和 节点 之间的一层虚拟分片单元。它的核心作用是将整个键空间划分为固定数量的槽位,再把这些槽位分配给不同的主节点,从而实现数据的分布式存储与灵活扩缩容。

Redis Cluster 将整个键空间划分为 16384 个虚拟哈希槽(编号 0~16383)。每个键通过 CRC16(key) % 16384 计算归属槽位,集群中的每个主节点负责维护其中一部分槽。

计算方式如下

1
slot = CRC16(key) mod 16384

也就是说,先对键名进行 CRC16 校验和计算,再对 16384 取模,得到的余数就是该键所在的槽编号。集群中的每个主节点负责维护一部分槽位,键最终就存储在那个拥有对应槽位的主节点上。

理论上 CRC16 能产生 65536 个值,但 Redis 作者选择了 16384(即 2^14),主要基于两点

  • 集群节点间需要定期发送心跳包,其中包含槽位分配信息的 bitmap。若槽数为 65536,消息头会达到约 8KB;而 16384 个槽时仅约 2KB,能显著节省带宽。
  • Redis Cluster 的主节点数量通常不建议超过 1000 个。在千节点规模下,16384 个槽已经足够精细地分配数据,没有必要使用更大的数字。

相比一致性哈希,哈希槽的设计让运维操作更加可控:

  • 扩缩容直观:新增节点时,只需从现有节点匀出一部分槽迁移给新节点;移除节点时,将该节点的槽全部迁走即可。槽的迁移粒度是固定的,管理起来很清晰。
  • 手动控制分布:如果某些节点的硬件配置更好,你可以手动给它们分配更多槽,而不像一致性哈希那样较难干预数据分布。

默认情况下,每个键独立计算槽位。但如果你需要让多个键落在同一个槽里,可以使用哈希标签。

如果键名中包含 {...},Redis 只会用花括号内的内容来计算槽位。例如 {user:1000}.name 和 {user:1000}.surname 会落在同一个槽,因为哈希计算只针对 user:1000 进行。

槽的归属可以动态改变,迁移过程中客户端可能会遇到两种重定向:

  • ASK 重定向:发生在槽迁移进行中。如果客户端请求的键已经不在源节点,源节点会返回 ASK,告诉客户端 这个键可能已经迁到目标节点了,你去那边试一下,但暂时不要更新你本地的槽映射表。客户端需要先向目标节点发送 ASKING 命令,才能执行后续请求。
  • MOVED 重定向:表示槽的归属已经永久变更。客户端收到 MOVED 后,应更新本地的 槽-节点 映射缓存,后续该槽的请求直接发往新节点。

客户端路由

Redis Cluster 的客户端路由核心在于客户端本地缓存一份 槽-节点 映射表,并自行计算键的归属槽,直接向正确节点发起请求,从而避免每次请求都经过服务端转发。

这是一个具备集群感知能力的客户端,其典型路由流程如下:

  • 初始化拓扑:客户端启动时连接集群中任意一个节点,发送 CLUSTER SLOTS(或 CLUSTER NODES)命令,获取完整的哈希槽分配表及对应的主从节点地址,并在本地缓存这份映射。
  • 计算并直达:当需要操作某个键时,客户端在本地用 CRC16(key) mod 16384 算出槽号,查缓存映射表找到负责该槽的节点,直接向该节点发送命令。理想情况下,一次网络往返即可完成。
  • 处理重定向:当集群拓扑发生变化时,客户端缓存的映射可能过时。此时目标节点会返回重定向错误:
    • MOVED:表示槽的归属已永久变更。客户端收到后,应触发一次拓扑刷新,更新本地映射,并将请求发往新节点。
    • ASK:表示槽正在迁移中,键可能已临时转移到目标节点。客户端需先向目标节点发送 ASKING 命令,再执行原请求,但不应因此更新本地槽映射,因为没迁移完

这时候,务必使用支持集群模式的客户端,如 Jedis,并确保其具备本地槽缓存与重定向处理能力。

集群规模与防脑裂

防脑裂机制是防止在节点失联时因错误选举而产生双主节点。

Redis Cluster 的故障转移依赖多数主节点投票。因此,集群必须拥有至少 3 个主节点,官方推荐的最小生产规模是 3 主 + 3 从,共 6 个节点,以确保任一主节点故障时,剩余主节点仍能构成多数并完成故障转移。

而 Redis Cluster 的主节点数量通常建议控制在 1000 个以内。这主要受限于 Gossip 协议的心跳开销:节点数越多,集群总线上的状态交换消息量增长越快,网络与 CPU 负担相应加重。

脑裂(Split-Brain)指因网络分区导致集群分裂,出现两个独立运作的 主节点 子集,各自接受写入,最终造成数据冲突或丢失。Redis Cluster 通过以下机制防范:

  • 一个主节点被标记为 PFAIL(疑似下线)后,需要集群中多数主节点也同意将其标记为 FAIL,故障转移才会启动。选举新主节点时,同样需要获得多数主节点的投票。这意味着,被隔离在少数派分区中的主节点无法被替换,因为少数派凑不够票数来发起选举,从而避免了双主的产生。
  • Redis 提供了 min-replicas-to-write 与 min-replicas-max-lag 参数,用于在主节点层面进行最后一道防护。当主节点发现自己可连接的合格从节点数量不足,或从节点复制延迟过大时,会主动拒绝写入。这样,一个因网络分区而与所有从节点失联的旧主节点,即使仍在运行,也无法继续接受写请求,从而大幅降低脑裂导致数据分叉的风险。
  • 合理配置 cluster-node-timeout,该参数决定节点多久无响应后被标记为 PFAIL。
  • 将主从节点分散部署在不同的物理机、机架或可用区,避免单一物理故障同时影响一个主节点及其所有从节点,从物理层面降低脑裂发生的概率。

分布式带来的新约束

在 Cluster 中,所有涉及多个键的命令,都要求这些键必须落在同一个哈希槽内。如果客户端尝试操作分属不同槽的键,服务端会直接返回 CROSSSLOT 错误。

单机 Redis 里随意使用的多键命令和事务,在 Cluster 中会受到严格限制。这是实际开发中最容易踩坑的地方。

这一限制直接波及了开发中最常用的两个特性:

  • 事务(MULTI/EXEC):事务内的所有命令涉及的键必须同槽,否则整个事务无法执行。同时,WATCH 命令也只能监视同一槽内的键,跨槽监视会导致乐观锁机制失效。
  • Lua 脚本:脚本中通过 KEYS 数组传入的所有键,也必须属于同一槽。此外,脚本内严禁使用动态生成的键名(必须用 KEYS[1] 这样的显式声明),否则会报错。

所以说,这里需要使用上面提到的哈希标签(Hash Tag)。在键名中加入 {...},Redis 只会对花括号内的内容计算哈希。例如,将 user:1001:name 和 user:1001:age 改为 {user:1001}:name 和 {user:1001}:age,即可强制它们落入同一槽,从而支持事务或批量操作。但需警惕,过度使用可能引发数据倾斜

单机 Redis 支持通过 SELECT 命令切换 16 个逻辑数据库(0-15)。但在 Cluster 模式下,SELECT 命令被完全禁用,集群只支持数据库 0。这是因为在分布式分片架构下,维护多数据库的原子操作会带来不必要的复杂性。因此,依赖多数据库隔离业务的旧代码,在迁移到 Cluster 时必须改造,改用键名前缀等方式来区分不同业务的数据。

因此,许多在单机下能返回全局信息的命令,在 Cluster 中语义发生了变化,通常只作用于当前连接所在的单个节点

Cluster 下的 Pipeline 行为与单机不同。由于命令必须发往正确的节点,一个 Pipeline 批次通常只能针对同一个节点。如果客户端试图在一个 Pipeline 中混合发往不同节点或包含跨槽键的命令,现代客户端(如 Jedis)会直接抛出异常或拒绝执行。这要求开发者在构建批量操作时,需要先按“目标节点”或“哈希槽”对命令进行分组,再分别打包发送。

所以说,Redis Cluster 的分布式约束本质上是将 全局原子性 和 全局视图 的成本转移给了应用层**。在设计阶段就需要有意识地通过哈希标签规划键的分布,并避免依赖跨节点的多键事务与全局遍历命令。如果你的业务场景强依赖跨键的复杂原子操作或全局遍历,那么 Cluster 可能并非最优选,或者需要引入应用层的协调逻辑来弥补这些限制。

Redis 在分布式下的使用

分布式锁

之前详细介绍过,不多介绍了

实际项目中使用Redis Cluster模式的案例

下面实现一个可以实际调用的“限量商品库存预占”案例:多名用户并发提交下单请求,库存不能被扣成负数;客户端因超时重试时,相同请求不能再次扣库存;Cluster 主节点故障后,客户端应在新拓扑上恢复操作。

文件 作用
ClusterClientFactory.java Redisson Cluster 连接、认证与拓扑刷新
ClusterOrderService.java 库存初始化、键设计与预占服务
ClusterOrderApplication.java 独立 HTTP 入口,监听 127.0.0.1:18088
place-order.lua 同槽 Lua:幂等检查、扣减与保存记录
ClusterOrderServiceIntegrationTest.java 真实 Cluster 的并发与边界集成测试

本例复用项目已有 Redisson 依赖,HTTP 适配使用 JDK 21 自带的 HttpServer

Cluster 连接与键设计

客户端使用多个种子节点,启用周期性拓扑扫描;读取走主节点,避免用延迟副本判断刚刚完成的库存变化。实际配置节选:

1
2
3
4
5
6
7
8
config.useClusterServers()
.addNodeAddress("redis://127.0.0.1:7300",
"redis://127.0.0.1:7301",
"redis://127.0.0.1:7302")
.setUsername("ErgouTree")
.setPassword("zjm10086")
.setReadMode(ReadMode.MASTER)
.setScanInterval(1000);

完整源码还设置了连接池、连接/命令超时与有限重试,凭据和节点可以通过环境变量覆盖。Redisson Cluster 配置说明。

每个 SKU 使用三个键:

1
2
3
lab:stock:{sku-1001}       String:剩余库存
lab:orders:{sku-1001} Hash:orderId -> 用户与创建时间
lab:requests:{sku-1001} Hash:requestId -> 用户与已生成的 orderId

同一 SKU 通过 {sku-1001} 保证三个键同槽,才能在一段 Lua 中原子处理;不同 SKU 使用不同 Hash Tag,仍可分片到不同主节点。不要给全部商品套同一个固定 Hash Tag,这样会把业务集中到一个槽。形成重心偏移

Lua 原子预占与幂等

完整 Lua 在源码中留存,核心执行顺序为:

  1. SKU 尚未初始化:返回 NOT_INITIALIZED。
  2. requestId 已存在且用户一致:返回 DUPLICATE 和原 orderId。
  3. requestId 被另一用户复用:返回 CONFLICT。
  4. 库存为 0:返回 SOLD_OUT。
  5. 否则执行 DECR,写入订单预占和幂等映射,返回 CREATED。

核心写入均在同一个 Lua 脚本中完成:

1
2
3
4
local remaining = redis.call('DECR', KEYS[1])
redis.call('HSET', KEYS[2], ARGV[2], ARGV[3] .. '|' .. ARGV[4])
redis.call('HSET', KEYS[3], ARGV[1], ARGV[3] .. '|' .. ARGV[2])
return {'CREATED', ARGV[2], remaining}

这里的片段前面已经完成库存和幂等判断,不能单独当作完整脚本使用。所有 Redis 键都由 Java 通过 KEYS 显式传入;SKU、用户、请求 ID 也限制了字符范围,防止传入花括号破坏 Hash Tag 设计。

启动并实际调用 API

从项目根目录执行启动

初始化库存为 3,再提交一次请求:

1
2
3
4
5
6
$api = 'http://127.0.0.1:18088'
Invoke-RestMethod "$api/api/demo/reset" -Method Post -ContentType 'application/json' `
-Body '{"sku":"sku-1001","stock":3}'

Invoke-RestMethod "$api/api/orders" -Method Post -ContentType 'application/json' `
-Body '{"sku":"sku-1001","requestId":"request-001","userId":"buyer-001"}'

本次首次调用返回 CREATED,库存从 3 变为 2。重复发送同一请求,返回 DUPLICATE,orderId 与第一次完全相同,库存仍为 2。换成另一个用户复用相同 requestId,则返回 CONFLICT。

项目 API 实测:下单、幂等重试与身份冲突

再调用 GET /api/stock/sku-1001,结果为 remainingStock=2、orderCount=1、requestCount=1。演示初始化接口只清理指定 SKU 的三个案例键,不执行 FLUSHALL。

并发验证:不超卖,也不重复扣减

本次实验使用 32 个并发 HTTP 客户端,让 300 个不同请求争抢 100 件库存,结果是:

指标 本次实测
CREATED 100
SOLD_OUT 200
最终库存 0
预占订单数 100

然后将另一个 SKU 库存初始化为 1,让 20 个线程使用同一个 requestId 和 userId 并发调用。结果只创建一次,其他请求返回相同 orderId,最终库存为 0,订单数为 1。

并发实测:300 抢 100 与 20 次重复请求

这些结果用于验证逻辑正确性。额

主节点故障时,客户端如何恢复

实验继续保持 Java API 进程运行,先为 sku-recovery 成功预占一件库存,再停止该 SKU 所在的主节点 7301。

本次新主节点为 7305,客户端约 5.58 秒恢复;恢复过程中记录到 4 次暂时失败,没有把故障切换描述为完全无中断。接口遇到 Redis 暂时不可用时返回 503,调用方保留原 requestId 重试。

用故障前的 requestId 再次调用,得到 DUPLICATE 和原来的 orderId,库存保持为 4;再用新的 requestId 下单,库存变为 3,订单总数为 2。整个过程中 Java API 没有重启。

项目故障恢复实测:原请求重试和新请求继续执行

实验在故障前通过同连接写入并执行 WAIT,确认这次演示数据已复制到副本;这不等于 Redis 在所有故障下都具备强一致性或零丢失保证。

验证不同 SKU 实际分布到不同主节点

最后通过同一个 Java API,分别初始化并下单三个 SKU,实测路由如下:

SKU 槽号 当时的主节点 下单后的库存
sku-shard-1 426 7300 1
sku-shard-2 12745 7303 1
sku-shard-3 8680 7305 1
多 SKU 分片实测:同一 API 访问三个主节点

业务代码没有按端口硬编码路由,而是把同 SKU 的原子操作留在同槽,由 Cluster 客户端处理不同 SKU 的分片访问。单个热门 SKU 仍然落在一个主节点上,Cluster 不会自动把这个 SKU 的一个库存键拆到多个节点。

这个案例是可运行的库存预占原型:订单预占记录暂存在 Redis,还没有支付、订单数据库、超时回补和幂等记录清理。真实项目需要结合业务数据库确认正式订单,并处理异步复制、超时重试与库存补偿。