SpringBoot + 分布式锁 + 事务日志:跨服务操作原子性兜底方案

一、跨服务操作的那些坑,你踩过几个?

你是否有遇到过:用户在APP上兑换了一个价值1000积分的优惠券,系统扣了积分,但是优惠券没有发放成功。用户投诉到客服,客服查了半天,发现是中间某个服务调用失败了,导致整个流程中断。

更麻烦的是,因为是跨服务操作,数据不一致的问题很难定位和修复,最后只能手动给用户补发优惠券。

这样的场景,作为后端开发的你,是不是似曾相识?

二、为什么跨服务操作这么难?

在微服务架构下,一个业务流程往往需要多个服务协同完成:

  • 订单服务创建订单
  • 库存服务扣减库存
  • 支付服务处理支付
  • 物流服务安排发货

每个服务都是独立部署的,都有自己的数据库。当一个业务流程需要跨越多个服务时,原子性就成了一个大问题。

什么是原子性?简单来说,就是"要么全做,要么全不做"。

在单体应用中,我们可以使用数据库事务来保证原子性。但在微服务架构下,分布式事务的实现变得非常复杂。

三、分布式事务的困境

提到分布式事务,很多人会想到:

  • 2PC(两阶段提交):性能差,容易阻塞
  • TCC(Try-Confirm-Cancel):实现复杂,需要业务侵入
  • Saga:长事务,状态管理复杂
  • 本地消息表:依赖消息队列,延迟较高

这些方案各有优缺点,但在实际项目中,很多时候我们需要一个更简单、更可靠的兜底方案。

四、兜底方案:SpringBoot + 分布式锁 + 事务日志

今天,我要和大家分享一个在实战中验证过的解决方案:SpringBoot + 分布式锁 + 事务日志

这套方案的核心思想是:

  1. 使用分布式锁保证并发安全
  2. 使用事务日志记录操作过程
  3. 使用补偿机制处理失败情况

五、方案详解

1. 分布式锁:保证并发安全

在跨服务操作中,并发是一个大问题。如果多个请求同时处理同一个业务流程,可能会导致数据不一致。

(1)分布式锁实现

我们可以使用Redis来实现分布式锁:

@Component
public class RedisDistributedLock {
    
    @Autowired
    private RedisTemplate<String, String> redisTemplate;
    
    /**
     * 获取分布式锁
     */
    public boolean tryLock(String key, String value, long expireTime) {
        return redisTemplate.opsForValue()
            .setIfAbsent(key, value, expireTime, TimeUnit.SECONDS);
    }
    
    /**
     * 释放分布式锁
     */
    public boolean unlock(String key, String value) {
        String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
        Object result = redisTemplate.execute(new DefaultRedisScript<>(script, Long.class), 
            Collections.singletonList(key), value);
        return result != null && Long.valueOf(1).equals(result);
    }
}

(2)使用场景

在执行跨服务操作前,先获取分布式锁:

@Service
public class ExchangeService {
    
    @Autowired
    private RedisDistributedLock distributedLock;
    
    public void exchangeCoupon(String userId, String couponId, int points) {
        // 生成锁key
        String lockKey = "exchange:lock:" + userId + ":" + couponId;
        String lockValue = UUID.randomUUID().toString();
        
        try {
            // 获取分布式锁,有效期30秒
            boolean locked = distributedLock.tryLock(lockKey, lockValue, 30);
            if (!locked) {
                throw new RuntimeException("操作过于频繁,请稍后重试");
            }
            
            // 执行兑换流程
            doExchange(userId, couponId, points);
            
        } finally {
            // 释放分布式锁
            distributedLock.unlock(lockKey, lockValue);
        }
    }
}

2. 事务日志:记录操作过程

事务日志是整个方案的核心,它记录了跨服务操作的每一步,为后续的补偿机制提供依据。

(1)事务日志表设计

CREATE TABLE `transaction_log` (
  `id` BIGINT PRIMARY KEY AUTO_INCREMENT,
  `transaction_id` VARCHAR(64) NOT NULL COMMENT '事务ID',
  `business_type` VARCHAR(32) NOT NULL COMMENT '业务类型',
  `business_id` VARCHAR(64) NOT NULL COMMENT '业务ID',
  `status` VARCHAR(16) NOT NULL COMMENT '状态:PENDING/COMPLETED/FAILED',
  `content` JSON NOT NULL COMMENT '操作内容',
  `retry_count` INT DEFAULT 0 COMMENT '重试次数',
  `create_time` DATETIME NOT NULL,
  `update_time` DATETIME NOT NULL,
  UNIQUE KEY `uk_transaction_id` (`transaction_id`)
) COMMENT '事务日志表';

(2)事务日志服务

@Service
public class TransactionLogService {
    
    @Autowired
    private TransactionLogRepository logRepository;
    
    /**
     * 创建事务日志
     */
    @Transactional
    public TransactionLog createLog(String businessType, String businessId, Object content) {
        String transactionId = UUID.randomUUID().toString();
        TransactionLog log = new TransactionLog();
        log.setTransactionId(transactionId);
        log.setBusinessType(businessType);
        log.setBusinessId(businessId);
        log.setStatus(TransactionStatus.PENDING);
        log.setContent(JSON.toJSONString(content));
        log.setRetryCount(0);
        log.setCreateTime(LocalDateTime.now());
        log.setUpdateTime(LocalDateTime.now());
        return logRepository.save(log);
    }
    
    /**
     * 更新事务状态
     */
    @Transactional
    public void updateStatus(String transactionId, TransactionStatus status) {
        TransactionLog log = logRepository.findByTransactionId(transactionId);
        if (log != null) {
            log.setStatus(status);
            log.setUpdateTime(LocalDateTime.now());
            logRepository.save(log);
        }
    }
    
    /**
     * 获取待处理的事务日志
     */
    public List<TransactionLog> getPendingLogs(int maxRetryCount) {
        return logRepository.findByStatusAndRetryCountLessThan(
            TransactionStatus.PENDING, maxRetryCount);
    }
}

3. 补偿机制:处理失败情况

即使我们做了充分的准备,跨服务操作仍然可能失败。这时,补偿机制就派上用场了。

(1)补偿任务

@Service
@Slf4j
public class CompensationTask {
    
    @Autowired
    private TransactionLogService logService;
    @Autowired
    private ExchangeService exchangeService;
    
    /**
     * 执行补偿任务
     */
    @Scheduled(cron = "0 0/5 * * * ?") // 每5分钟执行一次
    public void executeCompensation() {
        log.info("开始执行补偿任务");
        
        try {
            // 获取待处理的事务日志(最多重试3次)
            List<TransactionLog> pendingLogs = logService.getPendingLogs(3);
            
            for (TransactionLog log : pendingLogs) {
                try {
                    // 根据业务类型执行补偿操作
                    if ("EXCHANGE_COUPON".equals(log.getBusinessType())) {
                        // 解析操作内容
                        ExchangeContent content = JSON.parseObject(
                            log.getContent(), ExchangeContent.class);
                        
                        // 重新执行兑换操作
                        exchangeService.doExchange(
                            content.getUserId(),
                            content.getCouponId(),
                            content.getPoints()
                        );
                        
                        // 更新事务状态为完成
                        logService.updateStatus(log.getTransactionId(), 
                            TransactionStatus.COMPLETED);
                        
                        log.info("补偿任务执行成功:{}", log.getTransactionId());
                    }
                    
                } catch (Exception e) {
                    log.error("补偿任务执行失败:{}", log.getTransactionId(), e);
                    // 增加重试次数
                    log.setRetryCount(log.getRetryCount() + 1);
                    logService.save(log);
                }
            }
            
        } catch (Exception e) {
            log.error("执行补偿任务失败", e);
        }
        
        log.info("补偿任务执行完成");
    }
}

(2)完整的兑换流程

@Service
public class ExchangeService {
    
    @Autowired
    private TransactionLogService logService;
    @Autowired
    private PointsService pointsService;
    @Autowired
    private CouponService couponService;
    
    /**
     * 执行积分兑换
     */
    @Transactional
    public void doExchange(String userId, String couponId, int points) {
        String transactionId = null;
        
        try {
            // 1. 创建事务日志
            ExchangeContent content = new ExchangeContent();
            content.setUserId(userId);
            content.setCouponId(couponId);
            content.setPoints(points);
            
            TransactionLog log = logService.createLog(
                "EXCHANGE_COUPON", userId, content);
            transactionId = log.getTransactionId();
            
            // 2. 扣减积分
            pointsService.deductPoints(userId, points);
            
            // 3. 发放优惠券
            couponService.grantCoupon(userId, couponId);
            
            // 4. 更新事务状态为完成
            logService.updateStatus(transactionId, TransactionStatus.COMPLETED);
            
        } catch (Exception e) {
            log.error("积分兑换失败", e);
            
            // 5. 如果事务日志已创建,更新状态为失败
            if (transactionId != null) {
                logService.updateStatus(transactionId, TransactionStatus.FAILED);
            }
            
            throw e;
        }
    }
}

六、方案优势

1. 简单可靠

  • 实现简单:不需要复杂的分布式事务框架
  • 可靠性高:即使某个服务失败,也能通过补偿机制保证最终一致性
  • 性能较好:避免了2PC等方案的性能问题

2. 可观测性强

  • 操作可追踪:每个跨服务操作都有对应的事务日志
  • 问题可定位:通过事务日志可以快速定位失败原因
  • 状态可监控:可以监控事务的执行状态和重试情况

3. 扩展性好

  • 业务侵入小:只需要在关键操作处添加事务日志
  • 支持多种业务场景:不仅适用于积分兑换,还适用于订单、支付等场景
  • 易于集成:可以轻松集成到现有的SpringBoot项目中

七、实战经验总结

  1. 锁的粒度要合适:锁的范围太大影响性能,太小可能导致并发问题
  2. 日志要详细:事务日志要记录足够的信息,便于后续的补偿和排查
  3. 补偿要有策略:设置合理的重试次数和间隔,避免无效重试
  4. 监控要到位:对事务的执行状态、失败率等指标进行监控
  5. 人工干预机制:对于补偿失败的情况,要有人工干预的通道

八、完整架构图

┌─────────────┐          ┌─────────────┐          ┌─────────────┐
│             │          │             │          │             │
│  业务请求   │─────────>│  分布式锁   │─────────>│  事务日志   │
│             │          │             │          │             │
└─────────────┘          └─────────────┘          └─────────────┘
       ^                          │                      │
       │                          │                      │
       │                          ▼                      ▼
       │                  ┌─────────────┐          ┌─────────────┐
       │                  │             │          │             │
       └──────────────────│  补偿机制   │◄─────────┤  跨服务操作  │
                         │             │          │             │
                         └─────────────┘          └─────────────┘

九、写在最后

跨服务操作的原子性是微服务架构中的一个难题,没有银弹可以解决所有问题。

今天分享的这套方案,不是最完美的,但却是在实战中验证过的、最实用的方案之一。它通过分布式锁保证并发安全,通过事务日志记录操作过程,通过补偿机制处理失败情况,最终实现了跨服务操作的原子性。

这套方案不仅适用于积分兑换,还适用于很多其他场景:

  • 订单创建 + 库存扣减 + 支付处理
  • 用户注册 + 发送短信 + 赠送积分
  • 退款处理 + 库存恢复 + 账户余额更新

希望这套方案能给你带来一些启发,帮助你在实际项目中更好地处理跨服务操作的原子性问题。

如果你有更好的解决方案,欢迎在评论区留言交流!


服务端技术精选,专注分享后端开发实战经验,让技术落地更简单。

如果你觉得这篇文章有用,欢迎点赞、在看、分享三连!


标题:SpringBoot + 分布式锁 + 事务日志:跨服务操作原子性兜底方案
作者:jiangyi
地址:http://www.jiangyi.space/articles/2026/02/10/1770534271075.html

    评论
    0 评论
avatar

取消