Spring 错误使用事务导致数据可见性问题分析

背景

继续,案例同样来自于企业级代码,这里是一个错误使用事务导致的故障。

因为网络隔离,两个服务依赖于同一个数据库做数据传递,一个服务开启事务写入数据并通知另一个服务,另一个服务收到通知后更新数据。故障现象是两个服务和数据库都没有明显异常,但会偶尔发生数据未被更新。

经过排查确认第一个服务事务未被正确使用,如果第二个服务消费数据快于第一个服务事务提交速度就会发生数据更新丢失的问题,下面对问题做了抽象简要回顾。


1. 问题发生原因:单个事务控制不当导致调用服务更新丢失

1.1 问题场景描述

在典型的业务场景中,需要先创建数据记录,然后调用外部服务进行后续处理。错误的实现将数据库操作和外部服务调用放在同一个事务中:

java 复制代码
// ❌ 错误的实现方式
@Transactional(rollbackFor = Exception.class)
public void processBusinessRequest(BusinessRequest request, String operator) {
    // 1. 在事务中创建数据库记录
    String taskId = createBusinessRecord(request, operator);
    
    // 2. 在同一事务中调用外部服务
    callExternalService(taskId, request); // 问题发生点
}

1.2 问题表现

  1. 数据可见性问题:外部服务查询不到刚创建的业务记录
  2. 业务逻辑错误:外部服务基于空数据执行错误的业务逻辑
  3. 数据状态不一致:本地认为数据已创建,外部服务认为数据不存在
  4. 性能问题:长时间持有数据库锁,影响并发性能

2. 问题根本原因:事务传播机制与隔离机制

2.1 事务隔离级别机制

READ COMMITTED隔离级别(Spring与MySQL默认配置)

READ COMMITTED规则:

  • 只能读取已提交的数据
  • 防止脏读,但允许不可重复读
  • 未提交的事务对其他事务不可见

MVCC多版本并发控制

ini 复制代码
数据版本链机制:
时间点1: 事务A开始 (TxID=100)
时间点2: INSERT batch_001 -> [batch_001@100, 未提交] -> [空表@0, 已提交]
时间点3: 事务B查询 (TxID=101) -> 只能看到已提交版本 -> 返回空表
时间点4: 事务A提交 -> [batch_001@100, 已提交] -> 所有事务可见

2.2 事务传播机制

REQUIRED传播行为(默认)

java 复制代码
@Transactional // 默认 propagation = Propagation.REQUIRED
public void outerMethod() {
    // 外层事务开始
    innerMethod(); // 加入外层事务,不会创建新事务
    // 外层事务结束时才提交
}

@Transactional
public void innerMethod() {
    // 数据库操作在外层事务中执行
    // 此时数据对外部不可见
}

问题时序分析

sql 复制代码
时间轴    应用A(事务中)              外部服务B                数据库MVCC
  |                                 |                       |
  1   BEGIN TRANSACTION            |                       | 创建事务A(TxID=100)
  |                                 |                       |
  2   INSERT business_task         |                       | 写入数据(TxID=100,未提交)
  |                                 |                       | 版本链: [task_001@100] -> [空@0]
  |                                 |                       |
  3   HTTP调用外部服务 -----------> | 查询业务上下文          |
  |                                 |                       |
  4                                 | SELECT task_001 -----> | 新事务B(TxID=101)
  |                                 |                       | READ_committed规则:
  |                                 |                       | 只能读取已提交数据
  5                                 | <-- 返回空结果 -------- | task_001@100未提交,不可见
  |                                 |                       |
  6                                 | 基于空数据执行业务逻辑   |
  |                                 |                       |
  7   外部服务完成 <-------------- | 返回处理结果            |
  |                                 |                       |
  8   COMMIT -----------------> |                       | 提交事务A
  |                                 |                       |
  9   业务状态不一致!              |                       | 数据现在可见,但外部服务
      本地:任务已创建              |                       | 已基于"无数据"执行完毕
      外部:基于空数据处理          |                       |

2.3 技术验证

可以通过SQL直接验证隔离机制:

sql 复制代码
-- 会话1:模拟应用事务
BEGIN;
INSERT INTO business_task (task_id, status) VALUES ('task_001', 'PENDING');
-- 不提交事务

-- 会话2:模拟外部服务查询
SELECT * FROM business_task WHERE task_id = 'task_001';
-- 结果:Empty set (READ COMMITTED隔离机制)

-- 会话1:提交事务
COMMIT;

-- 会话2:再次查询
SELECT * FROM business_task WHERE task_id = 'task_001';
-- 结果:现在能查询到数据了

3. 修复方法:使用Self注入完成事务代理

3.1 基于代理的事务原理

Spring AOP事务代理机制

java 复制代码
// Spring为Service类创建的代理对象
public class BusinessServiceImpl$$SpringProxy implements BusinessService {
    
    private BusinessServiceImpl target; // 真实对象
    private TransactionInterceptor transactionInterceptor; // 事务拦截器
    
    @Override
    public String createDistributionRecordsInTransaction(ScriptDeployDTO deployDTO, String operator) {
        // 1. 事务拦截器开始事务
        TransactionStatus status = transactionManager.getTransaction(transactionDefinition);
        
        try {
            // 2. 调用真实对象的方法
            String result = target.createDistributionRecordsInTransaction(deployDTO, operator);
            
            // 3. 提交事务
            transactionManager.commit(status);
            return result;
            
        } catch (Exception e) {
            // 4. 回滚事务
            transactionManager.rollback(status);
            throw e;
        }
    }
}

内部方法调用的问题

java 复制代码
@Service
public class BusinessServiceImpl {
    
    public void deployScript() {
        // ❌ 直接调用内部方法,绕过了代理对象
        createRecords(); // 事务注解不生效!
    }
    
    @Transactional
    public void createRecords() {
        // 事务不会开启,因为没有通过代理调用
    }
}

3.2 Self注入解决方案

正确的实现方式

java 复制代码
@Service
public class BusinessServiceImpl implements BusinessService {
    
    @Autowired
    private BusinessServiceImpl self; // 注入代理对象
    
    @Override
    public void processBusinessRequest(BusinessRequest request, String operator) {
        // 1. 通过代理对象调用,确保事务生效
        String taskId = self.createBusinessRecordInTransaction(request, operator);
        // 第一个事务在这里提交,数据立即可见
        
        // 2. 事务提交后调用外部服务
        callExternalService(taskId, request);
    }
    
    @Transactional(rollbackFor = Exception.class)
    public String createBusinessRecordInTransaction(BusinessRequest request, String operator) {
        return createBusinessRecord(request, operator);
    } // 事务在此处提交
}

Self注入的技术原理

  1. Spring容器管理

    java 复制代码
    // Spring容器中的Bean定义
    @Bean
    public BusinessServiceImpl BusinessService() {
        return Proxy.newProxyInstance(
            classLoader,
            new Class[]{BusinessService.class},
            new TransactionInvocationHandler(new BusinessServiceImpl())
        );
    }
  2. 代理对象注入

    java 复制代码
    @Autowired
    private BusinessServiceImpl self; 
    // 注入的是代理对象,包含事务拦截逻辑
  3. 事务拦截流程

    java 复制代码
    self.createDistributionRecordsInTransaction() 
    -> TransactionInterceptor.invoke()
    -> PlatformTransactionManager.getTransaction()
    -> 目标方法执行
    -> PlatformTransactionManager.commit()

3.3 修复后的完整时序

sql 复制代码
时间轴    应用A                     外部服务B                数据库MVCC
  |                                 |                       |
  1   调用self.createRecords()     |                       |
  |                                 |                       |
  2   BEGIN TRANSACTION            |                       | 创建事务A(TxID=100)
  |                                 |                       |
  3   INSERT distribution_result   |                       | 写入数据(TxID=100)
  |                                 |                       |
  4   COMMIT -----------------> |                       | 提交事务A
  |                                 |                       | 数据对所有事务可见
  |                                 |                       |
  5   HTTP调用外部服务 -----------> | 立即回调查询            |
  |                                 |                       |
  6                                 | SELECT batch_001 ----> | 新事务B(TxID=101)
  |                                 |                       | 能读取已提交数据
  7                                 | <-- 返回数据 --------- | 返回batch_001记录
  |                                 |                       |
  8   外部服务成功 <-------------- |                       |

4. 事务常见使用错误与最佳实践

4.1 常见错误模式

错误1:内部方法调用事务失效

java 复制代码
// ❌ 错误示例
@Service
public class UserService {
    
    public void registerUser(User user) {
        validateUser(user);
        saveUser(user); // 事务不生效
        sendEmail(user);
    }
    
    @Transactional
    public void saveUser(User user) {
        userMapper.insert(user);
    }
}

// ✅ 正确示例
@Service
public class UserService {
    
    @Autowired
    private UserService self;
    
    public void registerUser(User user) {
        validateUser(user);
        self.saveUser(user); // 通过代理调用,事务生效
        sendEmail(user);
    }
    
    @Transactional
    public void saveUser(User user) {
        userMapper.insert(user);
    }
}

错误2:长事务包含外部调用

java 复制代码
// ❌ 错误示例
@Transactional
public void processOrder(Order order) {
    // 数据库操作
    orderMapper.insert(order);
    
    // 外部服务调用(可能很慢)
    paymentService.processPayment(order); // 持有数据库锁
    
    // 更多数据库操作
    orderMapper.updateStatus(order.getId(), "PAID");
}

// ✅ 正确示例
public void processOrder(Order order) {
    // 第一个事务:创建订单
    String orderId = self.createOrderInTransaction(order);
    
    try {
        // 外部服务调用(无事务)
        PaymentResult result = paymentService.processPayment(order);
        
        // 第二个事务:更新状态
        self.updateOrderStatusInTransaction(orderId, "PAID");
    } catch (Exception e) {
        // 第三个事务:处理失败
        self.updateOrderStatusInTransaction(orderId, "FAILED");
        throw e;
    }
}

错误3:异常处理不当导致事务不回滚

java 复制代码
// ❌ 错误示例
@Transactional
public void processData(List<Data> dataList) {
    for (Data data : dataList) {
        try {
            processItem(data);
        } catch (Exception e) {
            log.error("处理失败", e);
            // 异常被吞掉,事务不会回滚
        }
    }
}

// ✅ 正确示例
@Transactional(rollbackFor = Exception.class)
public void processData(List<Data> dataList) {
    for (Data data : dataList) {
        try {
            processItem(data);
        } catch (BusinessException e) {
            log.error("业务异常: {}", e.getMessage());
            throw e; // 重新抛出,触发回滚
        } catch (Exception e) {
            log.error("系统异常", e);
            throw new SystemException("处理失败", e);
        }
    }
}

错误4:事务传播行为使用不当

java 复制代码
// ❌ 错误示例:审计日志应该独立事务
@Transactional
public void businessOperation() {
    // 主业务逻辑
    performBusinessLogic();
    
    // 审计日志(如果主业务失败,审计日志也会回滚)
    auditService.recordOperation(); // 使用默认REQUIRED传播
}

// ✅ 正确示例:使用REQUIRES_NEW
@Service
public class AuditService {
    
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void recordOperation() {
        // 独立事务,即使主业务失败也会提交
        auditMapper.insert(new AuditLog());
    }
}

@Transactional
public void businessOperation() {
    try {
        performBusinessLogic();
        auditService.recordOperation(); // 记录成功日志
    } catch (Exception e) {
        auditService.recordOperation(); // 记录失败日志
        throw e;
    }
}

4.2 最佳实践总结

1. 事务边界设计原则

java 复制代码
// ✅ 推荐模式:短事务 + 状态管理
public void complexBusinessOperation() {
    // 1. 短事务创建初始状态
    String taskId = self.createTaskInTransaction(PENDING);
    
    try {
        // 2. 无事务的外部操作
        ExternalResult result = externalService.process(taskId);
        
        // 3. 短事务更新最终状态
        self.updateTaskStatusInTransaction(taskId, SUCCESS, result);
    } catch (Exception e) {
        // 4. 短事务记录失败状态
        self.updateTaskStatusInTransaction(taskId, FAILED, e.getMessage());
        throw e;
    }
}

2. 事务注解配置规范

java 复制代码
// ✅ 完整的事务配置
@Transactional(
    rollbackFor = Exception.class,           // 所有异常都回滚
    timeout = 30,                           // 30秒超时
    isolation = Isolation.READ_COMMITTED,   // 明确指定隔离级别
    propagation = Propagation.REQUIRED      // 明确指定传播行为
)
public void businessMethod() {
    // 业务逻辑
}

3. 异常处理规范

java 复制代码
// ✅ 分层异常处理
@Transactional(rollbackFor = Exception.class)
public void businessMethod() {
    try {
        riskyOperation();
    } catch (BusinessException e) {
        // 业务异常:记录并重新抛出
        log.warn("业务异常: {}", e.getMessage());
        throw e;
    } catch (SystemException e) {
        // 系统异常:记录并重新抛出
        log.error("系统异常", e);
        throw e;
    } catch (Exception e) {
        // 未知异常:包装后抛出
        log.error("未知异常", e);
        throw new SystemException("操作失败", e);
    }
}

4. 性能优化建议

java 复制代码
// ✅ 批量操作优化
@Transactional(rollbackFor = Exception.class)
public void batchProcess(List<Data> dataList) {
    // 分批处理,避免长事务
    int batchSize = 100;
    for (int i = 0; i < dataList.size(); i += batchSize) {
        List<Data> batch = dataList.subList(i, Math.min(i + batchSize, dataList.size()));
        processBatch(batch);
    }
}

// ✅ 只读事务优化
@Transactional(readOnly = true)
public List<Data> queryData(QueryCondition condition) {
    // 只读事务,数据库可以进行优化
    return dataMapper.selectByCondition(condition);
}

5. 监控和调试

java 复制代码
// ✅ 事务状态监控
@Service
public class TransactionMonitorService {
    
    public void checkTransactionStatus() {
        boolean isActive = TransactionSynchronizationManager.isActualTransactionActive();
        boolean isReadOnly = TransactionSynchronizationManager.isCurrentTransactionReadOnly();
        String name = TransactionSynchronizationManager.getCurrentTransactionName();
        
        log.info("事务状态: active={}, readOnly={}, name={}", isActive, isReadOnly, name);
    }
}

// ✅ 事务日志配置
# logback-spring.xml
<logger name="org.springframework.transaction" level="DEBUG"/>
<logger name="org.springframework.orm.jpa" level="DEBUG"/>

5. 总结

5.1 核心要点

  1. 事务隔离是竞态条件的根本原因:READ COMMITTED隔离级别导致未提交数据不可见
  2. 代理机制是事务生效的前提:内部方法调用必须通过代理对象
  3. 事务边界设计至关重要:数据库操作与外部调用应该分离
  4. 状态管理保证最终一致性:通过状态字段和补偿机制处理异常情况

5.2 实施建议

  1. 立即修复:使用self注入解决内部方法调用问题
  2. 重构长事务:将外部调用从事务中分离出来
  3. 完善监控:添加事务状态监控和性能指标

5.3 预期效果

  • 消除竞态条件:外部服务能正常查询到已提交数据
  • 提升并发性能:缩短事务持有锁的时间
  • 增强系统稳定性:减少因外部服务故障导致的事务回滚
  • 改善可维护性:清晰的事务边界和错误处理机制
相关推荐
廋到被风吹走2 小时前
【数据库】【Redis】数据结构全景图:命令、场景与避坑指南
数据结构·数据库·redis
xixingzhe22 小时前
数据、数据库分类
数据库
松涛和鸣2 小时前
34、 Linux IPC进程间通信:无名管道(Pipe) 和有名管道(FIFO)
linux·服务器·c语言·网络·数据结构·数据库
NMBG222 小时前
外卖综合项目
java·前端·spring boot
云老大TG:@yunlaoda3602 小时前
如何使用华为云国际站代理商的FunctionGraph进行事件驱动的应用开发?
大数据·数据库·华为云·云计算
清水白石0082 小时前
《用 Python 单例模式打造稳定高效的数据库连接管理器》
数据库·python·单例模式
小徐Chao努力2 小时前
Spring AI Alibaba A2A 使用指南
java·人工智能·spring boot·spring·spring cloud·agent·a2a
小虾米vivian2 小时前
dmetl5 web管理平台 监控-流程监控 看不到运行信息
linux·服务器·网络·数据库·达梦数据库
yuzhucu2 小时前
django4.1.2+xadmin配置
数据库·sqlite