Why @Transactional Can Crash Production and How to Prevent Long Transactions
This article recounts a production incident caused by using Spring's @Transactional annotation, explains how long-running transactions exhaust database connections, and provides practical strategies—including method splitting, programmatic transactions, and proper AOP proxy usage—to prevent such failures in backend Java applications.
@Transactional Caused a Production Incident
In 2019 an internal reimbursement system added @Transactional to the
save()method to ensure atomicity. The method called a remote workflow service, performed DTO conversion, and saved the bill and its details. During a peak period of taxi reimbursements, the single transaction held a database connection for a long time, leading to connection‑pool exhaustion, deadlocks, and
CannotGetJdbcConnectionExceptionerrors.
Root‑Cause Analysis
Spring implements @Transactional via AOP. When the annotation is present, Spring obtains a connection from the pool, starts a transaction, and binds the same connection to the entire method via ThreadLocal. If the method performs time‑consuming operations (e.g., remote RPC calls), the connection remains occupied, causing a classic long‑transaction problem.
A long transaction is a transaction that runs for an extended period without committing, often referred to as a “big transaction.” Its typical harms include:
Database connection pool exhaustion, preventing other requests from obtaining connections.
Increased likelihood of deadlocks.
Long rollback times.
Greater replication lag in master‑slave setups.
How to Avoid Long Transactions
The key is to split transactional work into smaller units, reducing transaction granularity. First, recall Spring’s two transaction styles:
Declarative Transaction
Using @Transactional on a method is simple and lets Spring automatically open, commit, or roll back the transaction. However, the transaction scope is the whole method, limiting fine‑grained control.
Programmatic Transaction
Developers can manage transactions manually via Spring’s
TransactionTemplate:
<code>@Autowired
private TransactionTemplate transactionTemplate;
public void save(RequestBill requestBill) {
transactionTemplate.execute(status -> {
requestBillDao.save(requestBill);
requestDetailDao.save(requestBill.getDetail());
return Boolean.TRUE;
});
}
</code>Programmatic transactions allow precise control over the transaction boundaries.
Therefore, to avoid long transactions, avoid placing @Transactional on methods that contain lengthy non‑database operations. Instead, separate the logic:
<code>@Service
public class OrderService {
public void createOrder(OrderCreateDTO createDTO) {
query();
validate();
saveData(createDTO);
}
@Transactional(rollbackFor = Throwable.class)
public void saveData(OrderCreateDTO createDTO) {
orderDao.insert(createDTO);
}
}
</code>Note that calling a @Transactional method from within the same class bypasses the proxy, causing the transaction to be ineffective. Common scenarios where @Transactional fails include:
@Transactional applied to non‑public methods Incorrect propagation settings Incorrect rollbackFor settings Self‑invocation within the same class Catching exceptions that prevent rollback
Correct splitting approaches:
Move the transactional method to a separate bean (e.g., a manager class) and inject it.
<code>@Service
public class OrderService {
@Autowired
private OrderManager orderManager;
public void createOrder(OrderCreateDTO createDTO) {
query();
validate();
orderManager.saveData(createDTO);
}
}
@Service
public class OrderManager {
@Autowired
private OrderDao orderDao;
@Transactional(rollbackFor = Throwable.class)
public void saveData(OrderCreateDTO createDTO) {
orderDao.saveData(createDTO);
}
}
</code>Enable proxy exposure and obtain the proxy via
AopContext.currentProxy()to invoke the transactional method.
<code>@EnableAspectJAutoProxy(exposeProxy = true)
@SpringBootApplication
public class SpringBootApplication {}
public void createOrder(OrderCreateDTO createDTO) {
OrderService proxy = (OrderService) AopContext.currentProxy();
proxy.saveData(createDTO);
}
</code>Conclusion
While @Transactional is convenient, careless use can create long‑transaction problems that exhaust database resources. For complex business logic, prefer programmatic transactions or carefully split methods to keep transactions short and efficient.
macrozheng
Dedicated to Java tech sharing and dissecting top open-source projects. Topics include Spring Boot, Spring Cloud, Docker, Kubernetes and more. Author’s GitHub project “mall” has 50K+ stars.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.