保持领先
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 示例仓库中找到。
后续还会有更多类似的文章。如果您希望我涵盖特定主题,请告诉我。