每日Java面试场景题知识点之-分布式事务
一、分布式事务问题由来
在单体应用时代,我们可以依赖数据库本地事务(ACID)来保证数据的一致性。一个业务操作涉及的多个数据库更新操作可以放在同一个事务中,要么全部成功,要么全部回滚。
然而随着业务发展,系统拆分为多个微服务后,情况变得复杂起来。一个业务操作可能需要调用多个微服务,每个微服务操作自己的数据库。此时,本地事务已经无法跨服务保证数据一致性,这就是分布式事务问题的由来。
典型场景举例
电商下单场景:用户下单后需要同时完成以下操作
- 在订单服务创建订单记录
- 调用库存服务扣减库存
- 调用账户服务扣减余额
- 调用积分服务增加用户积分
这些操作涉及多个独立的数据源,任何一个环节失败都会导致数据不一致。如果订单创建成功但库存扣减失败,就会出现超卖问题;如果库存扣减成功但余额扣减失败,就会出现用户未付款但库存已扣减的问题。
二、分布式事务的核心挑战
2.1 CAP理论
CAP理论指出,一个分布式系统最多只能同时满足以下三个特性中的两个:
- 一致性(Consistency):所有节点同时看到相同的数据
- 可用性(Availability):每次请求都能得到响应(成功或失败)
- 分区容错性(Partition Tolerance):系统在网络分区的情况下仍能继续运行
在分布式系统中,网络分区是不可避免的(P),因此我们只能在CP(一致性和分区容错)和AP(可用性和分区容错)之间做权衡。
2.2 BASE理论
BASE理论是对CAP理论的补充,提出基本可用、软状态和最终一致性的概念,适合互联网场景下的分布式事务处理。
- 基本可用(Basically Available):系统出现故障时,允许损失部分可用性
- 软状态(Soft State):允许数据存在中间状态,不影响系统可用性
- 最终一致性(Eventually Consistent):经过一段时间后,所有节点的数据最终达到一致
三、分布式事务解决方案详解
3.1 两阶段提交(2PC)
两阶段提交是最经典的强一致性分布式事务协议,由协调者和参与者组成。
第一阶段:准备阶段
- 协调者向所有参与者发送准备请求
- 参与者执行事务操作但不提交
- 参与者向协调者反馈是否可以提交
第二阶段:提交阶段
- 如果所有参与者都同意提交,协调者发送提交指令
- 如果有任一参与者拒绝,协调者发送回滚指令
- 参与者根据指令执行提交或回滚
2PC的缺点
- 同步阻塞:参与者在等待协调者指令期间处于阻塞状态
- 单点故障:协调者故障会导致所有参与者阻塞
- 数据不一致:协调者发送提交指令后部分参与者宕机
3.2 三阶段提交(3PC)
3PC在2PC的基础上增加了预准备阶段,降低了阻塞时间。
阶段划分
- CanCommit:协调者询问参与者是否可以执行事务
- PreCommit:参与者预执行事务,锁定资源
- DoCommit:协调者根据参与者反馈决定提交或回滚
3PC的改进与局限
改进点:引入超时机制,参与者可以自主决策 局限性:仍然存在单点故障风险,且增加了网络通信开销
3.3 TCC(Try-Confirm-Cancel)
TCC是应用层的分布式事务解决方案,每个业务操作需要实现三个方法。
TCC工作流程
- Try阶段:预留资源,检查业务条件
- Confirm阶段:确认执行,使用Try阶段预留的资源
- Cancel阶段:取消操作,释放Try阶段预留的资源
TCC示例(订单支付场景)
java
public interface OrderService {
// Try阶段:创建预扣款订单
@Compensable
boolean prepareOrder(Order order);
// Confirm阶段:确认订单
boolean confirmOrder(Order order);
// Cancel阶段:取消订单
boolean cancelOrder(Order order);
}
public interface AccountService {
// Try阶段:预扣余额
@Compensable
boolean prepareDeduct(String userId, BigDecimal amount);
// Confirm阶段:确认扣款
boolean confirmDeduct(String userId, BigDecimal amount);
// Cancel阶段:取消扣款
boolean cancelDeduct(String userId, BigDecimal amount);
}
TCC优缺点
优点:性能较好,不依赖数据库本地事务 缺点:业务代码侵入性强,需要实现三个方法,开发成本高
3.4 Saga模式
Saga将长事务拆分为多个本地短事务,每个短事务都有对应的补偿事务。
Saga执行方式
- 正向执行:依次执行各个本地事务
- 补偿执行:如果某一步失败,按相反顺序执行补偿事务
Saga实现示例
java
public class OrderSaga {
public void createOrder(Order order) {
try {
// 步骤1:创建订单
orderService.create(order);
// 步骤2:扣减库存
inventoryService.deduct(order.getProductId(), order.getQuantity());
// 步骤3:扣减余额
accountService.deduct(order.getUserId(), order.getAmount());
} catch (Exception e) {
// 补偿操作
accountService.compensateDeduct(order.getUserId(), order.getAmount());
inventoryService.compensateDeduct(order.getProductId(), order.getQuantity());
orderService.compensateCreate(order.getId());
throw e;
}
}
}
Saga优缺点
优点:适合长业务流程,可以跨多个服务 缺点:无法保证隔离性,存在脏读问题
3.5 本地消息表
本地消息表方案利用本地事务保证业务操作和消息发送的原子性。
实现原理
- 业务操作和消息记录在同一个本地事务中写入
- 定时任务扫描消息表,发送未确认的消息
- 消费者消费成功后更新消息状态
本地消息表示例
java
@Service
public class OrderServiceImpl implements OrderService {
@Autowired
private OrderMapper orderMapper;
@Autowired
private MessageMapper messageMapper;
@Autowired
private MessageProducer messageProducer;
@Transactional
public void createOrder(Order order) {
// 1. 创建订单
orderMapper.insert(order);
// 2. 创建消息记录(与订单在同一个事务中)
Message message = new Message();
message.setTopic("order.created");
message.setContent(JSON.toJSONString(order));
message.setStatus(MessageStatus.SENDING);
messageMapper.insert(message);
}
}
@Component
public class MessageTask {
@Scheduled(fixedDelay = 5000)
public void sendPendingMessages() {
List<Message> messages = messageMapper.selectByStatus(MessageStatus.SENDING);
for (Message message : messages) {
try {
messageProducer.send(message.getTopic(), message.getContent());
messageMapper.updateStatus(message.getId(), MessageStatus.SENT);
} catch (Exception e) {
log.error("发送消息失败", e);
}
}
}
}
3.6 事务消息
事务消息利用消息中间件(如RocketMQ)的事务消息机制保证最终一致性。
事务消息流程
- 发送半消息:消息不可见,不参与消费
- 执行本地事务
- 提交或回滚消息
- 消息中间件回查事务状态
RocketMQ事务消息示例
java
@Service
public class OrderTransactionListener implements TransactionListener {
@Autowired
private OrderService orderService;
@Override
public LocalTransactionState executeLocalTransaction(Message msg, Object arg) {
try {
Order order = JSON.parseObject(new String(msg.getBody()), Order.class);
orderService.createOrder(order);
return LocalTransactionState.COMMIT_MESSAGE;
} catch (Exception e) {
return LocalTransactionState.ROLLBACK_MESSAGE;
}
}
@Override
public LocalTransactionState checkLocalTransaction(MessageExt msg) {
Order order = orderService.queryByTxId(msg.getTransactionId());
if (order != null) {
return LocalTransactionState.COMMIT_MESSAGE;
}
return LocalTransactionState.UNKNOW;
}
}
@Component
public class OrderMessageProducer {
@Autowired
private RocketMQTemplate rocketMQTemplate;
public void sendOrderMessage(Order order) {
Message<Order> message = MessageBuilder.withPayload(order).build();
rocketMQTemplate.sendMessageInTransaction(
"order-group",
"order-topic",
message,
null
);
}
}
3.7 Seata分布式事务框架
Seata是阿里巴巴开源的分布式事务解决方案,支持多种事务模式。
Seata架构
- TC(Transaction Coordinator):事务协调器
- TM(Transaction Manager):事务管理器
- RM(Resource Manager):资源管理器
Seata AT模式示例
java
@Service
public class OrderServiceImpl {
@Autowired
private OrderMapper orderMapper;
@Autowired
private InventoryService inventoryService;
@Autowired
private AccountService accountService;
@GlobalTransactional(name = "create-order", rollbackFor = Exception.class)
public void createOrder(Order order) {
// 创建订单
orderMapper.insert(order);
// 调用库存服务(远程调用,自动纳入全局事务)
inventoryService.deduct(order.getProductId(), order.getQuantity());
// 调用账户服务(远程调用,自动纳入全局事务)
accountService.deduct(order.getUserId(), order.getAmount());
}
}
Seata TCC模式示例
java
@LocalTCC
public interface InventoryService {
@TwoPhaseBusinessAction(name = "prepareDeduct", commitMethod = "confirmDeduct", rollbackMethod = "cancelDeduct")
boolean prepareDeduct(@BusinessActionContextParameter(paramName = "productId") String productId,
@BusinessActionContextParameter(paramName = "quantity") int quantity);
boolean confirmDeduct(BusinessActionContext context);
boolean cancelDeduct(BusinessActionContext context);
}
@Service
public class InventoryServiceImpl implements InventoryService {
@Autowired
private InventoryMapper inventoryMapper;
@Autowired
private InventoryReservedMapper reservedMapper;
@Override
public boolean prepareDeduct(String productId, int quantity) {
// 检查库存
Inventory inventory = inventoryMapper.selectByProductId(productId);
if (inventory.getStock() < quantity) {
throw new RuntimeException("库存不足");
}
// 预留库存
InventoryReserved reserved = new InventoryReserved();
reserved.setProductId(productId);
reserved.setQuantity(quantity);
reservedMapper.insert(reserved);
return true;
}
@Override
public boolean confirmDeduct(BusinessActionContext context) {
String productId = context.getActionContext("productId").toString();
int quantity = Integer.parseInt(context.getActionContext("quantity").toString());
// 实际扣减库存
inventoryMapper.deductStock(productId, quantity);
// 删除预留记录
reservedMapper.deleteByBizKey(context.getActionContext("bizKey").toString());
return true;
}
@Override
public boolean cancelDeduct(BusinessActionContext context) {
// 删除预留记录
reservedMapper.deleteByBizKey(context.getActionContext("bizKey").toString());
return true;
}
}
四、分布式事务方案对比
4.1 强一致性方案
2PC、3PC、XA:保证强一致性,但性能较差,阻塞严重,适合对一致性要求极高且并发量不大的场景。
4.2 最终一致性方案
TCC、Saga、本地消息表、事务消息:保证最终一致性,性能较好,适合互联网高并发场景。
4.3 方案选择建议
- 高并发、允许最终不一致:TCC、Saga、事务消息
- 一致性要求高、并发量不大:2PC、XA、Seata AT
- 跨服务长流程:Saga模式
- 简单场景:本地消息表
五、分布式事务最佳实践
5.1 设计原则
- 尽量避免分布式事务:通过业务设计减少跨服务事务
- 选择合适的一致性级别:根据业务需求选择强一致或最终一致
- 幂等性设计:所有操作都要支持幂等
- 补偿机制:设计合理的补偿操作
5.2 注意事项
- 超时设置:合理设置超时时间,避免长时间阻塞
- 重试机制:设计合理的重试策略
- 监控告警:对分布式事务执行情况进行监控
- 日志记录:完整记录事务执行过程,便于排查问题
六、面试高频问题
- 什么是分布式事务?为什么需要分布式事务?
- CAP理论和BASE理论分别是什么?
- 2PC和3PC的区别是什么?
- TCC的实现原理是什么?有哪些优缺点?
- Saga模式如何保证最终一致性?
- 本地消息表和事务消息有什么区别?
- Seata支持哪些事务模式?AT模式和TCC模式的区别是什么?
- 如何选择合适的分布式事务方案?
感谢读者观看