分布式ID与幂等性面试题整理
文章目录
- 分布式ID与幂等性面试题整理
- 一、分布式ID
- 1. 为什么需要分布式ID?
- 2. 分布式ID的核心要求
- 3. 常见分布式ID方案
- (1) UUID
- (2) 数据库自增
- (3) Redis自增
- (4) 雪花算法(Snowflake)
- (5) 美团Leaf/百度UidGenerator
- 4. 雪花算法详解
- 二、幂等性
- 1. 什么是幂等性?
- 2. 为什么需要幂等性?
- 3. 实现幂等性的常见方案
- (1) 唯一索引
- (2) 乐观锁
- (3) 状态机
- (4) Token机制
- (5) 去重表
- (6) 全局ID
- 4. 不同场景的幂等实践
- HTTP接口幂等
- 消息队列幂等
- 定时任务幂等
- 5. 幂等性与并发控制
- 三、综合实战
- 1. 设计一个分布式ID生成服务
- 2. 支付系统的幂等设计
- 3. 分布式事务与幂等
一、分布式ID
1. 为什么需要分布式ID?
问题:在分布式系统中,为什么不能直接使用数据库自增ID?
答案:
- 单点故障:自增ID依赖单数据库,数据库挂了整个系统就瘫痪
- 性能瓶颈:所有ID生成请求都打到同一个数据库,压力大
- 扩展困难:分库分表时,自增ID会导致重复或需要复杂协调
- 安全问题:连续的数字ID容易暴露业务量,可能被爬取数据
2. 分布式ID的核心要求
问题:一个好的分布式ID生成方案需要满足哪些要求?
答案:
- 全局唯一:整个系统内绝对不能重复
- 趋势递增:最好是有序的,方便数据库索引
- 高可用:ID生成服务不能成为单点故障
- 高性能:每秒至少能生成几万ID
- 安全:不能暴露业务信息(如订单量)
3. 常见分布式ID方案
问题:常见的分布式ID生成方案有哪些?各自原理是什么?
答案:
(1) UUID
- 原理:随机生成128位数字,格式如
550e8400-e29b-41d4-a716-446655440000
- 优点:简单,本地生成无网络开销
- 缺点:无序导致索引性能差,字符串存储空间大
(2) 数据库自增
- 原理:单独数据库表,通过
REPLACE INTO
或事务获取ID - 优点:实现简单,ID有序
- 缺点:数据库单点风险,性能有限
(3) Redis自增
- 原理:利用Redis的
INCR
命令生成ID - 优点:性能比数据库好
- 缺点:需维护Redis集群,持久化问题可能导致ID重复
(4) 雪花算法(Snowflake)
- 原理:64位ID = 1位符号位(0) + 41位时间戳 + 10位机器ID + 12位序列号
- 优点:本地生成性能高,ID有序
- 缺点:依赖机器时钟,时钟回拨会导致ID重复
(5) 美团Leaf/百度UidGenerator
- 原理:改进版雪花算法,解决时钟回拨问题,引入ZK协调
- 优点:解决了原生雪花算法的问题
- 缺点:系统复杂度高
4. 雪花算法详解
问题:详细解释雪花算法的实现原理?
答案:
0 | 0001100 10100010 10111110 10001001 01011100 | 00 | 00001 | 000000000000
- 第1位:符号位,始终为0
- 中间41位:毫秒级时间戳,可用69年
- 接着10位:5位数据中心ID + 5位机器ID(最多1024个节点)
- 最后12位:序列号,每毫秒可生成4096个ID
时钟回拨问题处理:
- 轻微回拨:等待时间追上
- 严重回拨:报警人工介入
- 美团Leaf方案:使用ZK记录最大时间戳
二、幂等性
1. 什么是幂等性?
问题:用通俗语言解释什么是幂等性?
答案:
- 通俗理解:同样的操作执行一次和执行N次,效果一样
- 举例:
- 支付:同一笔订单只扣一次钱
- 短信:同一条通知只发一次
- 状态更新:最终状态一致
2. 为什么需要幂等性?
问题:分布式系统中为什么特别关注幂等性?
答案:
- 网络问题:请求超时可能导致客户端重试
- 微服务调用:服务调用失败会触发重试机制
- 消息队列:消息可能被重复消费
- 用户行为:用户可能多次点击提交按钮
3. 实现幂等性的常见方案
问题:有哪些实现幂等性的方案?各自适用场景?
答案:
(1) 唯一索引
- 原理:数据库唯一索引防止重复数据
- 场景:创建订单等插入操作
- 实现:订单ID、业务编号等加唯一索引
(2) 乐观锁
-
原理:通过版本号控制更新
-
场景:更新操作如账户余额变更
-
实现:
UPDATE account SET balance=balance-100, version=version+1 WHERE id=123 AND version=5
(3) 状态机
-
原理:业务状态流转控制
-
场景:订单状态等有明确流转的业务
-
实现:
UPDATE order SET status='paid' WHERE id=456 AND status='unpaid'
(4) Token机制
- 原理:客户端先获取令牌,服务端校验后删除
- 场景:防止表单重复提交
- 实现:
- 服务端生成token存入Redis
- 提交时携带token
- 校验后删除token
(5) 去重表
-
原理:记录已处理请求ID
-
场景:消息队列消费等
-
实现:
INSERT INTO request_log(request_id, biz_type) VALUES('req123', 'order') -- 先查是否存在再处理
(6) 全局ID
- 原理:利用分布式ID的唯一性
- 场景:所有需要幂等的场景
- 实现:结合雪花算法等生成唯一业务ID
4. 不同场景的幂等实践
问题:针对以下场景如何保证幂等性?
- HTTP接口
- 消息队列消费
- 定时任务
答案:
HTTP接口幂等
- GET:天然幂等
- POST:
- 前端:提交按钮禁用+loading
- 后端:Token机制+唯一索引
- PUT/DELETE:天然幂等(需正确实现)
消息队列幂等
- RabbitMQ:
- 消息唯一ID+去重表
- 手动ack确保处理完成
- Kafka:
- 利用offset控制
- 消费者组+分区保证顺序
定时任务幂等
- 加锁:分布式锁(Redis/ZK)
- 状态检查:记录上次执行结果
- 时间窗口:允许短时间重复但结果一致
5. 幂等性与并发控制
问题:幂等性和并发控制有什么区别?
答案:
- 幂等性:关注多次操作的结果一致性
- 并发控制:关注同时操作的顺序和正确性
- 联系:
- 都需要唯一标识
- 都可能导致数据不一致
- 区别:
- 幂等解决重复问题
- 并发控制解决竞争问题
三、综合实战
1. 设计一个分布式ID生成服务
问题:如何设计一个类似美团Leaf的分布式ID服务?
答案:
架构设计:
- 服务层:无状态服务,可水平扩展
- 存储层:
- ZK:协调worker节点分配
- Redis:缓存号段,提高性能
- ID生成:
- 号段模式:每次获取一批ID(如1-1000)
- 双Buffer:一个用完前预加载下一个
核心流程:
- 启动时向ZK注册获取workerID
- 从DB获取号段范围(UPDATE max_id=max_id+step)
- 内存中分配ID,快用完时异步加载下一号段
- 定期持久化当前分配位置
2. 支付系统的幂等设计
问题:支付系统如何防止重复扣款?
答案:
全链路设计:
-
订单创建:
- 订单ID使用雪花算法
- 订单表order_id唯一索引
-
支付请求:
- 前端:支付按钮防重
- 生成支付流水号(支付系统唯一)
-
支付核心:
// 伪代码 public Result pay(String orderId, BigDecimal amount) {// 1. 检查订单状态Order order = orderDao.get(orderId);if (order.isPaid()) {return Result.success("已支付");}// 2. 乐观锁更新int updated = orderDao.updateStatus(orderId, "unpaid", "paying");if (updated == 0) {return Result.fail("并发操作");}// 3. 实际扣款boolean success = accountService.debit(order.getUserId(), amount);// 4. 最终状态if (success) {orderDao.updateStatus(orderId, "paying", "paid");} else {orderDao.updateStatus(orderId, "paying", "failed");} }
-
对账补救:定时核对订单与支付记录
3. 分布式事务与幂等
问题:分布式事务场景下如何保证幂等性?
答案:
结合方案:
- TCC模式:
- Try阶段:生成事务ID,记录预备状态
- Confirm/Cancel:通过事务ID保证幂等
- 本地消息表:
- 业务与消息表在同一个事务
- 消息ID作为去重依据
- Saga模式:
- 每个步骤有补偿操作
- 通过业务ID保证补偿幂等
关键点:
- 事务ID全局唯一
- 操作前检查状态
- 补偿操作也要幂等