3、集群与分布式架构:集群概念与分类、分布式系统特性、一致性哈希、分布式锁(Redis、ZooKeeper)、分布式ID生成方案

3.1 集群概念与分类

集群,说白了就是把多台机器凑在一起干活。我刚开始做交易系统时,觉得一台服务器扛得住所有请求。后来发现,单点故障太可怕了——机器一挂,整个交易就停了。

集群的核心目标就两个:高可用高性能。高可用保证系统不挂,高性能保证系统跑得快。

集群一般分三类:

  • 高可用集群(HA Cluster):主备模式,一台挂了另一台顶上。我在项目中遇到过,数据库主库宕机,备库自动切换,整个过程对用户无感知。
  • 负载均衡集群(LB Cluster):多台机器同时干活,前端用负载均衡器分发请求。比如Nginx做反向代理,把请求分给多个Tomcat实例。
  • 高性能计算集群(HPC Cluster):把大任务拆成小任务,并行计算。交易系统中用得不多,但做风控模型训练时会用到。

重要提醒:集群不是万能的。集群解决了单点问题,但引入了新的问题——数据一致性、网络分区、脑裂等。这些坑,我后面会一一讲到。

3.2 分布式系统特性

分布式系统,就是一群计算机通过网络协作,对外表现像一台机器。嗯,听起来很美好,但实现起来全是坑。

分布式系统有三大特性,也叫CAP理论

特性 说明 交易系统中的应用
C(一致性) 所有节点数据时刻一致 账户余额必须强一致
A(可用性) 系统始终对外提供服务 交易系统7×24小时不能停
P(分区容错性) 网络分区时系统仍能工作 机房断网,其他机房继续服务

你想想看,CAP三者最多只能满足两个。交易系统怎么选?我个人习惯是优先保证AP,然后通过最终一致性来弥补C。为什么?因为交易系统不能停,用户下单时系统必须响应。至于数据一致,可以用异步对账、补偿事务来解决。

我的经验:曾经有个项目,我们选了CP(强一致+分区容错),结果网络抖动时整个系统不可用,用户投诉电话打爆了。后来改成AP+最终一致,虽然对账偶尔有延迟,但系统再也没停过。

3.3 一致性哈希

一致性哈希,是分布式缓存和负载均衡中的经典算法。为什么要用它?

假设你有3台Redis做缓存。最简单的做法是取模:hash(key) % 3。但问题来了——如果加一台机器,变成hash(key) % 4,几乎所有key的映射都会变,缓存瞬间失效,这就是缓存雪崩

一致性哈希怎么解决?它把哈希值空间组织成一个环(0到2^32-1),每个节点落在环上。数据也哈希到环上,然后顺时针找最近的节点。

Node1 Node2 Node3 Node4 key1 key2 key3 一致性哈希环:数据顺时针找最近的节点

加节点时,只影响该节点逆时针方向的一小段数据。减节点时,也只影响该节点本身的数据。这就是一致性哈希的最小化数据迁移特性。

避坑指南:我曾经在项目里直接用一致性哈希,结果节点太少时数据分布不均匀。后来加了虚拟节点(每个物理节点对应100-200个虚拟节点),才解决了倾斜问题。记住:虚拟节点是一致性哈希的标配

3.4 分布式锁

分布式锁,就是让多台机器互斥访问共享资源。交易系统中,扣库存、下单、转账都需要分布式锁。

常用的实现方式有两种:RedisZooKeeper

3.4.1 Redis分布式锁

Redis实现分布式锁,核心命令是SETNX(SET if Not eXists)。

// 加锁
SET lock_key unique_value NX PX 30000

// 解锁(用Lua保证原子性)
if redis.call("get", KEYS[1]) == ARGV[1] then
    return redis.call("del", KEYS[1])
else
    return 0
end

这里要注意几个点:

  • NX:只有key不存在时才设置,保证互斥
  • PX 30000:设置过期时间,防止死锁
  • unique_value:每个客户端生成唯一值,防止误删别人的锁
  • Lua脚本:保证判断和删除的原子性

我的建议:生产环境别自己写Redis分布式锁,直接用Redisson。它封装了看门狗机制(自动续期),能防止业务没执行完锁就过期了。这个坑我踩过——业务跑了5秒,锁过期了,另一个线程进来,数据就乱了。

3.4.2 ZooKeeper分布式锁

ZooKeeper实现分布式锁,基于临时顺序节点。

原理很简单:

  1. 在锁目录下创建临时顺序节点
  2. 获取所有子节点,判断自己是不是最小的
  3. 如果是,获得锁;如果不是,监听前一个节点
  4. 前一个节点删除后,自己变成最小,获得锁
// 伪代码
String path = zk.create("/locks/lock-", null, 
    CreateMode.EPHEMERAL_SEQUENTIAL);
List<String> children = zk.getChildren("/locks", false);
if (path.equals(children.get(0))) {
    // 获得锁
} else {
    // 监听前一个节点
    zk.exists("/locks/" + children.get(index - 1), watcher);
}

ZooKeeper锁的好处是没有死锁问题——客户端挂了,临时节点自动删除。但性能不如Redis,适合对一致性要求高的场景。

选型建议:Redis锁适合高并发、允许短暂不一致的场景(比如秒杀)。ZooKeeper锁适合强一致、低并发的场景(比如配置更新、任务调度)。我一般用Redis做业务锁,ZooKeeper做元数据锁。

3.5 分布式ID生成方案

分布式系统中,ID生成是个基础问题。不能自增ID(多库冲突),不能UUID(太长、无序)。

常见的方案有:

方案 原理 优点 缺点
UUID 32位随机字符串 本地生成,性能高 太长,无序,不适合做索引
数据库自增 利用数据库自增ID 简单,有序 有单点,性能瓶颈
Redis INCR 利用Redis原子自增 性能高,有序 依赖Redis,可能丢数据
雪花算法 64位long型ID 高性能,趋势递增 依赖时钟,回拨会重复

我个人最推荐雪花算法(Snowflake)。它生成的ID是64位long型,结构如下:

| 1位符号位 | 41位时间戳 | 10位工作机器ID | 12位序列号 |
  • 1位符号位:固定0,表示正数
  • 41位时间戳:毫秒级,可以用69年
  • 10位工作机器ID:支持1024台机器
  • 12位序列号:同一毫秒内支持4096个ID

避坑指南:我曾经遇到过时钟回拨问题——服务器NTP同步时,时间往回跳了1秒,结果生成了重复ID。解决方案有两个:一是等待时钟追上(最多等几毫秒),二是用ZooKeeper记录上次生成ID的时间戳,回拨时拒绝生成。我建议用第二种,更稳妥。

嗯,以上就是集群与分布式架构的核心内容。从集群分类到分布式特性,从一致性哈希到分布式锁,再到ID生成方案,每个点都是交易系统高可用的基石。记住一句话:没有银弹,只有权衡