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