0%

灰度发布过程中缓存数据一致性问题

灰度发布是一种常见的上线策略。如果灰度发布时间较长,且在灰度期间各分组共用缓存,那么该过程中可能会面临缓存数据一致性的挑战。本文将讨论在灰度发布过程中导致缓存数据不一致的问题和解决方案。

背景

通常,我们采用了灰度发布策略来确保新功能的平稳上线。我们将用户流量分为两个组(为了描述简单,假设灰度只有两个分组):A组和B组。在灰度发布的第一阶段,我们选择将20%的流量分配给B组,将剩余的80%分配给A组。这样可以确保新功能在一部分用户中进行测试和验证,同时保持对大多数用户的稳定性。然而,在灰度发布过程中由于代码版本的不一致,导致A组和B组的缓存数据出现了不一致的情况。

问题分析

在灰度发布期间可能出现以下两个场景导致数据不一致问题:

  1. 缓存增加元素: 在B组的代码版本(v2)中,我们增加了一个缓存对象的元素,但是A组的代码版本(v1)并没有包含这个元素。这导致A组写入缓存的数据,在B组读取时无法正确反序列化,导致数据解析失败。

  2. 计算逻辑变化: 缓存对象的元素(e1)是逻辑计算得出的结果,在B组的代码版本(v2)中我们修改了该元素计算逻辑,但是A组的代码版本(v1)仍是旧的计算逻辑;这导致A组写入的缓存B组读出不符合预期,而B组写入的缓存A组读出不符合预期。

解决方案

在面对这种缓存数据一致性问题时,我们采取了以下解决方案:

  1. a/b组缓存隔离:A/B组分别访问不同的缓存,不同的分组可以连接不同的缓存或者不同的分组以不同的key前缀区分。然而实际情况是以上问题只会发生在极个别的缓存元素上。这种方案虽然能解决以上问题,但缓存数量会随着分组的增加而成倍增加,造成资源的严重浪费,同时缓存命中率也会下降。

  2. 反序列化兼容增加的元素:基本所有的序列化/反序列化组件都支持该功能。比如JDK自带的功能确保serialVersionUID保持不变即可,或者使用protobuf、json等序列化类库都可以实现。该方法虽然能解决A组写入B读取反序列化失败的问题,但B组反序列化成功仍然缺少新增加的元素。

  3. 使用新key:在B组代码中使用新的key,这样A/B组缓存可以彻底隔离。但对于某些底层数据结构,可能被上层很多缓存引用,而且是通过多级引用,开发人员容易遗漏某些key。

  4. 版本控制:大致思路是每个缓存在key上携带本次变更的版本号。如果本次发布缓存元素没有变化,则版本号不变。具体实现过程如下:

    • 在Spring容器启动过程中获取所有标记@Cacheable注解的方法。
    • 对方法返回值对象内元素按照元素类型和一定的算法计算对象code值。如果对象元素发生变化,则对象code值一定变化。如果是元素是自定义对象需要递归计算。
    • 读写缓存时在缓存key增加code后缀。

结论

最后我们采用了版本控制的方案。在实施前,我们进行了充分的测试和验证,确保缓存的版本控制逻辑正确无误,并在灰度发布期间保持了缓存数据的一致性。