## 微信开发深度解析之缓存策略(上) 缓存是几乎所有大中型系统的核心组成部分之一。Senparc.Weixin SDK,作为由盛派网络自主研发的针对微信各模块的开发套件(C# SDK),其中的许多信息同样需要缓存的支持,例如凭证信息、开发者账号信息等。尤其在分布式系统中,分布式缓存的作用就更加明显,它还起到了在多台服务器之间同步和交换数据的功能。 本文将深入介绍 Senparc.Weixin SDK 中的分布式缓存策略框架,可同时支持对本地缓存和分布式缓存的扩展,力求在尽量轻便、支持本地缓存的同时,可以为大多数常用的分布式缓存提供良好的对接能力。 ### 设计原理 Senparc.Weixin SDK 的缓存策略主要目的是提供给数据容器(Container)使用(注意:这里所说的容器专指 SDK 的输入容器,非 Docker 之类的“容器”技术),同时也确保可以充分解耦以及对其他用途的弹性,因此我们不直接为 Container 建立缓存策略,而是先创建一个基础的缓存策略接口(IBaseCacheStrategy),以及一个派生自 IBaseCacheStrategy 的容器缓存策略接口(IContainerCacheStragegy),并为 IContainerCacheStragegy 提供足够灵活的接口,支持本地及多种分布式缓存扩展。基于 IContainerCacheStragegy,再派生各种类型的缓存(如:本地缓存、各种分布式缓存等)。总体设计思路如图1所示。 ![enter image description here](http://images.gitbook.cn/aab38480-fc34-11e7-9da3-77cc9d66cbb8) 图1 Senparc.Weixin SDK 缓存策略的总体设计思路图 本文将就基础缓存策略接口、容器缓存策略接口、本地数据容器缓存策略实现这几部分展开对缓存模块的介绍。 缓存相关的文件结构如图2所示。 ![enter image description here](http://images.gitbook.cn/06742270-fc35-11e7-9da3-77cc9d66cbb8) 图2 缓存相关文件结构图 ### 基础缓存策略接口:IBaseCacheStrategy 基础缓存策略接口(IBaseCacheStrategy)是所有缓存策略最基础的接口。 缓存通常遵循“键/值”配对的格式,因此在最初设计 IBaseCacheStrategy 的时候,我们赋予了 IBaseCacheStrategy 两个泛型,形成这样的定义:`IBaseCacheStrategy`。 在实际的开发过程中,有些情况下只需要知道这个实例是一个“基础缓存策略”对象,对其进行简单的操作,而许多情况下如果将实例对象强类型转化为 `IBaseCacheStrategy`可能有困难(例如通过工厂之后,我们并不确定 TKey 和 TValue 的类型),因此,我们对 `IBaseCacheStrategy`进行了重构,从中抽象出一个简单的接口,名字就叫 IBaseCacheStrategy,作为 `IBaseCacheStrategy`的基类。最终的结构如图3所示。 ![enter image description here](http://images.gitbook.cn/476fb960-fc35-11e7-8434-0ffd6443b18b) 图3 最终结构图 基础缓存策略用于提供最基础的缓存操作定义,如表1所示。 ![enter image description here](http://images.gitbook.cn/81ae27b0-fc35-11e7-8434-0ffd6443b18b) 表1 以上 IBaseCacheStrategy 相关接口定义比较简单,代码不再赘述。整个缓存相关的接口和类,都定义在命名空间 Senparc.Weixin.Cache 下。 ### 数据容器缓存策略接口:IContainerCacheStragegy 数据容器缓存策略是为数据容器而专门设计的缓存策略,用于提供可靠、可扩展的容器缓存管理功能,不但能支持本地缓存,也能够支持各类分布式缓存。接下来将分析基础策略的实现过程和思路。 #### 原始 IContainerCacheStragegy 设计思路 首先我们来定义 IContainerCacheStragegy: ``` namespace Senparc.Weixin.Cache { /// /// 容器缓存策略接口 /// public interface IContainerCacheStragegy : IBaseCacheStrategy> { /// /// 更新ContainerBag /// /// /// void UpdateContainerBag(string key, IBaseContainerBag containerBag); } } ``` 通过上面的代码我们可以看到,IContainerCacheStragegy 继承了 `IBaseCacheStrategy` 接口,其中 TKey 为 String 类型,TValue 为 `IDictionary` 类型。 #### 优化 IContainerCacheStragegy 设计思路 对于 TValue,`IDictionary`这样的定义显然不够友好,也无法得到扩展,于是我们将其封装一下,创建名为 IContainerItemCollection 的接口及 ContainerItemCollection 类,继承自 `IDictionary`,代码如下所示: ``` namespace Senparc.Weixin.Cache { /// /// IContainerItemCollection,对某个Container下的缓存值ContainerBag进行封装 /// public interface IContainerItemCollection : IDictionary { /// /// 创建时间 /// DateTime CreateTime { get; set; } } /// /// 储存某个Container下所有ContainerBag的字典集合 /// public class ContainerItemCollection : Dictionary, IContainerItemCollection { /// /// 创建时间 /// public DateTime CreateTime { get; set; } public ContainerItemCollection() { CreateTime = DateTime.Now; } } } ``` #### 优化 IContainerItemCollection 和 ContainerItemCollection 如果对系统架构的要求不高,这样或许已经可以了,上面的代码也已经足够简单,而且可以很好地运行。但是我们纵观整个缓存策略的设计,IContainerItemCollection 目前只是对 `Dictionary`进行了一次简单的继承和扩展,其储存机制仍然是使用本地内存中的一个字典作为缓存数据的容器。这种设计在此处有以下一些弊端。 - 在同一个缓存系统内部,需要维护两套底层的缓存策略(`IBaseCacheStrategy`和`Dictioncay`),这会增加额外的维护成本,可能会导致整个系统的协调性和稳定性受到破坏; - 如果 IContainerItemCollection 将来需要使用分布式缓存,将更加困难和混乱。 因此,我们让 IContainerItemCollection 继承 `IBaseCacheStrategy`,并实现其中统一的基础缓存策略接口中的方法,由于 Container已经增加了对凭证过期的判断,Senparc.Weixin SDK 中也提供了其他措施,这个缓存集合的数据源我们仍然使用一个类型为 `Dictionary`的私有变量来担当。 修改之后的 IContainerItemCollection 和 ContainerItemCollection 代码见下。 ``` namespace Senparc.Weixin.Cache { /// /// IContainerItemCollection,对某个Container下的缓存值ContainerBag进行封装 /// public interface IContainerItemCollection : IBaseCacheStrategy { /// /// 创建时间 /// DateTime CreateTime { get; set; } /// /// 索引器 /// /// 缓存键(通常为AppId,值和IBaseContainerBag.Key相等) /// IBaseContainerBag this[string key] { get; set; } } /// /// 储存某个Container下所有ContainerBag的字典集合 /// public class ContainerItemCollection : IContainerItemCollection { private Dictionary _cache; /// /// 索引器 /// /// 缓存键(通常为AppId,值和IBaseContainerBag.Key相等) /// public IBaseContainerBag this[string key] { get { return this.Get(key); } set { this.Update(key, value); } } public ContainerItemCollection() { _cache = new Dictionary(StringComparer.OrdinalIgnoreCase); CreateTime = DateTime.Now; } /// /// 创建时间 /// public DateTime CreateTime { get; set; } public string CacheSetKey { get; set; } #region 实现IContainerItemCollection : IBaseCacheStrategy接口 public void InsertToCache(string key, IBaseContainerBag value) { _cache[key] = value; } public void RemoveFromCache(string key) { _cache.Remove(key); } public IBaseContainerBag Get(string key) { if (this.CheckExisted(key)) { return _cache[key]; } return null; } public IDictionary GetAll() { return _cache; } public bool CheckExisted(string key) { return _cache.ContainsKey(key); } public long GetCount() { return _cache.Count; } public void Update(string key, IBaseContainerBag value) { _cache[key] = value; } #endregion } } ``` 上述修改的代码中,#endregion 块内的代码为基础缓存策略接口 `IBaseCacheStrategy` 的实现代码,除此以外我们还增加了一个私有变量作为缓存的数据源: ``` private Dictionary _cache; ``` 以及一个索引器,以增强对 _cache 的访问能力: ``` public IBaseContainerBag this[string key] { get { return this.Get(key); } set { this.Update(key, value); } } ``` 这样我们可以直接通过索引来访问或设置缓存中的数据,例如可以这样访问(get): ``` var bag = containerItemCollection[key]; ``` 或这样设置(set): ``` containerItemCollection[key] = new AccessTokenBag(); ``` 目前为止,整个缓存策略的储存结构如图4所示。 ![enter image description here](http://images.gitbook.cn/8edaaa40-fc39-11e7-b7b3-2b94bdd06d75) 图4 缓存策略的储存结构图 结合接口定义及图4,我们可以看到:IContainerCacheStragegy 继承自 `IBaseCacheStrategy`,Key 为 String 类型,储存用于区分不同 Container 的唯一标识(例如 AccessTokenContainer、JsTicketContainer 等);Value 为 IContainerItemCollection 类型(继承自 `IDictionary`),用于储存这个 Container 内所有不同 AppId 的 ContainerBag 的缓存,所以整个 IContainerCacheStragegy 中的每一项 Value,我们又可以看做是一个独立的缓存系统,不同的是它只储存和 Container 有关的数据。 IContainerItemCollection 的 Key 为 String 类型,用于储存每一个 ContainerBag 的唯一标识,通常为 AppId;Value 为 IBaseContainerBag 类型,用于储存这个 AppId 所对应的凭证等信息,例如在 AccessTokenContainer 中,IBaseContainerBag 就为 AccessTokenBag,其中储存了 AppId、AppSecret、AccessTokenExpireTime、AccessTokenResult 等属性。 图5展示了设计到目前为止,在实际运行的过程中,填充缓存数据之后的缓存结构和状态 。 ![enter image description here](http://images.gitbook.cn/d5fb6f90-fc39-11e7-b7b3-2b94bdd06d75) 图5 缓存结构和状态图 在当前的二级缓存策略的基础上,我们可以开始扩展出适合各种配置场景的缓存策略及其实现。