本周相关的一些事项,我认为最值得讲得就是某个数据库接入层服务的给每一条数据加上键过期时间。这个数据库接入层是一个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实例的规格我也在半年前降下来了,成本有所削减。