領先一步
VMware 提供培訓和認證,助您加速進步。
瞭解更多這是關於如何解決使用Spring Data JDBC時可能遇到的各種挑戰系列文章的第三篇。
本系列文章包括:
Spring Data JDBC - 如何實現快取? (本文)。
如果您是Spring Data JDBC的新手,您應該首先閱讀其介紹和這篇解釋聚合在Spring Data JDBC上下文中重要性的文章。相信我,這很重要。
本文基於我在2021年Spring One大會上的演講的一部分。
Spring Data JDBC的一個重要設計決定是“不”包含快取。這樣做的原因,與許多其他決策一樣,來自我們使用JPA的經驗。讓我們看看JPA及其處理快取的方式。
JPA做出了一個相當強烈的承諾:每當你在一個會話中載入邏輯上相同的實體時,你將總是獲得完全相同的例項。這聽起來確實很方便。當你透過ID訪問一個實體時,它通常可以節省一次資料庫往返。但這樣做的原因是,這實際上是JPA正常工作所必需的。JPA跟蹤實體的變化,以便最終將這些變化重新整理到資料庫。如果單個邏輯實體由多個可能具有不同和矛盾狀態的Java例項表示,這將無法工作。
為了實現這個承諾,JPA使用了“一級快取”,從而混合了兩個非常不同的任務:
在記憶體和資料庫之間傳輸物件。
快取。
這反過來又會引起問題,尤其是當開發人員忘記了快取或一開始就沒有了解它的時候。
他們使用SQL更新實體,但未能使用JPA載入更新後的狀態,因為JPA總是返回已經載入的實體。
他們在記憶體中編輯實體,並驚訝地發現它被儲存到資料庫中,儘管他們從未呼叫過執行此操作的方法。
他們在記憶體中編輯實體,並希望將其與資料庫中的狀態進行比較,結果再次驚訝地發現他們不斷獲得已經更改的版本。
他們執行大量批處理,並驚訝地發現他們的實體沒有被垃圾回收,導致巨大的記憶體佔用、糟糕的效能,並可能出現記憶體不足異常。
Spring Data JDBC中的關注點分離使事情變得更加透明。當你對相應的倉庫呼叫save()時,實體會被儲存到資料庫。當你呼叫從倉庫返回一個或多個實體的方法時,它會從資料庫載入。
毫無疑問,在某些情況下,快取是正確的做法。每當你有很多讀取但變化不快的資料時,快取都是一個合理的選擇。
由於快取不是Spring Data JDBC的一部分,並且Spring Data JDBC倉庫只是Spring Bean,你可以將其與任何你喜歡的快取解決方案結合使用。顯而易見的選擇當然是Spring的快取抽象,你可以在其背後放置任何快取解決方案。
這簡直太簡單了,難以置信。
為了演示目的,我再次使用了備受喜愛的Minion實體及其匹配的倉庫。
public class Minion {
@Id
Long id;
String name;
Minion(String name) {
this.name = name;
}
public Long getId(){
return id;
}
}
請注意倉庫上的快取相關注解。
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公開並新增一個公共的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示例倉庫中找到。
還會有更多類似的文章。如果您希望我涵蓋特定主題,請告訴我。