领先一步
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 中的关注点分离使事情更加透明。当您在相应的存储库上调用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);
}
}
最后,我们需要一个测试,该测试重复访问数据库,仅在保存后才会产生一个选择。
@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
的缓存避免了重复选择,而save
触发了从缓存中逐出实体。
对于此示例,我们使用简单的缓存,它只是一个ConcurrentMap
。对于生产环境,您可能希望使用可以配置驱逐策略等的适当缓存实现。但与 Spring Data JDBC 的用法保持不变。
Spring Data JDBC 专注于其工作:持久化和加载聚合。缓存与此正交,并且可以使用众所周知的 Spring Cache 抽象进行添加。
完整的示例代码可在Spring Data 示例存储库中找到。
还会有更多类似的文章。如果您想让我涵盖特定主题,请告诉我。