分布式事务

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脚本
相关推荐
weixin_419658312 小时前
RabbitMQ 的高级特性
java·分布式·rabbitmq
_F_y3 小时前
仿RabbitMQ实现消息队列-服务端核心模块实现(1)
分布式·rabbitmq
.柒宇.4 小时前
RabbitMQ入门教程
分布式·rabbitmq
代码漫谈5 小时前
RabbitMQ 单节点部署指南
分布式·消息队列·rabbitmq
aLTttY6 小时前
Spring Boot + Redis 实战分布式锁:从入门到精通
spring boot·redis·分布式
weixin_419658316 小时前
RabbitMQ 应用问题
java·分布式·中间件·rabbitmq
爱艺江河7 小时前
HarmonyOS智慧风控:基于分布式架构的安全与创新实践
分布式·架构·harmonyos
juniperhan7 小时前
Flink 系列第18篇:Flink 动态表、连续查询与 Changelog 机制
java·大数据·数据仓库·分布式·flink
juniperhan7 小时前
Flink 系列第19篇:深入理解 Flink SQL 的时间语义与时区处理:从原理到实战
java·大数据·数据仓库·分布式·sql·flink