一、事务传播行为陷阱
1. 同类方法自调用事务失效
@service
public class orderservice {
// ❌ 错误:内部调用不走 spring 代理,@transactional 不生效
public void createorder(order order) {
this.saveorder(order); // 直接调用,不经过代理
}
@transactional
public void saveorder(order order) {
orderrepository.save(order); // 这里没有事务保护!
}
}
原因:spring 的 @transactional 通过 aop 代理实现。this.method() 是直接方法调用,不走代理,注解不生效。
规避方案:
// 方案1:注入自身(推荐)
@service
public class orderservice {
@resource
private orderservice self; // 注入代理对象
public void createorder(order order) {
self.saveorder(order); // 通过代理调用,事务生效
}
@transactional
public void saveorder(order order) {
orderrepository.save(order);
}
}
// 方案2:拆分到另一个 service
@service
public class orderservice {
@resource
private ordertransactionservice txservice;
public void createorder(order order) {
txservice.saveorder(order); // 不同类的调用走代理
}
}
// 方案3:直接在外部方法加事务
@service
public class orderservice {
@transactional // 直接加在入口方法
public void createorder(order order) {
orderrepository.save(order);
}
}
2. requires_new 的嵌套陷阱
@transactional
public void outermethod() {
orderrepository.save(order); // 外层事务
try {
self.innermethod(); // 内层新事务
} catch (exception e) {
// 即使内层回滚,外层可以继续
log.warn("内层失败", e);
}
// 外层继续执行...
}
@transactional(propagation = propagation.requires_new)
public void innermethod() {
logrepository.save(log); // 独立事务
throw new runtimeexception("失败"); // 内层回滚,不影响外层
}
注意点:
- requires_new 会挂起外层事务,开一个全新事务
- 内层提交/回滚独立于外层
- 内层持有新的数据库连接(连接池要注意)
- 内层看不到外层未提交的数据
// ❌ 陷阱:requires_new 内查不到外层未提交的数据
@transactional
public void outermethod() {
order order = new order();
orderrepository.save(order); // 外层事务中,还没 commit
self.checkorder(order.getid()); // 新事务中查不到这条数据!
}
@transactional(propagation = propagation.requires_new)
public void checkorder(integer orderid) {
order order = orderrepository.findbyid(orderid).orelse(null);
// order == null!因为外层事务还没提交
}
3. 异常类型与回滚规则
// 默认只有 runtimeexception 和 error 触发回滚
@transactional // = @transactional(rollbackfor = runtimeexception.class)
public void method() {
throw new ioexception("文件错误"); // ❌ 不回滚!checked exception
throw new runtimeexception("运行时错误"); // ✅ 回滚
}
// ✅ 正确:明确指定 rollbackfor
@transactional(rollbackfor = exception.class)
public void method() {
throw new ioexception("文件错误"); // ✅ 回滚
}
规避:项目中统一使用 @transactional(rollbackfor = exception.class)。
4. catch 异常后事务仍会回滚
// ❌ 陷阱:虽然 catch 了,但 spring 已标记事务为 rollback-only
@transactional
public void outermethod() {
try {
self.innermethod(); // 抛异常
} catch (exception e) {
log.warn("忽略异常");
// 以为事务还能继续...
}
orderrepository.save(order); // 事务提交时报错:
// transaction silently rolled back because it has been marked as rollback-only
}
@transactional // 默认 required,加入外层事务
public void innermethod() {
throw new runtimeexception("失败");
// spring 将共享的事务标记为 rollback-only
}
规避:内层方法用 requires_new 或在内层方法内部 catch。
二、aftercommit 回调中的操作限制
1. 不能用 saveandflush
// ❌ 报错:no transaction is in progress
aftercommit() {
repository.saveandflush(entity);
}
// ✅ 正确:用 save
aftercommit() {
repository.save(entity);
}
2. 不能依赖当前的持久化上下文
// ❌ 陷阱:aftercommit 中持久化上下文可能已关闭
@transactional
public void method() {
user user = userrepository.findbyid(1).get(); // 托管态
aftercommit(() -> {
// 此时 user 可能已变为游离态
user.getorders(); // lazyinitializationexception(如果是懒加载)
});
}
// ✅ 正确:在 aftercommit 中重新查询
aftercommit(() -> {
user user = userrepository.findbyid(1).get(); // 新的持久化上下文
user.getorders(); // ok
});
3. aftercommit 中操作失败不影响主事务
@transactional
public void createorder(order order) {
orderrepository.save(order); // 主事务
aftercommit(() -> {
// 即使这里报错,订单已经提交成功了
notifyservice.send(order.getid()); // 失败不影响订单
});
}
// aftercommit 的异常不会导致主事务回滚(因为已经提交了)
// 但如果不 catch,异常会向上抛出(可能影响 mq ack 等)
规避:aftercommit 中的操作统一 try-catch。
三、jpa 操作顺序陷阱
1. 先签收后拒收的幂等冲突
// ❌ 错误顺序:签收后出库单变为已完成,拒收被幂等跳过
public void partialsignandreject() {
confirmsign(); // 出库单状态 → 7(已完成)
csgcreject(); // 检测到状态=7 → 跳过 ❌
}
// ✅ 正确顺序:先拒收再签收
public void partialsignandreject() {
csgcreject(); // 出库单还是5 → 正常执行拒收
confirmsign(); // 最后签收 → 出库单状态 → 7
}
规则:如果一个方法内有"幂等校验"依赖某个状态,那改变这个状态的操作应该放在最后。
2. saveandflush 后对象状态变化
@transactional
public void updateandcheck() {
outbounddetail detail = detailrepository.findbyid(1).get();
detail.setrejectedqty(2);
detailrepository.saveandflush(detail); // 立即写入db
// 注意:此时 detail 对象的 version 字段已被 hibernate 更新
// 如果后续再 save 一次,version 是新的,不会冲突
// 但如果另一个线程同时修改了这条记录:
detail.setrejectedqty(3);
detailrepository.saveandflush(detail); // 可能 optimisticlockexception
}
3. findbyid 后的对象是托管态
@transactional
public void example() {
user user = userrepository.findbyid(1).get(); // 托管态
user.setname("新名字"); // 直接修改,不需要调 save!
// 事务提交时,hibernate 脏检查发现 name 变了
// 自动生成 update sql
// 不需要手动 save/saveandflush
}
// 但如果不在事务中:
public void notransaction() {
user user = userrepository.findbyid(1).get(); // 游离态(事务结束了)
user.setname("新名字"); // 修改不会被追踪
// 必须手动 save
userrepository.save(user);
}
4. 批量操作的内存溢出
// ❌ 10万条数据全部加载到持久化上下文,内存爆
@transactional
public void batchupdate() {
list<user> users = userrepository.findall(); // 10万条全在内存
for (user user : users) {
user.setstatus(1);
}
// commit 时 flush 10万条 update
}
// ✅ 分批处理 + 定期清理持久化上下文
@transactional
public void batchupdate() {
int page = 0;
page<user> users;
do {
users = userrepository.findall(pagerequest.of(page++, 1000));
for (user user : users) {
user.setstatus(1);
}
userrepository.flush(); // 执行 sql
entitymanager.clear(); // 清理持久化上下文释放内存
} while (users.hasnext());
}
四、mybatis 与 jpa 混用注意事项
1. 同一事务内 jpa 写入后 mybatis 查不到
// ❌ jpa persist 后还没 flush,mybatis 直接查数据库查不到
@transactional
public void mixed() {
user user = new user();
user.setname("张三");
userrepository.save(user); // 只在持久化上下文中,还没 flush
// mybatis 直接执行 sql 查数据库
user found = usermapper.selectbyid(user.getid());
// found == null!因为 jpa 还没把 insert 发到数据库
}
// ✅ 先 flush 再用 mybatis 查
@transactional
public void mixed() {
user user = new user();
userrepository.save(user);
entitymanager.flush(); // 强制把 insert 发到数据库
user found = usermapper.selectbyid(user.getid()); // 能查到了
}
2. mybatis 修改后 jpa 缓存过期
// ❌ mybatis 直接改了 db,但 jpa 持久化上下文中还是旧值
@transactional
public void mixed() {
user user = userrepository.findbyid(1).get(); // name="张三",缓存在持久化上下文
usermapper.updatename(1, "李四"); // mybatis 直接 update 数据库
user found = userrepository.findbyid(1).get(); // 还是"张三"!
// jpa 一级缓存返回了持久化上下文中的旧对象
}
// ✅ mybatis 修改后清理 jpa 缓存
@transactional
public void mixed() {
user user = userrepository.findbyid(1).get();
usermapper.updatename(1, "李四");
entitymanager.clear(); // 清理持久化上下文缓存
user found = userrepository.findbyid(1).get(); // "李四" ✅
}
3. 事务管理器统一
# 确保 jpa 和 mybatis 使用同一个事务管理器和数据源
spring:
datasource:
url: jdbc:mysql://...
jpa:
hibernate:
ddl-auto: none
# mybatis 默认使用 spring 管理的 datasource,与 jpa 共享事务五、乐观锁相关陷阱
1. 读取和更新之间的时间窗口
// ❌ 两次数据库操作之间有时间窗口,可能被别人修改
public void deductstock(integer skuid, integer qty) {
stock stock = stockrepository.findbyid(skuid).get(); // version=1
// ... 这里可能经过了几秒(比如调了远程服务)
stock.setquantity(stock.getquantity() - qty);
stockrepository.save(stock); // version=1 → update where version=1
// 如果别人先改了(version变成2),这里 optimisticlockexception
}
// ✅ 捕获乐观锁异常并重试
@retryable(value = optimisticlockexception.class, maxattempts = 3)
@transactional
public void deductstock(integer skuid, integer qty) {
stock stock = stockrepository.findbyid(skuid).get();
stock.setquantity(stock.getquantity() - qty);
stockrepository.save(stock);
}
2. saveandflush 与乐观锁
@transactional
public void update() {
user user = userrepository.findbyid(1).get(); // version=5
user.setname("新名字");
// saveandflush 立即执行 update,立即知道是否冲突
userrepository.saveandflush(user); // 如果冲突立即抛 optimisticlockexception
// 而 save 延迟到 commit 时才知道冲突
userrepository.save(user); // 要到事务提交时才报错
}
六、常见的执行顺序问题
1. 事件发布与事务的关系
// ❌ 事件监听者在事务提交前执行,可能看到不一致状态
@transactional
public void createorder(order order) {
orderrepository.save(order);
eventpublisher.publishevent(new ordercreatedevent(order.getid()));
// 监听者此时能查到 order(同一事务),但如果事务回滚就不一致了
}
// ✅ 使用 @transactionaleventlistener 确保事务提交后再处理
@transactionaleventlistener(phase = transactionphase.after_commit)
public void handleordercreated(ordercreatedevent event) {
// 只在事务成功提交后才执行
notifyservice.send(event.getorderid());
}
2. mq 消息发送时机
// ❌ 事务内发 mq,如果事务回滚消息已发出
@transactional
public void createorder(order order) {
orderrepository.save(order);
mqsender.send("order.created", order.getid()); // mq 已发
validateorder(order); // 如果这里抛异常 → 事务回滚 → 但 mq 已发
}
// ✅ aftercommit 中发 mq
@transactional
public void createorder(order order) {
orderrepository.save(order);
transactionsynchronizationmanager.registersynchronization(
new transactionsynchronization() {
@override
public void aftercommit() {
mqsender.send("order.created", order.getid());
}
}
);
}
3. 远程调用放在事务内的风险
// ❌ 远程调用放在事务内:长事务 + 锁持有时间长
@transactional
public void process() {
stock stock = stockrepository.findforupdate(skuid); // 行锁
stock.setquantity(stock.getquantity() - 1);
// 远程调用耗时 3 秒 → 行锁持有 3 秒 → 其他线程全部等待
orderfeign.notifyorderservice(orderid); // 慢!
stockrepository.save(stock);
}
// ✅ 远程调用放在事务外(或事务提交后)
@transactional
public void process() {
stock stock = stockrepository.findforupdate(skuid);
stock.setquantity(stock.getquantity() - 1);
stockrepository.save(stock);
// 事务尽快提交,释放锁
}
// 事务提交后再调远程
public void processandnotify() {
process(); // 事务结束
orderfeign.notifyorderservice(orderid); // 不持有锁了
}
七、规避清单
| # | 陷阱 | 规避方式 |
|---|---|---|
| 1 | 自调用事务失效 | 注入 self 或拆 service |
| 2 | aftercommit 中用 saveandflush | 改用 save |
| 3 | aftercommit 中访问懒加载属性 | 提前加载或重新查询 |
| 4 | 先改状态后依赖状态校验 | 把依赖校验的操作放前面 |
| 5 | jpa 写入后 mybatis 查不到 | 先 flush 再查 |
| 6 | mybatis 改了 jpa 缓存过期 | 改后 entitymanager.clear() |
| 7 | 事务内发 mq/调外部 | 移到 aftercommit |
| 8 | 事务内长时间远程调用 | 缩短事务范围 |
| 9 | checked exception 不回滚 | 统一用 rollbackfor=exception.class |
| 10 | 内层 required 异常污染外层 | 内层用 requires_new 或内部 catch |
| 11 | 批量操作 oom | 分批 + flush + clear |
| 12 | 乐观锁冲突无处理 | @retryable 或手动重试 |
八、项目中的推荐实践
// 1. 统一事务注解模板
@transactional(rollbackfor = exception.class)
// 2. 需要新事务的场景
@transactional(propagation = propagation.requires_new, rollbackfor = exception.class)
// 3. aftercommit 中的数据库操作统一用 save
repository.save(entity); // 不用 saveandflush
// 4. aftercommit 统一 try-catch
aftercommit(() -> {
try {
dosomething();
} catch (exception e) {
log.warn("aftercommit异常", e);
}
});
// 5. jpa 和 mybatis 混用时显式 flush
entitymanager.flush(); // jpa 操作后,mybatis 操作前
// 6. 事务尽量短:先准备数据,再开事务
public void process() {
data data = preparedata(); // 事务外:查询、远程调用
transactionservice.save(data); // 事务内:只做写入
afteraction(); // 事务外:通知、mq
}
九、事务超时与连接泄漏
1. 长事务占用连接
// ❌ 事务内做耗时操作,长时间占用数据库连接
@transactional
public void exportreport() {
list<order> orders = orderrepository.findall(); // 获取连接
// 组装 excel 耗时 30 秒...
byte[] excel = buildexcel(orders);
// 上传 oss 耗时 10 秒...
ossclient.upload(excel);
// 整个过程数据库连接被占用 40 秒
// 连接池默认 10 个连接,4 个这样的请求就把连接池打满
}
// ✅ 只在需要数据库操作时才开事务
public void exportreport() {
list<order> orders = queryorders(); // 事务内,快速完成
byte[] excel = buildexcel(orders); // 事务外,不占连接
ossclient.upload(excel); // 事务外
}
@transactional(readonly = true)
public list<order> queryorders() {
return orderrepository.findall();
}
2. 事务超时配置
// 设置事务超时时间(秒)
@transactional(timeout = 30, rollbackfor = exception.class)
public void process() {
// 如果 30 秒内没完成,自动回滚
// 抛出 transactiontimedoutexception
}
# 全局默认超时
spring:
transaction:
default-timeout: 60 # 秒
3. 连接泄漏排查
# hikaricp 连接池泄漏检测
spring:
datasource:
hikari:
leak-detection-threshold: 30000 # 30秒未归还连接则告警
maximum-pool-size: 20
connection-timeout: 5000 # 获取连接超时5秒
// 泄漏告警日志示例:
warn hikaripool - connection leak detection triggered for connection xxx,
stack trace follows
java.lang.exception: apparent connection leak detected
at com.zaxxer.hikari.hikaridatasource.getconnection(...)
at com.example.orderservice.process(orderservice.java:45)
十、只读事务优化
1. readonly 的作用
@transactional(readonly = true)
public list<order> queryorders() {
return orderrepository.findall();
}
readonly=true 时:
- hibernate 不做脏检查(不需要 update)→ 性能提升
- 持久化上下文中实体标记为只读 → 节省内存快照
- 部分数据库会做读优化(如 mysql 走从库)
- 如果尝试写入会报错(部分实现)
2. 查询方法统一加 readonly
@service
@transactional(readonly = true) // 类级别默认只读
public class orderqueryservice {
public order getbyid(integer id) {
return orderrepository.findbyid(id).orelse(null);
}
public page<order> search(orderquery query) {
return orderrepository.findall(query.tospec(), query.topageable());
}
// 需要写操作的方法单独覆盖
@transactional(rollbackfor = exception.class)
public void updatestatus(integer id, string status) {
order order = orderrepository.findbyid(id).get();
order.setstatus(status);
}
}
十一、悲观锁与 select for update
1. jpa 中使用悲观锁
public interface stockrepository extends jparepository<stock, integer> {
// 方式1:@lock 注解
@lock(lockmodetype.pessimistic_write)
@query("select s from stock s where s.skuid = :skuid")
stock findbyskuidforupdate(@param("skuid") integer skuid);
// 方式2:entitymanager
// stock stock = em.find(stock.class, id, lockmodetype.pessimistic_write);
}
生成的 sql:
select * from stock where sku_id = ? for update
2. 悲观锁使用注意事项
// ❌ 锁住后做远程调用 → 死等 + 其他线程全阻塞
@transactional
public void deduct() {
stock stock = stockrepository.findbyskuidforupdate(skuid); // 加行锁
orderfeign.checkstock(skuid); // 远程调用 3 秒 → 行锁持有 3 秒
stock.setquantity(stock.getquantity() - 1);
}
// ✅ 先做远程调用再加锁
@transactional
public void deduct() {
orderfeign.checkstock(skuid); // 先做远程调用
stock stock = stockrepository.findbyskuidforupdate(skuid); // 最后加锁
stock.setquantity(stock.getquantity() - 1);
// 锁持有时间极短
}
3. 死锁风险
// ❌ 两个线程以不同顺序加锁 → 死锁
// 线程a:锁 stock_1 → 等锁 stock_2
// 线程b:锁 stock_2 → 等锁 stock_1
// ✅ 统一按 id 排序后加锁
@transactional
public void transfer(integer fromid, integer toid) {
integer firstid = math.min(fromid, toid);
integer secondid = math.max(fromid, toid);
stock first = stockrepository.findbyskuidforupdate(firstid);
stock second = stockrepository.findbyskuidforupdate(secondid);
// 所有线程按同一顺序加锁 → 不会死锁
}
十二、lazyinitializationexception
1. 问题场景
@entity
public class order {
@onetomany(fetch = fetchtype.lazy, mappedby = "order")
private list<orderitem> items; // 懒加载
}
// ❌ 事务/session 结束后访问懒加载属性
public orderdto getorder(integer id) {
order order = orderrepository.findbyid(id).get();
// 事务结束,session 关闭
return todto(order);
}
private orderdto todto(order order) {
orderdto dto = new orderdto();
dto.setitems(order.getitems()); // lazyinitializationexception!
// items 是懒加载代理,session 已关闭无法查询
return dto;
}
2. 规避方案
// 方案1:fetch join 查询(推荐)
@query("select o from order o join fetch o.items where o.id = :id")
order findbyidwithitems(@param("id") integer id);
// 方案2:@entitygraph
@entitygraph(attributepaths = {"items"})
optional<order> findbyid(integer id);
// 方案3:在事务内完成所有数据访问
@transactional(readonly = true)
public orderdto getorder(integer id) {
order order = orderrepository.findbyid(id).get();
orderdto dto = todto(order); // 在事务内访问 items,ok
return dto;
}
// 方案4:dto 投影(最佳性能)
@query("select new com.example.orderdto(o.id, o.code, o.status) from order o where o.id = :id")
orderdto finddtobyid(@param("id") integer id);
十三、n+1 查询问题
1. 问题
@transactional(readonly = true)
public list<orderdto> listorders() {
list<order> orders = orderrepository.findall(); // 1 次查询
return orders.stream().map(order -> {
orderdto dto = new orderdto();
dto.setitems(order.getitems()); // 每个 order 触发一次查询 → n 次
return dto;
}).collect(collectors.tolist());
}
// 总共 1+n 次查询(如果 100 条订单就是 101 次 sql)
2. 解决
// 方案1:join fetch
@query("select distinct o from order o join fetch o.items")
list<order> findallwithitems(); // 1 次 sql 搞定
// 方案2:@batchsize(hibernate 特有)
@entity
public class order {
@onetomany(mappedby = "order")
@batchsize(size = 100) // 每次批量加载 100 条关联数据
private list<orderitem> items;
}
// 方案3:entitygraph
@entitygraph(attributepaths = {"items"})
list<order> findall();
十四、mybatis 特有注意事项
1. #{} vs ${}
<!-- ✅ #{} 预编译参数(防sql注入) -->
<select id="findbyid">
select * from user where id = #{id}
</select>
<!-- 生成:select * from user where id = ? -->
<!-- ⚠️ ${} 字符串拼接(有sql注入风险,只用于表名/列名等) -->
<select id="findall">
select * from user order by ${sortcolumn} ${sortorder}
</select>
<!-- 生成:select * from user order by name asc -->
<!-- 如果 sortcolumn 是用户输入 → sql注入! -->2. 批量操作的正确方式
<!-- ❌ 循环调单条 insert -->
<!-- java 代码中 for 循环调 insert 方法 → n 次 sql -->
<!-- ✅ 批量 insert -->
<insert id="batchinsert">
insert into user (name, age) values
<foreach collection="list" item="item" separator=",">
(#{item.name}, #{item.age})
</foreach>
</insert>
<!-- ✅ 批量 update(mysql) -->
<update id="batchupdate">
<foreach collection="list" item="item" separator=";">
update user set name = #{item.name} where id = #{item.id}
</foreach>
</update>3. 一级缓存(sqlsession 级别)
// 同一个 sqlsession(同一个事务)内,相同查询命中缓存
@transactional
public void example() {
user user1 = usermapper.selectbyid(1); // 查数据库
user user2 = usermapper.selectbyid(1); // 命中一级缓存,不查库
// user1 == user2(同一个对象)
usermapper.updatename(1, "新名字"); // update 会清空一级缓存
user user3 = usermapper.selectbyid(1); // 重新查数据库
// user3.getname() == "新名字"
}
4. 事务内 insert 后立即查询
// mybatis 的 insert 立即执行 sql(不像 jpa 有延迟)
@transactional
public void example() {
usermapper.insert(user); // 立即执行 insert sql
// 同一事务内可以立即查到(不需要 flush)
user found = usermapper.selectbyid(user.getid()); // ✅ 能查到
}
这是 mybatis 和 jpa 的重要区别:mybatis 每条操作直接执行 sql,没有持久化上下文的延迟机制。
十五、总结:事务相关开发检查清单
每次写涉及事务的代码时,问自己:
- @transactional 加在哪?是否会被自调用绕过?
- rollbackfor 是否指定了 exception.class?
- 事务范围是否尽量小?有没有把远程调用放进来?
- aftercommit 中是否用了 save(而非 saveandflush)?
- aftercommit 中是否有 try-catch?
- 操作顺序是否考虑了幂等校验的时机?
- 悲观锁后是否有耗时操作?
- jpa 操作后 mybatis 能否查到(需要 flush 吗)?
- 懒加载属性的访问是否在事务/session 内?
- 批量操作是否会 oom(需要分批 + clear 吗)?
- 外部通知/mq 是否放在 aftercommit(而非事务内)?
到此这篇关于spring中事务与持久化操作常见陷阱及规避指南的文章就介绍到这了,更多相关spring事务与持久化操作内容请搜索代码网以前的文章或继续浏览下面的相关文章希望大家以后多多支持代码网!
发表评论