首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> Java 程式設計 >> Java 程式設計入門

Spring 事務導致 DAO 循環查詢數據不實時的解決方案

     編輯:Java 程式設計入門

問題現象

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 的記錄,這裡查不到
        }
    }
}

根本原因分析

1. Spring 事務與 Connection 綁定機制

DataSourceUtils.doGetConnection() 源碼揭示:

  • Spring 為每個執行緒、每個 DataSource建立一個 Connection
  • 此 Connection 與當前事務綁定,存放在 ThreadLocal 中
  • 同一事務內(無論是否有 @Transactional 方法),複用同一個 Connection

2. 資料庫隔離級別與快照讀

  • 預設隔離級別通常為 READ COMMITTED(MySQL/PostgreSQL)或 REPEATABLE READ(MySQL InnoDB 預設)
  • 同一 Connection(同一事務)內的多次查詢,在 REPEATABLE READ 下會看到事務開始時的快照
  • 即使其他事務已提交新資料,當前事務也看不到

3. @Transactional 在類別層級的影響

  • 類別標註 @Transactional → 所有 public 方法納入事務管理
  • 即使移除方法層級 @Transactional,類別層級仍生效
  • tx:annotation-driven 對所有 @Service 進行代理,預設傳播行為 REQUIRED

解決方案

方案一:@Transactional(propagation = Propagation.NOT_SUPPORTED) —— 推薦


@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,看到最新提交的資料。

方案二:手動獲取 Connection 繞過 Spring 事務


@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);
        }
        // 休眠...
    }
}

方案三:使用 DataSourceUtils 手動管理


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


@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;
        }
    }
}

深度原理:Spring 事務同步管理器


// 簡化版 TransactionSynchronizationManager
public abstract class TransactionSynchronizationManager {
    private static final ThreadLocal> resources = new ThreadLocal<>();
    
    static ConnectionHolder getResource(DataSource dataSource) {
        Map map = resources.get();
        return map != null ? map.get(dataSource) : null;
    }
    
    static void bindResource(DataSource dataSource, ConnectionHolder holder) {
        Map map = resources.get();
        if (map == null) {
            map = new HashMap<>();
            resources.set(map);
        }
        map.put(dataSource, holder);
    }
}

同一執行緒、同一 DataSource → 同一 ConnectionHolder → 同一實體 Connection。

最佳實踐建議

  1. 查詢類操作標註 NOT_SUPPORTED: 讀取最新資料的輪詢、報表查詢、驗證邏輯
  2. 避免長事務: 事務範圍越小越好,減少鎖持有時間
  3. 區分讀寫服務: 讀服務無事務/NOT_SUPPORTED,寫服務 REQUIRED
  4. 使用只讀事務優化: @Transactional(readOnly = true) 適用於單次查詢,不適用於需看最新資料的輪詢
  5. 連線池配置: 確保連線池大小足夠支援非事務查詢的並發

對比總結

方案優點缺點適用場景
NOT_SUPPORTED簡潔、聲明式、不影響其他方法需理解傳播行為最推薦,輪詢查詢、報表、驗證
手動 Connection完全控制、無 Spring 干擾樣板碼多、易洩漏連線特殊場景、批次處理
DataSourceUtilsSpring 管理連線池、正確釋放API 較底層需在事務中穿插非事務操作
分離 Service架構清晰、職責單一類別增加大型專案、明確分層

結語

Spring 事務將 Connection 綁定至執行緒,導致同一事務內重複查詢看到舊快照。核心解決思路是讓查詢脫離當前事務上下文,最簡潔的方式是在查詢方法上標註 @Transactional(propagation = Propagation.NOT_SUPPORTED)。理解 Spring 事務同步機制、資料庫隔離級別、Connection 綁定原理,能從根源避免此類問題。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved