Java案例怎么解决事务一致性?

wen java案例 77

本文目录导读:

Java案例怎么解决事务一致性?

  1. 单体应用:使用声明式事务(最常用)
  2. 微服务/分布式系统:使用分布式事务方案
  3. 代码设计层面的注意事项
  4. 总结与选择建议

Java中解决事务一致性,核心在于确保一组数据库操作要么全部成功,要么全部失败回滚,根据应用的复杂度、性能要求和分布式程度,解决方案可以分为几个层次。

以下是详细的解决方案和案例:

单体应用:使用声明式事务(最常用)

在单服务、单数据库模式下,Spring的@Transactional注解是最简单、最可靠的方案。

核心原理: Spring AOP 拦截方法,在方法执行前开启事务,执行后提交;如果抛出运行时异常(RuntimeException)则回滚。

案例:银行转账

@Service
public class TransferService {
    @Autowired
    private AccountDao accountDao;
    // 核心:添加 @Transactional 注解
    @Transactional(rollbackFor = Exception.class) // 回滚所有异常,包括 checked exception
    public void transfer(String from, String to, BigDecimal amount) {
        // 1. 扣减转出账户
        accountDao.decreaseBalance(from, amount);
        // 2. 增加转入账户
        // 假设这里发生了异常(如:数据库宕机、除零错误、网络超时)
        accountDao.increaseBalance(to, amount);
        // 3. 记录交易日志(可选)
        // ...
    }
}

要点:

  • rollbackFor:默认只回滚RuntimeExceptionError,对于SQLException等(需要显式声明rollbackFor = Exception.class,除非它们是RuntimeException的子类)。
  • propagation:事务传播行为,最常用的是REQUIRED(默认):如果没有事务就新建一个,如果有就加入当前事务。
  • isolation:隔离级别,解决脏读、不可重复读、幻读,通常使用READ_COMMITTED
  • 注意事项@Transactional默认只对public方法生效,在同一个类中,一个非@Transactional方法调用另一个@Transactional方法,事务会失效(类内部调用不走代理)。

微服务/分布式系统:使用分布式事务方案

在跨服务、跨数据库的场景下,单体事务无法保证一致性,需要使用分布式事务方案。

方案1:可靠消息最终一致性(最常用)

适用于非实时、高可用的场景(如:用户下单 -> 扣库存 -> 发积分)。

核心原理: 将本地事务与消息发送绑定,利用消息队列(如RocketMQ、Kafka)确保“业务操作”和“消息投递”要么同时成功,要么同时失败。

案例:下单扣库存

// Service A:订单服务
@Service
public class OrderServiceImpl implements OrderService {
    @Autowired
    private OrderMapper orderMapper;
    @Autowired
    private TransactionMsgProducer msgProducer; // 消息生产者
    @Override
    @Transactional
    public void createOrder(Order order) {
        // 1. 本地事务:插入订单记录
        orderMapper.insert(order);
        // 2. 发送预备消息(半消息),消息体包含"扣减库存"指令
        // 如果这里发送失败,本地事务回滚
        msgProducer.sendHalfMessage(new StockDeductionMessage(order.getProductId(), order.getQuantity()));
        // 3. 本地事务提交(或回滚)
        // 注意:如果本地事务提交成功,消息队列必须能自动将“预备消息”转为“正式消息”,供下游消费
        // 这是通过消息队列的事务回查机制实现的
    }
}

关键点:

  • 消息回查:消息队列(RocketMQ)会定期回查业务系统,确认本地事务是否执行成功,如果成功,才投递消息。
  • 幂等性:下游服务(库存服务)必须实现幂等(使用唯一键、版本号等),防止消息重复消费导致库存重复扣减。

方案2:TCC(Try-Confirm-Cancel)

适用于强一致性、短事务的场景(如:金融交易、分布式锁)。

核心原理: 将业务逻辑分为 Try(预留资源)、Confirm(确认执行)、Cancel(取消回滚)三个阶段。

案例:跨行转账(A银行转给B银行)

// 转账服务(协调者)
public class TransferCoordinator {
    @Autowired
    private BankAService bankA;
    @Autowired
    private BankBService bankB;
    public void transfer(String from, String to, BigDecimal amount) {
        try {
            // 1. Try:尝试预留资源
            boolean tryA = bankA.tryDeduce(from, amount); // 冻结金额
            boolean tryB = bankB.tryAdd(to, amount);      // 尝试增加金额(通常直接增加,预留不适用)
            // 2. Confirm:所有Try成功,执行确认
            if (tryA && tryB) {
                bankA.confirmDeduce(from, amount); // 确认扣减
                bankB.confirmAdd(to, amount);     // 确认增加
            } else {
                // 3. Cancel:任何Try失败,执行回滚
                if (tryA) bankA.cancelDeduce(from, amount);
                if (tryB) bankB.cancelAdd(to, amount);
                throw new BusinessException("转账失败");
            }
        } catch (Exception e) {
            // 网络超时或异常时,需要重试或保证幂等
            log.error("转账失败,需人工介入或定时任务补偿", e);
        }
    }
}

注意事项:

  • 空回滚:如果Try阶段失败,但Cancel方法被调用了,需要能正确处理(资源不存在时直接成功)。
  • 幂等:Confirm和Cancel方法必须支持幂等调用。
  • 防悬挂:避免Cancel在Try之前执行(通常通过事务ID和状态表实现)。

方案3:Seata AT 模式

核心原理: 为开发者屏蔽复杂性,性能略高于TCC,AT模式通过数据源代理全局锁实现,对代码侵入性较小。

  • 原理:在业务SQL执行前,Seata记录before image;执行后,记录after image,如果全局事务失败,使用after image回滚回before image
  • 适用场景:适合大部分基于ACID的分布式事务场景,对已有代码侵入小。
  • 缺点:对SQL类型有要求(不支持复杂的批量操作),且需要全局锁,高并发下性能下降。

代码设计层面的注意事项

无论使用哪种方案,代码中都容易踩坑,以下是3个经常导致事务一致性失效的场景:

事务方法内部 try-catch 了异常

@Transactional
public void transfer() {
    try {
        accountDao.decreaseBalance(...);
        // 发生异常
        accountDao.increaseBalance(...);
    } catch (Exception e) {
        // 错误做法:捕获异常后什么都不做,或者只是打印日志
        log.error("转账失败", e);
        // 没有抛出异常,Spring认为事务正常,会提交!
    }
}

解决: 在catch块中抛出RuntimeException异常,或者手动调用TransactionAspectSupport.currentTransactionStatus().setRollbackOnly()

多线程事务

@Transactional
public void process() {
    // 主线程操作
    // ...
    new Thread(() -> {
        // 子线程操作数据库
        // 子线程有自己的独立事务!无法与主线程合并
        // 主线程回滚,子线程不会回滚
    }).start();
}

解决: 谨慎使用多线程事务,如果非要使用,考虑分布式事务方案(如TCC、Seata)或使用TransactionTemplate在子线程中手动控制。

事务方法不是 public

@Transactional注解默认只对public方法生效,如果方法声明为privateprotected,事务不会生效。

总结与选择建议

方案 复杂度 性能 一致性强度 适用场景
@Transactional 强(单体) 单数据库、单服务
可靠消息最终一致性 最终 非实时、高可用、允许短暂不一致
TCC 强( 跨服务、短事务、对一致性要求高
Seata AT 跨服务、对一致性要求高、性能适中

建议:

  • @Transactional解决的,绝对不用分布式事务。
  • 跨服务时,优先考虑可靠消息最终一致性(用MQ把事务拆成异步),如果业务能接受短暂的不一致(订单创建1秒后通知库存扣减),这是最稳定且性能最好的方案。
  • 如果必须强一致(绝对不允许账户金额多扣或少扣),则考虑TCCSeata AT,TCC虽然复杂,但更容易控制资源隔离。

无论哪种方案,幂等性是保障最终一致性的基石,在没有幂等的情况下,任何重试机制都可能导致数据不一致。

抱歉,评论功能暂时关闭!