SpringBoot + 分布式锁 + 事务日志:跨服务操作原子性兜底方案
一、跨服务操作的那些坑,你踩过几个?
你是否有遇到过:用户在APP上兑换了一个价值1000积分的优惠券,系统扣了积分,但是优惠券没有发放成功。用户投诉到客服,客服查了半天,发现是中间某个服务调用失败了,导致整个流程中断。
更麻烦的是,因为是跨服务操作,数据不一致的问题很难定位和修复,最后只能手动给用户补发优惠券。
这样的场景,作为后端开发的你,是不是似曾相识?
二、为什么跨服务操作这么难?
在微服务架构下,一个业务流程往往需要多个服务协同完成:
- 订单服务创建订单
- 库存服务扣减库存
- 支付服务处理支付
- 物流服务安排发货
每个服务都是独立部署的,都有自己的数据库。当一个业务流程需要跨越多个服务时,原子性就成了一个大问题。
什么是原子性?简单来说,就是"要么全做,要么全不做"。
在单体应用中,我们可以使用数据库事务来保证原子性。但在微服务架构下,分布式事务的实现变得非常复杂。
三、分布式事务的困境
提到分布式事务,很多人会想到:
- 2PC(两阶段提交):性能差,容易阻塞
- TCC(Try-Confirm-Cancel):实现复杂,需要业务侵入
- Saga:长事务,状态管理复杂
- 本地消息表:依赖消息队列,延迟较高
这些方案各有优缺点,但在实际项目中,很多时候我们需要一个更简单、更可靠的兜底方案。
四、兜底方案:SpringBoot + 分布式锁 + 事务日志
今天,我要和大家分享一个在实战中验证过的解决方案:SpringBoot + 分布式锁 + 事务日志。
这套方案的核心思想是:
- 使用分布式锁保证并发安全
- 使用事务日志记录操作过程
- 使用补偿机制处理失败情况
五、方案详解
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项目中
七、实战经验总结
- 锁的粒度要合适:锁的范围太大影响性能,太小可能导致并发问题
- 日志要详细:事务日志要记录足够的信息,便于后续的补偿和排查
- 补偿要有策略:设置合理的重试次数和间隔,避免无效重试
- 监控要到位:对事务的执行状态、失败率等指标进行监控
- 人工干预机制:对于补偿失败的情况,要有人工干预的通道
八、完整架构图
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ │ │ │ │ │
│ 业务请求 │─────────>│ 分布式锁 │─────────>│ 事务日志 │
│ │ │ │ │ │
└─────────────┘ └─────────────┘ └─────────────┘
^ │ │
│ │ │
│ ▼ ▼
│ ┌─────────────┐ ┌─────────────┐
│ │ │ │ │
└──────────────────│ 补偿机制 │◄─────────┤ 跨服务操作 │
│ │ │ │
└─────────────┘ └─────────────┘
九、写在最后
跨服务操作的原子性是微服务架构中的一个难题,没有银弹可以解决所有问题。
今天分享的这套方案,不是最完美的,但却是在实战中验证过的、最实用的方案之一。它通过分布式锁保证并发安全,通过事务日志记录操作过程,通过补偿机制处理失败情况,最终实现了跨服务操作的原子性。
这套方案不仅适用于积分兑换,还适用于很多其他场景:
- 订单创建 + 库存扣减 + 支付处理
- 用户注册 + 发送短信 + 赠送积分
- 退款处理 + 库存恢复 + 账户余额更新
希望这套方案能给你带来一些启发,帮助你在实际项目中更好地处理跨服务操作的原子性问题。
如果你有更好的解决方案,欢迎在评论区留言交流!
服务端技术精选,专注分享后端开发实战经验,让技术落地更简单。
如果你觉得这篇文章有用,欢迎点赞、在看、分享三连!
标题:SpringBoot + 分布式锁 + 事务日志:跨服务操作原子性兜底方案
作者:jiangyi
地址:http://www.jiangyi.space/articles/2026/02/10/1770534271075.html
评论