Spring Data JDBC - 如何實現快取?

工程 | Jens Schauder | 2021年10月18日 | ...

這是關於如何解決使用Spring Data JDBC時可能遇到的各種挑戰系列文章的第三篇。

本系列文章包括:

  1. Spring Data JDBC - 如何使用自定義ID生成。

  2. Spring Data JDBC - 如何建立雙向關係?

  3. Spring Data JDBC - 如何實現快取? (本文)。

  4. Spring Data JDBC - 如何對聚合根進行區域性更新?

  5. Spring Data JDBC - 如何為我的領域模型生成Schema?

如果您是Spring Data JDBC的新手,您應該首先閱讀其介紹這篇解釋聚合在Spring Data JDBC上下文中重要性的文章。相信我,這很重要。

本文基於我在2021年Spring One大會上的演講的一部分。

為什麼Spring Data不快取?

Spring Data JDBC的一個重要設計決定是“不”包含快取。這樣做的原因,與許多其他決策一樣,來自我們使用JPA的經驗。讓我們看看JPA及其處理快取的方式。

JPA做出了一個相當強烈的承諾:每當你在一個會話中載入邏輯上相同的實體時,你將總是獲得完全相同的例項。這聽起來確實很方便。當你透過ID訪問一個實體時,它通常可以節省一次資料庫往返。但這樣做的原因是,這實際上是JPA正常工作所必需的。JPA跟蹤實體的變化,以便最終將這些變化重新整理到資料庫。如果單個邏輯實體由多個可能具有不同和矛盾狀態的Java例項表示,這將無法工作。

為了實現這個承諾,JPA使用了“一級快取”,從而混合了兩個非常不同的任務:

  1. 在記憶體和資料庫之間傳輸物件。

  2. 快取。

這反過來又會引起問題,尤其是當開發人員忘記了快取或一開始就沒有了解它的時候。

  • 他們使用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示例倉庫中找到。

還會有更多類似的文章。如果您希望我涵蓋特定主題,請告訴我。

獲取 Spring 新聞通訊

透過 Spring 新聞通訊保持聯絡

訂閱

領先一步

VMware 提供培訓和認證,助您加速進步。

瞭解更多

獲得支援

Tanzu Spring 提供 OpenJDK™、Spring 和 Apache Tomcat® 的支援和二進位制檔案,只需一份簡單的訂閱。

瞭解更多

即將舉行的活動

檢視 Spring 社群所有即將舉行的活動。

檢視所有