大型网站中都会用到分布式缓存,现在经常使用的成熟可靠的分布式缓存产品有Memcached、Redis、Velocity等等。开发中我们在设计实现缓存层的时候,通常会按照业务模块,定义一些有意义的缓存键。比如,在一个非常典型的电子商务网站中,我们会缓存常用的字典表,如省市区县、商品分类、商品等等,一种常用的定义缓存键的方式如下:
/// <summary> /// 缓存键管理 /// </summary> public class CacheKeyManager { /// <summary> /// 区域 /// </summary> public static readonly string Area = "{0}_area_{1}"; /// <summary> /// 商品 /// </summary> public static readonly string Product = "{0}_product_{1}"; /// <summary> /// 商品分类 /// </summary> public static readonly string ProductCatagory = "{0}_productcatagory_{1}"; }
对于字符串中的第一项{0},我们通常会传入一个用于标识业务的有意义的字符串,比如命名空间、业务部门名称等;第二项{1}则通常对应于标识字典表数据的唯一性的值(如主键等)。这样拼接字符串定义缓存的key可以降低不同业务部门不同开发人员命名相同缓存键的可能,这一点在一个业务部门众多而部署的分布式缓存系统只有一套的环境下显得尤为重要。
实际开发使用缓存键的时候,最终拼接的字符串可能如下面这种形式:
/// <summary> /// 区域 /// </summary> public static readonly string Area = "namespace_area_areaid"; /// <summary> /// 商品 /// </summary> public static readonly string Product = "namespace_product_productid"; /// <summary> /// 商品分类 /// </summary> public static readonly string ProductCatagory = "namespace_productcatagory_productcatagoryid";
正如你所看到的,namespace是对应的命名空间,由不同业务部门的开发自行定义;xxxid则用于标识字典表数据的唯一性。
缓存的数据通常都是短时间内相对稳定不会发生变化的,但互联网业务是瞬息万变的,业务人员的要求通常也不那么通情达理,不变的数据往往也会随时需要改变并立刻在生产环境中体现出来。于是,我们在前台站点用上分布式缓存之后,还需要在后台业务系统中写不少代码,用于在改变数据的时候,及时更新缓存。
但在后台代码中,更新缓存往往都是费力不讨好的事情,一个考虑不到就会出现数据不一致的问题。比如现在需要更新一个商品分类的属性,和商品分类相关的很多缓存键必须都要考虑到,比如可能按照商品分类主键或者分类名称缓存了一个商品分类,或者按照某种查询规则缓存了一个商品分类列表或字典,或者商品分类属性的修改直接导致商品信息级联的修改,你又不得不考虑到缓存的商品信息……所以,在后台清理缓存的操作通常不是那么稳定可靠让人放心。
针对上面描述的这种复杂多变的应用场景,有人可能会说适当时候直接让运维介入,对于常用分布式缓存产品,上面这种情形只要一个命令行就把缓存及时清理了。可实际情况是,可能只有一个开发小组的缓存数据需要改变,却把整个公司的缓存数据都清理了,这样显然非常不合理。
求人不如求己,下面就是本文要介绍的一种相对灵活控制缓存版本并“及时”清理缓存的方法,分如下几步:
1、在自己的业务系统中新建一张数据表(比如叫CacheVersion),只有一个字符串Version字段,初始化一条记录,且只有一条记录;
2、将CacheVersion表数据写入分布式缓存系统,按照自己所在业务定义Version对应的缓存键,如xxx_cacheversion,对于你所在的业务部门,这个缓存键字符串可以认为是个常量;
3、重新定义业务需要的缓存键,如下所示:
/// <summary> /// 区域 /// </summary> public static readonly string Area = "{0}_area_{1}_{2}";
其中{2}对应的就是读出的CacheVersion对应的那条数据,CacheVersion那条记录的获取也是先从分布式缓存中取,如没有再读数据库,最后拼好的字符串是如下这个形式:
/// <summary> /// 区域 /// </summary> public static readonly string Area = "namespace_area_areaid_version";
4、后台业务数据发生改变,直接更新业务数据对应表记录,不需要写任何代码用于更新缓存;
5、在一个独立的管理模块,如业务需更新缓存数据,则更新CacheVersion表的唯一记录,并重置分布式缓存中的CacheVersion对应数据。
上面的5步思路其实很简单,就是由原来的维护多个缓存键改为集中维护一个CacheVersion,“及时”过期策略其实就是换一个缓存版本而已。到这里分布式缓存数据的版本控制就大功告成了。
当然,这种方式也有缺点,主要包括:
1、新增了一个数据表CacheVersion需要维护,Version字段需考虑数据唯一的生成策略;
2、多了一个CacheVersion操作模块;
3、前台站点缓存键字符串构造多了一个Version参数,读取Version也需要先从分布式缓存中取,如没有再读数据库,多了网络传输和IO;
4、每次更新版本,依赖这个版本的分布式缓存都会“被过期”,如在并发访问高峰,可能得不偿失。
最后欢迎大家讨论:你是如何控制和实现缓存有效期过期策略的?CacheVersion这张表是不是可以不需要?如果是你来控制缓存版本,缓存Version该如何生成?