分布式事务(二)

Saga 模式

Saga模式适用于长事务场景,通过将大事务拆分为多个本地小事务,并定义补偿操作来回滚,实现最终一致性。与TCC不同,Saga没有预留资源阶段,而是直接提交每个本地事务,因此隔离性较弱(可能脏读),但业务侵入性较低。

核心思想

  1. 将一个大事务拆分为多个连续的本地事务(Saga子事务)。

  2. 每个子事务都有对应的补偿事务,用于失败时回滚。

  3. 执行方式:顺序执行所有子事务,若某个子事务失败,则按相反顺序执行补偿事务。

两种协调模式

  • 命令式:无中心协调器,每个服务产生事件并监听其他事件。

  • 编排式:有中心协调器负责集中决策。

核心原理

1. 事务拆分与补偿链

  • 正向事务:将分布式事务拆分为连续的子事务(T₁ → T₂ → T₃)
  • 补偿事务:为每个子事务定义逆向操作(C₁ ← C₂ ← C₃)
电商下单

在这里插入图片描述

电商下单:
  T₁: 创建订单          → C₁: 取消订单
  T₂: 扣减库存          → C₂: 恢复库存
  T₃: 扣款              → C₃: 退款
  T₄: 发送订单成功通知    → C₄: 发送失败通知(可选)

2. 执行规则

  • 成功场景:顺序执行 T₁ → T₂ → T₃ → T₄
  • 失败场景
    • 若 T₃ 失败,则执行 C₃ → C₂ → C₁(反向补偿)
    • 补偿必须幂等可重试

协调模式对比

1. 命令式

在这里插入图片描述

  • 特点
    • 无中心协调器
    • 服务间通过事件驱动(如 Kafka/RabbitMQ)
  • 适用场景:简单流程(3-5 个服务)

2. 编排式

在这里插入图片描述

  • 特点
    • 中心协调器控制流程
    • 状态持久化,易于监控
  • 适用场景:复杂流程(>5 个服务)

关键问题

问题方案
隔离性缺失版本控制(如库存版本号)、业务规则约束(如订单时效性)
补偿不确定性设计等幂补偿操作、记录操作日志
流程中断超时重试机制 + 人工干预后台
调试困难分布式链路追踪 + Saga 日志快照

代码(编排式 - Spring Boot)

// 协调器服务
@Service
public class OrderSagaOrchestrator {
    @Autowired 
    private OrderService orderService;
    @Autowired 
    private InventoryService inventoryService;
    @Autowired 
    private PaymentService paymentService;

    @Transactional
    public void createOrder(OrderRequest request) {
        // 1. 创建订单
        String orderId = orderService.createOrder(request);
        
        try {
            // 2. 扣减库存
            inventoryService.deductStock(orderId, request.getItems());
            
            // 3. 扣款
            paymentService.charge(orderId, request.getAmount());
            
        } catch (Exception ex) {
            // 补偿流程(反向执行)
            paymentService.refund(orderId);      // 可能为空操作(若未扣款)
            inventoryService.restoreStock(orderId);
            orderService.cancelOrder(orderId);
            throw ex;
        }
        
        // 4. 发送通知(可异步)
        notificationService.sendOrderSuccess(orderId);
    }
}

// 库存服务(幂等补偿)
@Service
public class InventoryService {
    @Transactional
    public void deductStock(String orderId, List<Item> items) {
        // 幂等检查:若已扣减则跳过
        if (deductLogDao.exists(orderId)) return;
        
        items.forEach(item -> {
            stockDao.reduce(item.sku(), item.quantity());
            deductLogDao.save(orderId, item.sku(), item.quantity()); // 记录日志
        });
    }

    public void restoreStock(String orderId) {
        // 根据日志恢复库存
        List<DeductLog> logs = deductLogDao.findByOrderId(orderId);
        logs.forEach(log -> {
            stockDao.increase(log.getSku(), log.getQuantity());
            deductLogDao.delete(log); // 防止重复补偿
        });
    }
}

适用场景

优势

  1. 无阻塞:本地事务提交即释放资源
  2. 低侵入:无需改造现有业务接口
  3. 适应长事务:支持小时/天级事务(如物流系统)

⚠️ 劣势

  1. 弱隔离性:可能脏读(如看到中间状态订单)
  2. 补偿复杂性:需设计全链路回滚
  3. 调试困难:分布式链路跟踪成本高

🚀 适用场景

  • 电商订单履约(创建订单 → 扣库存 → 支付 → 发货)
  • 旅行预订(订机票 → 订酒店 → 租车)
  • 微服务架构下的跨系统业务流程
  • 对强一致性要求不高的场景

实践

  1. 模式选择

    • 简单流程 → 事件编排式(Kafka + 服务监听)
    • 复杂流程 → 中心协调式(Camunda/Zeebe + 状态机)
  2. 状态机实现

// 使用状态机定义Saga流程
StateMachine<OrderState, OrderEvent> stateMachine = config.buildStateMachine();
stateMachine.sendEvent(OrderEvent.CREATE_ORDER);
stateMachine.sendEvent(OrderEvent.DEDUCT_STOCK); 
if (stateMachine.hasState(OrderState.FAILED)) {
    stateMachine.sendEvent(OrderEvent.COMPENSATE); // 触发补偿
}
  1. 可视化监控

    • 持久化 Saga 日志到数据库:
      CREATE TABLE saga_log (
          saga_id VARCHAR(36) PRIMARY KEY,
          current_step VARCHAR(50),
          status ENUM('PENDING','SUCCESS','FAILED','COMPENSATING'),
          created_time TIMESTAMP
      );
      
    • 结合 Grafana 展示事务状态大盘
  2. 容错设计

    • 补偿操作必须实现 等幂性
    • 部署后台任务修复中断的 Saga:
      @Scheduled(fixedDelay = 300000) // 每5分钟扫描
      public void repairSagas() {
          List<Saga> stuckSagas = sagaDao.findByStatusAndTimeout("PENDING", now()-1h);
          stuckSagas.forEach(saga -> sagaService.retryOrCompensate(saga));
      }
      

Saga 是处理长周期分布式事务的经典模式,适用于业务链路过长且无法同步阻塞的场景。关键成功要素在于补偿操作的完备性设计全链路可观测性。在金融等高一致性领域,建议结合业务规则增强隔离性(如预冻结资金)。


本地消息表

本地消息表是一种基于最终一致性的分布式事务方案,通过业务数据库+消息日志的组合,实现跨服务事务的可靠传递。

核心思想

先执行本地事务,再异步通知下游。
在这里插入图片描述

核心原理

1. 事务发起阶段

// 订单服务创建订单
@Transactional
public void createOrder(Order order) {
    // 1. 业务操作:创建订单记录
    orderDao.insert(order); 
    
    // 2. 事务操作:写入本地消息表(同库同事务)
    Message msg = new Message();
    msg.setBizId(order.getId());
    msg.setContent(JSON.toJSON(order));
    msg.setStatus("UNSENT"); // 初始状态
    messageDao.insert(msg);
    
    // 事务提交:订单数据和消息记录原子性持久化
}
  • 关键点:业务操作与消息写入在同一个数据库事务
  • 消息表结构
    CREATE TABLE local_message (
        id BIGINT PRIMARY KEY AUTO_INCREMENT,
        biz_id VARCHAR(64) NOT NULL,     -- 业务ID(如订单ID)
        content TEXT NOT NULL,           -- 消息内容(JSON)
        status ENUM('UNSENT','SENT','CONSUMED') NOT NULL,
        retry_count INT DEFAULT 0,
        created_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP
    );
    

2. 消息投递阶段

在这里插入图片描述

  • 可靠性保障
    • 定时任务扫描未发送(UNSENT)或发送失败的消息
    • 消息发送成功后更新状态为SENT
    • 失败重试(指数退避策略),超过阈值报警

3. 消息消费阶段

// 下游服务:库存消费扣减消息
@KafkaListener(topics = "order_created")
public void consumeOrderMessage(Message message) {
    // 1. 幂等检查(防止重复消费)
    if (deductLogService.exists(message.getBizId())) {
        return; 
    }
    
    // 2. 执行业务操作
    inventoryService.deductStock(message.getContent());
    
    // 3. 记录消费日志(业务事务)
    deductLogService.logSuccess(message.getBizId());
    
    // 4. 可选:向上游发送ACK
}
  • 关键设计
    • 消费幂等性:通过biz_id去重
    • 业务与日志原子性:扣减库存和记录日志在同一事务
    • ACK机制:消息队列自动ACK或手动ACK

消息可靠投递方案

方案1:本地消息表

  • 优点:实现简单,无中间件依赖
  • 缺点:消息表与业务库耦合

方案2:独立消息服务

在这里插入图片描述

  • 优势:业务与消息解耦
  • 代价:增加一次网络调用

方案3:RocketMQ事务消息

// RocketMQ 事务消息
public void createOrderWithMQ(Order order) {
    // 1. 发送半消息(对消费者不可见)
    TransactionSendResult result = producer.sendMessageInTransaction(
        new Message("order_topic", JSON.toJSONBytes(order)), 
        null
    );
    
    // 2. 执行本地事务
    if (result.getLocalTransactionState() == LocalTransactionState.COMMIT_MESSAGE) {
        orderDao.insert(order); 
    }
}

// 事务监听器(实现回查)
class OrderTransactionListener implements TransactionListener {
    @Override
    public LocalTransactionState executeLocalTransaction(Message msg, Object arg) {
        return LocalTransactionState.COMMIT_MESSAGE; // 根据业务结果返回
    }
}
  • 流程
    1. 发送半消息 → 2. 执行本地事务 → 3. 提交/回滚消息
  • 优势:无需本地消息表,由MQ保证最终一致性

方案4:MySQL Binlog监听(Canal + Kafka)

在这里插入图片描述

  • 适用场景:数据库变更通知(无需改业务代码)
  • 局限:只能捕获DB变更,无法携带业务上下文

消息丢失与重复的策略

风险产生原因方案
消息丢失投递前服务宕机事务保证消息持久化
重复消费消费后ACK失败消费端幂等设计 + 去重表
消息无序多分区并行消费相同biz_id路由到固定分区
消息积压消费速度<生产速度动态扩容消费者 + 降级策略

完整代码(Spring Boot + RabbitMQ)

1. 消息实体与存储

@Entity
@Table(name = "local_message")
public class LocalMessage {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String bizId;      // 业务ID(订单ID等)
    private String content;    // JSON消息体
    private String status;     // UNSENT/SENT/CONSUMED
    private int retryCount;
    private LocalDateTime createdTime;
}

2. 消息发送定时任务

@Scheduled(fixedRate = 10000) // 每10秒执行
public void sendPendingMessages() {
    List<LocalMessage> messages = messageRepo.findByStatus("UNSENT");
    
    for (LocalMessage msg : messages) {
        try {
            // 发送到RabbitMQ
            rabbitTemplate.convertAndSend("order.exchange", "order.key", msg.getContent());
            msg.setStatus("SENT");
            messageRepo.save(msg);
        } catch (Exception e) {
            msg.setRetryCount(msg.getRetryCount() + 1);
            if (msg.getRetryCount() > 5) {
                alertService.notifyAdmin("消息发送失败", msg.getId());
            }
        }
    }
}

3. 下游幂等消费

@RabbitListener(queues = "order.queue")
@Transactional
public void handleOrderMessage(String jsonMessage, Message amqpMessage) {
    OrderMessage message = JSON.parseObject(jsonMessage, OrderMessage.class);
    
    // 幂等检查
    if (deductLogRepository.existsByBizId(message.getOrderId())) {
        channel.basicAck(amqpMessage.getMessageProperties().getDeliveryTag(), false);
        return;
    }
    
    // 业务操作(扣减库存)
    inventoryService.deduct(message.getSku(), message.getQuantity());
    
    // 记录消费日志
    DeductLog log = new DeductLog(message.getOrderId(), "SUCCESS");
    deductLogRepository.save(log);
    
    // 手动ACK
    channel.basicAck(amqpMessage.getMessageProperties().getDeliveryTag(), false);
}

4. 对账补偿任务

@Scheduled(cron = "0 0 3 * * ?") // 每天凌晨3点
public void reconcileMessages() {
    // 检查已发送未消费的消息
    List<LocalMessage> sentMessages = messageRepo.findByStatus("SENT");
    sentMessages.forEach(msg -> {
        if (!deductLogRepository.existsByBizId(msg.getBizId())) {
            // 重新投递
            rabbitTemplate.convertAndSend("order.exchange", "order.key", msg.getContent());
        }
    });
}

适用场景

优势

  1. 业务侵入低:只需添加消息表写入
  2. 高可靠性:本地事务保证消息必达
  3. 架构简单:无需额外中间件(如Seata)

⚠️ 劣势

  1. 时效性差:异步投递有延迟(秒级~分钟级)
  2. 耦合业务库:消息表与业务库同实例
  3. 维护成本:需开发消息管理后台

🚀 适用场景

  • 订单支付后通知发货
  • 用户注册送积分
  • 数据变更同步(主库→搜索索引)
  • 所有接受最终一致性的业务场景

实践

  1. 消息表分库分表

    -- 按业务ID哈希分表
    CREATE TABLE local_message_00 ( ... ) ENGINE=InnoDB;
    CREATE TABLE local_message_01 ( ... ) ENGINE=InnoDB;
    
  2. 消息压缩与清理

    // 只保留7天消息
    @Scheduled(daily)
    public void cleanOldMessages() {
        messageRepo.deleteByStatusAndCreatedTimeBefore(
            "CONSUMED", LocalDateTime.now().minusDays(7)
        );
    }
    
  3. 监控指标

    • 消息积压量(UNSENT状态数)
    • 平均投递延迟(写入到发送的时间差)
    • 消费失败率(重试次数>3的消息比例)
  4. 灰度方案

    // 在消息头添加灰度标记
    MessageProperties props = new MessageProperties();
    props.setHeader("gray", "v2");
    rabbitTemplate.send(new Message(json.getBytes(), props));
    

技术选型建议

  • 简单场景 → 本地消息表
  • 高并发场景 → RocketMQ事务消息
  • 零侵入改造 → Binlog监听方案

本地消息表是分布式事务的实用主义解决方案,平衡了实现复杂度与可靠性。在80%的业务场景中,它比2PC/TCC更具性价比,尤其适合中长尾业务系统。


03-26
### 逆向工程与反编译概述 逆向工程是一种通过对软件的目标代码进行分析,将其转化为更高级别的表示形式的过程。这一过程通常用于研究现有系统的内部结构、功能以及实现细节。在Java和Android领域,反编译工具被广泛应用于逆向工程中。 #### Java逆向工程中的Jad反编译工具 Jad是一款经典的Java反编译工具,能够将`.class`字节码文件转换为可读的`.java`源代码[^1]。虽然它可能无法完全恢复原始源代码,但它提供了足够的信息来帮助开发者理解已编译的Java程序逻辑。Jad支持多种反编译模式,并允许用户自定义规则以适应不同的需求。此外,其命令行接口和图形界面使得复杂代码的分析变得更加便捷。 #### Android逆向工程中的JEB反编译工具 针对Android应用的逆向工程,JEB是由PNF Software开发的一款专业级工具[^2]。相较于其他同类产品,JEB不仅具备强大的APK文件反编译能力,还能对Dalvik字节码执行高效而精准的操作。它的核心优势在于以下几个方面: - **广泛的平台兼容性**:除Android外,还支持ARM、MIPS等多种架构的进制文件反汇编。 - **混淆代码解析**:内置模块能有效应对高度混淆的代码,提供分层重构机制以便于深入分析。 - **API集成支持**:允许通过编写Python或Java脚本来扩展功能并完成特定任务。 #### APK反编译流程及其意义 当涉及到具体的APK包时,可以通过一系列步骤提取其中的信息来进行全面的安全评估或者学习目的的研究工作[^3]。这些步骤一般包括但不限于获取资产目录(`assets`)内的资源数据;解密XML配置文档如`AndroidManifest.xml`定位应用程序启动点;最后利用上述提到的各种专用软件重现整个项目框架供进一步探讨。 ```bash # 使用apktool反编译APK示例 apktool d your_app.apk -o output_directory/ ``` 以上命令展示了如何借助开源工具ApkTool轻松拆卸目标安卓档案至易于探索的状态下。 ### 结论 无论是传统的桌面端还是现代移动端环境里头,恰当运用合适的反编译解决方案都是达成逆向工程项目成功不可或缺的一环。每种工具有各自专精之处,在实际应用场景当中应当依据具体需求做出明智的选择。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值