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),每个节点落在环上。数据也哈希到环上,然后顺时针找最近的节点。
加节点时,只影响该节点逆时针方向的一小段数据。减节点时,也只影响该节点本身的数据。这就是一致性哈希的最小化数据迁移特性。
避坑指南:我曾经在项目里直接用一致性哈希,结果节点太少时数据分布不均匀。后来加了虚拟节点(每个物理节点对应100-200个虚拟节点),才解决了倾斜问题。记住:虚拟节点是一致性哈希的标配。
3.4 分布式锁
分布式锁,就是让多台机器互斥访问共享资源。交易系统中,扣库存、下单、转账都需要分布式锁。
常用的实现方式有两种:Redis和ZooKeeper。
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实现分布式锁,基于临时顺序节点。
原理很简单:
- 在锁目录下创建临时顺序节点
- 获取所有子节点,判断自己是不是最小的
- 如果是,获得锁;如果不是,监听前一个节点
- 前一个节点删除后,自己变成最小,获得锁
// 伪代码
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生成方案,每个点都是交易系统高可用的基石。记住一句话:没有银弹,只有权衡。