取得領先
VMware 提供培訓和認證,以加速您的進度。
了解更多這是關於在使用 Spring Data JDBC 時可能遇到的各種挑戰的系列文章中的第三篇。
此系列包含
Spring Data JDBC - 如何實作快取? (本文)。
如果您是 Spring Data JDBC 的新手,您應該先閱讀其 introduction 和 這篇文章,其中說明了 aggregates 在 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 repositories 只是 Spring Beans,您可以將它與您喜歡的任何快取解決方案結合使用。 顯然的選擇當然是 Springs Caching abstraction,您可以在其後放置任何快取解決方案。
它幾乎太簡單了,令人難以置信。
為了演示目的,我再次使用心愛的 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
作為金鑰。 我們通過使用 SpEL 表達式來完成此操作。 id
通常僅在儲存實體後才可用,因此我們使用 beforeInvocation = false
。 並且使用 SpEL
會強制我們將 Minion
設為 public 並新增一個 public getId()
方法。
請注意,我們需要通過將 @EnableCaching
新增到我們的 Boot 應用程式來啟用快取。
@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 專注於其工作:持久化和載入 aggregates。 快取與此正交,可以使用眾所周知的 Spring Cache abstraction 來新增。
完整的範例程式碼可在 Spring Data Example repository 中取得。
還會有更多類似的文章。 如果您希望我涵蓋特定主題,請告訴我。