使用内存缓存需要注意的一个问题

最近做需求的时候遇到一个坑,对这个问题的思考在这里记录一下

在应用中为了使得数据尽快展示出来,在网络请求的数据回来之前都会先显示内存中缓存的数据。存取缓存的方法大概是这个样子

1
2
3
4
5
6
7
var setCacheInfo = function(key, value) {
key && (cacheWebData[key] = value);
}

var getCacheInfo = function(key) {
return cacheWebData[key];
}

cacheWebData 是一个内存中的对象,读取缓存就是直接反悔了对象中 key 对应的 value。然后将取出来的值进行传递。

先说下问题的现象,在调试的时候我发现,从内存缓存中取出来的值会莫名其妙的改变。比如:在 A 页面中使用key取到的值是{a:1,b:2},但是在进入过 B 页面之后同样使用key取到的值却变成了 {a:1,b:2,c:3},这过程中没有调用setCacheInfo方法来修改缓存中的值。

难不成他还自己变了?

这显然是不可能的,查找了好半天,我发现取出这个缓存的值以后,这个值被传递了好几层,最终被 $.extend(obj,{c:1}) 了一次。这一改不要紧,由于传递的是缓存中对象的引用,这下直接将缓存里面的值也修改了,到下次再使用缓存的时候取出来的值就已经不是原来的值了。然而在 extend 的地方,使用者是完全不知道这个对象引用的是什么地方的数据,这里是一个隐患。

避免出现这个问题有三个办法;

  1. getCacheInfo 方法改为以下的实现
1
2
3
var getCacheInfo = function(key) {
return $.extend(true, {}, cacheWebData[key]);
}

也就是将传出的值进行深拷贝,切断与缓存内部对象的引用关系。
2. 模仿 localStorage 的接口,令缓存对象只接受字符串类型的 value,那么读取方法要这样实现

1
2
3
4
5
6
7
var setCacheInfo = function(key, value) {
key && (cacheWebData[key] = JSON.stringify(value));
}

var getCacheInfo = function(key) {
return JSON.parse(cacheWebData[key]);
}
  1. 直接规定不允许直接 extend 对象…,或者只允许为空对象扩展属性(并不能完全排除隐患)
1
$.extend({}, obj, {c:3})

于是我想看下 jquery,node-cache 之类的框架中是如何处理缓存读写的。
jQuery.data 的实现如下:

1
2
3
get: function (owner, key) {
return (key === undefined) ? (this.cache (owner)): (owner[this.expando] && owner[this.expando][jQuery.camelCase (key)]);
}

同样存在这个问题。

node-cache 的实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
exports.get = function(key) {
var data = cache[key];
if (typeof data != "undefined") {
if (isNaN(data.expire) || data.expire >= Date.now()) {
if (debug) hitCount++;
return data.value;
} else {
// free some space
if (debug) missCount++;
size--;
delete cache[key];
}
} else if (debug) {
missCount++;
}
return null;
};

蛤,也有这个问题。。

看来主流框架告诉我们是不应该随意地对对象进行扩展了,不然一不留神就会出现这种问题…想要防患于未然最好还是使用深拷贝或者只存字符串类型值的方法吧。