文章目录
-
- [1. CAP 定理和 BASE 理论是什么?](#1. CAP 定理和 BASE 理论是什么?)
- [2. 分布式事务有哪些实现方案?](#2. 分布式事务有哪些实现方案?)
-
- [1. 2PC(两阶段提交)](#1. 2PC(两阶段提交))
- [2. 3PC(三阶段提交)](#2. 3PC(三阶段提交))
- [3. TCC 模式(Try-Confirm-Cancel)](#3. TCC 模式(Try-Confirm-Cancel))
- [4. Saga 模式](#4. Saga 模式)
- [5. 本地消息表](#5. 本地消息表)
-
- [1. 本地事务执行(原子性保证)](#1. 本地事务执行(原子性保证))
- [2. 消息发送(定时任务)](#2. 消息发送(定时任务))
- [3. 消费者处理(最终一致性)](#3. 消费者处理(最终一致性))
- [4. 补偿机制(处理失败场景)](#4. 补偿机制(处理失败场景))
- [6. Seata 框架](#6. Seata 框架)
- [3. Seata 的 AT 模式原理是什么?](#3. Seata 的 AT 模式原理是什么?)
- [4. TCC 模式的原理和适用场景?](#4. TCC 模式的原理和适用场景?)
- [4.1. TCC 模式下的防悬挂、幂等、空回滚有几种实现方式?](#4.1. TCC 模式下的防悬挂、幂等、空回滚有几种实现方式?)
-
- [1. 幂等性实现方式](#1. 幂等性实现方式)
- [2. 空回滚实现方式](#2. 空回滚实现方式)
- [3. 防悬挂实现方式](#3. 防悬挂实现方式)
- [Seata TCC 模式的实现方式](#Seata TCC 模式的实现方式)
-
- [1. 幂等性实现](#1. 幂等性实现)
- [2. 空回滚实现](#2. 空回滚实现)
- [3. 防悬挂实现](#3. 防悬挂实现)
- [Seata TCC 的实现机制](#Seata TCC 的实现机制)
- [Seata TCC 的优势](#Seata TCC 的优势)
- [Seata TCC vs 手动实现](#Seata TCC vs 手动实现)
- [5. 分布式锁有几种实现方式?](#5. 分布式锁有几种实现方式?)
-
- [1. 数据库实现](#1. 数据库实现)
- [2. Redis SETNX](#2. Redis SETNX)
- [3. Redisson](#3. Redisson)
- [4. ZooKeeper](#4. ZooKeeper)
- [5. etcd](#5. etcd)
- [6. Redis 分布式锁怎么实现?](#6. Redis 分布式锁怎么实现?)
- [7. RedLock 的原理和争议?](#7. RedLock 的原理和争议?)
- [8. ZooKeeper 分布式锁的实现原理?](#8. ZooKeeper 分布式锁的实现原理?)
- [9. 缓存和数据库一致性怎么保证?](#9. 缓存和数据库一致性怎么保证?)
- [10. 缓存穿透、击穿、雪崩是什么?怎么解决?](#10. 缓存穿透、击穿、雪崩是什么?怎么解决?)
- [11. 分布式 ID 怎么生成?](#11. 分布式 ID 怎么生成?)
- [12. 雪花算法是怎么实现的?](#12. 雪花算法是怎么实现的?)
- [13. 雪花算法的时钟回拨问题怎么解决?](#13. 雪花算法的时钟回拨问题怎么解决?)
- [14. 消息队列如何保证消息不丢失?](#14. 消息队列如何保证消息不丢失?)
- [15. 消息队列如何保证消息不重复消费?](#15. 消息队列如何保证消息不重复消费?)
- [16. 消息队列如何保证消息顺序性?](#16. 消息队列如何保证消息顺序性?)
- [17. 微服务之间如何保证事务?](#17. 微服务之间如何保证事务?)
- [18. 什么是脑裂问题?怎么解决?](#18. 什么是脑裂问题?怎么解决?)
- [19. 服务注册发现的原理?Nacos 和 Eureka 的区别?](#19. 服务注册发现的原理?Nacos 和 Eureka 的区别?)
- [20. 熔断和限流的区别?Sentinel 怎么实现?](#20. 熔断和限流的区别?Sentinel 怎么实现?)
- [21. 分库分表怎么做?有什么问题?](#21. 分库分表怎么做?有什么问题?)
- [22. 如何设计一个高可用系统?](#22. 如何设计一个高可用系统?)
- [23. A 操作和 B 操作处于一个事务,C 操作处于另一个事务,如何保证 C 在 AB 事务提交后才执行?如果 A 和 C 可以同时执行,但 C 必须在 AB 都提交后再提交,应该怎么设计?](#23. A 操作和 B 操作处于一个事务,C 操作处于另一个事务,如何保证 C 在 AB 事务提交后才执行?如果 A 和 C 可以同时执行,但 C 必须在 AB 都提交后再提交,应该怎么设计?)
-
- [场景一:AB 事务提交后 C 才开始执行](#场景一:AB 事务提交后 C 才开始执行)
- [场景二:A 和 C 可以同时执行,但 C 必须在 AB 都提交后再提交](#场景二:A 和 C 可以同时执行,但 C 必须在 AB 都提交后再提交)
- [方案二:CountDownLatch + 编程式事务](#方案二:CountDownLatch + 编程式事务)
- [方案三:CompletableFuture + 编程式事务](#方案三:CompletableFuture + 编程式事务)
- 方案二:本地消息表(适用于场景一)
- [方案三:消息队列 + 延迟检查](#方案三:消息队列 + 延迟检查)
- 方案四:2PC(两阶段提交)
- 方案对比
-
- [场景一:AB 事务提交后 C 才开始执行](#场景一:AB 事务提交后 C 才开始执行)
- [场景二:A 和 C 可以同时执行,但 C 必须在 AB 都提交后再提交](#场景二:A 和 C 可以同时执行,但 C 必须在 AB 都提交后再提交)
- 场景选择
1. CAP 定理和 BASE 理论是什么?
回答:CAP 定理指分布式系统中一致性(Consistency)、可用性(Availability)、分区容错性(Partition tolerance)三者最多只能同时满足两个,由于网络分区不可避免,实际上是在 C 和 A 之间权衡。BASE 理论是 CAP 的延伸,包括基本可用(Basically Available)、软状态(Soft State)、最终一致性(Eventually Consistent),是对 CAP 中 AP 方案的补充,通过牺牲强一致性换取可用性,允许数据在一段时间内不一致,但最终达到一致状态。
2. 分布式事务有哪些实现方案?
回答:分布式事务主要有以下方案:
1. 2PC(两阶段提交)
实施描述:
- 阶段一(Prepare):协调者向所有参与者发送 prepare 请求,参与者执行事务但不提交,返回 Yes/No
- 阶段二(Commit/Rollback):如果所有参与者都返回 Yes,协调者发送 commit;否则发送 rollback
代码示例:
java
// 协调者
public class TwoPhaseCommitCoordinator {
public boolean executeTransaction(List<Participant> participants) {
// 阶段一:准备阶段
List<Boolean> prepareResults = new ArrayList<>();
for (Participant p : participants) {
prepareResults.add(p.prepare()); // 执行但不提交
}
// 阶段二:提交或回滚
if (prepareResults.stream().allMatch(r -> r)) {
participants.forEach(Participant::commit);
return true;
} else {
participants.forEach(Participant::rollback);
return false;
}
}
}
优缺点:
- 优点:强一致性,实现简单
- 缺点:同步阻塞、单点故障、数据不一致风险(协调者宕机)
2. 3PC(三阶段提交)
实施描述:
- 阶段一(CanCommit):协调者询问参与者是否可以提交,参与者返回 Yes/No(不锁定资源)
- 阶段二(PreCommit):如果都返回 Yes,发送 preCommit,参与者执行事务但不提交
- 阶段三(DoCommit):发送 commit,参与者提交事务
改进点:
- 增加 CanCommit 阶段,提前发现不可提交的情况
- 引入超时机制,参与者超时自动提交(假设协调者正常)
优缺点:
- 优点:减少阻塞时间,降低数据不一致风险
- 缺点:实现复杂,仍存在数据不一致可能
3. TCC 模式(Try-Confirm-Cancel)
实施描述:
- Try:尝试执行,预留资源(如冻结库存、预扣余额)
- Confirm:确认执行,真正扣减资源(如扣减库存、扣减余额)
- Cancel:取消操作,释放预留资源(如解冻库存、退回余额)
代码示例:
java
// 库存服务
public class InventoryService {
@TCC
public boolean tryReserve(Long productId, Integer quantity) {
// Try:冻结库存
return inventoryMapper.freeze(productId, quantity) > 0;
}
public boolean confirmReserve(Long productId, Integer quantity) {
// Confirm:扣减库存
return inventoryMapper.deduct(productId, quantity) > 0;
}
public boolean cancelReserve(Long productId, Integer quantity) {
// Cancel:解冻库存
return inventoryMapper.unfreeze(productId, quantity) > 0;
}
}
注意事项:
- 幂等性:三个方法都要保证幂等
- 空回滚:Try 未执行时 Cancel 也要能执行
- 悬挂问题:Cancel 先于 Try 执行的情况
优缺点:
- 优点:性能好(无全局锁)、最终一致性强
- 缺点:业务侵入性强,需要实现三个接口
4. Saga 模式
实施描述:
- 将长事务拆分为多个本地事务(子事务)
- 每个子事务都有对应的补偿操作
- 如果某个子事务失败,执行前面所有子事务的补偿操作
两种实现方式:
- 编排式(Orchestration):中央协调器统一调度
- 协同式(Choreography):各服务通过事件通信
代码示例:
java
// 订单服务
public class OrderSaga {
public void createOrder(Order order) {
// 1. 创建订单(本地事务)
orderMapper.insert(order);
// 2. 扣减库存(调用库存服务)
inventoryService.deduct(order.getProductId(), order.getQuantity());
// 3. 扣减余额(调用账户服务)
accountService.deduct(order.getUserId(), order.getAmount());
}
// 补偿操作
public void compensateOrder(Order order) {
// 反向操作:取消订单、退回库存、退回余额
orderMapper.cancel(order.getId());
inventoryService.refund(order.getProductId(), order.getQuantity());
accountService.refund(order.getUserId(), order.getAmount());
}
}
优缺点:
- 优点:适合长事务、性能好
- 缺点:补偿逻辑复杂,可能出现补偿失败
5. 本地消息表
实施描述:
- 业务操作和消息写入同一本地事务
- 定时任务扫描消息表,发送未发送的消息
- 消息队列消费者处理消息,完成后回调确认
- 定时任务删除已确认的消息
事务执行逻辑:
1. 本地事务执行(原子性保证)
java
@Service
public class OrderService {
@Autowired
private OrderMapper orderMapper;
@Autowired
private LocalMessageMapper localMessageMapper;
/**
* 创建订单 - 本地事务
* 业务操作和消息写入在同一事务中,要么都成功,要么都失败
*/
@Transactional(rollbackFor = Exception.class)
public void createOrder(Order order) {
try {
// 1. 创建订单(业务操作)
orderMapper.insert(order);
// 2. 写入本地消息表(消息记录)
LocalMessage message = new LocalMessage();
message.setMessageId(UUID.randomUUID().toString());
message.setBusinessId(order.getId()); // 业务ID
message.setContent(JSON.toJSONString(order));
message.setStatus("PENDING"); // 待发送
message.setRetryCount(0);
message.setCreateTime(new Date());
localMessageMapper.insert(message);
// 如果这里抛出异常,整个事务会回滚
// 订单和消息都不会写入数据库
} catch (Exception e) {
// 事务自动回滚(@Transactional)
// 订单和消息都不会写入,保证一致性
log.error("创建订单失败,事务回滚", e);
throw e;
}
}
}
事务失败处理:
- 如果
orderMapper.insert()失败 → 整个事务回滚,消息不会写入 - 如果
localMessageMapper.insert()失败 → 整个事务回滚,订单不会写入 - 如果任何步骤抛异常 →
@Transactional自动回滚,数据库恢复到事务前状态
2. 消息发送(定时任务)
java
@Component
public class MessageSender {
@Autowired
private LocalMessageMapper localMessageMapper;
@Autowired
private RocketMQTemplate rocketMQTemplate;
/**
* 定时任务:扫描待发送消息并发送
*/
@Scheduled(fixedDelay = 5000) // 每5秒执行一次
public void sendPendingMessages() {
// 查询待发送的消息(状态为 PENDING)
List<LocalMessage> messages = localMessageMapper.selectByStatus("PENDING");
for (LocalMessage msg : messages) {
try {
// 发送到消息队列
SendResult result = rocketMQTemplate.syncSend(
"order-topic",
MessageBuilder.withPayload(msg.getContent()).build()
);
if (result.getSendStatus() == SendStatus.SEND_OK) {
// 发送成功,更新状态为已发送
msg.setStatus("SENT");
msg.setSendTime(new Date());
localMessageMapper.update(msg);
}
} catch (Exception e) {
// 发送失败,增加重试次数
msg.setRetryCount(msg.getRetryCount() + 1);
// 超过最大重试次数,标记为失败
if (msg.getRetryCount() >= 3) {
msg.setStatus("FAILED");
// 可以发送告警或记录日志
log.error("消息发送失败,超过最大重试次数: {}", msg.getMessageId());
}
localMessageMapper.update(msg);
}
}
}
}
3. 消费者处理(最终一致性)
java
@Component
@RocketMQMessageListener(
topic = "order-topic",
consumerGroup = "order-consumer-group"
)
public class OrderConsumer implements RocketMQListener<String> {
@Autowired
private InventoryService inventoryService;
@Autowired
private AccountService accountService;
@Autowired
private LocalMessageMapper localMessageMapper;
/**
* 消费消息:扣减库存和余额
*/
@Override
public void onMessage(String messageContent) {
try {
// 1. 解析消息
Order order = JSON.parseObject(messageContent, Order.class);
// 2. 幂等性检查:查询消息是否已处理
LocalMessage msg = localMessageMapper.selectByBusinessId(order.getId());
if (msg != null && "CONSUMED".equals(msg.getStatus())) {
log.info("消息已处理,跳过: {}", order.getId());
return; // 已处理,直接返回
}
// 3. 处理业务逻辑
inventoryService.deduct(order.getProductId(), order.getQuantity());
accountService.deduct(order.getUserId(), order.getAmount());
// 4. 更新消息状态为已消费
msg.setStatus("CONSUMED");
msg.setConsumeTime(new Date());
localMessageMapper.update(msg);
} catch (Exception e) {
log.error("消费消息失败", e);
// 消费失败,消息会重新投递(RocketMQ 自动重试)
// 或者可以记录到死信队列,人工处理
throw e; // 抛出异常,触发重试
}
}
}
4. 补偿机制(处理失败场景)
java
@Component
public class CompensationHandler {
@Autowired
private OrderMapper orderMapper;
@Autowired
private LocalMessageMapper localMessageMapper;
/**
* 定时任务:处理失败的消息
* 1. 消息发送失败超过3次 → 人工介入或补偿
* 2. 消息消费失败超过重试次数 → 回滚本地业务
*/
@Scheduled(fixedDelay = 60000) // 每分钟执行一次
public void handleFailedMessages() {
// 查询发送失败的消息
List<LocalMessage> failedMessages = localMessageMapper.selectByStatus("FAILED");
for (LocalMessage msg : failedMessages) {
try {
// 根据业务类型执行补偿
Order order = JSON.parseObject(msg.getContent(), Order.class);
// 补偿逻辑:取消订单
orderMapper.cancelOrder(order.getId());
// 更新消息状态为已补偿
msg.setStatus("COMPENSATED");
localMessageMapper.update(msg);
} catch (Exception e) {
log.error("补偿处理失败: {}", msg.getMessageId(), e);
}
}
}
/**
* 死信队列消费者:处理消费失败的消息
*/
@RocketMQMessageListener(
topic = "order-topic-dlq", // 死信队列
consumerGroup = "dlq-consumer-group"
)
public class DeadLetterConsumer implements RocketMQListener<String> {
@Override
public void onMessage(String messageContent) {
// 解析消息,执行补偿或告警
Order order = JSON.parseObject(messageContent, Order.class);
// 1. 回滚本地业务(取消订单)
orderMapper.cancelOrder(order.getId());
// 2. 发送告警通知
alertService.sendAlert("订单处理失败,已回滚: " + order.getId());
// 3. 记录日志,便于排查
log.error("死信队列消息,已执行补偿: {}", order.getId());
}
}
}
完整流程图:
1. 创建订单(本地事务)
├─ 插入订单表
├─ 插入消息表(状态:PENDING)
└─ 事务提交 ✅ 或 回滚 ❌
2. 定时任务发送消息
├─ 查询 PENDING 状态消息
├─ 发送到消息队列
└─ 更新状态为 SENT ✅ 或 重试/失败 ❌
3. 消费者处理消息
├─ 幂等性检查
├─ 扣减库存和余额
└─ 更新状态为 CONSUMED ✅ 或 重试 ❌
4. 补偿机制
├─ 发送失败 → 人工介入或自动补偿
└─ 消费失败 → 死信队列 → 回滚本地业务
关键点总结:
- 本地事务保证原子性:业务操作和消息写入在同一事务,失败时自动回滚
- 消息发送失败处理:重试机制,超过次数标记失败,触发补偿
- 消费失败处理:RocketMQ 自动重试,最终失败进入死信队列,执行补偿
- 幂等性保证:通过消息状态和业务ID判断是否已处理
- 最终一致性:通过重试和补偿机制保证最终一致
优缺点:
- 优点:实现简单、最终一致、不依赖第三方事务管理器、支持补偿
- 缺点:需要维护消息表、有延迟、可能出现消息丢失(需要监控和告警)
6. Seata 框架
实施描述 :
Seata 是阿里开源的分布式事务框架,支持四种模式:
- AT 模式:无侵入,自动生成回滚 SQL,适合大多数场景
- TCC 模式:需要实现 Try/Confirm/Cancel 接口,性能好
- Saga 模式:长事务,通过状态机编排
- XA 模式:基于 XA 协议,强一致但性能差
AT 模式使用示例:
java
@GlobalTransactional
public void createOrder(Order order) {
// 1. 创建订单(本地事务)
orderMapper.insert(order);
// 2. 扣减库存(远程调用,自动参与分布式事务)
inventoryService.deduct(order.getProductId(), order.getQuantity());
// 3. 扣减余额(远程调用,自动参与分布式事务)
accountService.deduct(order.getUserId(), order.getAmount());
}
核心组件:
- TC(Transaction Coordinator):事务协调器,管理全局事务
- TM(Transaction Manager):事务管理器,开启/提交/回滚全局事务
- RM(Resource Manager):资源管理器,管理分支事务
优缺点:
- 优点:AT 模式无侵入、支持多种模式、社区活跃
- 缺点:需要部署 TC 服务、AT 模式有性能损耗
选择建议:
- 一般场景:Seata AT 模式(无侵入,易用)
- 高并发场景:TCC 模式(性能好,无全局锁)
- 允许延迟场景:本地消息表 + 消息队列(最终一致,解耦)
- 长事务场景:Saga 模式(适合业务流程长的场景)
3. Seata 的 AT 模式原理是什么?
回答:Seata AT 模式是一种无侵入的分布式事务方案。原理是:
- 一阶段:业务 SQL 执行前,记录数据快照(beforeImage);执行后记录 afterImage;生成行锁,提交本地事务
- 二阶段提交:删除 undo log 和行锁,异步完成
- 二阶段回滚:根据 beforeImage 生成反向 SQL,恢复数据,删除 undo log
核心组件:TC(事务协调器)、TM(事务管理器)、RM(资源管理器)。AT 模式对业务无侵入,只需加 @GlobalTransactional 注解,适合大多数业务场景。
4. TCC 模式的原理和适用场景?
回答:TCC 是 Try-Confirm-Cancel 的缩写:
- Try:预留资源,如冻结库存、预扣余额
- Confirm:确认执行,真正扣减资源
- Cancel:取消操作,释放预留资源
特点:业务侵入性强,需要实现三个接口;性能好,无全局锁;适合高并发、资金类业务。
需要注意:幂等性(重复调用结果一致)、空回滚(Try 未执行时的 Cancel)、悬挂问题(Cancel 先于 Try 执行)。
4.1. TCC 模式下的防悬挂、幂等、空回滚有几种实现方式?
总结 :TCC 模式需要解决三个核心问题:1. 幂等性 :通过状态记录、唯一索引、Redis 去重等方式实现;2. 空回滚 :通过检查 Try 记录、状态标记、时间窗口等方式实现;3. 防悬挂:通过状态检查、时间戳、分布式锁等方式实现。主要实现方式包括:状态表记录、Redis 缓存、数据库唯一索引、时间戳判断等。
回答
问题场景:
在 TCC 分布式事务中,由于网络延迟、重试机制等原因,可能出现以下问题:
- 幂等性问题:同一个方法被重复调用,需要保证结果一致
- 空回滚问题:Try 未执行,但 Cancel 先执行
- 悬挂问题:Cancel 先于 Try 执行,导致 Try 无法执行
- 并发问题:多个线程同时执行同一个方法,可能导致重复执行
并发控制方案:
-
数据库行锁(SELECT FOR UPDATE,推荐):
- 在事务中使用
SELECT FOR UPDATE加行锁 - 保证同一时间只有一个线程能执行
- 性能好,实现简单
- 在事务中使用
-
分布式锁(Redis/Redisson):
- 使用分布式锁保证全局唯一
- 适合多实例部署场景
- 性能略低于数据库锁
-
唯一索引 + 异常处理:
- 利用数据库唯一索引防止重复插入
- 如果插入失败,说明已执行过
- 适合简单场景
解决方案列表:
1. 幂等性实现方式
核心原理:通过记录执行状态,重复调用时检查状态,已执行则直接返回。
实现方式:
方式1:状态表记录 + 唯一索引 + SELECT FOR UPDATE(推荐):
java
// 创建事务记录表(branch_id 需要唯一索引)
CREATE TABLE tcc_transaction (
tx_id VARCHAR(64) PRIMARY KEY,
branch_id VARCHAR(64) UNIQUE, -- 唯一索引,防止并发插入
status TINYINT, -- 状态:-1:Try占位, 0:Try已执行, 1:Confirm已执行, 2:Cancel已执行
create_time BIGINT,
update_time BIGINT,
INDEX idx_branch_status (branch_id, status)
);
@Service
public class InventoryService {
@Autowired
private TccTransactionMapper tccTransactionMapper;
// Try 方法(幂等 + 并发控制)
@Transactional
public boolean tryReserve(String txId, String branchId, Long productId, Integer quantity) {
try {
// 1. 插入占位记录(唯一索引防止并发插入)
tccTransactionMapper.insertPlaceholder(txId, branchId, -1);
} catch (DuplicateKeyException e) {
// 记录已存在,继续执行查询逻辑
}
// 2. 加行锁查询(SELECT FOR UPDATE 防止并发更新)
TccTransaction record = tccTransactionMapper.selectByBranchIdForUpdate(branchId);
// 3. 检查状态(幂等)
if (record.getStatus() == 0) {
return true; // 已执行过 Try
}
if (record.getStatus() == 2) {
throw new RuntimeException("Cancel 已执行,Try 不能执行"); // 防悬挂
}
// 4. 执行 Try 操作并更新状态
boolean result = inventoryMapper.freeze(productId, quantity) > 0;
if (result) {
tccTransactionMapper.updateStatus(branchId, 0);
} else {
tccTransactionMapper.deleteByBranchId(branchId);
}
return result;
}
// Confirm 方法(幂等)
@Transactional
public boolean confirmReserve(String txId, String branchId, Long productId, Integer quantity) {
TccTransaction record = tccTransactionMapper.selectByBranchIdForUpdate(branchId);
if (record.getStatus() == 1) {
return true; // 已执行过 Confirm(幂等)
}
if (record.getStatus() != 0) {
throw new RuntimeException("Try 未执行或执行失败");
}
boolean result = inventoryMapper.deduct(productId, quantity) > 0;
if (result) {
tccTransactionMapper.updateStatus(branchId, 1);
}
return result;
}
// Cancel 方法(幂等 + 空回滚)
//
// 关键理解:Cancel 阶段判断 Try 阶段插入的记录是否存在,如果不存在,则不执行回滚操作
//
// 场景1:Cancel 先执行,Try 未执行(记录不存在)
// T1: Cancel 执行 → 查询记录 → 不存在 → 直接返回(不执行回滚)
// T2: Try 执行 → 插入占位记录(status=-1)→ 执行 Try 操作 → 更新为 status=0
// 说明:Cancel 不执行回滚,因为 Try 记录不存在,没有需要回滚的资源
//
// 场景2:Try 占位后,Cancel 执行(记录存在,status=-1)
// T1: Try 执行 → 插入占位记录(status=-1)→ 执行 Try 操作...
// T2: Cancel 执行 → 查询记录 → status=-1 → 不执行回滚(Try 未执行,无资源需要回滚)
// T3: Try 执行完成 → 更新为 status=0
// 说明:Cancel 不执行回滚,因为 Try 操作可能还未完成,没有资源需要回滚
//
// 场景3:Try 已执行,Cancel 执行(正常回滚)
// T1: Try 执行 → 更新状态为 0 → 执行 Try 操作成功(冻结资源)
// T2: Cancel 执行 → 查询记录 → status=0 → 执行解冻 → 更新为 status=2
// 说明:Cancel 执行回滚,因为 Try 已执行,需要解冻资源
//
// 场景4:Cancel 已执行(幂等)
// T1: Cancel 执行 → 查询记录 → status=2 → 幂等返回
@Transactional
public boolean cancelReserve(String txId, String branchId, Long productId, Integer quantity) {
// 1. 先查询记录是否存在(不加锁,用于判断是否需要回滚)
TccTransaction record = tccTransactionMapper.selectByBranchId(branchId);
// 2. 如果记录不存在,说明 Try 未执行,不执行回滚操作
if (record == null) {
return true; // Try 未执行,无需回滚
}
// 3. 加行锁查询(如果其他线程正在执行 Cancel,会等待锁释放)
record = tccTransactionMapper.selectByBranchIdForUpdate(branchId);
// 4. 检查状态(幂等)
if (record.getStatus() == 2) {
// 已执行过 Cancel,幂等返回
return true;
}
// 5. 执行 Cancel 操作
boolean result = true;
if (record.getStatus() == 0) {
// Try 已执行,需要解冻(正常回滚)
result = inventoryMapper.unfreeze(productId, quantity) > 0;
} else if (record.getStatus() == -1) {
// Try 占位状态,Try 未执行,不执行回滚操作
// 说明:Try 可能正在执行或未执行,没有资源需要回滚
result = true; // 无需回滚,直接返回成功
}
// 6. 更新状态为 Cancel(只有 Try 已执行时才更新状态)
if (result) {
if (record.getStatus() == 0) {
// Try 已执行,更新状态为 Cancel
tccTransactionMapper.updateStatus(branchId, 2);
}
// 如果 status == -1,不更新状态,保持 Try 占位状态
// 如果 status == 2,已经在步骤4中幂等返回了
} else {
// Cancel 失败,删除记录(只有业务操作失败时才删除)
tccTransactionMapper.deleteByBranchId(branchId);
}
return result;
}
}
@Mapper
public interface TccTransactionMapper {
@Select("SELECT * FROM tcc_transaction WHERE branch_id = #{branchId}")
TccTransaction selectByBranchId(String branchId);
@Select("SELECT * FROM tcc_transaction WHERE branch_id = #{branchId} FOR UPDATE")
TccTransaction selectByBranchIdForUpdate(String branchId);
@Insert("INSERT INTO tcc_transaction (tx_id, branch_id, status, create_time) VALUES (#{txId}, #{branchId}, #{status}, #{createTime})")
void insertPlaceholder(@Param("txId") String txId, @Param("branchId") String branchId, @Param("status") int status);
@Update("UPDATE tcc_transaction SET status = #{status} WHERE branch_id = #{branchId}")
void updateStatus(@Param("branchId") String branchId, @Param("status") int status);
@Delete("DELETE FROM tcc_transaction WHERE branch_id = #{branchId}")
void deleteByBranchId(String branchId);
}
其他实现方式:
方式2:Redis 去重(使用 SETNX 原子操作):
java
// 使用 SETNX 原子操作,防止并发
Boolean setResult = redisTemplate.opsForValue().setIfAbsent(key, "1", 1, TimeUnit.HOURS);
if (Boolean.FALSE.equals(setResult)) {
return true; // 已执行过(幂等)
}
// 执行操作...
方式3:数据库唯一索引:
java
// 利用唯一索引防止重复插入
try {
tccTryRecordMapper.insert(new TccTryRecord(txId, branchId));
// 执行操作...
} catch (DuplicateKeyException e) {
return true; // 已执行过(幂等)
}
方式4:分布式锁:
java
// 使用 Redisson 分布式锁
RLock lock = redissonClient.getLock("tcc:lock:" + branchId);
if (lock.tryLock(5, 10, TimeUnit.SECONDS)) {
// 检查状态并执行操作
}
2. 空回滚实现方式
核心原理:Cancel 阶段判断 Try 阶段插入的记录是否存在,如果不存在,则不执行回滚操作。
实现方式:
方式1:状态表记录(推荐) :
在 Cancel 方法中,先查询 Try 记录是否存在:
- 如果记录不存在:说明 Try 未执行,无需回滚,直接返回
- 如果记录存在且 status=0:说明 Try 已执行,执行解冻操作(正常回滚)
- 如果记录存在且 status=-1:说明 Try 占位但未执行,无需回滚,直接返回
详见上面的 Cancel 方法实现。
方式2:Redis 去重:
java
// Cancel 方法中,检查 Redis 中是否存在 Try 记录
String tryKey = "tcc:try:" + branchId;
if (!redisTemplate.hasKey(tryKey)) {
return true; // Try 未执行,空回滚
}
// 执行 Cancel 操作...
方式3:分布式锁 + 状态检查:
java
// 使用分布式锁保证并发安全,同时检查状态
RLock lock = redissonClient.getLock("tcc:lock:" + branchId);
if (lock.tryLock(5, 10, TimeUnit.SECONDS)) {
try {
// 检查 Try 记录是否存在
TccTransaction record = tccTransactionMapper.selectByBranchId(branchId);
if (record == null) {
return true; // Try 未执行,空回滚
}
// 执行 Cancel 操作...
} finally {
lock.unlock();
}
}
3. 防悬挂实现方式
核心原理:Try 执行前检查 Cancel 是否已执行,如果已执行则抛出异常。
实现方式:
方式1:状态表记录(推荐) :
在 Try 方法中,查询记录状态,如果 status=2(Cancel 已执行),抛出异常。详见上面的 Try 方法实现。
方式2:Redis 去重:
java
// Try 方法中,检查 Redis 中是否存在 Cancel 记录
String cancelKey = "tcc:cancel:" + branchId;
if (redisTemplate.hasKey(cancelKey)) {
throw new RuntimeException("Cancel 已执行,Try 不能执行"); // 防悬挂
}
// 执行 Try 操作...
方式3:分布式锁 + 状态检查:
java
// 使用分布式锁保证并发安全,同时检查状态
RLock lock = redissonClient.getLock("tcc:lock:" + branchId);
if (lock.tryLock(5, 10, TimeUnit.SECONDS)) {
try {
// 检查 Cancel 是否已执行
TccTransaction record = tccTransactionMapper.selectByBranchId(branchId);
if (record != null && record.getStatus() == 2) {
throw new RuntimeException("Cancel 已执行,Try 不能执行"); // 防悬挂
}
// 执行 Try 操作...
} finally {
lock.unlock();
}
}
重要说明:
- 状态表记录:可靠性高,可追溯,适合生产环境(与 Seata 的实现方式类似)
- Redis 去重:性能好,但数据可能丢失,需要持久化机制
- 分布式锁:主要用于并发控制,需要配合状态检查才能实现空回滚和防悬挂
- Seata 实现 :基于
tcc_fence_log表(类似于状态表记录),框架自动处理,无需手动实现
⚠️ 重要问题:SELECT FOR UPDATE 在记录不存在时的问题:
java
// 问题:SELECT FOR UPDATE 只能锁定已存在的行,不能锁定不存在的行
// 解决方案:先插入占位记录(唯一索引),再使用 SELECT FOR UPDATE 查询
关键点说明:
-
SELECT FOR UPDATE 的限制:
- ⚠️ 记录不存在时不会加锁,必须先插入占位记录(唯一索引),再查询
-
唯一索引 vs SELECT FOR UPDATE:
- 唯一索引:防止并发插入,保证只有一个线程能插入成功
- SELECT FOR UPDATE:防止并发更新,保证查询和更新的原子性
- 两者配合:唯一索引保证插入安全,SELECT FOR UPDATE 保证更新安全
-
执行流程:
线程1:插入占位记录(唯一索引)→ SELECT FOR UPDATE(获得锁)→ 执行操作 → 更新状态 线程2:插入失败(唯一索引)→ SELECT FOR UPDATE(等待锁)→ 获得锁后检查状态(幂等)
实现方式对比:
| 实现方式 | 优点 | 缺点 | 并发控制 | 适用场景 |
|---|---|---|---|---|
| 状态表记录 + 行锁 | 可靠性高、可追溯、支持复杂状态、并发安全 | 需要数据库、性能略低 | SELECT FOR UPDATE | 生产环境推荐 |
| 状态表记录(无锁) | 可靠性高、可追溯 | 并发不安全、可能重复执行 | ❌ 无 | 不推荐 |
| Redis 去重 | 性能好、实现简单 | 数据可能丢失、需要持久化 | Redis 原子操作 | 高并发场景 |
| 唯一索引 | 实现简单、数据库保证 | 只能防重复插入 | 数据库唯一约束 | 简单场景 |
| 分布式锁 | 强一致性、多实例安全 | 性能开销、可能死锁 | Redis/Redisson | 多实例部署 |
| 时间窗口 | 灵活、可配置 | 时间判断不准确、并发不安全 | ❌ 无 | 不推荐 |
并发控制方式对比:
| 并发控制方式 | 实现方式 | 优点 | 缺点 | 推荐度 |
|---|---|---|---|---|
| 数据库行锁 | SELECT FOR UPDATE | 可靠性高、实现简单、性能好 | 需要事务、单实例 | ⭐⭐⭐⭐⭐ |
| 分布式锁 | Redis/Redisson | 多实例安全、功能完善 | 性能开销、依赖Redis | ⭐⭐⭐⭐ |
| 唯一索引 | 数据库唯一约束 | 实现简单、数据库保证 | 只能防插入、不能防更新 | ⭐⭐⭐ |
| 无锁 | 无 | 性能最好 | 并发不安全、可能重复执行 | ❌ 不推荐 |
最佳实践:
-
推荐方案:唯一索引 + SELECT FOR UPDATE 行锁
- 可靠性高,支持幂等、空回滚、防悬挂
- 并发安全,通过唯一索引 + 行锁保证原子性
- 可追溯,便于排查问题
- 必须配合使用 :
- 唯一索引:防止并发插入
- SELECT FOR UPDATE:防止并发更新
-
并发控制(重要!):
- 唯一索引的作用:防止并发插入,保证只有一个线程能插入成功
- SELECT FOR UPDATE 的作用:防止并发更新,保证查询和更新的原子性
- 为什么两者都需要 :
- 唯一索引只能防止插入,不能防止更新
- 如果只使用唯一索引,多个线程可能同时更新同一条记录的状态
- SELECT FOR UPDATE 在查询和更新之间加锁,保证原子性
- 实现方式 :
- 先插入占位记录(利用唯一索引防止并发插入)
- 然后使用 SELECT FOR UPDATE 查询(此时记录一定存在)
- 在锁的保护下检查状态和更新状态
- 事务保证 :使用
@Transactional确保整个方法在事务中执行 - 锁的范围:只锁定当前分支事务的记录,不影响其他分支
- 多实例部署:如果多实例部署,考虑使用分布式锁(Redis/Redisson)
-
性能优化:
- 状态表添加索引:
(branch_id, status) - 使用 Redis 缓存热点数据
- 批量处理状态更新
- 行锁只锁定必要的数据,减少锁竞争
- 状态表添加索引:
-
监控告警:
- 监控空回滚次数
- 监控悬挂情况
- 监控幂等调用次数
- 监控并发冲突次数(锁等待时间)
Seata TCC 模式的实现方式
Seata 在 1.5.1 版本引入了 tcc_fence_log 表,通过状态记录机制解决幂等、悬挂和空回滚问题。
1. 幂等性实现
核心原理 :通过 tcc_fence_log 表记录每个事务分支的执行状态,查询该表判断某个阶段是否已执行。
实现方式:
sql
-- Seata 自动创建 tcc_fence_log 表
CREATE TABLE tcc_fence_log (
xid VARCHAR(128) NOT NULL,
branch_id BIGINT NOT NULL,
action_name VARCHAR(64) NOT NULL,
status TINYINT NOT NULL, -- 0:Committed, 1:Rollbacked, 2:Suspended
gmt_create DATETIME(3) NOT NULL,
gmt_modified DATETIME(3) NOT NULL,
PRIMARY KEY (xid, branch_id, action_name),
KEY idx_gmt_modified (gmt_modified),
KEY idx_status (status)
);
工作流程:
- Try 阶段:插入记录,
status=0(Committed) - Confirm 阶段:查询记录,如果
status=0则执行 Confirm,更新状态 - Cancel 阶段:查询记录,如果
status=0则执行 Cancel,更新status=1(Rollbacked)
代码示例(Seata 自动处理):
java
@LocalTCC // 标识这是一个 TCC 接口
public interface InventoryService {
@TwoPhaseBusinessAction(
name = "inventoryService", // 事务名称
commitMethod = "confirm", // Confirm 方法名
rollbackMethod = "cancel", // Cancel 方法名
useTCCFence = true // 启用 TCC Fence 机制(1.5.1+ 版本)
)
boolean tryReserve(BusinessActionContext context, Long productId, Integer quantity);
boolean confirm(BusinessActionContext context);
boolean cancel(BusinessActionContext context);
}
注解说明:
@LocalTCC:标识这是一个 TCC 接口,框架会自动处理 TCC 相关逻辑@TwoPhaseBusinessAction:标识 Try 方法,指定 Confirm 和 Cancel 方法名name:事务名称,唯一标识commitMethod:Confirm 方法名rollbackMethod:Cancel 方法名useTCCFence:是否启用 TCC Fence 机制 (Seata 1.5.1+ 版本)true:启用 TCC Fence,框架自动处理幂等、空回滚、防悬挂(推荐)false:不启用 TCC Fence,需要业务代码手动处理幂等、空回滚、防悬挂
- TCC Fence 机制 :当
useTCCFence = true时,Seata 框架通过 AOP/拦截器自动处理幂等、空回滚、防悬挂- 框架会自动操作
tcc_fence_log表 - 框架会在 Try/Confirm/Cancel 方法执行前后自动插入/查询/更新状态记录
- 业务代码只需实现业务逻辑,无需关心幂等、空回滚、防悬挂的处理
- 框架会自动操作
重要说明:
- Seata 1.5.1+ 版本 :引入了
useTCCFence参数,默认值为false(为了兼容旧版本) - 推荐设置 :
useTCCFence = true,让框架自动处理幂等、空回滚、防悬挂 - 数据库要求 :启用 TCC Fence 需要创建
tcc_fence_log表(框架会自动创建,或手动创建)
2. 空回滚实现
核心原理 :Cancel 执行前检查 tcc_fence_log 表中是否存在对应的 Try 记录,如果不存在,说明是空回滚,直接返回成功。
实现方式:
java
// Seata 框架自动处理空回滚
// 1. Cancel 执行前,查询 tcc_fence_log 表
// 2. 如果记录不存在(Try 未执行),直接返回成功(空回滚)
// 3. 如果记录存在且 status=0,执行 Cancel 操作,更新 status=1
工作流程:
Cancel 执行:
↓
查询 tcc_fence_log 表(xid, branch_id, action_name)
↓
记录不存在?
├─ 是 → 空回滚,直接返回成功 ✅
└─ 否 → 检查 status
├─ status=0 → 执行 Cancel 操作,更新 status=1
└─ status=1 → 幂等返回(已执行过 Cancel)
3. 防悬挂实现
核心原理 :Cancel 或 Confirm 执行时,如果 Try 记录不存在,插入一条 status=2(Suspended)的记录,阻止后续的 Try 操作执行。
实现方式:
java
// Seata 框架自动处理防悬挂
// 1. Cancel/Confirm 执行时,如果 Try 记录不存在
// 2. 插入一条 status=2(Suspended)的记录
// 3. Try 后续执行时,查询到 status=2,抛出异常(防悬挂)
工作流程:
场景:Cancel 先执行,Try 未执行
↓
Cancel 执行:
↓
查询 tcc_fence_log 表(Try 记录)
↓
记录不存在?
├─ 是 → 插入 status=2(Suspended)记录,空回滚成功
└─ 否 → 执行 Cancel 操作
↓
Try 后续执行:
↓
查询 tcc_fence_log 表
↓
status=2(Suspended)?
├─ 是 → 抛出异常(防悬挂)✅
└─ 否 → 正常执行 Try
Seata TCC 的实现机制
useTCCFence 参数说明:
- 作用:控制是否启用 TCC Fence 机制,自动处理幂等、空回滚、防悬挂
- 版本要求:Seata 1.5.1+ 版本支持
- 默认值 :
false(为了兼容旧版本,需要显式设置为true才能启用) - 推荐设置 :
useTCCFence = true,让框架自动处理,减少业务代码复杂度
框架自动处理机制 (当 useTCCFence = true 时):
-
AOP/拦截器机制:
- Seata 框架通过 AOP 或方法拦截器,在 Try/Confirm/Cancel 方法执行前后自动处理
- 框架会自动操作
tcc_fence_log表,业务代码无需关心
-
Try 方法执行流程:
java// 框架自动处理流程 1. 方法执行前:查询 tcc_fence_log 表,检查是否已执行或已悬挂 2. 如果 status=2(Suspended),抛出异常(防悬挂) 3. 如果 status=0(Committed),直接返回(幂等) 4. 执行业务逻辑(Try 方法) 5. 方法执行后:插入/更新 tcc_fence_log 表,status=0(Committed) -
Confirm 方法执行流程:
java// 框架自动处理流程 1. 方法执行前:查询 tcc_fence_log 表,检查状态 2. 如果记录不存在,插入 status=2(Suspended)记录(防悬挂) 3. 如果 status=1(Rollbacked),抛出异常(已回滚,不能提交) 4. 如果 status=2(Suspended),抛出异常(已悬挂,不能提交) 5. 如果已执行过 Confirm,直接返回(幂等) 6. 执行业务逻辑(Confirm 方法) 7. 方法执行后:更新 tcc_fence_log 表状态 -
Cancel 方法执行流程:
java// 框架自动处理流程 1. 方法执行前:查询 tcc_fence_log 表,检查状态 2. 如果记录不存在,插入 status=2(Suspended)记录,直接返回(空回滚) 3. 如果 status=1(Rollbacked),直接返回(幂等) 4. 如果 status=2(Suspended),直接返回(空回滚) 5. 如果 status=0(Committed),执行业务逻辑(Cancel 方法) 6. 方法执行后:更新 tcc_fence_log 表,status=1(Rollbacked)
关键点:
- 必须设置
useTCCFence = true:在@TwoPhaseBusinessAction注解中显式设置,才能启用 TCC Fence 机制 - 无需额外注解 :只需使用
@LocalTCC和@TwoPhaseBusinessAction定义接口 - 框架自动处理 :当
useTCCFence = true时,幂等、空回滚、防悬挂由框架自动处理,业务代码无需关心 - 透明化:业务代码只需实现业务逻辑,框架自动处理状态管理
- 数据库要求 :启用 TCC Fence 需要
tcc_fence_log表(框架会自动创建,或手动创建)
Seata TCC 的优势
- 自动处理:框架自动处理幂等、空回滚、防悬挂,业务代码无需关心
- 统一管理 :通过
tcc_fence_log表统一管理所有 TCC 分支事务状态 - 可靠性高:基于数据库事务,保证状态记录的一致性
- 易于排查 :通过
tcc_fence_log表可以追溯所有事务分支的执行历史 - 代码简洁:只需使用注解定义接口,无需手动实现状态管理逻辑
Seata TCC vs 手动实现
| 对比项 | Seata TCC | 手动实现 |
|---|---|---|
| 幂等性 | 框架自动处理(基于 tcc_fence_log 表) |
多种方式:状态表记录、Redis 去重、唯一索引、分布式锁 |
| 空回滚 | 框架自动处理(基于 tcc_fence_log 表) |
多种方式:状态表记录、Redis 去重、分布式锁+状态检查 |
| 防悬挂 | 框架自动处理(基于 tcc_fence_log 表) |
多种方式:状态表记录、Redis 去重、分布式锁+状态检查 |
| 实现方式 | 统一使用 tcc_fence_log 表 |
可选择:状态表、Redis、分布式锁等 |
| 代码侵入 | 只需实现业务接口 | 需要实现完整的状态管理逻辑 |
| 可靠性 | 框架保证(基于数据库事务) | 需要自己保证并发安全和数据一致性 |
| 维护成本 | 低 | 高(需要维护多种实现方式) |
手动实现的多种方式对比:
| 实现方式 | 幂等性 | 空回滚 | 防悬挂 | 并发控制 | 可靠性 | 推荐度 |
|---|---|---|---|---|---|---|
| 状态表记录 + 行锁 | ✅ | ✅ | ✅ | SELECT FOR UPDATE | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ |
| Redis 去重 | ✅ | ✅ | ✅ | Redis 原子操作 | ⭐⭐⭐ | ⭐⭐⭐ |
| 分布式锁 + 状态检查 | ✅ | ✅ | ✅ | Redis/Redisson | ⭐⭐⭐⭐ | ⭐⭐⭐⭐ |
| 唯一索引 | ✅ | ❌ | ❌ | 数据库唯一约束 | ⭐⭐⭐ | ⭐⭐ |
使用建议:
- 推荐使用 Seata TCC :框架自动处理幂等、空回滚、防悬挂,减少业务代码复杂度,统一使用
tcc_fence_log表 - 手动实现 :只有在特殊场景下(如无法使用 Seata)才考虑手动实现
- 首选:状态表记录 + 行锁(与 Seata 实现方式类似,可靠性高)
- 高并发场景:Redis 去重(性能好,但需要持久化机制)
- 多实例部署:分布式锁 + 状态检查(多实例安全)
5. 分布式锁有几种实现方式?
回答:分布式锁主要有以下几种实现方式:
| 实现方式 | 原理 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|---|
| 数据库 | 基于唯一索引或乐观锁 | 实现简单、无需额外组件 | 性能差、有死锁风险 | 并发量低、简单场景 |
| Redis | SETNX + 过期时间 | 性能好、实现简单 | 主从切换可能丢锁 | 高并发、最终一致可接受 |
| Redisson | Redis + 看门狗机制 | 自动续期、功能完善 | 依赖 Redis | 生产环境推荐 |
| RedLock | 多 Redis 节点投票 | 提高可靠性 | 仍有争议、实现复杂 | 对可靠性要求高 |
| ZooKeeper | 临时顺序节点 | 强一致、自动释放 | 性能较低、需维护 ZK | 强一致、可靠性要求高 |
| etcd | Lease + Revision | 强一致、性能好 | 需要维护 etcd | 云原生、K8s 环境 |
详细说明:
1. 数据库实现
基于唯一索引或乐观锁实现,适合并发量低的场景。
sql
// 基于唯一索引
CREATE TABLE distributed_lock (
lock_key VARCHAR(64) PRIMARY KEY,
lock_value VARCHAR(64),
expire_time BIGINT,
INDEX idx_expire_time (expire_time)
);
// 加锁
INSERT INTO distributed_lock (lock_key, lock_value, expire_time)
VALUES ('order:123', 'uuid', UNIX_TIMESTAMP() + 30)
ON DUPLICATE KEY UPDATE lock_value = 'uuid', expire_time = UNIX_TIMESTAMP() + 30;
// 解锁
DELETE FROM distributed_lock WHERE lock_key = 'order:123' AND lock_value = 'uuid';
2. Redis SETNX
使用 SET key value NX EX 30 实现,性能好但主从切换可能丢锁。
java
// 加锁
String result = jedis.set(key, value, "NX", "EX", 30);
// 解锁(Lua 脚本保证原子性)
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
jedis.eval(script, Collections.singletonList(key), Collections.singletonList(value));
3. Redisson
封装了加锁、续期、解锁逻辑,支持看门狗自动续期,生产环境推荐。
java
RLock lock = redisson.getLock("myLock");
lock.lock(); // 自动续期
try {
// 业务逻辑
} finally {
lock.unlock();
}
4. ZooKeeper
基于临时顺序节点实现,强一致性但性能较低。
java
// 创建临时顺序节点
String lockPath = zkClient.create("/locks/order-", data, CreateMode.EPHEMERAL_SEQUENTIAL);
// 判断是否是最小节点,获取锁
5. etcd
基于 Lease(租约)和 Revision(版本号)实现,强一致且性能好,适合云原生环境。
核心机制:
- Lease(租约):为 key 设置过期时间,类似 Redis 的过期机制
- Revision(版本号):etcd 中每个 key 都有全局递增的版本号,用于实现公平锁
- Watch(监听):监听 key 变化,实现阻塞等待
- Prefix(前缀):通过前缀查询,实现锁队列
etcd 分布式锁的优势:
- 强一致性:基于 Raft 算法,保证强一致
- 自动释放:Lease 过期自动释放,防止死锁
- 公平锁:通过 Revision 实现 FIFO 队列
- 性能好:比 ZooKeeper 性能更好
- Watch 机制:支持阻塞等待,无需轮询
与 Redis 和 ZooKeeper 的对比:
| 特性 | etcd | Redis | ZooKeeper |
|---|---|---|---|
| 一致性 | 强一致(Raft) | 最终一致 | 强一致(ZAB) |
| 性能 | 高 | 最高 | 中等 |
| 自动释放 | Lease 机制 | 过期时间 | 临时节点 |
| 公平锁 | Revision 排序 | 不支持 | 顺序节点 |
| 适用场景 | 云原生、K8s | 高并发缓存 | 配置中心 |
选择建议:
- 高并发、性能优先:Redis/Redisson
- 强一致性、可靠性优先:ZooKeeper/etcd
- 简单场景、低并发:数据库
- 云原生环境:etcd
6. Redis 分布式锁怎么实现?
回答:Redis 分布式锁实现方式:
- 基础方案 :
SET key value NX EX 30,NX 保证互斥,EX 设置过期防死锁 - Redisson 方案:封装了加锁、续期、解锁逻辑,支持看门狗自动续期
- RedLock 方案:多个独立 Redis 节点,获取大多数锁才算成功
看门狗机制 :Redisson 的 lock() 方法会启动后台线程,每 10 秒(默认 30 秒的 1/3)检查锁是否还持有,自动续期,防止业务未完成锁过期。
注意事项:解锁时需验证是否是自己的锁(用 Lua 脚本保证原子性);主从切换可能导致锁丢失。
7. RedLock 的原理和争议?
回答:RedLock 是 Redis 作者提出的分布式锁算法:
原理:
- 获取当前时间
- 依次向 N 个独立 Redis 节点请求加锁
- 如果在大多数节点(N/2+1)上加锁成功,且总耗时小于锁有效期,则加锁成功
- 解锁时向所有节点发送解锁请求
争议(Martin Kleppmann 的批评):
- 时钟漂移可能导致锁失效
- GC 停顿可能导致锁过期后仍认为持有锁
- 网络延迟可能导致多客户端同时持有锁
结论:RedLock 不能保证强一致性,对一致性要求高的场景建议用 ZooKeeper 或 etcd。
8. ZooKeeper 分布式锁的实现原理?
回答:ZooKeeper 分布式锁基于临时顺序节点实现:
- 客户端在锁节点下创建临时顺序节点
- 获取锁节点下所有子节点,判断自己是否是最小序号
- 如果是最小序号,获取锁成功
- 如果不是,监听前一个节点的删除事件
- 前一个节点删除后,重新判断是否是最小序号
优点 :强一致性(CP)、自动释放(临时节点)、公平锁(顺序节点)
缺点:性能较 Redis 低、需要维护 ZK 集群
9. 缓存和数据库一致性怎么保证?
回答:主要方案:
- Cache-Aside(旁路缓存):读时先查缓存,未命中查库再写缓存;写时先更新库再删缓存
- 延迟双删:先删缓存、更新库、延迟后再删缓存,减少不一致窗口
- 消息队列异步更新:更新库后发消息,消费者异步更新缓存,保证最终一致
为什么删缓存而不是更新缓存:避免并发写导致缓存脏数据;缓存可能需要复杂计算,删除更简单。
并发问题:先更新库再删缓存,可能出现短暂不一致,但概率低、影响小,是最佳实践。
10. 缓存穿透、击穿、雪崩是什么?怎么解决?
回答:
| 问题 | 定义 | 解决方案 |
|---|---|---|
| 穿透 | 查询不存在的数据,缓存和数据库都没有 | 缓存空值、布隆过滤器、参数校验 |
| 击穿 | 热点 key 过期,大量请求打到数据库 | 互斥锁、热点数据永不过期、预热 |
| 雪崩 | 大量 key 同时过期,数据库压力骤增 | 过期时间加随机值、多级缓存、限流降级 |
11. 分布式 ID 怎么生成?
回答:常见方案:
- UUID:简单但无序、太长,不适合做主键
- 数据库自增:简单但性能瓶颈、单点问题
- 雪花算法:64 位(1 符号位 + 41 时间戳 + 10 机器 ID + 12 序列号),有序、高性能,需解决时钟回拨
- 号段模式:从数据库批量获取 ID 段,本地分配,如美团 Leaf
- Redis INCR:原子递增,性能好但依赖 Redis
生产推荐:雪花算法或号段模式(Leaf、UidGenerator)。
号段模式详解
原理:
- 从数据库批量获取一段 ID(如 1-1000),存储在本地内存
- 应用从本地内存中分配 ID,无需每次请求数据库
- 当本地号段用完后,再次从数据库获取新的号段
数据库表结构:
sql
CREATE TABLE id_generator (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
biz_tag VARCHAR(128) NOT NULL UNIQUE COMMENT '业务标识',
max_id BIGINT NOT NULL COMMENT '当前最大ID',
step INT NOT NULL DEFAULT 1000 COMMENT '号段步长',
description VARCHAR(256) COMMENT '描述',
update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);
-- 初始化
INSERT INTO id_generator (biz_tag, max_id, step) VALUES ('order', 0, 1000);
核心实现:
java
@Component
public class SegmentIdGenerator {
@Autowired
private IdGeneratorMapper idGeneratorMapper;
// 当前号段
private volatile Segment currentSegment;
// 下一个号段(预加载)
private volatile Segment nextSegment;
// 加载下一个号段的线程
private Thread loadingThread;
/**
* 获取下一个 ID
*/
public synchronized long nextId(String bizTag) {
// 1. 检查当前号段是否可用
if (currentSegment == null || currentSegment.isExhausted()) {
// 等待下一个号段加载完成
waitForNextSegment();
// 切换号段
currentSegment = nextSegment;
nextSegment = null;
// 异步加载下一个号段
loadNextSegmentAsync(bizTag);
}
// 2. 从当前号段获取 ID
return currentSegment.nextId();
}
/**
* 从数据库获取号段
*/
private Segment loadSegment(String bizTag) {
// 使用乐观锁更新 max_id
int rows = idGeneratorMapper.updateMaxId(bizTag);
if (rows == 0) {
// 更新失败,重试
return loadSegment(bizTag);
}
// 查询更新后的 max_id
IdGenerator generator = idGeneratorMapper.selectByBizTag(bizTag);
long start = generator.getMaxId();
long end = start + generator.getStep();
// 更新数据库中的 max_id
idGeneratorMapper.updateMaxIdToEnd(bizTag, end);
return new Segment(start, end);
}
/**
* 异步加载下一个号段
*/
private void loadNextSegmentAsync(String bizTag) {
if (loadingThread != null && loadingThread.isAlive()) {
return; // 已在加载中
}
loadingThread = new Thread(() -> {
Segment segment = loadSegment(bizTag);
synchronized (this) {
nextSegment = segment;
notifyAll(); // 通知等待的线程
}
});
loadingThread.start();
}
/**
* 等待下一个号段加载完成
*/
private void waitForNextSegment() {
if (nextSegment == null) {
// 同步加载第一个号段
currentSegment = loadSegment("order");
loadNextSegmentAsync("order");
} else {
// 等待异步加载完成
try {
while (nextSegment == null) {
wait();
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
}
/**
* 号段类
*/
class Segment {
private final AtomicLong current;
private final long end;
public Segment(long start, long end) {
this.current = new AtomicLong(start);
this.end = end;
}
public long nextId() {
long id = current.getAndIncrement();
if (id >= end) {
throw new IllegalStateException("号段已用完");
}
return id;
}
public boolean isExhausted() {
return current.get() >= end;
}
public double getIdleRate() {
return (end - current.get()) / (double)(end - current.get() + current.get());
}
}
Mapper 接口:
java
@Mapper
public interface IdGeneratorMapper {
/**
* 乐观锁更新 max_id
*/
@Update("UPDATE id_generator SET max_id = max_id + step WHERE biz_tag = #{bizTag}")
int updateMaxId(@Param("bizTag") String bizTag);
/**
* 更新 max_id 到指定值
*/
@Update("UPDATE id_generator SET max_id = #{maxId} WHERE biz_tag = #{bizTag}")
int updateMaxIdToEnd(@Param("bizTag") String bizTag, @Param("maxId") long maxId);
/**
* 查询
*/
@Select("SELECT * FROM id_generator WHERE biz_tag = #{bizTag}")
IdGenerator selectByBizTag(@Param("bizTag") String bizTag);
}
美团 Leaf 的实现特点:
-
双 Buffer 机制:
- 当前号段使用到 10% 时,异步加载下一个号段
- 两个号段交替使用,保证 ID 分配的连续性
-
监控告警:
- 监控号段使用率
- 号段即将用完时告警
-
高可用:
- 支持多数据库实例
- 数据库故障时,使用本地缓存
号段模式的优点:
- 性能好:批量获取,减少数据库访问
- 趋势递增:ID 有序,适合数据库主键
- 无单点故障:支持多数据库实例
- 无时钟依赖:不依赖系统时钟
号段模式的缺点:
- 依赖数据库:需要数据库支持
- 号段浪费:服务重启时,未使用的号段会丢失
- ID 不连续:不同业务或实例的 ID 不连续
与雪花算法对比:
| 特性 | 号段模式 | 雪花算法 |
|---|---|---|
| 性能 | 高(批量获取) | 极高(本地生成) |
| 有序性 | 趋势递增 | 趋势递增 |
| 依赖 | 数据库 | 系统时钟 |
| ID 长度 | 可配置 | 固定 64 位 |
| 适用场景 | 数据库主键 | 分布式系统 |
12. 雪花算法是怎么实现的?
回答:雪花算法是 Twitter 开源的分布式 ID 生成算法,生成 64 位 long 型 ID。
64 位结构:
| 部分 | 位数 | 说明 |
|---|---|---|
| 符号位 | 1 | 固定为 0,保证 ID 为正数 |
| 时间戳 | 41 | 毫秒级,可用约 69 年 |
| 机器 ID | 10 | 5 位数据中心 + 5 位机器,支持 1024 个节点 |
| 序列号 | 12 | 同一毫秒内的序列,支持 4096 个 ID |
核心流程:
- 获取当前时间戳,检测时钟回拨
- 同一毫秒内序列号递增,序列号用尽则等待下一毫秒
- 新毫秒序列号重置为 0
- 组装 ID:
(时间戳差值 << 22) | (机器ID << 12) | 序列号
优点 :高性能(本地生成)、趋势递增(利于索引)、不依赖外部存储
缺点:依赖系统时钟,时钟回拨会导致 ID 重复或生成失败
13. 雪花算法的时钟回拨问题怎么解决?
回答:时钟回拨是指系统时间因 NTP 同步等原因往回调,导致当前时间戳小于上次生成 ID 的时间戳。
产生的问题:
- ID 重复:回拨后时间戳相同,可能生成重复 ID
- ID 不递增:新 ID 比旧 ID 小,破坏趋势递增特性
- 服务不可用:如果直接抛异常,会导致 ID 生成失败
解决方案:
| 方案 | 实现 | 优点 | 缺点 |
|---|---|---|---|
| 抛异常 | 检测到回拨直接报错 | 简单 | 影响可用性 |
| 等待 | 回拨时间短则自旋等待 | 简单、不影响 ID | 回拨大时阻塞久 |
| 备用机器 ID | 预留多个机器 ID,回拨时切换 | 不阻塞 | 浪费机器 ID |
| 扩展位 | 用 2-3 bit 记录回拨次数 | 不阻塞、不浪费 | 实现复杂 |
| 外部时钟 | 依赖 ZK/DB 获取时间戳(美团 Leaf) | 可靠 | 增加依赖 |
14. 消息队列如何保证消息不丢失?
回答:从三个环节保证:
- 生产端:开启确认机制(RocketMQ 的 SendResult、Kafka 的 acks=all)
- Broker 端:消息持久化、多副本同步(Kafka ISR、RocketMQ 主从同步)
- 消费端:手动确认(ACK),消费成功后再提交偏移量
RocketMQ:同步发送 + 同步刷盘 + 主从同步 + 手动 ACK
Kafka:acks=all + min.insync.replicas + 手动提交偏移量
15. 消息队列如何保证消息不重复消费?
回答:消息队列只能保证 At Least Once,去重需要业务端实现幂等:
- 唯一 ID + 去重表:消费前查询是否已处理
- 数据库唯一索引:利用唯一约束防重
- Redis Set:消费前 SETNX 判断
- 状态机:业务状态流转,已处理的状态不再处理
- 乐观锁:版本号控制,重复消费不会更新
16. 消息队列如何保证消息顺序性?
回答:
- 全局顺序:单分区/单队列,性能差
- 分区顺序 :相同业务 key 的消息发到同一分区,分区内有序
- RocketMQ:MessageQueueSelector 指定队列
- Kafka:指定 partition 或相同 key
注意:消费端也要保证顺序,单线程消费或分区内串行处理。
17. 微服务之间如何保证事务?
回答:
-
Seata 分布式事务:
- AT 模式:无侵入,自动补偿
- TCC 模式:高并发,手动实现 Try/Confirm/Cancel
- Saga 模式:长事务,正向操作 + 补偿操作
-
消息队列 + 本地消息表:
- 业务操作和消息写入同一本地事务
- 定时任务扫描消息表发送消息
- 消费端处理后回调确认
-
最大努力通知:
- 多次重试通知,不保证一定成功
- 适合对一致性要求不高的场景
选择:强一致用 Seata,最终一致用消息队列。
18. 什么是脑裂问题?怎么解决?
回答:脑裂是指分布式系统因网络分区,分裂成多个独立部分,每部分都认为自己是主节点,导致数据不一致。
解决方案:
- Quorum 机制:只有获得大多数节点支持才能成为主节点
- Fencing Token:每次选主生成递增的 token,旧主的操作会被拒绝
- Lease 机制:主节点持有租约,过期前其他节点不能成为主
- STONITH:Shoot The Other Node In The Head,强制关闭疑似故障节点
Redis Sentinel/Cluster、ZooKeeper、etcd 都通过 Quorum 机制避免脑裂。
19. 服务注册发现的原理?Nacos 和 Eureka 的区别?
回答:
原理:
- 服务启动时向注册中心注册(IP、端口、服务名)
- 消费者从注册中心获取服务列表
- 注册中心通过心跳检测服务健康状态
- 服务下线时从注册中心注销
Nacos vs Eureka:
| 特性 | Nacos | Eureka |
|---|---|---|
| 一致性 | CP + AP 可切换 | AP |
| 健康检查 | 主动探测 + 心跳 | 仅心跳 |
| 配置中心 | 支持 | 不支持 |
| 雪崩保护 | 支持 | 支持 |
| 维护状态 | 活跃 | 停止维护 |
推荐使用 Nacos,功能更全面。
20. 熔断和限流的区别?Sentinel 怎么实现?
回答:
区别:
- 限流:控制请求速率,防止系统过载
- 熔断:服务异常时快速失败,防止级联故障
Sentinel 实现:
- 限流:支持 QPS 限流、并发线程数限流
- 熔断:慢调用比例、异常比例、异常数三种策略
- 流量整形:直接拒绝、Warm Up、排队等待
熔断状态:关闭 → 打开 → 半开 → 关闭/打开
21. 分库分表怎么做?有什么问题?
回答:
分片策略:
- 水平分表:按行拆分,如按用户 ID 取模
- 垂直分表:按列拆分,冷热数据分离
- 水平分库:数据分散到多个数据库
常见问题:
- 跨库 Join:应用层组装或冗余数据
- 分布式事务:Seata 或最终一致性
- 全局 ID:雪花算法、号段模式
- 数据迁移:双写、增量同步
- 扩容:一致性哈希减少数据迁移
中间件:ShardingSphere、MyCat
22. 如何设计一个高可用系统?
回答:
- 冗余设计:多副本、多机房、异地多活
- 负载均衡:Nginx、Gateway 分发流量
- 服务治理:注册发现、熔断限流、降级
- 数据层:主从复制、读写分离、分库分表
- 缓存层:多级缓存、缓存预热、热点数据处理
- 消息队列:异步解耦、削峰填谷
- 监控告警:链路追踪、日志分析、实时告警
- 故障演练:混沌工程、定期演练
核心指标:可用性(99.99%)、响应时间、吞吐量、故障恢复时间(RTO)。
23. A 操作和 B 操作处于一个事务,C 操作处于另一个事务,如何保证 C 在 AB 事务提交后才执行?如果 A 和 C 可以同时执行,但 C 必须在 AB 都提交后再提交,应该怎么设计?
回答:这是一个典型的分布式事务场景,根据不同的需求有两种场景:
方案列表:
场景一:AB 事务提交后 C 才开始执行
- 方案一:RocketMQ 事务消息(推荐)
- 方案二:本地消息表
- 方案三:消息队列 + 延迟检查
- 方案四:2PC(两阶段提交)
场景二:A 和 C 可以同时执行,但 C 必须在 AB 都提交后再提交
- 方案一:TransactionSynchronization + 编程式事务(推荐)
- 方案二:CountDownLatch + 编程式事务
- 方案三:CompletableFuture + 编程式事务
场景一:AB 事务提交后 C 才开始执行
需求:C 操作必须在 AB 事务提交完成后才开始执行(顺序执行)。
方案:RocketMQ 事务消息(推荐)
原理:使用 RocketMQ 的事务消息机制,通过半消息(Half Message)和本地事务监听器(Local Transaction Listener)保证消息发送和本地事务的一致性。
执行流程:
- 生产者发送半消息到 Broker(消息对消费者不可见)
- Broker 收到半消息后,立即回调
executeLocalTransaction方法 executeLocalTransaction中执行 A 和 B 操作(在@Transactional事务中)- 如果 A 或 B 失败,返回
ROLLBACK,消息被删除,C 不会执行 - 如果 A 和 B 都成功,返回
COMMIT,消息变为可见 - 消费者消费消息,执行 C 操作(此时 AB 事务已提交)
特点:
- ✅ 保证 C 操作执行时,AB 事务已经提交完成
- ❌ A 和 C 不能并行执行,C 必须等待 AB 事务提交后才开始执行
核心代码:
java
// 1. 生产者:发送事务消息
@Service
public class OrderService {
@Autowired
private RocketMQTemplate rocketMQTemplate;
public void executeAAndB(Order order) {
TransactionSendResult result = rocketMQTemplate.sendMessageInTransaction(
"order-topic",
MessageBuilder.withPayload(order)
.setHeader("orderId", order.getId())
.build(),
order
);
}
}
// 2. 事务监听器:执行本地事务(A 和 B 操作)
@Component
@RocketMQTransactionListener(txProducerGroup = "order-producer-group")
public class OrderTransactionListener implements RocketMQLocalTransactionListener {
@Autowired
private OrderMapper orderMapper;
@Autowired
private AccountMapper accountMapper;
/**
* 执行本地事务(A 和 B 操作)
*
* 作用:Broker 收到半消息后立即回调此方法,执行本地事务(A 和 B 操作)
* 执行时机:同步执行,在 sendMessageInTransaction 方法中阻塞等待
* 事务边界:@Transactional 保证 A 和 B 在同一事务中
* 返回值:
* - COMMIT:事务提交成功,消息变为可见,消费者可以消费(执行 C 操作)
* - ROLLBACK:事务回滚,消息被删除,消费者看不到,C 操作不会执行
*/
@Override
@Transactional(rollbackFor = Exception.class)
public RocketMQLocalTransactionState executeLocalTransaction(Message msg, Object arg) {
try {
Order order = (Order) arg;
// A 操作:插入订单
orderMapper.insertOrder(order);
// B 操作:扣减账户余额
accountMapper.deduct(order.getUserId(), order.getAmount());
return RocketMQLocalTransactionState.COMMIT;
} catch (Exception e) {
return RocketMQLocalTransactionState.ROLLBACK;
}
}
/**
* 检查本地事务状态(解决事务状态不确定问题)
*
* 作用:当 executeLocalTransaction 返回 UNKNOWN 或网络异常时,Broker 会回查此方法
* 执行时机:消息发送后 1 分钟首次回查,之后每 1 分钟回查一次,最多 15 次
* 触发条件:
* - executeLocalTransaction 返回 UNKNOWN
* - 网络超时,Broker 未收到 executeLocalTransaction 的返回值
* - 应用崩溃,事务状态未知
* 实现方式:通过查询业务数据(订单是否存在)判断事务是否已提交
* 返回值:
* - COMMIT:事务已提交,消息变为可见,消费者可以消费
* - ROLLBACK:事务已回滚,消息被删除,消费者看不到
*/
@Override
public RocketMQLocalTransactionState checkLocalTransaction(Message msg) {
String orderId = (String) msg.getHeaders().get("orderId");
Order order = orderMapper.selectById(orderId);
if (order != null && order.getStatus() == 1) {
return RocketMQLocalTransactionState.COMMIT;
}
return RocketMQLocalTransactionState.ROLLBACK;
}
}
// 3. 消费者:执行 C 操作
@Component
@RocketMQMessageListener(topic = "order-topic", consumerGroup = "order-consumer-group")
public class OrderConsumer implements RocketMQListener<Order> {
@Autowired
private InventoryService inventoryService;
@Override
public void onMessage(Order order) {
// C 操作:扣减库存
inventoryService.deductInventory(order.getProductId(), order.getQuantity());
}
}
优点:
- 保证消息发送和本地事务的一致性
- 自动处理事务状态不确定问题(checkLocalTransaction)
- 对业务代码侵入性小
缺点:
- 依赖 RocketMQ 的事务消息功能
- 需要实现事务状态检查逻辑
- A 和 C 不能并行执行,C 必须等待 AB 事务提交后才开始执行
场景二:A 和 C 可以同时执行,但 C 必须在 AB 都提交后再提交
需求:A 和 C 可以并行执行(不阻塞),但 C 的事务提交必须等待 AB 事务提交完成。
方案:TransactionSynchronization + 编程式事务(推荐)
原理 :使用 Spring 的 TransactionSynchronization 监听 AB 事务的提交,C 操作使用编程式事务手动控制提交时机。
执行流程:
- 执行 A 操作(在 AB 事务中)
- 同时启动 C 操作(在另一个事务中,使用编程式事务,不自动提交)
- 执行 B 操作(在 AB 事务中)
- 注册
TransactionSynchronization监听 AB 事务提交 - AB 事务提交(@Transactional 自动提交)
afterCommit回调触发,通知 C 操作- C 操作提交事务(此时 AB 已提交)
核心代码:
java
// 1. 订单服务
@Service
public class OrderService {
@Autowired
private OrderMapper orderMapper;
@Autowired
private InventoryService inventoryService;
@Transactional(rollbackFor = Exception.class)
public void executeAAndBWithParallelC(Order order) {
CountDownLatch abCommitLatch = new CountDownLatch(1);
AtomicBoolean abCommitSuccess = new AtomicBoolean(false);
AtomicReference<TransactionStatus> cTransactionStatusRef = new AtomicReference<>();
// 执行 A 操作
orderMapper.insertOrder(order);
// 启动 C 操作(异步执行,与 A 并行)
CompletableFuture.runAsync(() -> {
try {
TransactionStatus status = inventoryService.prepareCWithoutCommit(order);
cTransactionStatusRef.set(status);
abCommitLatch.await();
if (abCommitSuccess.get()) {
inventoryService.commitC(order, cTransactionStatusRef.get());
} else {
inventoryService.rollbackC(order, cTransactionStatusRef.get());
}
} catch (Exception e) {
if (cTransactionStatusRef.get() != null) {
inventoryService.rollbackC(order, cTransactionStatusRef.get());
}
}
});
// 执行 B 操作
order.setStatus(1);
orderMapper.updateOrderStatus(order);
// 注册事务同步器,监听 AB 事务提交
TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {
@Override
public void afterCommit() {
abCommitSuccess.set(true);
abCommitLatch.countDown();
}
@Override
public void afterCompletion(int status) {
if (status == TransactionSynchronization.STATUS_ROLLED_BACK) {
abCommitSuccess.set(false);
abCommitLatch.countDown();
}
}
});
}
}
// 2. 库存服务
@Service
public class InventoryService {
@Autowired
private InventoryMapper inventoryMapper;
@Autowired
private PlatformTransactionManager transactionManager;
public TransactionStatus prepareCWithoutCommit(Order order) {
DefaultTransactionDefinition def = new DefaultTransactionDefinition();
def.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW);
TransactionStatus status = transactionManager.getTransaction(def);
try {
inventoryMapper.deductInventory(order.getProductId(), order.getQuantity());
return status;
} catch (Exception e) {
transactionManager.rollback(status);
throw e;
}
}
public void commitC(Order order, TransactionStatus status) {
if (status != null && !status.isCompleted()) {
transactionManager.commit(status);
}
}
public void rollbackC(Order order, TransactionStatus status) {
if (status != null && !status.isCompleted()) {
transactionManager.rollback(status);
}
}
}
优点:
- ✅ A 和 C 可以并行执行,提高性能
- ✅ C 的事务提交等待 AB 事务提交完成,保证一致性
- ✅ 使用 Spring 原生机制,不依赖外部组件
缺点:
- 需要手动管理事务状态
- 实现相对复杂
- 需要处理多线程同步问题
方案二:CountDownLatch + 编程式事务
原理 :使用 CountDownLatch 作为信号量,让 C 操作等待 AB 事务提交完成,C 操作使用编程式事务手动控制提交时机。
核心代码:
java
@Service
public class OrderService {
@Autowired
private OrderMapper orderMapper;
@Autowired
private InventoryService inventoryService;
@Autowired
private PlatformTransactionManager transactionManager;
@Transactional(rollbackFor = Exception.class)
public void executeAAndBWithParallelC(Order order) {
CountDownLatch abCommitLatch = new CountDownLatch(1);
AtomicBoolean abCommitSuccess = new AtomicBoolean(false);
AtomicReference<TransactionStatus> cTransactionStatusRef = new AtomicReference<>();
// 执行 A 操作
orderMapper.insertOrder(order);
// 启动 C 操作(异步执行,与 A 并行)
CompletableFuture.runAsync(() -> {
try {
// C 操作执行(使用编程式事务,不自动提交)
DefaultTransactionDefinition def = new DefaultTransactionDefinition();
def.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW);
TransactionStatus status = transactionManager.getTransaction(def);
cTransactionStatusRef.set(status);
inventoryService.deductInventory(order.getProductId(), order.getQuantity());
// 等待 AB 事务提交完成
abCommitLatch.await();
if (abCommitSuccess.get()) {
// AB 事务提交成功,C 操作提交事务
transactionManager.commit(cTransactionStatusRef.get());
} else {
// AB 事务回滚,C 操作也回滚
transactionManager.rollback(cTransactionStatusRef.get());
}
} catch (Exception e) {
if (cTransactionStatusRef.get() != null) {
transactionManager.rollback(cTransactionStatusRef.get());
}
}
});
// 执行 B 操作
order.setStatus(1);
orderMapper.updateOrderStatus(order);
// 方法返回时,@Transactional 自动提交 AB 事务
// 在 finally 块中释放 CountDownLatch(实际应该在事务提交后释放)
// 注意:这里需要确保在事务提交后才释放,可以使用 TransactionSynchronization
TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {
@Override
public void afterCommit() {
abCommitSuccess.set(true);
abCommitLatch.countDown();
}
@Override
public void afterCompletion(int status) {
if (status == TransactionSynchronization.STATUS_ROLLED_BACK) {
abCommitSuccess.set(false);
abCommitLatch.countDown();
}
}
});
}
}
优点:
- 实现直观,易于理解
- 使用
CountDownLatch作为信号量,逻辑清晰
缺点:
- 仍然需要
TransactionSynchronization来确保在事务提交后释放信号量 - 需要手动管理事务状态
方案三:CompletableFuture + 编程式事务
原理 :使用 CompletableFuture 的链式调用和组合能力,灵活处理异步执行和事务提交时机,通过 thenCompose、thenApply 等方法组合多个异步操作。
核心代码:
java
@Service
public class OrderService {
@Autowired
private OrderMapper orderMapper;
@Autowired
private InventoryService inventoryService;
@Autowired
private PlatformTransactionManager transactionManager;
@Transactional(rollbackFor = Exception.class)
public void executeAAndBWithParallelC(Order order) {
AtomicBoolean abCommitSuccess = new AtomicBoolean(false);
AtomicReference<TransactionStatus> cTransactionStatusRef = new AtomicReference<>();
CountDownLatch abCommitLatch = new CountDownLatch(1);
// 执行 A 操作
orderMapper.insertOrder(order);
// 启动 C 操作(异步执行,与 A 并行)
CompletableFuture<TransactionStatus> cFuture = CompletableFuture.supplyAsync(() -> {
try {
// C 操作执行(使用编程式事务,不自动提交)
DefaultTransactionDefinition def = new DefaultTransactionDefinition();
def.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW);
TransactionStatus status = transactionManager.getTransaction(def);
inventoryService.deductInventory(order.getProductId(), order.getQuantity());
return status;
} catch (Exception e) {
throw new RuntimeException("C 操作执行失败", e);
}
});
// 执行 B 操作
order.setStatus(1);
orderMapper.updateOrderStatus(order);
// 注册事务同步器,监听 AB 事务提交
TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {
@Override
public void afterCommit() {
abCommitSuccess.set(true);
abCommitLatch.countDown();
}
@Override
public void afterCompletion(int status) {
if (status == TransactionSynchronization.STATUS_ROLLED_BACK) {
abCommitSuccess.set(false);
abCommitLatch.countDown();
}
}
});
// 使用 CompletableFuture 链式调用:等待 C 操作完成 → 等待 AB 事务提交 → 提交/回滚 C 操作
cFuture.thenCompose(cStatus -> {
cTransactionStatusRef.set(cStatus);
return CompletableFuture.runAsync(() -> {
try {
abCommitLatch.await();
if (abCommitSuccess.get()) {
transactionManager.commit(cStatus);
} else {
transactionManager.rollback(cStatus);
}
} catch (Exception e) {
transactionManager.rollback(cStatus);
}
});
}).exceptionally(e -> {
// 处理异常
if (cTransactionStatusRef.get() != null) {
transactionManager.rollback(cTransactionStatusRef.get());
}
return null;
});
}
}
优点:
- 使用
CompletableFuture的链式调用,代码更灵活 - 支持复杂的异步组合场景
- 可以方便地组合多个异步操作
缺点:
- 实现相对复杂
- 需要理解
CompletableFuture的执行机制 - 错误处理需要额外注意
方案二:本地消息表(适用于场景一)
原理:将消息写入本地数据库,与业务操作在同一事务中,通过定时任务异步发送消息。
执行流程:
- A 和 B 操作与消息写入在同一本地事务中
- 事务提交后,消息状态为"待发送"
- 定时任务扫描消息表,发送消息到消息队列
- 消费者消费消息,执行 C 操作
核心代码:
java
@Service
public class OrderService {
@Autowired
private OrderMapper orderMapper;
@Autowired
private AccountMapper accountMapper;
@Autowired
private LocalMessageMapper localMessageMapper;
/**
* 执行 A 和 B 操作,并写入消息表
*/
@Transactional
public void executeAAndB() {
// A 操作:创建订单
Order order = new Order();
orderMapper.insert(order);
// B 操作:扣减账户余额
accountMapper.deduct(order.getUserId(), order.getAmount());
// 写入消息表(在同一事务中)
LocalMessage message = new LocalMessage();
message.setBusinessId(order.getId());
message.setContent(JSON.toJSONString(order));
message.setStatus("PENDING");
localMessageMapper.insert(message);
// 事务提交后,消息状态为"待发送"
}
}
// 定时任务:发送消息
@Component
public class MessageSender {
@Scheduled(fixedDelay = 5000)
public void sendPendingMessages() {
List<LocalMessage> messages = localMessageMapper.selectByStatus("PENDING");
for (LocalMessage msg : messages) {
try {
rocketMQTemplate.send("order-topic", msg.getContent());
msg.setStatus("SENT");
localMessageMapper.update(msg);
} catch (Exception e) {
// 发送失败,重试
msg.setRetryCount(msg.getRetryCount() + 1);
localMessageMapper.update(msg);
}
}
}
}
优点:
- 实现简单,不依赖特定消息队列
- 保证消息一定会发送(定时任务重试)
缺点:
- 消息发送有延迟(定时任务扫描间隔)
- 需要维护消息表
- 可能出现重复发送(需要消费端做幂等)
方案三:消息队列 + 延迟检查
原理:先发送消息,消费者收到消息后延迟检查业务数据,确保 AB 事务已提交。
执行流程:
- A 操作执行时发送消息(消息对消费者可见)
- 消费者收到消息后,延迟一段时间(如 1 秒)
- 延迟后检查业务数据(订单是否存在)
- 如果订单存在,执行 C 操作;否则丢弃消息
核心代码:
java
// 生产者
@Service
public class OrderService {
@Transactional
public void executeAAndB() {
// A 操作:创建订单
Order order = new Order();
orderMapper.insert(order);
// 发送消息(事务未提交)
rocketMQTemplate.send("order-topic", JSON.toJSONString(order));
// B 操作:扣减账户余额
accountMapper.deduct(order.getUserId(), order.getAmount());
// 事务提交
}
}
// 消费者
@RocketMQMessageListener(topic = "order-topic", consumerGroup = "order-consumer")
public class OrderConsumer implements RocketMQListener<String> {
@Override
public void onMessage(String message) {
Order order = JSON.parseObject(message, Order.class);
// 延迟检查:等待 AB 事务提交
Thread.sleep(1000);
// 检查订单是否存在
Order dbOrder = orderMapper.selectById(order.getId());
if (dbOrder != null && dbOrder.getStatus() == 1) {
// 订单存在,说明 AB 事务已提交,执行 C 操作
inventoryService.deduct(order.getProductId(), order.getQuantity());
} else {
// 订单不存在,说明 AB 事务已回滚,丢弃消息
log.warn("订单不存在,丢弃消息: {}", order.getId());
}
}
}
优点:
- 实现简单
- 不依赖特定消息队列功能
缺点:
- 延迟时间不确定(可能过长或过短)
- 可能出现消息丢失(事务回滚但消息已发送)
- 需要消费端做幂等处理
方案四:2PC(两阶段提交)
适用场景 :场景一(AB 事务提交后 C 才开始执行)
原理:使用分布式事务协调器,保证所有参与者的操作要么全部提交,要么全部回滚。
执行流程:
- 协调者向所有参与者(A、B、C)发送 prepare 请求
- 参与者执行操作但不提交,返回 Yes/No
- 如果所有参与者都返回 Yes,协调者发送 commit
- 所有参与者提交事务
为什么适用于场景一,不适用于场景二:
| 特性 | 场景一需求 | 场景二需求 | 2PC 支持情况 |
|---|---|---|---|
| 执行顺序 | C 等待 AB 提交后才开始执行 | A 和 C 可以并行执行 | ✅ 支持(顺序执行) |
| 并行性 | 不需要并行 | 需要 A 和 C 并行 | ❌ 不支持(同步阻塞) |
| 事务提交 | C 在 AB 提交后执行 | C 在 AB 提交后提交 | ✅ 支持(统一提交) |
| 性能 | 顺序执行,性能可接受 | 需要并行,性能要求高 | ❌ 性能差(同步阻塞) |
2PC 的特点:
- ✅ 顺序执行:所有操作必须按顺序执行,等待协调者指令
- ✅ 统一提交:所有参与者要么全部提交,要么全部回滚
- ❌ 不支持并行:所有操作都是同步阻塞的,无法并行执行
- ❌ 性能差:同步阻塞导致性能较差
2PC 执行流程示例:
场景一(2PC 适用):
T1: 协调者发送 prepare 给 A、B、C
T2: A 执行操作,返回 Yes(不提交)
T3: B 执行操作,返回 Yes(不提交)
T4: C 执行操作,返回 Yes(不提交)
T5: 协调者收到所有 Yes,发送 commit
T6: A 提交事务
T7: B 提交事务
T8: C 提交事务
✅ 所有操作顺序执行,统一提交
场景二(2PC 不适用):
❌ 2PC 无法让 A 和 C 并行执行
❌ 2PC 必须等待所有参与者都 prepare 完成后才能 commit
❌ 无法实现"AC 并行执行,但 C 提交等待 AB 提交"的需求
缺点:
- 性能较差(同步阻塞,所有操作必须顺序执行)
- 单点故障风险(协调者宕机导致整个事务失败)
- 实现复杂(需要协调者、参与者、网络通信等)
- 不支持并行执行(无法满足场景二的需求)
适用场景:
- ✅ 场景一:C 操作可以等待 AB 事务提交后再开始执行
- ❌ 场景二:A 和 C 需要并行执行(2PC 不支持)
方案对比
场景一:AB 事务提交后 C 才开始执行
| 方案 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| RocketMQ 事务消息 | 保证一致性,自动处理状态不确定 | 依赖 RocketMQ,A 和 C 不能并行 | 推荐使用 |
| 本地消息表 | 实现简单,不依赖特定 MQ | 消息发送有延迟 | 对延迟不敏感的场景 |
| 延迟检查 | 实现简单 | 延迟不确定,可能丢消息 | 不推荐 |
| 2PC | 强一致性 | 性能差,实现复杂 | 不推荐 |
推荐 :优先使用 RocketMQ 事务消息 ,如果消息队列不支持事务消息,使用 本地消息表。
场景二:A 和 C 可以同时执行,但 C 必须在 AB 都提交后再提交
| 方案 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| TransactionSynchronization + 编程式事务 | A 和 C 并行执行,C 提交等待 AB 提交,使用 Spring 原生机制 | 需要手动管理事务状态,实现相对复杂 | 推荐使用(需要并行执行时) |
| CountDownLatch + 编程式事务 | 直观,易于理解 | 需要手动管理信号量 | 适合简单场景 |
| CompletableFuture + 编程式事务 | 灵活,支持链式调用 | 实现相对复杂 | 适合复杂异步场景 |
推荐 :优先使用 TransactionSynchronization + 编程式事务,它是 Spring 提供的标准机制,适合需要 A 和 C 并行执行的场景。
场景选择
- 场景一:如果 C 操作可以等待 AB 事务提交后再开始执行,使用 RocketMQ 事务消息
- 场景二:如果 A 和 C 需要并行执行以提高性能,但 C 的事务提交必须等待 AB 事务提交完成,使用 TransactionSynchronization + 编程式事务