Spring 環境下,@Transactional 服務中循環調用 DAO 查詢,即使資料庫已有新資料插入,查詢結果仍不更新,導致循環永不結束。
// Spring 事務配置
@Bean
public DataSourceTransactionManager transactionManager(DataSource dataSource) {
return new DataSourceTransactionManager(dataSource);
}
@EnableTransactionManagement
// DAO
@Repository
public class UserDao {
public User getUser() {
return jdbcTemplate.queryForObject(
"SELECT * FROM user ORDER BY id DESC LIMIT 1",
(rs, rowNum) -> new User(rs.getLong("id"), rs.getString("name"))
);
}
}
// Service
@Service
@Transactional // 關鍵:類別層級事務
public class SecurityService {
@Autowired UserDao userDao;
public void checkUserInfo() {
while (true) {
User user = userDao.getUser(); // 每次查詢結果相同!
if (user != null && "Tom".equals(user.getName())) {
System.out.println("Tom is here");
break;
}
// 即使資料庫插入了 name=Tom 的記錄,這裡查不到
}
}
}
DataSourceUtils.doGetConnection() 源碼揭示:
ConnectionConnection 與當前事務綁定,存放在 ThreadLocal 中@Transactional 方法),複用同一個 ConnectionConnection(同一事務)內的多次查詢,在 REPEATABLE READ 下會看到事務開始時的快照@Transactional → 所有 public 方法納入事務管理@Transactional,類別層級仍生效tx:annotation-driven 對所有 @Service 進行代理,預設傳播行為 REQUIRED
@Service
@Transactional // 類別層級保留(其他方法可能需要事務)
public class SecurityService {
@Autowired UserDao userDao;
// 此方法不需要事務,掛起當前事務
@Transactional(propagation = Propagation.NOT_SUPPORTED)
public void checkUserInfo() {
while (true) {
User user = userDao.getUser();
if (user != null && "Tom".equals(user.getName())) {
System.out.println("Tom is here");
break;
}
// 短暫休眠避免 CPU 佔用過高
try { Thread.sleep(100); } catch (InterruptedException e) { break; }
}
}
}
原理: NOT_SUPPORTED 表示「以非事務方式執行,若有現有事務則掛起」。每次 DAO 查詢都會獲取新的 Connection,看到最新提交的資料。
@Autowired DataSource dataSource;
@Transactional(propagation = Propagation.NOT_SUPPORTED)
public void checkUserInfo() {
while (true) {
try (Connection conn = dataSource.getConnection();
PreparedStatement ps = conn.prepareStatement("SELECT * FROM user ORDER BY id DESC LIMIT 1");
ResultSet rs = ps.executeQuery()) {
if (rs.next() && "Tom".equals(rs.getString("name"))) {
System.out.println("Tom is here");
break;
}
} catch (SQLException e) {
throw new RuntimeException(e);
}
// 休眠...
}
}
public void checkUserInfo() {
while (true) {
Connection conn = null;
try {
// 獲取非事務綁定的 Connection
conn = DataSourceUtils.getConnection(dataSource);
// 執行查詢...
} finally {
// 釋放而非關閉(歸還連線池)
DataSourceUtils.releaseConnection(conn, dataSource);
}
}
}
@Transactional(isolation = Isolation.READ_COMMITTED)
public void checkUserInfo() { ... }
注意: 即使設為 READ_COMMITTED,同一事務內的多次查詢在某些資料庫(如 Oracle)仍可能看到舊快照。且會影響其他業務邏輯的一致性。
@Service
public class UserQueryService { // 無 @Transactional
@Autowired UserDao userDao;
public User getLatestUser() {
return userDao.getUser();
}
}
@Service
@Transactional
public class SecurityService {
@Autowired UserQueryService userQueryService; // 注入非事務服務
public void checkUserInfo() {
while (true) {
User user = userQueryService.getLatestUser(); // 非事務調用
if (user != null && "Tom".equals(user.getName())) break;
}
}
}
// 簡化版 TransactionSynchronizationManager
public abstract class TransactionSynchronizationManager {
private static final ThreadLocal
同一執行緒、同一 DataSource → 同一 ConnectionHolder → 同一實體 Connection。
@Transactional(readOnly = true) 適用於單次查詢,不適用於需看最新資料的輪詢| 方案 | 優點 | 缺點 | 適用場景 |
|---|---|---|---|
| NOT_SUPPORTED | 簡潔、聲明式、不影響其他方法 | 需理解傳播行為 | 最推薦,輪詢查詢、報表、驗證 |
| 手動 Connection | 完全控制、無 Spring 干擾 | 樣板碼多、易洩漏連線 | 特殊場景、批次處理 |
| DataSourceUtils | Spring 管理連線池、正確釋放 | API 較底層 | 需在事務中穿插非事務操作 |
| 分離 Service | 架構清晰、職責單一 | 類別增加 | 大型專案、明確分層 |
Spring 事務將 Connection 綁定至執行緒,導致同一事務內重複查詢看到舊快照。核心解決思路是讓查詢脫離當前事務上下文,最簡潔的方式是在查詢方法上標註 @Transactional(propagation = Propagation.NOT_SUPPORTED)。理解 Spring 事務同步機制、資料庫隔離級別、Connection 綁定原理,能從根源避免此類問題。