其它(6):分布式知识体系

分布式知识体系

一、分布式锁

单机锁只能管住一个进程内的线程,比如 sync.Mutexsynchronized。一旦服务部署成多实例,请求可能打到不同机器,单机锁就不够了。

分布式锁要解决的是:多台机器、多个进程同时竞争同一份资源时,只允许一个执行者进入临界区

典型场景:

  1. 定时任务多实例部署,只允许一个实例执行。
  2. 防止重复扣库存、重复发券、重复支付回调。
  3. 缓存重建时避免大量请求同时打到数据库。
  4. 分布式环境下保护某个共享资源。

分布式锁核心要求:

要求 说明
互斥性 同一时刻只能有一个客户端持有锁
防死锁 持锁方宕机后,锁最终能释放
可重入/续期 长任务需要考虑锁过期问题
安全释放 只能释放自己持有的锁
高可用 锁服务本身不能轻易成为单点

1. Redis 实现分布式锁

Redis 分布式锁最常见,性能高,实现简单,适合大多数业务互斥场景。

1.加锁

正确加锁方式应该是原子命令:

bash 复制代码
SET lock:order:123 request-id NX EX 30

含义:

参数 说明
lock:order:123 锁 key
request-id 锁持有者唯一标识
NX key 不存在时才设置,保证互斥
EX 30 30 秒过期,防止死锁

不要用下面这种分两步的方式:

bash 复制代码
SETNX lock value
EXPIRE lock 30

因为 SETNX 成功后,如果进程在 EXPIRE 前宕机,锁可能永远不释放。

2.解锁

解锁不能直接 DEL lock,因为可能误删别人的锁。

问题场景:

  1. A 获取锁,过期时间 30 秒。
  2. A 执行业务超过 30 秒,锁自动过期。
  3. B 获取到同一把锁。
  4. A 执行完直接 DEL lock,把 B 的锁删掉。

所以解锁要先判断 value 是否是自己的 request-id,再删除。这个判断和删除必须用 Lua 保证原子性:

lua 复制代码
if redis.call("get", KEYS[1]) == ARGV[1] then
  return redis.call("del", KEYS[1])
else
  return 0
end

一句话:加锁用 SET NX EX,解锁用 Lua 比对 value 后删除。

3.锁过期和续期

如果业务执行时间可能超过锁过期时间,有两种处理方式:

  1. 估算一个足够长的过期时间。
  2. 使用看门狗机制自动续期。

看门狗的思路是:持锁线程还活着,就周期性延长锁过期时间;业务结束后主动释放锁。

Redisson 的分布式锁就内置了类似机制。

4.Redis 锁的问题

Redis 锁不是没有风险,主要问题在高可用切换场景。

例如 Redis 主从架构:

  1. 客户端 A 在 master 上加锁成功。
  2. 锁还没同步到 slave。
  3. master 宕机,slave 被提升为新 master。
  4. 客户端 B 在新 master 上也加锁成功。

这时可能出现两个客户端同时认为自己持有锁。

所以 Redis 锁适合多数业务互斥场景,但如果是金融级强一致互斥,要更谨慎,可以考虑 ZooKeeper、etcd,或者从业务层做幂等和状态机兜底。


2. ZooKeeper / etcd 实现分布式锁

ZooKeeper 和 etcd 更偏强一致协调服务,适合对锁一致性要求更高的场景。

1.ZooKeeper 锁原理

常见做法是使用临时顺序节点:

text 复制代码
/locks/order/lock-00000001
/locks/order/lock-00000002
/locks/order/lock-00000003

加锁流程:

  1. 客户端在锁目录下创建临时顺序节点。
  2. 获取当前目录下所有节点并排序。
  3. 如果自己的节点序号最小,则获取锁成功。
  4. 如果不是最小节点,就监听自己前一个节点。
  5. 前一个节点删除后,再判断自己是否最小。

释放锁:

  1. 正常执行完删除自己的临时节点。
  2. 客户端宕机或会话断开,临时节点自动删除。

2.为什么监听前一个节点

如果所有客户端都监听最小节点,最小节点释放时会唤醒大量客户端,形成惊群。

监听前一个节点可以做到链式唤醒:

text 复制代码
lock-01 释放 -> 唤醒 lock-02
lock-02 释放 -> 唤醒 lock-03

这样更有序,也更节省资源。


3.数据库实现分布式锁

数据库也可以实现分布式锁,常见方式有唯一索引和悲观锁。

1.唯一索引加锁

建一张锁表:

sql 复制代码
CREATE TABLE distributed_lock (
  lock_name VARCHAR(128) PRIMARY KEY,
  owner VARCHAR(128),
  expire_at DATETIME
);

如果插入成功,说明拿到锁;如果主键冲突,说明锁已被别人持有。

2.数据库锁的特点

优点:

  1. 依赖少,如果系统本来就有数据库,实现方便。
  2. 可以利用数据库事务和唯一约束。
  3. 对一致性要求比 Redis 更直观。

缺点:

  1. 性能不如 Redis。
  2. 高并发下数据库压力大。
  3. 需要处理过期锁、死锁、事务超时等问题。

数据库锁适合并发量不高、依赖简单、业务更看重一致性的场景。


二、分布式 ID

单机自增 ID 在单库单表里很好用,但系统分布式后会遇到问题:

  1. 分库分表后,不同库的自增 ID 可能冲突。
  2. 多服务并发生成 ID,需要全局唯一。
  3. 订单号、消息 ID、链路追踪 ID 等需要跨系统流转。
  4. 有些场景还要求趋势递增,方便数据库索引和排序。

分布式 ID 常见要求:

要求 说明
全局唯一 不同机器生成的 ID 不能重复
高性能 生成 ID 不能成为系统瓶颈
趋势递增 对数据库 B+Tree 索引更友好
高可用 ID 服务故障不能影响全站
可解析 最好能看出时间、机器号等信息

1.UUID

UUID 最简单,应用本地直接生成,不依赖中心服务。

text 复制代码
550e8400-e29b-41d4-a716-446655440000

优点:

  1. 本地生成,性能高。
  2. 不依赖数据库、Redis 或 ID 服务。
  3. 全局唯一概率极高。

缺点:

  1. 太长,占用存储空间大。
  2. 无序,作为数据库主键会导致索引频繁分裂。
  3. 可读性差,不方便排查。

UUID 适合日志追踪、请求 ID、幂等 token,不太适合做核心业务表的自增主键。


2. 数据库自增和号段模式

1.数据库自增

最简单方式是用数据库自增主键:

sql 复制代码
id BIGINT AUTO_INCREMENT PRIMARY KEY

优点是简单、递增、容易理解。

缺点是分库分表后不好保证全局唯一,而且强依赖数据库,性能和可用性都受数据库影响。

2.号段模式

号段模式是数据库自增的增强版。ID 服务每次从数据库申请一段 ID,缓存到本地慢慢发。

例如数据库记录当前最大值:

text 复制代码
biz_tag = order
max_id = 100000
step = 1000

ID 服务申请一次号段后,拿到:

text 复制代码
100001 ~ 101000

后续 1000 个 ID 都可以在内存里生成,不用每次访问数据库。

优点:

  1. 性能比每次查数据库高很多。
  2. ID 趋势递增,对数据库索引友好。
  3. 可以按业务隔离号段,比如订单、用户、消息分别分配。

缺点:

  1. ID 服务重启可能浪费一部分号段。
  2. 数据库仍是号段分配的核心依赖。
  3. 需要处理双 buffer、提前加载、数据库故障兜底。

典型方案:美团 Leaf 号段模式。


3.Snowflake 雪花算法

Snowflake 是 Twitter 提出的分布式 ID 生成方案,核心思想是把一个 64 位整数拆成几段:

text 复制代码
0 | timestamp | machine-id | sequence

常见结构:

部分 作用
符号位 固定为 0,保证正数
时间戳 当前时间减去自定义起始时间
机器 ID 标识是哪台机器生成的
序列号 同一毫秒内自增

示意:

text 复制代码
1ms 内:
machine-01 生成 sequence 0,1,2,3...
machine-02 生成 sequence 0,1,2,3...

只要机器 ID 不冲突,同一毫秒内序列号不溢出,ID 就不会重复。

优点:

  1. 本地生成,不依赖数据库。
  2. 性能高。
  3. 生成的是整数,适合做数据库主键。
  4. 大体按时间趋势递增。

缺点:

  1. 依赖机器时钟,时钟回拨可能导致重复。
  2. 需要分配和管理机器 ID。
  3. 单机同一毫秒序列号有上限。

1.时钟回拨怎么处理

常见处理方式:

  1. 小幅回拨:等待时间追上来。
  2. 大幅回拨:拒绝生成 ID,报警处理。
  3. 使用备用 workerId 或时钟序列位。
  4. 通过 NTP 管理机器时间,避免频繁回拨。

面试要点:Snowflake 的核心风险是时钟回拨和机器 ID 冲突。


4.Redis 生成 ID

Redis 可以用 INCR 生成全局递增 ID:

bash 复制代码
INCR id:order

为了避免单 key 过大,也可以按日期分 key:

bash 复制代码
INCR id:order:20260607

生成订单号时可以组合:

text 复制代码
日期 + 自增序列
202606070000001

优点:

  1. 实现简单。
  2. 天然递增。
  3. 性能较高。

缺点:

  1. 强依赖 Redis。
  2. Redis 持久化和主从切换要处理好,否则可能丢失或回退。
  3. 超高并发下单 key 可能成为热点。

适合中小规模业务、订单号、流水号等场景。


5.方案对比

方案 优点 缺点 适合场景
UUID 本地生成、简单、无中心依赖 长、无序、索引不友好 请求 ID、日志追踪
数据库自增 简单、递增 分库分表困难、依赖数据库 单库单表
号段模式 趋势递增、性能高 实现复杂、依赖数据库分配号段 业务主键、订单 ID
Snowflake 本地生成、高性能、整数趋势递增 时钟回拨、机器 ID 管理 高并发业务主键
Redis INCR 简单、递增、性能较好 依赖 Redis、可能热点 流水号、订单号

选择建议:

  1. 只是请求追踪:用 UUID。
  2. 单库单表:数据库自增足够。
  3. 高并发业务主键:优先 Snowflake 或号段模式。
  4. 需要严格递增流水号:可以用 Redis INCR 或数据库号段。
  5. 分库分表主键:不要直接依赖单库自增。

三、总结

分布式锁:

text 复制代码
分布式锁常见实现有 Redis、ZooKeeper/etcd、数据库。
Redis 锁性能好,通常用 SET key value NX EX 加锁,
用 Lua 判断 value 后删除,避免误删别人的锁。
ZooKeeper/etcd 一致性更强,常用临时顺序节点或租约机制,
适合强一致协调场景。
数据库锁实现简单,但性能一般,适合并发不高的业务。

分布式 ID :

text 复制代码
分布式 ID 要保证全局唯一,最好高性能、趋势递增。
UUID 简单但无序;数据库自增不适合分库分表;
号段模式通过批量申请 ID 提升性能;
Snowflake 通过时间戳、机器 ID、序列号组合生成 64 位整数,
性能高但要处理时钟回拨和机器 ID 冲突;
Redis INCR 简单递增,但依赖 Redis,可用于流水号。
相关推荐
迷茫运维路2 小时前
golang_Viper配置管理器
后端·golang
java_cj2 小时前
Elasticsearch索引管理完全指南:从基础API到ILM生命周期管理
大数据·后端·elasticsearch·性能优化
geovindu2 小时前
go: Broadcast Pattern
开发语言·后端·设计模式·golang·广播模式
Alson_Code2 小时前
Spring AI-1.1.0
java·人工智能·后端·spring·ai编程
~|Bernard|2 小时前
关于go语言中二维切片的append操作陷阱
开发语言·后端·golang
李昊哲小课3 小时前
Spring Boot 4.0.6 全栈教程案例
spring boot·后端
千云3 小时前
100w大表0停机回滚:我们为什么放弃Undo Log,选择表名切换?
数据库·后端·mysql
云恒要逆袭3 小时前
Hello World背后的秘密:Java程序是这样运行的
java·后端·程序员
蝎子莱莱爱打怪3 小时前
XZLL-IM干货系列 01|万字拆解分布式 IM 架构:7 个微服务 + 自研 Flutter SDK
java·后端·面试