Java 分布式事务是指:一个业务操作涉及多个服务或多个数据库时,保证这些操作要么全部成功,要么全部失败。
比如一个电商下单流程:
创建订单(订单服务 + 订单数据库)
扣减库存(库存服务 + 库存数据库)
扣减余额(账户服务 + 账户数据库)
如果其中一个失败:
订单不能创建成功
库存不能扣
余额不能扣
否则数据就会不一致。
一、为什么会有分布式事务
单体系统:
一个应用 一个数据库
使用本地事务即可:
@Transactional
数据库保证:
ACID
但在微服务架构中:
订单服务 -> 订单库 库存服务 -> 库存库 账户服务 -> 账户库
事务跨多个服务:
本地事务无法保证一致性
所以需要 分布式事务。
二、分布式事务核心理论
最重要的是 CAP 和 BASE。
CAP
分布式系统三要素:
C Consistency 一致性 A Availability 可用性 P Partition 分区容错
分布式系统只能保证:
CP 或 AP
互联网系统通常选择:
AP
牺牲:
强一致性
换取:
高可用
BASE理论
互联网系统常用:
Basically Available Soft State Eventually Consistent
意思:
最终一致性
三、常见分布式事务方案
Java常见方案有 4 种:
1 2PC(两阶段提交) 2 TCC 3 本地消息表 4 可靠消息最终一致性 5 Seata
一、2PC(Two Phase Commit 两阶段提交)
2PC 是最经典的分布式事务协议,主要由 事务协调者(Coordinator) 控制。
参与角色:
Coordinator 事务协调者 Participant 参与者
例如:
订单服务 库存服务 账户服务
1 流程
第一阶段:Prepare(准备阶段)
协调者通知所有参与者:
是否可以提交事务?
参与者执行 SQL,但不提交:
write undo log 锁定资源 返回 yes/no
流程:
Coordinator │ prepare │ ┌─┴────────┐ 订单服务 库存服务 执行SQL 执行SQL 锁资源 锁资源
第二阶段:Commit / Rollback
如果全部返回 yes
commit
如果有一个失败:
rollback
流程:
Coordinator │ commit │ ┌─┴────────┐ 订单服务 库存服务 提交事务 提交事务
2 Java实现(JTA + XA)
常见实现:
Atomikos Bitronix Narayana
Spring Boot 示例:
依赖:
XML<dependency> <groupId>com.atomikos</groupId> <artifactId>transactions-spring-boot-starter</artifactId> </dependency>
配置两个数据源:
java
@Bean
public DataSource orderDataSource(){
AtomikosDataSourceBean ds = new AtomikosDataSourceBean();
ds.setUniqueResourceName("orderDB");
ds.setXaDataSourceClassName("com.mysql.cj.jdbc.MysqlXADataSource");
return ds;
}
业务代码:
java
@Transactional
public void createOrder(){
orderDao.insert();
stockDao.deduct();
}
JTA 自动协调:
prepare commit
3 优缺点
优点:
强一致性 实现标准
缺点:
同步阻塞 性能差 锁资源时间长 协调者单点
互联网基本不用。
二、TCC事务(Try Confirm Cancel)
TCC 是 补偿型事务。
三个阶段:
Try Confirm Cancel
1 流程
例如下单:
Try阶段
预留资源:
冻结库存 冻结余额
流程:
订单服务 │ Try │ ┌─┴────────┐ 库存服务 账户服务 冻结库存 冻结余额
Confirm阶段
真正执行:
扣库存 扣余额
Cancel阶段
如果失败:
释放库存 解冻余额
2 Java代码示例
Try:
java
public boolean tryDeduct(Long productId, int count){
stockDao.freezeStock(productId, count);
return true;
}
Confirm:
java
public void confirmDeduct(Long productId, int count){
stockDao.deductStock(productId, count);
}
Cancel:
java
public void cancelDeduct(Long productId, int count){
stockDao.releaseStock(productId, count);
}
订单服务调用:
java
public void createOrder(){
stockService.tryDeduct();
accountService.tryPay();
}
3 优缺点
优点:
性能高 不会长时间锁资源 适合高并发
缺点:
业务侵入强 需要写3个接口 开发复杂
适用:
金融系统 支付系统
三、本地消息表
阿里巴巴早期方案。
核心思想:
本地事务 + 消息表 + MQ
1 流程
订单服务:
订单表 消息表
步骤:
1 创建订单 2 写消息表 3 提交事务
然后:
定时任务发送MQ
流程:
订单服务 │ 本地事务 │ 订单表 + 消息表 │ 定时任务 │ MQ │ 库存服务
2 Java代码
订单事务:
java
@Transactional
public void createOrder(){
orderDao.insert(order);
messageDao.insert(
new Message("order_create", order.getId())
);
}
定时任务发送消息:
java
@Scheduled(fixedDelay = 5000)
public void sendMessage(){
List<Message> list = messageDao.getUnSend();
for(Message m : list){
mq.send(m);
messageDao.markSend(m.getId());
}
}
库存服务:
java
@MQListener
public void deductStock(OrderMessage msg){
stockDao.deduct(msg.getProductId());
}
3 优缺点
优点:
可靠性高 实现简单
缺点:
需要维护消息表 定时任务扫描
四、可靠消息最终一致性(MQ事务消息)
这是 互联网最常用方案。
常见 MQ:
RocketMQ Kafka RabbitMQ
1 流程
订单服务:
发送半消息
然后:
执行本地事务
成功:
提交消息
失败:
回滚消息
流程:
订单服务 │ Half Message │ MQ │ 执行本地事务 │ commit message │ 库存服务消费
2 Java示例(RocketMQ)
发送事务消息:
java
rocketMQTemplate.sendMessageInTransaction(
"order_tx_topic",
MessageBuilder.withPayload(order).build(),
null
);
事务监听器:
java
@RocketMQTransactionListener
public class OrderTransactionListener
implements RocketMQLocalTransactionListener {
@Override
public RocketMQLocalTransactionState executeLocalTransaction(Message msg, Object arg) {
try{
orderDao.insert(order);
return RocketMQLocalTransactionState.COMMIT;
}catch(Exception e){
return RocketMQLocalTransactionState.ROLLBACK;
}
}
@Override
public RocketMQLocalTransactionState checkLocalTransaction(Message msg) {
return RocketMQLocalTransactionState.COMMIT;
}
}
库存服务消费:
java
@RocketMQMessageListener(topic = "order_tx_topic")
public class StockConsumer implements RocketMQListener<Order>{
public void onMessage(Order order){
stockDao.deduct(order.getProductId());
}
}
3 优缺点
优点:
解耦 高并发 可靠
缺点:
最终一致性 不是强一致
互联网系统最常见。
五、Seata(AT模式)
Seata 是 Java 微服务最流行的分布式事务框架。
角色:
TC 事务协调器 TM 事务管理器 RM 资源管理器
1 流程
订单服务开启全局事务:
@GlobalTransactional
流程:
订单服务 │ TC注册事务 │ 订单服务SQL 记录undo_log │ 库存服务SQL 记录undo_log │ 提交 or 回滚
回滚通过:
undo_log
恢复数据。
2 Java代码
订单服务:
java
@GlobalTransactional
public void createOrder(){
orderDao.insert(order);
stockService.deduct(order.getProductId());
}
库存服务:
java
@Transactional
public void deduct(Long productId){
stockDao.deduct(productId);
}
Seata 自动:
记录undo_log 回滚事务
3 undo_log表
Seata自动创建:
undo_log
作用:
记录SQL前镜像 记录SQL后镜像
回滚时恢复。
六、真实互联网使用比例
| 方案 | 使用情况 |
|---|---|
| 2PC | 几乎不用 |
| TCC | 金融系统 |
| 本地消息表 | 部分系统 |
| MQ最终一致性 | 互联网最常见 |
| Seata | Java微服务常见 |
最主流:
MQ最终一致性 Seata AT
================================================================
一个 互联网电商真实下单架构(订单 + 库存 + 支付 + 积分) 的分布式事务设计。
这是很多公司常见实现:MQ最终一致性 + 本地事务。
核心思想:
订单服务只保证自己的事务成功,然后通过 MQ 通知其他服务。
系统结构:
用户下单 │ 订单服务 │ 本地事务(订单库) │ 发送MQ消息 │ ┌─────────┼─────────┐ 库存服务 支付服务 积分服务 扣库存 扣余额 增加积分
一、数据库设计
订单库:
order id user_id product_id amount status create_time
库存库:
stock product_id count
账户库:
account user_id balance
积分库:
points user_id points
二、下单完整流程
流程图:
用户下单 │ 订单服务 createOrder() │ 本地事务 │ 写订单表 │ 发送MQ订单消息 │ MQ │ ┌─────────┬─────────┬─────────┐ 库存服务 支付服务 积分服务 扣库存 扣余额 增加积分
特点:
订单成功 其他服务最终一致
三、订单服务实现
依赖:
SpringBoot MyBatis RocketMQ
1 订单实体
java
public class Order {
private Long id;
private Long userId;
private Long productId;
private BigDecimal amount;
private Integer status;
}
2 创建订单
java
@Service
public class OrderService {
@Autowired
private OrderMapper orderMapper;
@Autowired
private RocketMQTemplate rocketMQTemplate;
@Transactional
public void createOrder(Order order){
order.setStatus(0);
orderMapper.insert(order);
rocketMQTemplate.convertAndSend(
"order_create_topic",
order
);
}
}
事务保证:
订单写入成功 消息发送
四、库存服务
监听订单消息:
java
@RocketMQMessageListener(topic = "order_create_topic")
@Service
public class StockConsumer implements RocketMQListener<Order> {
@Autowired
private StockMapper stockMapper;
@Override
public void onMessage(Order order){
stockMapper.deduct(order.getProductId(),1);
}
}
库存SQL:
sql
update stock
set count = count - 1
where product_id = ?
and count > 0
五、支付服务
java
@RocketMQMessageListener(topic = "order_create_topic")
@Service
public class PayConsumer implements RocketMQListener<Order> {
@Autowired
private AccountMapper accountMapper;
@Override
public void onMessage(Order order){
accountMapper.deduct(
order.getUserId(),
order.getAmount()
);
}
}
SQL:
sql
update account
set balance = balance - ?
where user_id = ?
六、积分服务
java
@RocketMQMessageListener(topic = "order_create_topic")
@Service
public class PointConsumer implements RocketMQListener<Order> {
@Autowired
private PointMapper pointMapper;
@Override
public void onMessage(Order order){
int point = order.getAmount().intValue();
pointMapper.addPoint(
order.getUserId(),
point
);
}
}
SQL:
sql
update points
set points = points + ?
where user_id = ?
七、完整流程图
用户 │ ▼ 订单服务 createOrder │ ▼ 本地事务 ┌───────────────┐ │ 插入订单数据 │ └───────────────┘ │ ▼ 发送MQ消息 │ ▼ RocketMQ │ ┌─────────┼─────────┬─────────┐ ▼ ▼ ▼ 库存服务 支付服务 积分服务 扣库存 扣余额 增积分
八、幂等性处理(非常重要)
MQ可能 重复消费。
解决:
幂等表
表:
mq_log msg_id status
代码:
java
public void onMessage(Order order){
if(mqLogService.exists(order.getId())){
return;
}
stockMapper.deduct(order.getProductId(),1);
mqLogService.save(order.getId());
}
保证:
消息只执行一次
九、失败补偿机制
可能情况:
订单成功 库存失败
解决:
重试机制
MQ自动:
消费失败 -> 重新投递
或者:
死信队列
十、真实互联网架构
大型电商通常是:
用户下单 │ 订单服务 │ RocketMQ │ ┌─────┬─────┬─────┬─────┐ 库存 支付 积分 物流
特点:
高并发 解耦 最终一致
十一、真实面试追问
面试官几乎一定会问:
1 MQ消息丢失怎么办? 2 如何保证消息不重复消费? 3 如何保证订单和MQ一致? 4 如何保证库存不会超卖?
这些是 电商架构最核心问题。
解答:
1 MQ消息丢失怎么办?
MQ消息可能丢失的地方有三处:
生产者 -> MQ MQ -> 磁盘 MQ -> 消费者
必须分别保证。
1 生产者消息丢失
可能情况:
订单写入成功 MQ发送失败
解决方案:
发送确认机制(ACK)
RocketMQ / Kafka 都支持。
示例:
java
SendResult result = rocketMQTemplate.syncSend(
"order_topic",
order
);
if(result.getSendStatus() != SendStatus.SEND_OK){
throw new RuntimeException("消息发送失败");
}
如果发送失败:
事务回滚
2 MQ服务器丢失
如果 MQ 宕机,消息可能丢失。
解决:
消息持久化 + 多副本
RocketMQ:
SYNC_FLUSH SYNC_MASTER
Kafka:
replication.factor=3 acks=all
保证:
消息写入多个节点
3 消费者丢失
如果消费者处理失败:
消费成功 但没处理完
解决:
消费确认机制
RocketMQ:
消费失败 -> 自动重试
代码:
java
public void onMessage(Order order){
try{
stockService.deduct(order);
}catch(Exception e){
throw new RuntimeException("消费失败");
}
}
MQ会:
重新投递消息
2 如何保证消息不重复消费?
MQ天然可能 重复投递。
必须做:
幂等性处理
方案1:幂等表
表:
mq_consume_log msg_id consumer status
流程:
先查是否处理过 没处理才执行
代码:
java
public void onMessage(Order order){
if(logService.exists(order.getId())){
return;
}
stockService.deduct(order);
logService.save(order.getId());
}
方案2:唯一索引
利用数据库唯一键。
例如:
order_id UNIQUE
SQL:
sql
insert into stock_log(order_id)
values(?)
如果重复:
insert失败
3 如何保证订单和MQ一致?
这是 最经典问题。
可能情况:
订单成功 MQ没发出去
会导致:
库存没扣
方案1:本地消息表(最经典)
流程:
本地事务 订单表 + 消息表
结构:
order message
代码:
java
@Transactional
public void createOrder(Order order){
orderMapper.insert(order);
messageMapper.insert(
new Message(order.getId(),"order_create")
);
}
定时任务:
java
@Scheduled
public void sendMsg(){
List<Message> list = messageMapper.getUnSend();
for(Message m : list){
rocketMQTemplate.convertAndSend(
"order_topic",
m
);
messageMapper.updateStatus(m.getId());
}
}
优点:
绝对不会丢消息
方案2:MQ事务消息
RocketMQ支持。
流程:
1 发送half消息 2 执行本地事务 3 commit消息
代码:
java
rocketMQTemplate.sendMessageInTransaction(
"order_tx_topic",
MessageBuilder.withPayload(order).build(),
null
);
监听器:
java
public RocketMQLocalTransactionState executeLocalTransaction(
Message msg, Object arg) {
try{
orderDao.insert(order);
return RocketMQLocalTransactionState.COMMIT;
}catch(Exception e){
return RocketMQLocalTransactionState.ROLLBACK;
}
}
4 如何保证库存不会超卖?
超卖:
库存10 并发1000
必须控制。
常见4种方案。
方案1 数据库乐观锁
表:
stock count version
SQL:
sql
update stock
set count = count - 1,
version = version + 1
where product_id = ?
and version = ?
如果版本不一致:
更新失败
方案2 Redis预扣库存(最常用)
流程:
先扣Redis库存 再写数据库
代码:
java
Long stock = redisTemplate.opsForValue().decrement("stock:1001");
if(stock < 0){
throw new RuntimeException("库存不足");
}
Redis:
原子操作
非常快。
方案3 分布式锁
例如:
Redisson
代码:
java
RLock lock = redissonClient.getLock("stock_lock");
lock.lock();
try{
stockService.deduct();
}finally{
lock.unlock();
}
缺点:
性能低
方案4 Lua脚本(秒杀常用)
Redis Lua:
Lua
local stock = redis.call("get", KEYS[1])
if tonumber(stock) <= 0 then
return -1
end
redis.call("decr", KEYS[1])
return 1
Java:
java
redisTemplate.execute(luaScript,keys);
优点:
原子 高并发
五、真实电商库存架构
大厂一般这样:
用户下单 │ Redis预扣库存 │ 发送MQ │ 库存服务 │ 更新数据库库存
架构:
用户 │ ▼ Redis库存 │ ▼ MQ │ ▼ 库存服务 │ ▼ MySQL库存
优点:
抗高并发 不会超卖
六、面试标准回答总结
面试官问:
如何保证消息可靠?
回答结构:
1 生产者确认机制 2 MQ持久化 + 多副本 3 消费者ACK机制
如何保证不重复消费?
幂等设计 唯一索引 幂等表
订单和MQ一致?
本地消息表 事务消息
库存防超卖?
Redis预扣库存 + Lua脚本