2016-08-05 77 views
1

我想了解Spring的緩存如何工作,特別是與事務和更多線程一起工作。使用事務和並行線程時出現錯誤的Spring緩存狀態

讓我們的服務緩存其結果

public class ServiceWithCaching { 

    @Cacheable(value="my-cache") 
    public String find() { 
     ...load from DB 
    } 

    @CacheEvict(value="my-cache", allEntries=true) 
    public void save(String value) { 
     ...save to DB 
    } 
} 

現在考慮運行兩個並行線程測試。其中一個使用事務來保存一個值,另一個讀取一個值。

service.save("initial");    // initial state 
assert service.find() == "initial"; // load cache 

CountDownLatch latch = new CountDownLatch(1); 

Thread saveThread = new Thread(() -> { 

    TransactionDefinition transactionDefinition = new DefaultTransactionDefinition(); 
    TransactionTemplate transactionTemplate = new TransactionTemplate(transactionManager, transactionDefinition); 
    transactionTemplate.execute(new TransactionCallbackWithoutResult() { 

     @Override 
     protected void doInTransactionWithoutResult(TransactionStatus status) { 
      service.save("test"); // evict cache 
      latch.await(); 
     } 
    }); 

}); 
saveThread.start(); 

Thread readThread = new Thread(() -> { 
    service.find(); // load cache 
    latch.countDown(); 
}); 
readThread.start(); 

saveThread.join(); 
assert service.find() == "test"; 

聲明失敗,因爲service.find()返回「initial」。這是因爲第二個線程在第一個線程提交事務之前加載先前被驅逐的緩存。

結果是:

  • 持久化的價值= 「測試」
  • 緩存值= 「初始」

是否有任何春路該怎麼解決這個問題?

+0

也許你的答案在這裏。 http://stackoverflow.com/questions/21467439/multiple-threads-calling-the-cacheable-method-spring-cache-3-2-6-is-allowing – duardito

回答

1

那麼,在回顧上面的代碼之後,它看起來是正確的,但是我相信導致您的測試失敗的線程計時有一些細微之處。即儘管您試圖正確協調線程(即read,savemain),但您的測試仍有可能的爭用條件(例如check-then-act)。

從技術上講,具體而言,您的線程協調邏輯並不保證JRE(與OS線程調度程序結合)交叉執行線程的操作將導致預期的結果。

考慮以下...

讓:

R == Reader Thread 
S == Save Thread 
M == Main Thread 

然後線以下的操作交叉是可能的:

T0. M @ S.start() 

T1. M @ R.start() 

T2. S @ transactionTemplate.execute() // Starts a (local) Transaction context 

T3. S @ txCallback.doInTransactionWithoutResult() 

T4. S @ cache.evict() // Evicts all entries 

T5. S @ service.save("test") 

T6. S @ db.insert(..) // block call to the DB  

T7. R @ server.find() 

T8. R @ cache.get() // results in cache miss due to eviction in T4 

T9. R @ db.load(key) // loads "initial" since TX in T6 has not committed yet 

T10. R @ cache.put(key, "initial"); 

T11. R @ latch.countDown() 

T12. S @ db.insert(..) // returns updateCount == 1 

T13. S @ tx.commit(); 

T14. S @ latch.await(); // proceeds 

T15. M @ saveThread.join() // waits for S to terminate, then... 

T16. M @ assert service.find() == "test" // cache hit; returns "initial"; assertion fails. 

首先,如你所知,Thread.start()不會引起一個線程運行。 start()向運行時發信號通知線程已準備好被調度並由OS運行。你可以操縱線程優先級,但這不會有太大的幫助,也不能解決你的競爭條件。

其次,您可以通過切換在你的讀者的電話latch.await()latch.countDown()解決您的測試,並保存線程是這樣的...

Thread saveThread = new Thread(() -> { 
    ... 
    transactionTemplate.execute(new TransactionCallbackWithoutResult() { 
     @Override 
     protected void doInTransactionWithoutResult(TransactionStatus status) { 
      service.save("test"); // evict all entries in cache 
      latch.countDown(); 
     } 
    }); 
}); 

然後......

Thread readThread = new Thread(() -> { 
    latch.await(); 
    service.find(); 
}); 

readThread.join(); 

但是,由於您在啓動任何線程之前預先加載緩存......

service.save("initial");    // initial state 
assert service.find() == "initial"; // load cache 

,然後進行調用saveThread終止後service.find(),沒有真正爲readThread任何一點,因爲main線程可以作爲「讀者」線程。那麼...

saveThread.join(); 
assert service.find() == "test"; 

同樣,我不是100%確定這是發生在你的情況,但它是可能的。

我已經編碼了一個類似的測試(根據上面的測試代碼)here。有一些差異。

首先,我使用了一個簡單但優雅的併發測試框架,名爲MultithreadedTC,以便對線程進行精確和精確的控制。

其次,我使用了Spring的@Transactional註釋支持,而不是像您在測試中完成的程序化事務管理。

最後,我使用嵌入式HSQL數據庫(DataSource)以及DataSourcePlatformTransactionManager來測試緩存上下文中的事務行爲。 SQL初始化腳本在這裏(schema)和這裏(data)。

如果您運行此測試,請確保在您的類路徑中聲明適當的依賴關係。

這個測試通過如預期,所以我會Spring的緩存抽象函數正確地說,在高速緩存的情況下,提供的東西是協調多個線程之間正常。

有幾件事要記住。

  1. @CacheEvict annotation是(「後」 AOP通知,這是「默認」的行爲,即),這意味着僅在該方法的成功執行,將逐出從高速緩存條目的法後調用操作。您可以通過在@CacheEvict批註中指定beforeInvocation屬性來更改此行爲。

  2. 將多種類型的建議組合到應用程序服務方法(例如事務或緩存)時,您可能需要指定建議執行的順序以實現正確的應用程序行爲。

  3. 記住,如果多個線程調用同一個@Cacheable方法,您可能需要使用sync屬性(參見here瞭解詳細信息)正確同步操作。如果您需要協調多個基於緩存的操作(例如,可以同時調用@Cacheable方法和@CacheEvict方法),則需要使用服務對象的監視器同步這些方法。

  4. 讓我們來看看,還有什麼?

希望這有助於!

-John

相關問題