sychronized与@Transactional一起使用需要注意的问题

66 阅读2分钟

Java 并发Spring事务管理结合时,synchronized@Transactional 同时使用确实会踩坑。

🧩 一、机制差异

特性synchronized@Transactional
作用层面JVM 内部的线程锁(单进程级别Spring 管理的数据库事务(数据库级别
控制对象同一个 JVM 内的线程访问代码块同一个数据库连接中的 SQL 操作
生命周期锁持有期间方法执行期间(代理层包裹)
异常处理不会主动回滚数据库抛出 RuntimeException 时触发回滚

⚠️ 二、常见问题与坑点

1️⃣ @Transactional 失效(被 synchronized 影响)

问题场景:

@Transactional
public synchronized void doSomething() {
    // DB操作
}

Spring 的事务是通过 AOP 代理 实现的(即代理调用目标方法时开启事务)。
当方法被 synchronized 修饰时,它在进入代理前就被锁了,但事务的开启时机在锁之后

⚠️ 如果此方法被内部调用(this.doSomething()) ,Spring AOP不会生效 → 事务失效

建议:

  • 不要在 @Transactional 方法上直接加 synchronized
  • 如果确实需要同步,放在业务层外层或使用 Redis/Zookeeper 分布式锁控制。 2️⃣ 锁与事务交叉导致性能或死锁问题 例如:
public synchronized void updateAccount() {
    doUpdate(); // 调用了@Transactional方法
}

这里的 synchronized 锁住了整个方法;
但事务中的数据库锁(例如行锁)又会持有连接,
→ 导致多个线程都在等待锁释放,容易出现:

  • JVM 级别锁等待;
  • DB 级别锁等待;
  • 二者叠加 → 死锁或长时间阻塞

建议:

  • 让事务尽量“短”;
  • 锁只保护临界区逻辑
  • 不要把 synchronized 和数据库操作长时间混用。

3️⃣ 多节点集群中 synchronized 无效

@Transactional 事务作用于数据库(集群共享),
synchronized 只在单 JVM 实例中有效。

所以在分布式部署下:

// 2个服务节点调用同一个方法
synchronized void transfer() { ... }

→ 各节点各锁自己的内存,不会互斥!
导致并发问题仍会发生。

建议:
使用:

  • Redis分布式锁(如 Redisson)
  • Zookeeper分布式锁
  • 数据库唯一约束悲观锁

4️⃣ @Transactional 异常回滚 + 锁未释放 如果你在锁中抛出异常导致事务回滚:

public synchronized void saveData() {
    throw new RuntimeException("fail"); // 事务回滚
}

锁是 JVM 层面的,异常不会影响锁释放(JVM自动释放 monitor),
但如果你手动实现了锁(如 ReentrantLock)且忘记 finally unlock(),可能造成锁泄露。

✅ 一定要:

lock.lock();
try {
    // transactional logic
} finally {
    lock.unlock();
}

✅ 三、最佳实践建议总结

场景推荐方式
单机同步 + 事务使用 synchronizedReentrantLock,但锁区域要小,避免嵌套数据库操作。
分布式系统Redis/Zookeeper 分布式锁 控制同步,事务控制在数据库中进行。
内部方法调用需要事务不要 this.method() 调用,用 @Autowired 注入自身代理(AopContext.currentProxy())。
避免 synchronized + @Transactional 直接叠加用外层锁或分布式锁控制,事务放在内层逻辑中。