深分页、幂等与状态机

共 29 题
📑 题目列表 29 题
#
★★★

1. 基于游标的分页模式,cursor-based pagination?

什么是基于游标的分页(cursor-based pagination)?它与传统的 OFFSET/LIMIT 分页有何区别?

  • 游标分页的定位方式(基于排序键定位而非偏移量)
  • 与 OFFSET 分页在性能、一致性上的差异
  • 适用场景与限制

游标分页是在排序键上通过 WHERE 条件定位"上一页最后一条记录之后"的方式,而不是通过 OFFSET 跳过若干行。例如按 id 排序时,用 WHERE id > last_id ORDER BY id LIMIT 20 取下一页。所有页共用同一索引,因此每一页都从索引定位点开始扫描,不会像 OFFSET 那样逐页丢弃前面已读过的行,从而避免深分页时的性能退化。游标分页天然稳定,即使数据在翻页期间被插入或删除,也不会出现"重复或跳行"的问题,因为它是基于排序键位置而不是固定偏移量。

OFFSET 分页的代价随页数线性增长(每页都要扫描并丢弃 offset 行),而游标分页每次只扫描一页,复杂度稳定。它的代价是只能顺序翻页(next/prev),无法随意跳页,且要求排序键唯一稳定。

-- OFFSET 深分页:第 10000 页,需扫描并丢弃 200000 行
SELECT * FROM orders ORDER BY id LIMIT 20 OFFSET 200000;
-- 游标分页:基于 id 定位,每页只扫描 20 行
SELECT * FROM orders WHERE id > 200000 ORDER BY id LIMIT 20;
#
★★★

2. Seek(键集)分页与索引的配合,为什么基于 WHERE 游标的分页能利用索引避免深翻页,其排序键的唯一性要求是什么?

Seek(键集)分页如何与索引配合从而避免深翻页?其排序键为什么要求唯一?

  • Seek 分页的索引定位机制
  • 排序键唯一性对分页正确性的影响
  • 复合排序键的构造

Seek(键集)分页把"上一页的最后一条记录"当作锚点,用 WHERE 条件(如 id > last_id)在索引上做定位,随后按排序键扫描一页。因为索引天然有序,定位后只需扫描一页的叶子节点,时间复杂度与 OFFSET 无关,因此能避免深翻页。排序键必须唯一,否则当多行具有相同的排序键值时,>< 比较会跳过或重复这些行,导致分页数据丢失或重复。实践中常用主键或唯一键作为排序键;若排序键是复合列(如 (created_at, id)),则 WHERE 条件也要同时带上这两个键以保证唯一与稳定。

唯一性保证每次定位都能精确确定"锚点之后"的边界,避免相等键值带来的歧义。复合排序键需按"大于上一行"的字典序构造条件,例如 WHERE (created_at, id) > (:last_created_at, :last_id)

-- 用主键 id 做排序键,唯一稳定
SELECT * FROM orders WHERE id > :last_id ORDER BY id LIMIT 20;
-- 复合排序键:按时间排序,用 id 兜底保证唯一
SELECT * FROM orders
WHERE (created_at, id) > (:last_created_at, :last_id)
ORDER BY created_at, id LIMIT 20;
#
★★★

3. 去重(Dedup)的实现,唯一索引、Redis SET、消息队列?

去重(Dedup)有哪些常见实现方式?唯一索引、Redis SET、消息队列各自的应用场景是什么?

  • 唯一索引作为数据库层面的强去重约束
  • Redis SET 的高性能去重
  • 消息队列 Exactly-Once 语义下的去重手段

去重的实现手段由数据一致性与性能要求决定。数据库唯一索引(UNIQUE KEY)是最强、最可靠的去重手段,通过约束保证物理上不可能插入重复行,适合对唯一性要求极高的数据(如订单号、幂等键);Redis SET 利用内存哈希结构提供极高的去重性能,适合短期、大并发、允许少量同步误差的去重场景(如限流、布隆过滤器、去重跳过的中间件),但 Redis 本身不是持久化权威,故障时可能丢失;消息队列场景下,由于 At-Least-Once 投递可能重复消费,通常结合业务主键做幂等表或唯一索引去重。实践中常把"唯一索引兜底 + Redis 前置过滤"组合使用,兼顾性能与可靠性。

去重的本质是"在唯一约束下保证数据只出现一次"。数据库唯一索引是权威后盾,Redis 是性能前置,消息队列则需要配合幂等处理。选型取决于对可靠性与性能的权衡。

CREATE TABLE orders (
  id BIGINT PRIMARY KEY,
  order_no VARCHAR(32) NOT NULL UNIQUE,  -- 唯一索引兜底去重
  ...
);
-- 配合幂等键:避免重复插入
INSERT INTO orders (id, order_no, ...) VALUES (...)
ON DUPLICATE KEY UPDATE order_no = order_no;
#
★★★

4. 幂等表(Idempotency Table)的设计,请求指纹 + 响应缓存?

幂等表(Idempotency Table)如何设计?请求指纹与响应缓存如何配合实现接口幂等?

  • 请求指纹(业务幂等键)的生成
  • 幂等表结构(唯一键 + 状态 + 响应缓存)
  • 并发请求与重复请求的处理

幂等表通过在数据库里维护一张专门记录"幂等键及其处理结果"的表,配合唯一索引保证同一请求只处理一次。请求指纹由业务幂等键(Idempotency Key)生成,通常是客户端传入的 UUID 或由请求参数(如 userId + 订单号 + 时间戳)哈希得到。表结构一般包含幂等键(唯一)、处理状态、请求体、响应体缓存、创建/更新时间。处理流程:先尝试按幂等键插入一条"处理中"记录,若唯一索引冲突说明重复请求,则返回已缓存的结果;成功插入新记录后执行业务处理,再更新状态为"成功"并写入响应缓存。这样重复请求直接返回首次结果,避免重复扣款、重复下单等副作用。

幂等表的关键是"唯一索引 + 状态机",用数据库约束保证并发下只有一个请求真正执行,其余请求返回缓存结果。请求指纹与响应缓存让重复请求得到相同响应,实现对外幂等。

CREATE TABLE idempotency (
  idempotency_key VARCHAR(64) PRIMARY KEY,
  status VARCHAR(16) NOT NULL,          -- processing / success / failed
  request_body JSON,
  response_body JSON,
  created_at TIMESTAMP DEFAULT now(),
  updated_at TIMESTAMP DEFAULT now()
);
#
★★★

5. 状态机(State Machine)的数据库建模,status 字段 + transition 函数?

数据库如何为状态机建模?status 字段与 transition 函数如何配合?

  • status 字段与转移表的设计
  • 状态转移的合法性(transition 函数)
  • 与业务对象的一体化建模

状态机的数据库建模通常把状态字段(status)直接放在业务实体表上,并维护一张状态转移定义(transition 表或应用层映射),说明哪些状态允许转移到哪些状态。例如订单表有 status 字段(CREATED、PAID、SHIPPED、COMPLETED、CANCELLED),transition 函数定义合法转移关系(如 CREATED→PAID 合法,COMPLETED→PAID 非法)。应用层在更新 status 前调用 transition 函数校验合法性;数据库层可用 CHECK 约束或触发器兜底。状态转移通常伴随事件记录(操作时间、操作人),可用单独的状态历史表存每次转移的流水,便于审计回溯。

"status 字段 + transition 函数"把状态数据与转移规则分开:status 是数据,transition 是规则。规则可放在应用层便于维护,也可下沉到数据库(CHECK/触发器)增强约束。状态历史表提供审计能力。

CREATE TABLE orders (
  id BIGINT PRIMARY KEY,
  status VARCHAR(16) NOT NULL,
  CHECK (status IN ('CREATED','PAID','SHIPPED','COMPLETED','CANCELLED'))
);
-- 状态历史表,记录每次转移形成审计流水
CREATE TABLE order_status_history (
  id BIGINT PRIMARY KEY,
  order_id BIGINT NOT NULL,
  from_status VARCHAR(16),
  to_status VARCHAR(16) NOT NULL,
  changed_at TIMESTAMP DEFAULT now(),
  operator VARCHAR(64)
);
#
★★★

6. 状态转移的合法性校验,CHECK 约束、触发器、应用层?

状态转移的合法性校验有哪几种实现层次?CHECK 约束、触发器、应用层各有什么特点?

  • CHECK 约束的静态校验能力
  • 触发器对复杂转移的校验
  • 应用层校验的灵活性与不足

状态转移合法性校验可以在三个层次实现。应用层最简单灵活,在业务代码里通过 transition 函数判断,但无法防止绕过应用层的直接 SQL 操作,且多进程并发时可能出现竞态。CHECK 约束能校验"状态值本身合法"(如 status 必须在枚举内),但无法表达"从状态 A 才能到状态 B"这类跨行上下文转移,因为 CHECK 只针对单行。触发器(BEFORE UPDATE)能力最强,可以读取 NEW/OLD 的旧状态,在数据库层校验转移合法性,即使绕过应用层也能拦截,但会增加写入开销、维护复杂。成熟方案常是"应用层校验为主 + 数据库约束兜底"。

校验层次本质是"灵活性 vs 强制性的权衡"。应用层灵活但可绕过,数据库层强制但复杂。对关键状态机(如支付、订单)建议用触发器或数据库约束兜底,普通业务用应用层即可。

-- 触发器校验状态转移合法性
CREATE TRIGGER trg_validate_order_status
BEFORE UPDATE ON orders
FOR EACH ROW
BEGIN
  IF NEW.status = 'PAID' AND OLD.status <> 'CREATED' THEN
    SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'invalid transition';
  END IF;
END;
#
★★★

7. 状态转移的并发控制,乐观锁(版本号)、悲观锁(SELECT FOR UPDATE)?

状态转移的并发控制如何实现?乐观锁(版本号)与悲观锁(SELECT FOR UPDATE)如何选择?

  • 乐观锁版本号机制
  • 悲观锁行级锁机制
  • 两种方案的适用场景与取舍

状态转移的并发控制防止两个并发请求同时对同一实体做非法转移。乐观锁基于版本号:更新时 WHERE id=? AND version=?,同时把 version+1,若影响行数为 0 说明已被他人修改,需重试或报错。悲观锁用 SELECT ... FOR UPDATE 先锁住行,其他事务等待,串行化临界区后更新。乐观锁适合读多写少、冲突概率低的场景,无锁开销但失败需重试;悲观锁适合写冲突频繁、对一致性要求极高的场景(如库存扣减、状态机核心转移),但会增加锁等待与死锁风险。

乐观锁"失败即重试"适合冲突少的场景,悲观锁"先锁后改"适合冲突多的场景。状态机转移常配合悲观锁或乐观锁版本号,确保"从旧状态到新状态"的竞争下只有一个成功。

-- 乐观锁:版本号控制
UPDATE orders SET status='PAID', version=version+1
WHERE id=? AND version=?;  -- 返回 0 行则冲突,需重试
-- 悲观锁:先锁后改
SELECT * FROM orders WHERE id=? FOR UPDATE;
UPDATE orders SET status='PAID' WHERE id=?;
#
★★★

8. Etcd 分布式锁,Lease + Revision?

Etcd 分布式锁的机制是什么?Lease 与 Revision 如何协作实现互斥与自动续期?

  • etcd 的 key 与 Lease(租约)机制
  • Revision(版本号)与原子操作
  • Watch 与续期机制

Etcd 分布式锁基于"创建 key + 租约 + 版本号"实现。持锁时用 PUT key value --lease=leaseID 创建带租约的 key,租约(Lease)有 TTL,若持有者不续期,租约到期后 key 被自动删除,锁自动释放,从而避免持有者崩溃导致死锁。借助 etcd 的原子 compare-and-swap(CAS)与 Revision(全局单调递增版本号),可保证多个竞争者同时抢锁时只有一个成功。其余竞争者通过 Watch 监听 key 被删除事件,key 释放后唤醒竞争。持有者需周期性续租(LeaseKeepAlive)延长锁的存活时间。

Lease 解决"自动释放避免死锁",Revision 解决"原子互斥",Watch 解决"释放感知与公平排队"。相较 Redis 锁,etcd 有更强的线性一致性(Raft),适合元数据级、精度要求高的分布式锁。

#
★★★

9. ZooKeeper 分布式锁的实现,临时顺序节点如何保证互斥与公平排队,Watch 机制如何感知锁释放,与 etcd Lease+Revision、Redis 锁在一致性与可用性上的差异?

ZooKeeper 分布式锁如何用临时顺序节点保证互斥与公平排队?Watch 机制如何感知锁释放?它与 etcd、Redis 锁在一致性与可用性上有何差异?

  • 临时顺序节点(EPHEMERAL_SEQUENTIAL)的互斥与排队
  • Watch 机制感知锁释放
  • 三者一致性(CP 强一致 vs Redis 概率一致)与可用性差异

ZooKeeper 分布式锁用"临时顺序节点"实现:每个竞争者在锁节点下创建一个临时顺序节点,编号最小的节点持有锁,其他节点排队,并 Watch 前一个节点。当持有者释放锁(节点被删除,临时节点在会话断开时也会自动删除),后一个节点通过 Watch 感知到删除事件后尝试获取锁,形成公平的 FIFO 排队。互斥由"编号最小者持锁"保证,公平性由"顺序递增编号"保证,Watch 是释放感知机制。在一致性上,ZK 与 etcd 都基于共识(ZAB/Raft)提供强一致(CP),节点故障时可能短暂不可用但不会产生脑裂;Redis 锁基于异步复制,性能高但可用性优先(AP),极端情况下可能同时有两个持有者,若用 RedLock 也只是概率性保证。

三者本质是"强一致 vs 高可用"的取舍。ZK/etcd 适合对一致性要求高、允许短暂不可用的场景(如配置、元数据锁);Redis 锁适合高并发、低延迟、允许极小概率不一致的场景。临时节点利用会话失效自动释放,与 etcd 租约原理相通。

#
★★★

10. 分布式锁(Distributed Lock)的应用,定时任务、库存扣减、限流?

分布式锁主要应用在哪些场景?定时任务、库存扣减、限流如何用分布式锁?

  • 定时任务避免多实例重复执行
  • 库存扣减的并发控制
  • 限流与集群互斥

分布式锁用于在多实例/多进程环境下对共享资源做互斥访问。典型应用:定时任务在集群多实例部署时,用分布式锁保证同一时刻只有一个实例执行任务,避免重复作业;库存扣减用分布式锁(或数据库行锁/乐观锁)串行化扣减,防止超卖;限流、集群内互斥、发布开关等也依赖分布式锁。实现上可以是 Redis SETNX、etcd/ZK 的租赁锁,或数据库唯一索引/悲观锁。选型取决于一致性要求:影响资金的库存扣减更重视一致性,元数据/任务调度可用 Redis 等高性能锁。

分布式锁的本质是把"单机互斥"扩展到"集群互斥"。定时任务防重复、库存扣减防超卖、限流防击穿都是典型场景。锁的可靠性决定数据的正确性。

#
★★

11. 一致性消息(Transactional Message)的实现,RocketMQ、Kafka 事务消息?

一致性消息(Transactional Message)如何实现?RocketMQ 与 Kafka 的事务消息机制有何区别?

  • RocketMQ 半消息 + 回查机制
  • Kafka 事务消息的幂等与事务协调
  • 与本地事务的一致性

一致性消息解决"本地数据库事务与消息发送不能原子"的问题。RocketMQ 采用"半消息 + 回查":先发送半消息(对消费者不可见),本地事务执行成功后提交半消息,若提交失败或超时,Broker 回查业务方确认事务结果再决定投递或回滚。Kafka 事务消息基于事务协调器(Transaction Coordinator)和幂等 producer,通过事务 ID 把多个分区的消息写入绑定,实现跨分区原子性(但 Kafka 事务只保证 Kafka 内部多个分区的原子写,不直接与业务库事务绑定,需配合 Outbox 模式)。两者都保证"本地事务与消息发送"的一致性,只是 RocketMQ 提供半消息回查,Kafka 依赖事务协调器。

事务消息的核心是"消息与业务操作原子一致"。RocketMQ 的半消息回查是同步语义,Kafka 事务是跨分区原子性。两者解决同一类问题但实现不同。

#
★★

12. 事务性发件箱(Transactional Outbox)模式,业务事件写入 outbox 表?

事务性发件箱(Transactional Outbox)模式是什么?业务事件如何写入 outbox 表?

  • outbox 表与业务表同事务写入
  • 独立投递器扫描 outbox 发送消息
  • 与本地事务消息的对比

事务性发件箱(Outbox)模式把业务操作与事件写入放在同一个数据库事务里:业务表更新时,同时向 outbox 表插入一条待发事件,两者同事务提交,保证原子性。随后独立的投递器(Dispatcher/Relay)定期扫描 outbox 表,把未发送的事件投递到消息队列,投递成功后标记事件为已发送。这样避免了"先发消息后改库"或"先改库后发消息"导致的不一致。Outbox 表需要去重与幂等投递,配合消息的幂等键使用。

Outbox 把"本地事务 + 消息"的一致性转化为"数据库事务内写入 + 异步投递器扫描",利用数据库事务保证原子性,是业界普遍采用的可靠消息方案,比 RocketMQ 半消息更简单通用(任何 MQ 都可用)。

BEGIN;
-- 同一事务:业务更新 + 事件写入 outbox
UPDATE orders SET status='PAID' WHERE id=?;
INSERT INTO outbox (event_id, event_type, payload, status)
VALUES (?, 'ORDER_PAID', ?, 'PENDING');
COMMIT;
-- 投递器扫描 PENDING 事件并投递,成功后置为 SENT
#
★★

13. 本地消息表(Local Message Table)模式,业务库 + 消息表同事务写入?

本地消息表(Local Message Table)模式是什么?业务库与消息表如何同事务写入?

  • 本地消息表与业务库同事务
  • 定时任务扫描、发送与删除
  • 与 Outbox 的异同

本地消息表模式与 Outbox 类似:业务操作与消息写入放在同一个数据库事务中,业务库更新数据的同时向本地消息表插入一条待发消息,二者同事务提交保证原子性。随后由定时任务或消息扫描器周期性地读取消息表,将消息发送到消息队列,发送成功后在消息表标记或删除。为保证不重复发送,发送时需幂等处理;若服务器崩溃,未发送的消息仍留在消息表,重启后重新扫描发送,从而保证消息不丢失(At-Least-Once)。

本地消息表模式本质是"数据库事务 + 消息扫描"的可靠消息方案,保证消息不丢失但可能重复,需配合幂等消费。它与 Outbox 在思想上一脉相承,Outbox 是更现代的表达。

BEGIN;
UPDATE orders SET status='PAID' WHERE id=?;
INSERT INTO message (msg_id, body, status) VALUES (?, ?, 'PENDING');
COMMIT;
-- 定时任务:SELECT * FROM message WHERE status='PENDING' LIMIT 100; 发送后置 SENT
#
★★

14. Exactly-Once 投递的语义,消息系统与业务系统的协同?

Exactly-Once 投递的语义是什么?消息系统与业务系统如何协同实现?

  • 三种投递语义的含义
  • 幂等消费与去重
  • 消息系统与业务系统的分工

Exactly-Once 指消息恰好被处理一次,既不多也不少。但在分布式系统中,消息系统往往只能保证 At-Least-Once(至少一次,可能重复)或 At-Most-Once(至多一次,可能丢失),真正的 Exactly-Once 需要"消息系统 + 业务系统"协同:消息系统保证不丢失(At-Least-Once),业务系统通过幂等消费(幂等键、唯一索引、状态校验)把重复消息去重,从而在业务层面达到"恰好一次"的效果。例如 Kafka 的 Exactly-Once 跨分区事务 + 业务幂等表。本质上"Exactly-Once 是幂等消费 + 可靠投递的组合"。

分布式系统无法通过网络层保证"恰好一次",因此工程上采用"可靠投递 + 幂等消费 = 恰好一次"。消息系统负责不丢,业务系统负责不重,两者协同实现 Exactly-Once 语义。

#
★★

15. 幂等性(Idempotency)的定义,相同请求执行多次与一次效果相同?

幂等性的定义是什么?为什么"相同请求执行多次与一次效果相同"很重要?

  • 幂等性的定义
  • 幂等与非幂等的接口区分
  • 幂等在重试体系中的作用

幂等性(Idempotency)指执行多次操作与执行一次操作的效果相同,即同一请求重复提交不会产生额外的副作用。例如"查询"天然幂等,"把订单状态置为 PAID"也幂等(重复置为 PAID 结果相同),而"余额增加 +100"非幂等(执行两次会加两次)。幂等性是可靠重试、消息消费、网络重发的基石:因为网络可能重发、消费者可能重启,只有接口幂等才能安全重试而不产生重复副作用。HTTP 方法中 GET、PUT、DELETE 语义上幂等,POST 非幂等。实现幂等常用幂等键、唯一索引、版本号等。

幂等性的价值在于"容忍重试而不产生错误副作用"。它把分布式系统的不确定性(重发、重试、重复消费)转化为安全的确定性结果。

#
★★

16. 幂等键(Idempotency Key)的应用,支付、退款、消息处理?

幂等键(Idempotency Key)在支付、退款、消息处理等场景如何应用?

  • 幂等键的生成与传递
  • 支付/退款场景防重复扣款
  • 消息处理中防重复消费

幂等键(Idempotency Key)是一个唯一标识一次业务操作的键,客户端在发起请求时传入,服务端用它来识别重复请求。支付场景:客户端生成幂等键(如 orderId+userId),服务端用唯一索引或幂等表保证同一幂等键只扣一次款,重复点击不会重复扣款;退款场景同理,同一退款单只能退一次;消息处理场景:消费端拿到消息中的业务幂等键(如订单号、事件 ID),在处理前检查是否已处理过,若已处理则丢弃,防止 MQ 重复投递导致重复加减库存、重复记账。幂等键通常与"唯一索引 + 状态机"配合,保证并发下也只有一个请求被真正处理。

幂等键是"业务幂等"的载体,把"这次操作"抽象成唯一标识,配合数据库唯一约束实现可靠去重。支付、退款、消息三类场景都是"重复提交产生资金/数据副作用"的高危场景,因此必须用幂等键防御。

#
★★

17. 消息队列的重复消费,Exactly-Once vs At-Least-Once?

消息队列的重复消费问题是什么?Exactly-Once 与 At-Least-Once 语义如何取舍?

  • At-Least-Once 与重复消费
  • Exactly-Once 的成本
  • 应用层幂等消化重复

消息队列的投递语义通常默认 At-Least-Once(至少一次):消费者在消息处理完成但未提交 offset 时崩溃,重启后会重新消费该消息,导致重复消费。At-Most-Once(至多一次)可能丢失消息,业务不可接受。Exactly-Once(恰好一次)在广义上需要消息系统事务 + 业务幂等配合,成本高。实践上绝大多数业务采用"At-Least-Once 投递 + 应用层幂等消费":接受可能重复,但通过幂等键、唯一索引、去重表把重复消息变成无副作用,从而在业务层面达到恰好一次的效果。这样既保证消息不丢,又避免重复副作用。

重复消费是分布式消息系统的常态,不能靠 MQ 本身完全消除,因此工程上以"可靠投递 + 幂等消费"消化重复。这是成本与正确性的平衡。

#
★★

18. 重复事件(Duplicate Event)的检测,幂等键、版本号?

重复事件(Duplicate Event)如何检测?幂等键与版本号如何配合?

  • 幂等键去重
  • 版本号防旧事件覆盖新事件
  • 事件表唯一约束

重复事件检测常用"幂等键 + 版本号"双重机制。幂等键(如事件 ID、业务订单号)用唯一索引或去重表保证同一事件只处理一次;版本号(乐观锁)用于处理"乱序到达"的旧事件,防止旧事件覆盖新事件的状态。例如事件表以 (event_id) 为唯一键去重,同时记录 version/时间戳,只有版本更新的事件才允许更新业务数据,旧事件直接丢弃。两者结合:幂等键去重"同一事件重复",版本号去重"同一语义的旧版本"。

重复事件分成两类:完全相同的重复事件(幂等键解决)和语义相同但版本不同的事件(版本号/时间戳解决)。区分"是否重复"和"是否过期"是去重设计的两个维度。

CREATE TABLE order_events (
  event_id VARCHAR(64) PRIMARY KEY,   -- 幂等键去重
  order_id BIGINT NOT NULL,
  version INT NOT NULL,
  payload JSON,
  UNIQUE (order_id, version)
);
-- 只有版本更高的事件才更新,旧版本被拒绝
#
★★

19. 重试(Retry)的策略,指数退避、抖动、最大重试次数?

重试(Retry)策略如何设计?指数退避、抖动、最大重试次数如何配合?

  • 指数退避(Exponential Backoff)
  • 抖动(Jitter)避免惊群
  • 最大重试次数与熔断

重试策略的核心是指数退避:每次重试间隔按指数增长(如 1s、2s、4s、8s……),避免频繁重试加剧故障。但纯指数退避会让所有客户端在同一时刻重试,形成"惊群"(thundering herd),因此加入随机抖动(Jitter),让重试时间在退避区间内随机化,打散集中重试。同时必须设置最大重试次数,超过后放弃重试并走失败处理(记录、告警、另队列),避免无限重试耗尽资源。更完善的策略还包括"重试 + 熔断 + 降级",对下游故障做保护。重试还要求操作幂等,否则重试会重复副作用。

重试策略三要素:退避(增长间隔)、抖动(随机化)、上限(限制次数)。它们共同保证"既能在瞬时故障后恢复,又不至于放大故障"。幂等是重试的前提。

#
★★

20. 复杂状态机,Hierarchical State Machine(HSM)、Workflow Engine?

复杂状态机如何建模?Hierarchical State Machine(HSM)与 Workflow Engine 提供了哪些能力?

  • HSM 的层级状态
  • Workflow Engine 的编排能力
  • 与简单状态机的差异

当状态机过于复杂时,可用 Hierarchical State Machine(HSM,层级状态机)把相似状态归组为父状态,子状态继承父状态的转移,减少重复定义;或用 Workflow Engine(工作流引擎)把状态机扩展为可编排、可持久化、可人工干预的流程,支持分支、并行、超时、补偿、人工审批等。工作流引擎(如 Temporal、Camunda、Flowable)把状态转移与业务编排下沉为可配置的流程定义,支持持久化中断点与恢复。相比简单状态机,HSM 处理"层级复用",工作流引擎处理"复杂编排与长时间运行流程"。

简单状态机适合状态有限、转移明确的场景;复杂业务流程(订单、审批、多租户流程)需要 HSM 的层级抽象或工作流引擎的编排能力。选择取决于复杂度与变更频率。

#
★★

21. Spring Batch 的分块与重启实现?

Spring Batch 的分块(Chunk)与重启机制如何实现?

  • Chunk 式读-处理-写
  • 事务边界与提交
  • 重启的断点续跑

Spring Batch 采用 Chunk 式处理:每个 Chunk 由"读一批(如 100 条)→ 处理 → 写"组成,每完成一个 Chunk 提交一次事务。这样把大批量作业拆成小事务,失败时只需回滚当前 Chunk,避免整批回滚。重启机制依赖 JobRepository 持久化作业执行状态(JobInstance、JobExecution、StepExecution、ExecutionContext),记录每个 Step 的已处理位置与 checkpoint。作业中断后重启,Spring Batch 识别执行状态,从断点继续(不重复执行已完成部分),其中 Chunk 用"重新读取当前 chunk 并去重"的方式避免重复写。它需要配合数据库记录执行状态,实现可重启的批处理。

分块控制事务粒度,重启控制断点续跑。两者结合使批处理"失败可恢复、不重复不遗漏"。Chunk 大小与事务提交频率、性能、失败恢复代价相关。

#
★★

22. 批处理的重启(Restart)策略,断点续传、幂等执行?

批处理的重启策略如何设计?断点续传与幂等执行如何配合?

  • 断点续传(checkpoint)
  • 幂等执行
  • 重启的正确性

批处理重启策略的核心是"断点续传 + 幂等执行"。断点续传通过持久化 checkpoint(如已处理到的记录 ID、偏移量、批次号),重启后从断点继续处理,避免重复处理大量数据。幂等执行保证即使某个批次被重复处理(如上一个 chunk 提交后崩溃、重启重新读取该 chunk),重复写也不会产生额外副作用,例如用唯一键 upsert、按批次号去重。两者结合实现"重启不丢数据、不重复副作用"。断点续传减少了恢复工作量,幂等执行保证了正确性。

重启策略的关键是"判断已处理到哪"(断点)与"重复处理是否安全"(幂等)。只有断点续传没有幂等,重启可能重复;只有幂等没有断点,恢复慢。两者缺一不可。

#
★★

23. 批处理(Batch Job)的分块(Chunking)原则,每块多少行?

批处理的分块(Chunking)原则是什么?每块处理多少行合适?

  • Chunk 大小与事务粒度
  • 吞吐与内存的权衡
  • 失败恢复代价

批处理分块的原则是"在吞吐、内存、失败恢复代价之间取得平衡"。每块处理的行数(Chunk Size)决定事务粒度:块太大,事务提交慢、内存占用高、失败回滚代价大;块太小,提交频繁、开销大、吞吐下降。常见做法是把每块控制在 100~1000 行(经验值),并根据数据大小、处理耗时、内存预算调整。分块还考虑"每块的耗时"而非仅行数,例如按"每块处理约 1 秒"目标动态调整。分块的核心收益是"失败时只回滚当前块,便于断点续跑"。

分块本质是"以可恢复的最小单位推进大批量处理"。合适的块大小使单次失败的影响可控,同时保持较高吞吐。块大小需结合具体场景调优,没有固定值。

#
★★

24. 批处理的进度跟踪,checkpoint、状态表?

批处理的进度如何跟踪?checkpoint 与状态表如何实现?

  • checkpoint 持久化
  • 状态表记录批次进度
  • 进度可视化与恢复

批处理进度跟踪通过 checkpoint 与状态表实现。checkpoint 记录"已处理到哪"(如最后处理的主键、偏移量、批次号),周期性持久化到数据库或文件,崩溃后据此恢复。状态表(job 表 + job_step 表)记录每个批次的执行状态:总行数、已处理行数、状态(RUNNING/SUCCESS/FAILED)、开始/结束时间、错误信息等。应用层可查询状态表展示进度、发现卡住的任务、触发重跑。checkpoint 偏重"恢复点",状态表偏重"元数据与审计",两者可结合:checkpoint 用于断点续跑,状态表用于监控与调度。

进度跟踪使批处理"可观测、可恢复、可审计"。checkpoint 解决"恢复",状态表解决"监控与治理"。对长任务,定期更新 checkpoint 是关键。

CREATE TABLE batch_job (
  job_id BIGINT PRIMARY KEY,
  job_name VARCHAR(64),
  status VARCHAR(16),          -- RUNNING/SUCCESS/FAILED
  total_rows BIGINT,
  processed_rows BIGINT,
  started_at TIMESTAMP,
  finished_at TIMESTAMP
);
-- 每次处理若干行后,更新 processed_rows 作为 checkpoint
UPDATE batch_job SET processed_rows = ? WHERE job_id = ?;
#
★★

25. 回调(Callback)的实现,长轮询、Webhook、消息推送?

回调(Callback)的实现方式有哪些?长轮询、Webhook、消息推送各自的适用场景?

  • 长轮询(Long Polling)
  • Webhook 推送
  • 消息队列/消息推送

回调用于异步通知调用方结果,主要实现方式:长轮询(Long Polling)是客户端发起请求后服务端挂起,直到有结果或超时才返回,模拟"实时推送"但仍是 HTTP 请求驱动,适合服务端无法主动推送的场景;Webhook 是服务端主动向预先注册的 URL 推送事件(HTTP POST),适合服务端能主动外呼且调用方有固定公网地址的场景,需提供重试与鉴权;消息推送(MQ/WebSocket/APNs)通过消息队列或推送通道异步送达,适合高并发、多接收方、需要解耦的场景。选型取决于"服务端能否主动外呼、延迟要求、可靠性与解耦需求"。

三种回调本质是"主动拉取 vs 被动推送"的权衡。长轮询是"伪推送",Webhook 是"主动推送",消息推送是"第三方总线解耦"。工程上常组合使用,如 Webhook + MQ 缓冲 + 重试。

#
★★

26. 异步任务(Async Task)的数据库实现,任务表 + 状态机?

异步任务(Async Task)如何在数据库层面实现?任务表与状态机如何配合?

  • 任务表的设计
  • 状态机驱动任务流转
  • 调度与重试

异步任务的数据库实现是"任务表 + 状态机":任务表记录每个任务的元数据(任务 ID、类型、参数、状态、重试次数、下次执行时间),状态机定义任务的生命周期(PENDING→RUNNING→SUCCESS/FAILED/RETRY)。调度器扫描任务表,把到期的 PENDING/RETRY 任务取出执行,执行时把任务置为 RUNNING(常配合乐观锁或 SELECT FOR UPDATE 防止并发重复执行),成功后置 SUCCESS,失败按重试策略置 RETRY 并更新下次执行时间。任务表持久化保证任务不丢失,重启后仍能恢复未完成任务。相比内存任务队列,数据库方案可靠但调度频率有限,适合对可靠性要求高的后台任务。

任务表 + 状态机把"异步任务"变成"可持久化、可恢复、可重试"的数据库实体。调度器周期扫描实现"准实时"执行,配合锁避免重复执行。可靠性优先时选数据库方案。

CREATE TABLE async_task (
  task_id BIGINT PRIMARY KEY,
  task_type VARCHAR(32),
  params JSON,
  status VARCHAR(16) DEFAULT 'PENDING',  -- PENDING/RUNNING/SUCCESS/FAILED/RETRY
  retry_count INT DEFAULT 0,
  next_run_at TIMESTAMP,
  version INT DEFAULT 0
);
-- 领取任务:乐观锁防并发
UPDATE async_task SET status='RUNNING', version=version+1
WHERE task_id=? AND version=? AND status IN ('PENDING','RETRY');
#
★★

27. 最大努力通知(Best-Effort Notification),业务 + 异步通知?

最大努力通知(Best-Effort Notification)是什么?业务与异步通知如何配合?

  • 最大努力通知的定义
  • 与强一致通知的差异
  • 重试与最终一致

最大努力通知(Best-Effort Notification)指在业务成功后,尽力异步通知下游,但不保证通知一定成功或时序一致,只追求"尽力而为 + 最终一致性"。实现上业务事务提交后,通过消息队列/异步任务发送通知,配合重试和补偿,但允许通知可能丢失或延迟。与可靠消息(如 Outbox 强一致)不同,最大努力通知不要求"业务与通知原子一致",适合对通知数据一致性要求不高的场景(如订阅通知、活动邀请)。它依赖"业务成功 + 异步通知 + 定期补偿"保证最终达成。

最大努力通知是"尽力而为"的最终一致方案,牺牲了强一致换取消耦与简单。适用于通知类、非关键数据同步场景;关键业务(支付、库存)应使用可靠消息保证一致性。

#
★★

28. 可靠消息投递的语义,At-Least-Once、At-Most-Once、Exactly-Once?

可靠消息投递的三种语义是什么?At-Least-Once、At-Most-Once、Exactly-Once 如何取舍?

  • 三种投递语义的含义
  • 各自保证与代价
  • 工程上的选择

可靠消息投递有三种语义:At-Most-Once(至多一次)允许消息丢失但不允许重复,实现最简单但可能丢消息,业务不可接受;At-Least-Once(至少一次)保证消息不丢失但可能重复,是消息队列最常用的默认语义,配合消费端幂等即可;Exactly-Once(恰好一次)即不丢也不重,需要消息系统事务 + 业务幂等配合,成本最高。工程上绝大多数业务选择"At-Least-Once + 消费端幂等",因为它在"不丢消息"与"成本可控"之间取得平衡,重复消息由业务去重消化。

三种语义是"保证强度 vs 实现成本"的权衡。At-Least-Once 是最实用折中,因为消息不丢是刚需,重复可容忍。Exactly-Once 是"可靠投递 + 幂等消费"的组合结果。

#

29. 分布式场景中抖动(Jitter)的应用,重试退避中引入随机抖动如何避免惊群与同步风暴,指数退避与全抖动/等抖动策略如何设计?

分布式场景中抖动(Jitter)如何应用?重试退避中引入随机抖动如何避免惊群与同步风暴?指数退避与全抖动/等抖动策略如何设计?

  • 惊群(thundering herd)与同步风暴
  • 全抖动(Full Jitter)与等抖动(Equal Jitter)
  • 指数退避的抖动化设计

分布式重试中,若所有客户端在同一时间点重试,会形成"惊群"(thundering herd)与"同步风暴",加剧下游故障。引入随机抖动(Jitter)让重试时间在区间内随机化,打散集中重试。常见策略:全抖动(Full Jitter)在退避区间内随机取一点,如 sleep = random(0, min(cap, base * 2^n));等抖动(Equal Jitter)保留退避的最小值,只在增量部分随机,如 sleep = min(cap, base*2^n)/2 + random(0, base*2^(n-1)),既保留退避趋势又引入随机性。抖动使重试分布更平滑,避免瞬时高峰。

抖动是"用随机性换取系统稳定性"。纯指数退避会让故障恢复瞬间所有请求同时涌入,抖动则把恢复压力分摊到时间窗口内。全抖动随机化更强,等抖动保留退避下限,两者都是工程常用策略。