本周相关的一些事项,我认为最值得讲得就是某个数据库接入层服务的给每一条数据加上键 过期时间。这个数据库接入层是一个 Java 服务,本质上做的工作就是将 Redis 与数据库 结合起来。对上的接口遵循某一个公司内部的特定协议,而对下则使用开源且很多人使用的 Java 包来访问 Redis 或者数据库。访问 Redis 使用的是 lettuce,而访问数据库用的就 是 mybatis。原来访问 Redis 其实用的是 Jedis,后面之所以改用 lettuce,是因为它在 高并发下的性能/资源表现更好一些。我认为具体地主要体现在多路复用连接,使得不需要 在连接池中维护很多连接,总之就是在连接上的开销不会很大。

然后对于数据库来说,并非直接写入,而是在消息队列中写入一条插入指令并附上一些上下 文。这样消息中包含了写入数据库需要的所有信息了。然后这条消息队列也是自己在消费, 消费的时候将数据写入数据库,如果成功就 ACK 消息,失败了就让这条消息 5 分钟后再回 来。

这个服务总的读写策略就是,如果一条数据查询来了,Redis 中没有数据时就将数据库中的 数据拉到 Redis。然后返回 Redis 中的数据。Redis 中有数据则直接返回。对于写入数据 来说,这条数据会双写 Redis 和数据库。修改数据的时候,操作复杂得多。因为这个 Redis 中数据是使用 HSET 的方式进行存储的,然后一条修改指令可能只是修改 HSET 中的 一部分字段。这个就需要对一些具体的情况进行讨论了,数据库指令也是根据具体情况来构 造。

值得一提的事写数据库的时候,目前看前人升级了策略,不直接拿消息中的数据来写,而是 消费到消息时,直接从 Redis 中读数据(这个时候 Redis 是早就被双写了的),然后直接 将 Redis 中读区到的数据写入数据库。我认为这样做的好处是,让数据库中的信息尽快更 新。因为从消息投递到消息队列再到消费的过程中,这一条数据可能已经修改了好几次了。

在这几周我发现这个服务在 Redis 中的缓存并未设置过期时间,而是依赖 Redis 实例中设 置的统一过期策略来进行数据淘汰。这个策略是在 Redis 数据存满的时候才会执行的,这 就会引入一个风险。根据我们云上分布式 Redis 的设计,如果 Redis 存储一直都是满的然 后这个时候有大量的数据写入操作(某种特殊的业务高峰期),这样云上分布式 Redis 为 了一次性腾出足够的空间会集中进行淘汰操作。这个时候无法处理任何请求,这种情况将持 续几分钟。这对于业务来说,存在较大影响,因为这个时候上游业务服务将无法读或者写入 数据了。而现实情况就是,这个服务的 Redis 一直处于满的状态。

为了降低风险,最好就是为每一条数据设置过期时间。使得 Redis 中存储的始终是较热的 数据。而设置过期时间,采取的策略是取一个固定的时间 Y,然后再生成一个均匀分布的浮 点随机数因子 a,从 0-1 变化。然后我们对于某一条数据的过期时间就是 Y+aY。这样设置 能尽可能避免某些情况下大量数据集中过期造成的某段时间内数据库压力过大问题。

当时 Redis 中有 64GB 的数据,且整个 Redis 实例是满的。我评估了一下,如果只是增量 进行数据更新,确保新的数据是设置了过期时间的,这样应该能让存储空间缓慢降下来。最 终应该是会剩下少量的数据没有更新,占比应该不大,因为我们的业务不是存量业务,大部 分用户应该会继续使用。

2024 年 4 月

将近一年多时间观察下来,Redis 中的数据只剩下了 10GB 左右。而 Redis 实例的规格我 也在半年前降下来了,成本有所削减。