一、问题背景
某次开发中,我需要处理一个批量插入提醒数据的业务。核心代码如下:
//@Transactional(rollbackFor = Exception.class)
public void processRemindData(String batchNo) {
// 1.查询待处理数据
List<CuSixNoTranTemp> list = queryData(batchNo);
// 2.为每个数据生成日志对象并设置UUID主键
List<BatchNoteSendLog> logs = list.stream()
.map(item -> {
BatchNoteSendLog log = convertToLog(item);
log.setUuid(sequenceService.getNext()); //获取序列值
return log;
}).collect(Collectors.toList());
// 3.批量更新状态并插入日志
updateData(list);
batchNoteSendLogDao.saveBatch(logs);
}
在未添加@Transactional
注解时,程序运行正常。但当添加事务注解后,批量插入时却出现了主键冲突异常,且所有日志的UUID值完全相同。
二、问题分析
1. 事务与MyBatis一级缓存的关系
当添加@Transactional
后,整个方法会在同一个数据库会话(Session)中执行。MyBatis默认启用一级缓存(SqlSession级别),在同一会话中重复的查询会直接返回缓存结果。
查看获取序列值的实现:
<!-- SequenceMapper.xml -->
<select id="getNext" resultType="java.lang.Long">
SELECT my_sequence.nextval FROM dual
</select>
2. 缓存导致的序列重复
未开启事务时,每次getNext()
调用都会创建新会话,查询直接命中数据库。而开启事务后:
- 所有
getNext()
调用共享同一会话 - 第一次查询
my_sequence.nextval
后,结果被缓存 - 后续查询直接从缓存返回相同值
- 最终所有日志对象使用相同UUID导致主键冲突
三、解决方案
方案1:禁用指定查询的缓存
对于XML映射方式:
<select id="getNext" resultType="java.lang.Long"
flushCache="true" useCache="false">
SELECT my_sequence.nextval FROM dual
</select>
对于注解方式:
@Select("SELECT to_char(sysdate,'yyyyMMdd')||(LPAD( ${sequenceName}.nextval, 10 ,'0' )) FROM dual")
@Options(flushCache = Options.FlushCachePolicy.TRUE, useCache = false)
String getNext(@Param("sequenceName") String sequenceName);
-
@Options(flushCache = true)
:执行后清空缓存 -
useCache = false
:禁止缓存本次结果
方案2:调整事务范围
@Service
public class TransactionService {
@Transactional
public void executeInTransaction(List<CuSixNoTranTemp> list,
List<BatchNoteSendLog> logs) {
// 更新和插入操作
}
}
// 在业务类中注入事务服务
@Autowired
private TransactionService transactionService;
public void processRemindData(String batchNo) {
// 非事务操作...
transactionService.executeInTransaction(list, logs); // 跨类调用事务方法
}
四、知识扩展
1. @Transactional使用规范
- 方法必须为public:Spring AOP代理无法拦截私有方法
- 避免同类调用:通过注入自身代理或拆分到不同类中解决
2. MyBatis缓存机制详解
特性 | 一级缓存 | 二级缓存 |
---|---|---|
作用范围 | SqlSession级别 | Mapper(Namespace)级别 |
生命周期 | 随会话关闭销毁 | 应用生命周期内有效 |
缓存清除策略 | 执行更新操作自动清除 | 需手动配置过期策略 |
3. 事务传播机制
当使用@Transactional(propagation = Propagation.REQUIRES_NEW)
时,即使存在外层事务,也会创建新事务。可用于分离关键操作。
五、最佳实践建议
-
序列查询强制走数据库
无论使用XML还是注解方式,务必添加缓存控制参数:@Options(flushCache = Options.FlushCachePolicy.TRUE, useCache = false)
-
事务方法分层设计
@Service public class DataService { // 非事务方法 public void prepareData() { ... } } @Service public class TransactionService { // 事务方法 @Transactional public void saveData() { ... } }
-
高并发场景优化
// 使用分布式ID生成器 @Component public class SnowflakeIdGenerator { public String nextId() { // 实现雪花算法... } }
六、总结
通过这个案例,我们串联起三个关键技术点:
- Spring事务的代理机制决定了事务方法必须通过代理调用
- MyBatis缓存设计需要与业务场景匹配
- 序列生成方式必须考虑会话生命周期
最终的解决方案体现了两个重要思想:
- 关注点分离:将事务操作与非事务操作分层
- 显式控制:对关键查询主动声明缓存策略
在日后的开发中,建议:
- 对核心数据库操作添加单元测试
- 在预发环境进行并发压力测试
- 定期Review缓存配置与事务边界
技术细节决定系统稳定性,只有深入理解框架原理,才能写出健壮的代码。希望这个案例能帮助大家少走弯路!