如果缓存对象的数据结构要变更,也就是说,新的代码里将使用新的数据结构,那么上线时,线上已有缓存对象的数据结构可能跟新代码中定义的数据结构不匹配,在反序列化时可能会产生数据完整性的问题。
为了解决这个问题,就
要让新代码取不到旧缓存或者忽略旧缓存。
一个解决方案是在新代码中使用新的序列号,新代码获取旧缓存时会发生反序列化错误; 我们再显式地捕捉这种错误,在发生这种错误时再去后端取数据,并使用取来的数据覆盖旧缓存。
然而,“更新序列号”这个方案有个触不到的地方:泛型的更换。如果缓存对象的数据结构从ArrayList<A>变成了ArrayList<B>,即使A和B的序列号不同,新代码在遇到旧缓存时也不会发生序列化错误。后果是,
新代码以为它拿到的是ArrayList<B>对象,实际上却是ArrayList<A>对象,最终的后果就是在访问List中元素时发生ClassCastException.
你会问,能否侦测这种情况?比如说,新代码从缓存中拿到一个ArrayList时,判断它的泛型是不是ArrayList<B>,如果不是则忽略? 答案是做不到,java的“伪泛型”不支持这种判断。
另一个解决方案是上线前先清空所有旧缓存,这样新代码永远取不到旧缓存。然而它亦有短板:
如果你们采用了灰度发布流程,旧缓存仍然可能跟新代码接触。 所谓灰度发布是指将新代码发布到集群服务器时,先发到部分机器,针对这部分机器验证通过后再继续发下去。 显然,在这个过程中,新旧代码会同时存在于集群中;运行中的旧代码有可能会产生旧的缓存,且被新的代码获取,发生问题;同理,新代码产生的缓存也会被旧代码取到,照样发生问题。
清空旧缓存这种方案还有一个问题是,你需要在发布时手工介入,比较麻烦。
那怎么办才好呢? 有个方案是:除了使用新的序列号,
新代码也要使用新的缓存key.如果老代码使用 key_
record_id来存取,新代码就用new_key_
record_id来存取. 这样的话,新代码永远取不到老的缓存;老的代码也取不到新的缓存。 有了这个基础,泛型变换、灰度发布时的新旧代码共存都不是问题。
使用新的缓存key,可以把新旧缓存隔离开来,相当于在逻辑上产生了两套缓存空间,是一种相当彻底的解决方案; 另外,这种方案也非常简单,不需要在发布时手动做什么事情。 不过,实施这种方案有个前提:由于旧缓存会被新代码忽略,从而成为无用的缓存;新系统上线时会有大量的Cache Miss,你要确保这不会引发性能问题。