Spring Data Redis两个问题:内存泄露和并发 - europace

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Spring Data Redis两个问题:内存泄露和并发 - europace相关的知识,希望对你有一定的参考价值。

参考技术A

我们最近将会话管理从 MongoDB 迁移到了 Redis。迁移本身是由我们使用 MongoDB 的经验推动的,它不能特别好地处理高频率更新和更频繁地读取。另一方面,Redis 被称为经过验证的存储,可以准确处理该用例。

数据库迁移并不总是那么容易,因为我们需要学习其他服务的新模式、最佳实践和怪癖。我们的目标是让我们的 Java 服务层尽可能简单,使其稳定且面向未来:会话管理当然是具有相当稳定功能集的服务之一,并且不会经常触及其代码。因此,对于几年后窥探它的任何人来说,保持它的简单易懂是一个重要方面。

我们面临两个问题:

本文总结了我们在使用 Spring Data 作为持久层的瘦 Java 服务中采用 Redis 的经验。

带有二级索引和 EXPIRE/TTL 的 Spring Data Redis

在 Redis 中采用 Spring Data可直接开始:您需要的只是 Gradle 或 Maven 构建的依赖项以及@EnableRedisRepositoriesSpring Boot 应用程序中的注释。Spring Boot 的大多数默认设置都是有意义的,并且可以让您非常顺利地运行 Redis 实例。

但是会遭遇:Redis内存使用量不断增长的问题,下面看看这个认识过程:

我们由该存储库管理的实体也开始变得尽可能简单:

您会注意到我们选择对ttl属性建模,该属性被@TimeToLive转换为 EXPIRE 实体。我们不想手动跟踪过期会话,但希望 Redis 透明地删除过期会话。该ttl会定期刷新用户活动期间,如果手工删除,可能会被注销。

当用户实际按下注销按钮时会发生什么,或者我们如何禁用用户帐户并使正在运行的会话无效?简单:我们也有一个userId作为会话数据SessionData的一部分,并且可以执行以userId查询查找每个会话。上述类型所需的更改如下所示:

SessionDataCrudRepository:

SessionData:

@Indexed注解在 Spring Data 中触发了一个特殊的行为:该注解实际上告诉 Spring Data在实体上创建和维护另一个索引,以便我们可以根据给定userId查询SessionData.

但是,二级索引和实体自动到期的组合使设置变得更加复杂。当引用的实体被删除时,Redis 不会自动更新二级索引,因此 Spring Data 需要处理这种情况。

然而,Spring Data 不会经常查询 Redis 的过期实体(键),这就是为什么 Spring Data 依赖于 R Redis Keyspace Notifications for expiring keys 所谓的 Phantom Copies( 幻影副本 )来失效过期键:

当到期时间设置为正值时,将运行相应的 EXPIRE 命令。除了保留原始副本外,Redis 中还保留了一个幻影副本,并设置为在原始副本之后 5 分钟过期。这样做是为了使 Repository 支持发布 RedisKeyExpiredEvent,只要一个键过期 expiring key ,就会在 Spring 的 ApplicationEventPublisher 中间保存过期的值,即使原始值已经被删除。

下一段有一个小细节需要注意:

默认情况下,初始化应用程序时禁用 expiring keys 侦听器。可以在 @EnableRedisRepositories 或 RedisKeyValueAdapter 中调整启动模式,以使用应用程序或在第一次插入具有 TTL 的实体时启动侦听器。有关可能的值,请参阅 EnableKeyspaceEvents。

遗憾的是,当时我们还没有阅读到这点。这就是为什么我们体验到启用EXPIRE禁用的expiring keys侦听器以及不断增长的二级索引的效果的原因。长话短说:我们观察到越来越多的键和不断增长的内存使用量 - 直到达到 Redis 的内存限制。

检查 Redis 键可以很明显地找到配置错误的位置,最终启用键空间事件的注释@EnableRedisRepositories使我们修复了内存泄露。

我们还禁用了 的自动服务器配置notify-keyspace-events property,因为我们在服务器端启用了该设置:

我们还必须手动清理陈旧的数据,所以我们还要提一下,在处理大型数据集时,您应该总是更选择 SCAN 而不是KEYS。Netflix 的 nf-data-explorer 可能会有所帮助,如果您不喜欢使用本机redis-cli.

并发读取和写入期间缺少实体

随着内存使用量不断增长的问题得到解决,我们最终将新服务作为我们会话的主要来源。

当请求击中我们的安全链时,我们总是验证用户的会话是否有效。这些验证是在会话管理中的简单查找sessionId。通常,404 NOT FOUND会话管理的状态指示sessionId无效(未知)或会话已过期(并被 Redis 删除)。

除了使用新 API 的应用程序中的一些相关更改外,我们还观察到了另一种奇怪的行为:无法找到某些会话,尽管我们 100% 确定会话应该仍然有效(已知且未过期)。在会话查找失败后,大多数重试都成功了,所以我们知道数据没有丢失,只是无法找到。

我们无法主动重现错误行为,收集日志、指标和跟踪也没有起到作用。在此过程中,我们添加了缓存和其他解决方法,并进行了一些更改以改进整体行为,但我们实际上并未解决该问题。

如果您仔细阅读本文的第一部分,您可能还记得有关我们刷新ttl. 我们不仅刷新ttl,而且还刷新作为SessionData的一部分lastResponse时间戳

因此,让我们更详细地了解有关会话管理的请求处理。用户发送一个请求,以及一个sessionId,表明他们已登录。我们使用它执行查找sessionId以验证用户的会话。如果会话被认为是有效的,则应用程序可以继续执行请求的操作。应用程序处理完请求后,安全链会定期更新会话,重置ttl和写入当前lastResponse时间戳。通常,用户执行多个请求——可能不是真正的人,而是在浏览器中运行的前端应用程序。该前端应用程序并不真正关心它发送新请求的频率,因此我们可以假设多个请求可能同时到达我们的后端。

正在验证多个请求。多个请求触发会话刷新以及SessionData的写操作.

我们仍然使用 Spring DataCrudRepository来读取和更新会话,使用以下代码:

有时,repository.findById(...)没有产生任何东西,所以我们专注于那部分。不过,问题是由repository.save(...)电话引发的。经过几周的谷歌搜索并盯着日志和跟踪,我们发现了refreshSessionTtl和getSession调用之间的相关性。

互联网上的许多文章已经训练我们将 Redis 视为单线程服务,按顺序执行每个请求。谷歌搜索“spring data redis concurrent writes”,我们找到了stackoverflow和 spring-projects/spring-data-redis/issues/1826 中的问题,在那里描述甚至解释了我们的问题 - 以及修复.

长话短说:Spring Data 将更新实现为DEL和HMSET两个步骤时,没有任何事务保证。换句话说:通过 CrudRepositories 更新实体不提供原子性。我们的HGETALL请求有时恰好发生在DEL和之间HMSET,导致空结果,或者有时有结果,但结果为 负ttl 。

我们的问题现在可以通过集成测试重现并使用 PartialUpdate .

所以上面的实现改为:

概括

过期键、二级索引和将所有魔法委托给 Spring Data Redis 的组合需要正确配置键空间事件侦听器。否则,由于幻影副本,您使用的内存会随着时间的推移而增长。考虑@EnableRedisRepositories(enableKeyspaceEvents = ON_STARTUP)在您的应用中使用类似的配置。

在并发读取和更新的环境,提防Spring Data的CrudRepository工具的更新的过程分为两个步骤DEL和HMSET。如果您观察到零星丢失的键或结果为负值TTL,则您可能遇到了并发问题。检查您的写入操作并考虑使用 PartialUpdate和 Spring Data 的RedisKeyValueTemplate update方法更新需要改变的属性

以上是关于Spring Data Redis两个问题:内存泄露和并发 - europace的主要内容,如果未能解决你的问题,请参考以下文章

Spring Data 教程 - Redis

spring-data-redis 动态切换数据源

Spring boot->redis value值出现两个双引号

如何将值添加到 Spring Data Redis 中的嵌入集

spring-data-redis时效设置

SpringBoot操作Redis