保持領先
VMware 提供培訓和認證,助力您的進步。
瞭解更多本文是關於如何在使用 Spring Data JDBC 時應對各種挑戰系列文章的第三篇。
本系列包括:
Spring Data JDBC - 如何實現快取?(本文)
如果您是 Spring Data JDBC 的新手,應該先閱讀其介紹和這篇解釋聚合在 Spring Data JDBC 上下文中的重要性的文章。相信我,這很重要。
本文基於我在 Spring One 2021 上發表的演講的一部分。
Spring Data JDBC 的一個重要設計決策是**不**包含快取。其原因與許多其他決策一樣,源於我們使用 JPA 的經驗。讓我們看看 JPA 以及它如何處理快取。
JPA 做出了一個相當強的承諾:無論何時在會話中載入邏輯上相同的實體,您總是會獲得完全相同的例項。這聽起來當然很方便。當您透過 ID 訪問實體時,這通常可以節省一次資料庫往返。但這樣做的原因是,它實際上是 JPA 正常工作所必需的。JPA 會跟蹤您實體的更改,以便最終將這些更改重新整理到資料庫。如果一個邏輯實體由多個具有潛在不同和相互矛盾狀態的 Java 例項表示,這將無法工作。
為了實現這個承諾,JPA 使用了“一級快取”,從而混合了兩個截然不同的任務:
在記憶體和資料庫之間傳輸物件。
快取。
這反過來又會引起問題,特別是當開發人員忘記快取或一開始就沒有了解它時。
他們使用 SQL 更新實體,但無法使用 JPA 載入更新後的狀態,因為 JPA 總是返回已載入的實體。
他們在記憶體中編輯實體,卻驚訝地發現它被儲存到資料庫中,儘管他們從未呼叫過執行此操作的方法。
他們在記憶體中編輯實體,並想將其與資料庫中的狀態進行比較,再次驚訝地發現他們一直在獲取已更改的版本。
他們執行大型批處理任務,卻驚訝地發現他們的實體沒有被垃圾回收,導致巨大的記憶體佔用、糟糕的效能以及可能的記憶體不足異常。
Spring Data JDBC 中的關注點分離使得事情更加透明。當您在相應的 Repository 上呼叫 save()
時,實體會被儲存到資料庫。當您呼叫一個從 Repository 返回一個或多個實體的方法時,它會從資料庫載入。
毫無疑問,在某些情況下快取是正確的選擇。無論何時,如果您有大量讀取但變化不快的資料,快取都是一個合理的選項。
由於快取不是 Spring Data JDBC 的一部分,並且 Spring Data JDBC 的 Repository 只是 Spring Bean,您可以將其與任何您喜歡的快取解決方案結合使用。顯而易見的選擇當然是Spring 的快取抽象,您可以在其背後放置任何快取解決方案。
這簡直簡單得令人難以置信。
為了演示目的,我再次使用了備受喜愛的 Minion
實體及其匹配的 Repository。
public class Minion {
@Id
Long id;
String name;
Minion(String name) {
this.name = name;
}
public Long getId(){
return id;
}
}
注意 Repository 上與快取相關的註解。
interface MinionRepository extends CrudRepository<Minion, Long> {
@Override
@CacheEvict(cacheNames = "minions", beforeInvocation = false, key = "#result.id")
<S extends Minion> S save(S s);
@Override
@Cacheable("minions")
Optional<Minion> findById(Long aLong);
}
@CacheEvict
註解不像人們期望的那麼簡單,因為 save
方法接受一個實體,但我們需要它的 id
作為 key。我們透過使用 SpEL 表示式來實現這一點。一般來說,id
只有在儲存實體後才可用,因此我們使用了 beforeInvocation = false
。而使用 SpEL
迫使我們將 Minion
設定為 public 並新增一個 public 的 getId()
方法。
注意,我們需要透過向 Boot 應用程式新增 @EnableCaching
來啟用快取。
@EnableCaching
@SpringBootApplication
class CachingApplication {
public static void main(String[] args) {
SpringApplication.run(CachingApplication.class, args);
}
}
最後,我們需要一個測試來驗證重複訪問資料庫時,只有在儲存後才會進行一次 SELECT 操作。
@SpringBootTest
class CachingApplicationTests {
private Long bobsId;
@Autowired MinionRepository minions;
@BeforeEach
void setup() {
Minion bob = minions.save(new Minion("Bob"));
bobsId = bob.id;
}
@Test
void saveloadMultipleTimes() {
Optional<Minion> bob = null;
for (int i = 0; i < 10; i++) {
bob = minions.findById(bobsId);
}
minions.save(bob.get());
for (int i = 0; i < 10; i++) {
bob = minions.findById(bobsId);
}
}
}
為了觀察執行測試時發生了什麼,我們可以在 application.properties
中啟用 SQL 語句的日誌記錄。
logging.level.org.springframework.jdbc.core.JdbcTemplate=DEBUG
這些是在日誌中出現的 SQL 語句:
INSERT INTO "MINION" ("NAME") VALUES (?)]
SELECT "MINION"."ID" AS "ID", "MINION"."NAME" AS "NAME" FROM "MINION" WHERE "MINION"."ID" = ?]
UPDATE "MINION" SET "NAME" = ? WHERE "MINION"."ID" = ?]
SELECT "MINION"."ID" AS "ID", "MINION"."NAME" AS "NAME" FROM "MINION" WHERE "MINION"."ID" = ?]
因此,快取按預期工作。對 findById
進行快取避免了重複的 SELECT 查詢,而 save
操作會觸發從快取中逐出實體。
在本示例中,我們使用了簡單的快取,它只是一個 ConcurrentMap
。對於生產環境,您可能需要一個更完善的快取實現,可以配置逐出策略等。但它與 Spring Data JDBC 的用法保持不變。
Spring Data JDBC 專注於其本職工作:持久化和載入聚合。快取是與此正交的功能,可以使用眾所周知的 Spring Cache 抽象來新增。
完整的示例程式碼可在 Spring Data 示例倉庫中找到。
後續還會有更多類似的文章。如果您希望我涵蓋特定主題,請告訴我。