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 - 如何为我的领域模型生成模式?

如果您不熟悉 Spring Data JDBC,则应首先阅读其介绍这篇文章,其中解释了聚合在 Spring Data JDBC 上下文中的相关性。相信我,这很重要。

本文基于我在Spring One 2021 上发表的演讲的一部分。

为什么 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);
	}

}

最后,我们需要一个测试,该测试重复访问数据库,仅在保存后才会产生一个选择。

@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 示例存储库中找到

还会有更多类似的文章。如果您想让我涵盖特定主题,请告诉我。

获取 Spring 新闻通讯

与 Spring 新闻通讯保持联系

订阅

领先一步

VMware 提供培训和认证,以加速您的进步。

了解更多

获取支持

Tanzu Spring 在一个简单的订阅中提供 OpenJDK™、Spring 和 Apache Tomcat® 的支持和二进制文件。

了解更多

即将举行的活动

查看 Spring 社区中所有即将举行的活动。

查看全部