← 学习门户 每日面试题 学习看板

2026-09-14 · 高阶题

高阶题 · 性能优化 / 线程池优化 / 架构设计

3 题 · 平均 3.88/5 · 高阶题 · 第 2 轮

#1 性能优化 进阶 practice

题目

性能优化

我的回答

暂无回答记录

点评

暂无点评

完整讲解

子问题 1:最终一致性方案设计

# 1.1 方案选型

| 方案 | 一致性 | 性能 | 复杂度 | 适用场景 |
|------|--------|------|--------|---------|
| **2PC(两阶段提交)** | 强一致 | 低 | 高 | 银行转账、跨 DB 事务 |
| **TCC(Try-Confirm-Cancel)** | 强一致 | 中 | 高 | 支付、库存扣减 |
| **本地消息表 + MQ** | 最终一致 | 高 | 中 | 订单、通知、日志 |
| **Saga** | 最终一致 | 高 | 中 | 长流程事务 |

**推荐方案**:本地消息表 + 可靠消息(最终一致性)

**理由**:
- 性能高(异步处理)
- 复杂度高(不需要改造业务代码)
- 适合订单场景(允许 30s 不一致)

# 1.2 核心流程

```
1. 用户下单 → 订单服务
2. 订单服务开启本地事务:
a. 落订单(状态=待支付)到 MySQL
b. 落本地消息表(状态=待发送)
c. 提交本地事务
3. 定时任务扫描本地消息表 → 发送 MQ 消息
4. 库存服务消费 MQ:扣减库存(幂等)
5. 订单服务消费 MQ:更新订单状态(幂等)
6. 消息消费者确认消费成功 → 删除本地消息表记录
```

**时序图**:

```
用户 → 订单服务 → [MySQL 订单表 + 本地消息表](本地事务)
↓
定时任务扫描 → 发送 MQ
↓
库存服务 ← MQ 消息 → 订单服务
(扣库存) (更新状态)
↓
确认消费 → 删除消息表记录
```

# 1.3 本地消息表实现

```sql
-- 本地消息表
CREATE TABLE local_message (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
biz_id VARCHAR(64) NOT NULL, -- 业务唯一 ID(订单号)
topic VARCHAR(64) NOT NULL, -- MQ topic
message_body TEXT NOT NULL, -- 消息内容
status TINYINT NOT NULL DEFAULT 0, -- 0=待发送, 1=已发送, 2=已消费
retry_count INT NOT NULL DEFAULT 0, -- 重试次数
next_retry_time DATETIME, -- 下次重试时间
create_time DATETIME NOT NULL,
update_time DATETIME NOT NULL,
INDEX idx_status_retry (status, next_retry_time)
);
```

```java
@Transactional
public void createOrder(OrderRequest req) {
// 1. 落订单
orderDao.insert(buildOrder(req, OrderStatus.PENDING_PAYMENT));

// 2. 落本地消息表(同一个事务!)
localMessageDao.insert(LocalMessage.builder()
.bizId(req.getOrderId())
.topic("order_created")
.messageBody(JSON.toJSONString(req))
.status(0)
.nextRetryTime(LocalDateTime.now())
.build());

// 事务提交后,由定时任务异步发送 MQ
}
```

```java
// 定时任务:扫描待发送消息
@Scheduled(fixedDelay = 1000)
public void scanAndSendMessages() {
List<LocalMessage> messages = localMessageDao.selectPending(100);
for (LocalMessage msg : messages) {
try {
rocketMqProducer.send(msg.getTopic(), msg.getMessageBody());
localMessageDao.updateStatus(msg.getId(), 1); // 标记已发送
} catch (Exception e) {
log.error("Send message failed", e);
localMessageDao.incrementRetry(msg.getId()); // 增加重试次数
}
}
}
```

# 1.4 幂等设计

| 环节 | 幂等键 | 实现方式 |
|------|--------|---------|
| 订单创建 | `order_id` | 唯一索引,重复插入报错 |
| 库存扣减 | `order_id + sku_id` | 唯一索引 + 乐观锁 |
| 消息消费 | `msg_id`(MQ 消息 ID) | 去重表,消费前检查 |

```java
// 库存服务消费 MQ 时的幂等处理
@Transactional
public void deductStock(StockMessage msg) {
// 1. 检查是否已处理
if (deductionRecordDao.exists(msg.getOrderId(), msg.getSkuId())) {
log.info("Already processed, skip: {}", msg.getOrderId());
return; // 幂等:重复消息直接跳过
}

// 2. 扣减库存(乐观锁)
int affected = stockDao.deductStockWithVersion(
msg.getSkuId(), msg.getQuantity(), msg.getVersion()
);

if (affected == 0) {
// 版本冲突,重试或失败
throw new OptimisticLockException("并发冲突");
}

// 3. 记录扣减记录(幂等依据)
deductionRecordDao.insert(DeductionRecord.builder()
.orderId(msg.getOrderId())
.skuId(msg.getSkuId())
.quantity(msg.getQuantity())
.build());
}
```

```java
// 消息消费幂等(基于 MQ 消息 ID)
@RocketMQMessageListener(topic = "order_created", consumerGroup = "order_group")
public class OrderMessageConsumer implements RocketMQListener<Message> {

@Override
public void onMessage(Message message) {
String msgId = message.getMsgId();

// 1. 检查是否已消费
if (messageConsumeRecordDao.exists(msgId)) {
log.info("Message already consumed, skip: {}", msgId);
return;
}

// 2. 处理消息
OrderMessage msg = JSON.parseObject(message.getBody(), OrderMessage.class);
processOrder(msg);

// 3. 记录消费记录
messageConsumeRecordDao.insert(msgId);
}
}
```

---

子问题 2:超卖 0.3% 根因与解法

# 2.1 根因分析

| 根因 | 概率 | 说明 |
|------|------|------|
| **缓存与 DB 不一致** | 高 | 缓存有库存,DB 实际已卖完,读到脏数据 |
| **并发扣减无锁** | 中 | 两个请求同时读到库存=1,都扣成功 |
| **扣减非原子操作** | 中 | `SELECT` + `UPDATE` 非原子,中间有间隙 |
| **缓存过期时间不同步** | 低 | 缓存延迟导致短时间脏读 |

**典型超卖场景**:

```
T1: 请求 A 读缓存 → 库存=1
T2: 请求 B 读缓存 → 库存=1(缓存未更新)
T3: 请求 A 扣减 DB → 成功(库存=0)
T4: 请求 B 扣减 DB → 成功(库存=-1)← 超卖!
```

# 2.2 解决方案 A:Redis 预扣减 + DB 确认

**适用场景**:高并发、库存精度要求不高(允许少量误差)

```java
// 下单时:先扣 Redis 库存
public boolean preDeductStock(Long skuId, int quantity) {
String key = "stock:" + skuId;

// Lua 脚本保证原子性
String luaScript = """
local stock = tonumber(redis.call('GET', KEYS[1]))
if stock == nil or stock < tonumber(ARGV[1]) then
return -1
end
redis.call('DECRBY', KEYS[1], ARGV[1])
return stock - tonumber(ARGV[1])
""";

Long result = redisTemplate.execute(
new DefaultRedisScript<>(luaScript, Long.class),
Collections.singletonList(key),
String.valueOf(quantity)
);

return result != null && result >= 0;
}

// 支付成功后:DB 最终确认
@Transactional
public void confirmStock(Long skuId, int quantity) {
int affected = stockDao.deductStockWithVersion(skuId, quantity);
if (affected == 0) {
// DB 扣减失败,回补 Redis
redisTemplate.opsForValue().increment("stock:" + skuId, quantity);
throw new InsufficientStockException("库存不足");
}
}

// 支付超时:回补 Redis
@Scheduled(fixedDelay = 60000)
public void rollbackTimeoutOrders() {
List<Order> timeoutOrders = orderDao.selectTimeout(30, TimeUnit.MINUTES);
for (Order order : timeoutOrders) {
redisTemplate.opsForValue().increment(
"stock:" + order.getSkuId(), order.getQuantity()
);
orderDao.updateStatus(order.getId(), OrderStatus.CANCELLED);
}
}
```

**缓存与 DB 同步策略(Cache Aside)**:

```java
@Transactional
public void deductStockWithVersion(Long skuId, int quantity) {
// 1. 更新 DB
int affected = stockDao.deductStockWithVersion(skuId, quantity);

// 2. 删除缓存(不是更新!)
redisTemplate.delete("stock:" + skuId);

return affected;
}

// 读缓存 miss 时回源
public long getStock(Long skuId) {
String key = "stock:" + skuId;
Long stock = redisTemplate.opsForValue().get(key);
if (stock == null) {
stock = stockDao.getStock(skuId);
// 随机过期时间(防雪崩)
long expire = 3600 + ThreadLocalRandom.current().nextLong(600);
redisTemplate.opsForValue().set(key, stock, expire, TimeUnit.SECONDS);
}
return stock;
}
```

**优点**:
- Redis 原子扣减,高并发下性能极佳(10 万+ QPS)
- DB 最终确认,保证数据一致性

**缺点**:
- 缓存与 DB 有短暂不一致窗口(删除缓存后、回源前)
- 需要处理缓存穿透、雪崩

# 2.3 解决方案 B:DB 乐观锁扣减

**适用场景**:库存精度要求高、并发量中等(< 1 万 QPS)

```java
// 库存表加 version 字段
ALTER TABLE stock ADD COLUMN version INT NOT NULL DEFAULT 0;

// 扣减时乐观锁
@Transactional
public void deductStockOptimistic(Long skuId, int quantity, int maxRetry) {
for (int i = 0; i < maxRetry; i++) {
Stock stock = stockDao.select(skuId);
if (stock.getStock() < quantity) {
throw new InsufficientStockException("库存不足");
}

int affected = stockDao.updateWithVersion(
skuId, quantity, stock.getVersion()
);

if (affected > 0) {
return; // 扣减成功
}
// 版本冲突,重试
}
throw new OptimisticLockException("并发冲突,重试失败");
}
```

**优点**:
- DB 强一致,无缓存不一致问题
- 实现简单,无需维护缓存同步

**缺点**:
- 并发高时重试频繁,性能下降
- 适合中等并发场景(< 1 万 QPS)

# 2.4 方案对比

| 对比项 | 方案 A(Redis 预扣减) | 方案 B(DB 乐观锁) |
|--------|----------------------|-------------------|
| 并发能力 | 10 万+ QPS | < 1 万 QPS |
| 一致性 | 最终一致(短暂窗口) | 强一致 |
| 超卖概率 | 极低(< 0.01%) | 无(乐观锁保证) |
| 实现复杂度 | 高(需处理缓存同步) | 低 |
| 适用场景 | 秒杀、大促 | 常规销售 |

---

子问题 3:异常场景兜底方案

# 3.1 对账 + 自动修复

```java
@Scheduled(cron = "0 0 * * * ?")
public void reconcile() {
// 1. 扫描订单表 vs 库存表 vs 消息表
List<ReconciliationDiff> diffs = reconciliationDao.selectDiffs();

for (ReconciliationDiff diff : diffs) {
if (canAutoFix(diff)) {
autoFix(diff); // 自动修复
} else if (needAlert(diff)) {
sendAlert(diff); // 告警
} else {
createManualTicket(diff); // 人工介入
}
}
}

private boolean canAutoFix(ReconciliationDiff diff) {
switch (diff.getDiffType()) {
case ORDER_PAID_STOCK_NOT_DEDUCTED:
case ORDER_CANCELLED_STOCK_NOT_ROLLBACK:
case MESSAGE_SENT_ORDER_NOT_UPDATED:
return true; // 可自动修复
default:
return false;
}
}

@Transactional
public void autoFix(ReconciliationDiff diff) {
switch (diff.getDiffType()) {
case ORDER_PAID_STOCK_NOT_DEDUCTED:
stockService.deductStock(diff.getSkuId(), diff.getQuantity());
break;
case ORDER_CANCELLED_STOCK_NOT_ROLLBACK:
stockService.rollbackStock(diff.getSkuId(), diff.getQuantity());
break;
case MESSAGE_SENT_ORDER_NOT_UPDATED:
orderService.updateStatus(diff.getOrderId(), diff.getExpectedStatus());
break;
}

// 记录修复日志
reconciliationFixLogDao.insert(FixLog.builder()
.diffId(diff.getId())
.fixType(diff.getDiffType())
.fixTime(LocalDateTime.now())
.fixBy("system")
.build());
}
```

# 3.2 异常场景清单

| 异常场景 | 影响 | 兜底方案 |
|---------|------|---------|
| **消息发送失败** | 订单已落,库存未扣 | 本地消息表重试 + 对账修复 |
| **消息消费失败** | 库存已扣,订单未更新 | 幂等重试 + 对账修复 |
| **DB 写入失败** | 订单未落 | 事务回滚 + 本地消息表不写 |
| **Redis 宕机** | 库存无法扣减 | 降级到 DB 直接扣减(限流保护) |
| **MQ 宕机** | 消息无法发送 | 本地消息表堆积 + 延迟重试 |
| **库存超卖** | 卖多了 | 对账修复 + 人工介入 |
| **支付超时** | 库存未回补 | 定时任务扫描 + 自动回补 |

# 3.3 限流 + 熔断

```java
@SentinelResource(
value = "createOrder",
fallback = "createOrderFallback",
circuitBreaker = @SentinelCircuitBreaker(
slowRatioThreshold = 0.5,
slowRatioMaxRt = 500,
minRequestAmount = 10,
timeWindow = 10
)
)
public OrderResult createOrder(OrderRequest req) {
return orderService.createOrder(req);
}

public OrderResult createOrderFallback(OrderRequest req, Throwable ex) {
log.warn("Order creation fallback", ex);
// 降级:返回友好提示
return OrderResult.fail("系统繁忙,请稍后再试");
}
```

# 3.4 告警 + 人工介入

```java
private void sendAlert(ReconciliationDiff diff) {
// 1. 钉钉/企微告警
AlertMsg msg = AlertMsg.builder()
.level("WARN")
.title("订单对账差异")
.content(String.format("订单 %s 库存差异 %d,需人工确认",
diff.getOrderId(), diff.getQuantity()))
.diffId(diff.getId())
.build();
alertService.send(msg);
}

private void createManualTicket(ReconciliationDiff diff) {
// 1. 创建工单
Ticket ticket = Ticket.builder()
.title("订单对账差异需人工处理")
.content(diff.getDetail())
.priority(diff.getQuantity() > 10 ? "HIGH" : "MEDIUM")
.assignee("ops_team")
.build();
ticketService.create(ticket);

// 2. 标记差异状态
diff.setStatus(DiffStatus.PENDING_MANUAL);
reconciliationDao.update(diff);
}
```

---

#2 线程池优化 进阶 practice

题目

线程池优化

我的回答

暂无回答记录

点评

暂无点评

完整讲解

子问题 1:最终一致性方案设计

# 1.1 方案选型

| 方案 | 一致性 | 性能 | 复杂度 | 适用场景 |
|------|--------|------|--------|---------|
| **2PC(两阶段提交)** | 强一致 | 低 | 高 | 银行转账、跨 DB 事务 |
| **TCC(Try-Confirm-Cancel)** | 强一致 | 中 | 高 | 支付、库存扣减 |
| **本地消息表 + MQ** | 最终一致 | 高 | 中 | 订单、通知、日志 |
| **Saga** | 最终一致 | 高 | 中 | 长流程事务 |

**推荐方案**:本地消息表 + 可靠消息(最终一致性)

**理由**:
- 性能高(异步处理)
- 复杂度高(不需要改造业务代码)
- 适合订单场景(允许 30s 不一致)

# 1.2 核心流程

```
1. 用户下单 → 订单服务
2. 订单服务开启本地事务:
a. 落订单(状态=待支付)到 MySQL
b. 落本地消息表(状态=待发送)
c. 提交本地事务
3. 定时任务扫描本地消息表 → 发送 MQ 消息
4. 库存服务消费 MQ:扣减库存(幂等)
5. 订单服务消费 MQ:更新订单状态(幂等)
6. 消息消费者确认消费成功 → 删除本地消息表记录
```

**时序图**:

```
用户 → 订单服务 → [MySQL 订单表 + 本地消息表](本地事务)
↓
定时任务扫描 → 发送 MQ
↓
库存服务 ← MQ 消息 → 订单服务
(扣库存) (更新状态)
↓
确认消费 → 删除消息表记录
```

# 1.3 本地消息表实现

```sql
-- 本地消息表
CREATE TABLE local_message (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
biz_id VARCHAR(64) NOT NULL, -- 业务唯一 ID(订单号)
topic VARCHAR(64) NOT NULL, -- MQ topic
message_body TEXT NOT NULL, -- 消息内容
status TINYINT NOT NULL DEFAULT 0, -- 0=待发送, 1=已发送, 2=已消费
retry_count INT NOT NULL DEFAULT 0, -- 重试次数
next_retry_time DATETIME, -- 下次重试时间
create_time DATETIME NOT NULL,
update_time DATETIME NOT NULL,
INDEX idx_status_retry (status, next_retry_time)
);
```

```java
@Transactional
public void createOrder(OrderRequest req) {
// 1. 落订单
orderDao.insert(buildOrder(req, OrderStatus.PENDING_PAYMENT));

// 2. 落本地消息表(同一个事务!)
localMessageDao.insert(LocalMessage.builder()
.bizId(req.getOrderId())
.topic("order_created")
.messageBody(JSON.toJSONString(req))
.status(0)
.nextRetryTime(LocalDateTime.now())
.build());

// 事务提交后,由定时任务异步发送 MQ
}
```

```java
// 定时任务:扫描待发送消息
@Scheduled(fixedDelay = 1000)
public void scanAndSendMessages() {
List<LocalMessage> messages = localMessageDao.selectPending(100);
for (LocalMessage msg : messages) {
try {
rocketMqProducer.send(msg.getTopic(), msg.getMessageBody());
localMessageDao.updateStatus(msg.getId(), 1); // 标记已发送
} catch (Exception e) {
log.error("Send message failed", e);
localMessageDao.incrementRetry(msg.getId()); // 增加重试次数
}
}
}
```

# 1.4 幂等设计

| 环节 | 幂等键 | 实现方式 |
|------|--------|---------|
| 订单创建 | `order_id` | 唯一索引,重复插入报错 |
| 库存扣减 | `order_id + sku_id` | 唯一索引 + 乐观锁 |
| 消息消费 | `msg_id`(MQ 消息 ID) | 去重表,消费前检查 |

```java
// 库存服务消费 MQ 时的幂等处理
@Transactional
public void deductStock(StockMessage msg) {
// 1. 检查是否已处理
if (deductionRecordDao.exists(msg.getOrderId(), msg.getSkuId())) {
log.info("Already processed, skip: {}", msg.getOrderId());
return; // 幂等:重复消息直接跳过
}

// 2. 扣减库存(乐观锁)
int affected = stockDao.deductStockWithVersion(
msg.getSkuId(), msg.getQuantity(), msg.getVersion()
);

if (affected == 0) {
// 版本冲突,重试或失败
throw new OptimisticLockException("并发冲突");
}

// 3. 记录扣减记录(幂等依据)
deductionRecordDao.insert(DeductionRecord.builder()
.orderId(msg.getOrderId())
.skuId(msg.getSkuId())
.quantity(msg.getQuantity())
.build());
}
```

```java
// 消息消费幂等(基于 MQ 消息 ID)
@RocketMQMessageListener(topic = "order_created", consumerGroup = "order_group")
public class OrderMessageConsumer implements RocketMQListener<Message> {

@Override
public void onMessage(Message message) {
String msgId = message.getMsgId();

// 1. 检查是否已消费
if (messageConsumeRecordDao.exists(msgId)) {
log.info("Message already consumed, skip: {}", msgId);
return;
}

// 2. 处理消息
OrderMessage msg = JSON.parseObject(message.getBody(), OrderMessage.class);
processOrder(msg);

// 3. 记录消费记录
messageConsumeRecordDao.insert(msgId);
}
}
```

---

子问题 2:超卖 0.3% 根因与解法

# 2.1 根因分析

| 根因 | 概率 | 说明 |
|------|------|------|
| **缓存与 DB 不一致** | 高 | 缓存有库存,DB 实际已卖完,读到脏数据 |
| **并发扣减无锁** | 中 | 两个请求同时读到库存=1,都扣成功 |
| **扣减非原子操作** | 中 | `SELECT` + `UPDATE` 非原子,中间有间隙 |
| **缓存过期时间不同步** | 低 | 缓存延迟导致短时间脏读 |

**典型超卖场景**:

```
T1: 请求 A 读缓存 → 库存=1
T2: 请求 B 读缓存 → 库存=1(缓存未更新)
T3: 请求 A 扣减 DB → 成功(库存=0)
T4: 请求 B 扣减 DB → 成功(库存=-1)← 超卖!
```

# 2.2 解决方案 A:Redis 预扣减 + DB 确认

**适用场景**:高并发、库存精度要求不高(允许少量误差)

```java
// 下单时:先扣 Redis 库存
public boolean preDeductStock(Long skuId, int quantity) {
String key = "stock:" + skuId;

// Lua 脚本保证原子性
String luaScript = """
local stock = tonumber(redis.call('GET', KEYS[1]))
if stock == nil or stock < tonumber(ARGV[1]) then
return -1
end
redis.call('DECRBY', KEYS[1], ARGV[1])
return stock - tonumber(ARGV[1])
""";

Long result = redisTemplate.execute(
new DefaultRedisScript<>(luaScript, Long.class),
Collections.singletonList(key),
String.valueOf(quantity)
);

return result != null && result >= 0;
}

// 支付成功后:DB 最终确认
@Transactional
public void confirmStock(Long skuId, int quantity) {
int affected = stockDao.deductStockWithVersion(skuId, quantity);
if (affected == 0) {
// DB 扣减失败,回补 Redis
redisTemplate.opsForValue().increment("stock:" + skuId, quantity);
throw new InsufficientStockException("库存不足");
}
}

// 支付超时:回补 Redis
@Scheduled(fixedDelay = 60000)
public void rollbackTimeoutOrders() {
List<Order> timeoutOrders = orderDao.selectTimeout(30, TimeUnit.MINUTES);
for (Order order : timeoutOrders) {
redisTemplate.opsForValue().increment(
"stock:" + order.getSkuId(), order.getQuantity()
);
orderDao.updateStatus(order.getId(), OrderStatus.CANCELLED);
}
}
```

**缓存与 DB 同步策略(Cache Aside)**:

```java
@Transactional
public void deductStockWithVersion(Long skuId, int quantity) {
// 1. 更新 DB
int affected = stockDao.deductStockWithVersion(skuId, quantity);

// 2. 删除缓存(不是更新!)
redisTemplate.delete("stock:" + skuId);

return affected;
}

// 读缓存 miss 时回源
public long getStock(Long skuId) {
String key = "stock:" + skuId;
Long stock = redisTemplate.opsForValue().get(key);
if (stock == null) {
stock = stockDao.getStock(skuId);
// 随机过期时间(防雪崩)
long expire = 3600 + ThreadLocalRandom.current().nextLong(600);
redisTemplate.opsForValue().set(key, stock, expire, TimeUnit.SECONDS);
}
return stock;
}
```

**优点**:
- Redis 原子扣减,高并发下性能极佳(10 万+ QPS)
- DB 最终确认,保证数据一致性

**缺点**:
- 缓存与 DB 有短暂不一致窗口(删除缓存后、回源前)
- 需要处理缓存穿透、雪崩

# 2.3 解决方案 B:DB 乐观锁扣减

**适用场景**:库存精度要求高、并发量中等(< 1 万 QPS)

```java
// 库存表加 version 字段
ALTER TABLE stock ADD COLUMN version INT NOT NULL DEFAULT 0;

// 扣减时乐观锁
@Transactional
public void deductStockOptimistic(Long skuId, int quantity, int maxRetry) {
for (int i = 0; i < maxRetry; i++) {
Stock stock = stockDao.select(skuId);
if (stock.getStock() < quantity) {
throw new InsufficientStockException("库存不足");
}

int affected = stockDao.updateWithVersion(
skuId, quantity, stock.getVersion()
);

if (affected > 0) {
return; // 扣减成功
}
// 版本冲突,重试
}
throw new OptimisticLockException("并发冲突,重试失败");
}
```

**优点**:
- DB 强一致,无缓存不一致问题
- 实现简单,无需维护缓存同步

**缺点**:
- 并发高时重试频繁,性能下降
- 适合中等并发场景(< 1 万 QPS)

# 2.4 方案对比

| 对比项 | 方案 A(Redis 预扣减) | 方案 B(DB 乐观锁) |
|--------|----------------------|-------------------|
| 并发能力 | 10 万+ QPS | < 1 万 QPS |
| 一致性 | 最终一致(短暂窗口) | 强一致 |
| 超卖概率 | 极低(< 0.01%) | 无(乐观锁保证) |
| 实现复杂度 | 高(需处理缓存同步) | 低 |
| 适用场景 | 秒杀、大促 | 常规销售 |

---

子问题 3:异常场景兜底方案

# 3.1 对账 + 自动修复

```java
@Scheduled(cron = "0 0 * * * ?")
public void reconcile() {
// 1. 扫描订单表 vs 库存表 vs 消息表
List<ReconciliationDiff> diffs = reconciliationDao.selectDiffs();

for (ReconciliationDiff diff : diffs) {
if (canAutoFix(diff)) {
autoFix(diff); // 自动修复
} else if (needAlert(diff)) {
sendAlert(diff); // 告警
} else {
createManualTicket(diff); // 人工介入
}
}
}

private boolean canAutoFix(ReconciliationDiff diff) {
switch (diff.getDiffType()) {
case ORDER_PAID_STOCK_NOT_DEDUCTED:
case ORDER_CANCELLED_STOCK_NOT_ROLLBACK:
case MESSAGE_SENT_ORDER_NOT_UPDATED:
return true; // 可自动修复
default:
return false;
}
}

@Transactional
public void autoFix(ReconciliationDiff diff) {
switch (diff.getDiffType()) {
case ORDER_PAID_STOCK_NOT_DEDUCTED:
stockService.deductStock(diff.getSkuId(), diff.getQuantity());
break;
case ORDER_CANCELLED_STOCK_NOT_ROLLBACK:
stockService.rollbackStock(diff.getSkuId(), diff.getQuantity());
break;
case MESSAGE_SENT_ORDER_NOT_UPDATED:
orderService.updateStatus(diff.getOrderId(), diff.getExpectedStatus());
break;
}

// 记录修复日志
reconciliationFixLogDao.insert(FixLog.builder()
.diffId(diff.getId())
.fixType(diff.getDiffType())
.fixTime(LocalDateTime.now())
.fixBy("system")
.build());
}
```

# 3.2 异常场景清单

| 异常场景 | 影响 | 兜底方案 |
|---------|------|---------|
| **消息发送失败** | 订单已落,库存未扣 | 本地消息表重试 + 对账修复 |
| **消息消费失败** | 库存已扣,订单未更新 | 幂等重试 + 对账修复 |
| **DB 写入失败** | 订单未落 | 事务回滚 + 本地消息表不写 |
| **Redis 宕机** | 库存无法扣减 | 降级到 DB 直接扣减(限流保护) |
| **MQ 宕机** | 消息无法发送 | 本地消息表堆积 + 延迟重试 |
| **库存超卖** | 卖多了 | 对账修复 + 人工介入 |
| **支付超时** | 库存未回补 | 定时任务扫描 + 自动回补 |

# 3.3 限流 + 熔断

```java
@SentinelResource(
value = "createOrder",
fallback = "createOrderFallback",
circuitBreaker = @SentinelCircuitBreaker(
slowRatioThreshold = 0.5,
slowRatioMaxRt = 500,
minRequestAmount = 10,
timeWindow = 10
)
)
public OrderResult createOrder(OrderRequest req) {
return orderService.createOrder(req);
}

public OrderResult createOrderFallback(OrderRequest req, Throwable ex) {
log.warn("Order creation fallback", ex);
// 降级:返回友好提示
return OrderResult.fail("系统繁忙,请稍后再试");
}
```

# 3.4 告警 + 人工介入

```java
private void sendAlert(ReconciliationDiff diff) {
// 1. 钉钉/企微告警
AlertMsg msg = AlertMsg.builder()
.level("WARN")
.title("订单对账差异")
.content(String.format("订单 %s 库存差异 %d,需人工确认",
diff.getOrderId(), diff.getQuantity()))
.diffId(diff.getId())
.build();
alertService.send(msg);
}

private void createManualTicket(ReconciliationDiff diff) {
// 1. 创建工单
Ticket ticket = Ticket.builder()
.title("订单对账差异需人工处理")
.content(diff.getDetail())
.priority(diff.getQuantity() > 10 ? "HIGH" : "MEDIUM")
.assignee("ops_team")
.build();
ticketService.create(ticket);

// 2. 标记差异状态
diff.setStatus(DiffStatus.PENDING_MANUAL);
reconciliationDao.update(diff);
}
```

---

#3 架构设计 高阶 practice

题目

架构设计

我的回答

暂无回答记录

点评

暂无点评

完整讲解

子问题 1:最终一致性方案设计

# 1.1 方案选型

| 方案 | 一致性 | 性能 | 复杂度 | 适用场景 |
|------|--------|------|--------|---------|
| **2PC(两阶段提交)** | 强一致 | 低 | 高 | 银行转账、跨 DB 事务 |
| **TCC(Try-Confirm-Cancel)** | 强一致 | 中 | 高 | 支付、库存扣减 |
| **本地消息表 + MQ** | 最终一致 | 高 | 中 | 订单、通知、日志 |
| **Saga** | 最终一致 | 高 | 中 | 长流程事务 |

**推荐方案**:本地消息表 + 可靠消息(最终一致性)

**理由**:
- 性能高(异步处理)
- 复杂度高(不需要改造业务代码)
- 适合订单场景(允许 30s 不一致)

# 1.2 核心流程

```
1. 用户下单 → 订单服务
2. 订单服务开启本地事务:
a. 落订单(状态=待支付)到 MySQL
b. 落本地消息表(状态=待发送)
c. 提交本地事务
3. 定时任务扫描本地消息表 → 发送 MQ 消息
4. 库存服务消费 MQ:扣减库存(幂等)
5. 订单服务消费 MQ:更新订单状态(幂等)
6. 消息消费者确认消费成功 → 删除本地消息表记录
```

**时序图**:

```
用户 → 订单服务 → [MySQL 订单表 + 本地消息表](本地事务)
↓
定时任务扫描 → 发送 MQ
↓
库存服务 ← MQ 消息 → 订单服务
(扣库存) (更新状态)
↓
确认消费 → 删除消息表记录
```

# 1.3 本地消息表实现

```sql
-- 本地消息表
CREATE TABLE local_message (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
biz_id VARCHAR(64) NOT NULL, -- 业务唯一 ID(订单号)
topic VARCHAR(64) NOT NULL, -- MQ topic
message_body TEXT NOT NULL, -- 消息内容
status TINYINT NOT NULL DEFAULT 0, -- 0=待发送, 1=已发送, 2=已消费
retry_count INT NOT NULL DEFAULT 0, -- 重试次数
next_retry_time DATETIME, -- 下次重试时间
create_time DATETIME NOT NULL,
update_time DATETIME NOT NULL,
INDEX idx_status_retry (status, next_retry_time)
);
```

```java
@Transactional
public void createOrder(OrderRequest req) {
// 1. 落订单
orderDao.insert(buildOrder(req, OrderStatus.PENDING_PAYMENT));

// 2. 落本地消息表(同一个事务!)
localMessageDao.insert(LocalMessage.builder()
.bizId(req.getOrderId())
.topic("order_created")
.messageBody(JSON.toJSONString(req))
.status(0)
.nextRetryTime(LocalDateTime.now())
.build());

// 事务提交后,由定时任务异步发送 MQ
}
```

```java
// 定时任务:扫描待发送消息
@Scheduled(fixedDelay = 1000)
public void scanAndSendMessages() {
List<LocalMessage> messages = localMessageDao.selectPending(100);
for (LocalMessage msg : messages) {
try {
rocketMqProducer.send(msg.getTopic(), msg.getMessageBody());
localMessageDao.updateStatus(msg.getId(), 1); // 标记已发送
} catch (Exception e) {
log.error("Send message failed", e);
localMessageDao.incrementRetry(msg.getId()); // 增加重试次数
}
}
}
```

# 1.4 幂等设计

| 环节 | 幂等键 | 实现方式 |
|------|--------|---------|
| 订单创建 | `order_id` | 唯一索引,重复插入报错 |
| 库存扣减 | `order_id + sku_id` | 唯一索引 + 乐观锁 |
| 消息消费 | `msg_id`(MQ 消息 ID) | 去重表,消费前检查 |

```java
// 库存服务消费 MQ 时的幂等处理
@Transactional
public void deductStock(StockMessage msg) {
// 1. 检查是否已处理
if (deductionRecordDao.exists(msg.getOrderId(), msg.getSkuId())) {
log.info("Already processed, skip: {}", msg.getOrderId());
return; // 幂等:重复消息直接跳过
}

// 2. 扣减库存(乐观锁)
int affected = stockDao.deductStockWithVersion(
msg.getSkuId(), msg.getQuantity(), msg.getVersion()
);

if (affected == 0) {
// 版本冲突,重试或失败
throw new OptimisticLockException("并发冲突");
}

// 3. 记录扣减记录(幂等依据)
deductionRecordDao.insert(DeductionRecord.builder()
.orderId(msg.getOrderId())
.skuId(msg.getSkuId())
.quantity(msg.getQuantity())
.build());
}
```

```java
// 消息消费幂等(基于 MQ 消息 ID)
@RocketMQMessageListener(topic = "order_created", consumerGroup = "order_group")
public class OrderMessageConsumer implements RocketMQListener<Message> {

@Override
public void onMessage(Message message) {
String msgId = message.getMsgId();

// 1. 检查是否已消费
if (messageConsumeRecordDao.exists(msgId)) {
log.info("Message already consumed, skip: {}", msgId);
return;
}

// 2. 处理消息
OrderMessage msg = JSON.parseObject(message.getBody(), OrderMessage.class);
processOrder(msg);

// 3. 记录消费记录
messageConsumeRecordDao.insert(msgId);
}
}
```

---

子问题 2:超卖 0.3% 根因与解法

# 2.1 根因分析

| 根因 | 概率 | 说明 |
|------|------|------|
| **缓存与 DB 不一致** | 高 | 缓存有库存,DB 实际已卖完,读到脏数据 |
| **并发扣减无锁** | 中 | 两个请求同时读到库存=1,都扣成功 |
| **扣减非原子操作** | 中 | `SELECT` + `UPDATE` 非原子,中间有间隙 |
| **缓存过期时间不同步** | 低 | 缓存延迟导致短时间脏读 |

**典型超卖场景**:

```
T1: 请求 A 读缓存 → 库存=1
T2: 请求 B 读缓存 → 库存=1(缓存未更新)
T3: 请求 A 扣减 DB → 成功(库存=0)
T4: 请求 B 扣减 DB → 成功(库存=-1)← 超卖!
```

# 2.2 解决方案 A:Redis 预扣减 + DB 确认

**适用场景**:高并发、库存精度要求不高(允许少量误差)

```java
// 下单时:先扣 Redis 库存
public boolean preDeductStock(Long skuId, int quantity) {
String key = "stock:" + skuId;

// Lua 脚本保证原子性
String luaScript = """
local stock = tonumber(redis.call('GET', KEYS[1]))
if stock == nil or stock < tonumber(ARGV[1]) then
return -1
end
redis.call('DECRBY', KEYS[1], ARGV[1])
return stock - tonumber(ARGV[1])
""";

Long result = redisTemplate.execute(
new DefaultRedisScript<>(luaScript, Long.class),
Collections.singletonList(key),
String.valueOf(quantity)
);

return result != null && result >= 0;
}

// 支付成功后:DB 最终确认
@Transactional
public void confirmStock(Long skuId, int quantity) {
int affected = stockDao.deductStockWithVersion(skuId, quantity);
if (affected == 0) {
// DB 扣减失败,回补 Redis
redisTemplate.opsForValue().increment("stock:" + skuId, quantity);
throw new InsufficientStockException("库存不足");
}
}

// 支付超时:回补 Redis
@Scheduled(fixedDelay = 60000)
public void rollbackTimeoutOrders() {
List<Order> timeoutOrders = orderDao.selectTimeout(30, TimeUnit.MINUTES);
for (Order order : timeoutOrders) {
redisTemplate.opsForValue().increment(
"stock:" + order.getSkuId(), order.getQuantity()
);
orderDao.updateStatus(order.getId(), OrderStatus.CANCELLED);
}
}
```

**缓存与 DB 同步策略(Cache Aside)**:

```java
@Transactional
public void deductStockWithVersion(Long skuId, int quantity) {
// 1. 更新 DB
int affected = stockDao.deductStockWithVersion(skuId, quantity);

// 2. 删除缓存(不是更新!)
redisTemplate.delete("stock:" + skuId);

return affected;
}

// 读缓存 miss 时回源
public long getStock(Long skuId) {
String key = "stock:" + skuId;
Long stock = redisTemplate.opsForValue().get(key);
if (stock == null) {
stock = stockDao.getStock(skuId);
// 随机过期时间(防雪崩)
long expire = 3600 + ThreadLocalRandom.current().nextLong(600);
redisTemplate.opsForValue().set(key, stock, expire, TimeUnit.SECONDS);
}
return stock;
}
```

**优点**:
- Redis 原子扣减,高并发下性能极佳(10 万+ QPS)
- DB 最终确认,保证数据一致性

**缺点**:
- 缓存与 DB 有短暂不一致窗口(删除缓存后、回源前)
- 需要处理缓存穿透、雪崩

# 2.3 解决方案 B:DB 乐观锁扣减

**适用场景**:库存精度要求高、并发量中等(< 1 万 QPS)

```java
// 库存表加 version 字段
ALTER TABLE stock ADD COLUMN version INT NOT NULL DEFAULT 0;

// 扣减时乐观锁
@Transactional
public void deductStockOptimistic(Long skuId, int quantity, int maxRetry) {
for (int i = 0; i < maxRetry; i++) {
Stock stock = stockDao.select(skuId);
if (stock.getStock() < quantity) {
throw new InsufficientStockException("库存不足");
}

int affected = stockDao.updateWithVersion(
skuId, quantity, stock.getVersion()
);

if (affected > 0) {
return; // 扣减成功
}
// 版本冲突,重试
}
throw new OptimisticLockException("并发冲突,重试失败");
}
```

**优点**:
- DB 强一致,无缓存不一致问题
- 实现简单,无需维护缓存同步

**缺点**:
- 并发高时重试频繁,性能下降
- 适合中等并发场景(< 1 万 QPS)

# 2.4 方案对比

| 对比项 | 方案 A(Redis 预扣减) | 方案 B(DB 乐观锁) |
|--------|----------------------|-------------------|
| 并发能力 | 10 万+ QPS | < 1 万 QPS |
| 一致性 | 最终一致(短暂窗口) | 强一致 |
| 超卖概率 | 极低(< 0.01%) | 无(乐观锁保证) |
| 实现复杂度 | 高(需处理缓存同步) | 低 |
| 适用场景 | 秒杀、大促 | 常规销售 |

---

子问题 3:异常场景兜底方案

# 3.1 对账 + 自动修复

```java
@Scheduled(cron = "0 0 * * * ?")
public void reconcile() {
// 1. 扫描订单表 vs 库存表 vs 消息表
List<ReconciliationDiff> diffs = reconciliationDao.selectDiffs();

for (ReconciliationDiff diff : diffs) {
if (canAutoFix(diff)) {
autoFix(diff); // 自动修复
} else if (needAlert(diff)) {
sendAlert(diff); // 告警
} else {
createManualTicket(diff); // 人工介入
}
}
}

private boolean canAutoFix(ReconciliationDiff diff) {
switch (diff.getDiffType()) {
case ORDER_PAID_STOCK_NOT_DEDUCTED:
case ORDER_CANCELLED_STOCK_NOT_ROLLBACK:
case MESSAGE_SENT_ORDER_NOT_UPDATED:
return true; // 可自动修复
default:
return false;
}
}

@Transactional
public void autoFix(ReconciliationDiff diff) {
switch (diff.getDiffType()) {
case ORDER_PAID_STOCK_NOT_DEDUCTED:
stockService.deductStock(diff.getSkuId(), diff.getQuantity());
break;
case ORDER_CANCELLED_STOCK_NOT_ROLLBACK:
stockService.rollbackStock(diff.getSkuId(), diff.getQuantity());
break;
case MESSAGE_SENT_ORDER_NOT_UPDATED:
orderService.updateStatus(diff.getOrderId(), diff.getExpectedStatus());
break;
}

// 记录修复日志
reconciliationFixLogDao.insert(FixLog.builder()
.diffId(diff.getId())
.fixType(diff.getDiffType())
.fixTime(LocalDateTime.now())
.fixBy("system")
.build());
}
```

# 3.2 异常场景清单

| 异常场景 | 影响 | 兜底方案 |
|---------|------|---------|
| **消息发送失败** | 订单已落,库存未扣 | 本地消息表重试 + 对账修复 |
| **消息消费失败** | 库存已扣,订单未更新 | 幂等重试 + 对账修复 |
| **DB 写入失败** | 订单未落 | 事务回滚 + 本地消息表不写 |
| **Redis 宕机** | 库存无法扣减 | 降级到 DB 直接扣减(限流保护) |
| **MQ 宕机** | 消息无法发送 | 本地消息表堆积 + 延迟重试 |
| **库存超卖** | 卖多了 | 对账修复 + 人工介入 |
| **支付超时** | 库存未回补 | 定时任务扫描 + 自动回补 |

# 3.3 限流 + 熔断

```java
@SentinelResource(
value = "createOrder",
fallback = "createOrderFallback",
circuitBreaker = @SentinelCircuitBreaker(
slowRatioThreshold = 0.5,
slowRatioMaxRt = 500,
minRequestAmount = 10,
timeWindow = 10
)
)
public OrderResult createOrder(OrderRequest req) {
return orderService.createOrder(req);
}

public OrderResult createOrderFallback(OrderRequest req, Throwable ex) {
log.warn("Order creation fallback", ex);
// 降级:返回友好提示
return OrderResult.fail("系统繁忙,请稍后再试");
}
```

# 3.4 告警 + 人工介入

```java
private void sendAlert(ReconciliationDiff diff) {
// 1. 钉钉/企微告警
AlertMsg msg = AlertMsg.builder()
.level("WARN")
.title("订单对账差异")
.content(String.format("订单 %s 库存差异 %d,需人工确认",
diff.getOrderId(), diff.getQuantity()))
.diffId(diff.getId())
.build();
alertService.send(msg);
}

private void createManualTicket(ReconciliationDiff diff) {
// 1. 创建工单
Ticket ticket = Ticket.builder()
.title("订单对账差异需人工处理")
.content(diff.getDetail())
.priority(diff.getQuantity() > 10 ? "HIGH" : "MEDIUM")
.assignee("ops_team")
.build();
ticketService.create(ticket);

// 2. 标记差异状态
diff.setStatus(DiffStatus.PENDING_MANUAL);
reconciliationDao.update(diff);
}
```

---

📚 查看全部面试记录