缓存类踩坑:解析“为什么从缓存中获取相同数据的两次结果不同”?
2023-11-24 16:17:30
当我们处理缓存类时,缓存数据应该始终保持一致。但开发中常会踩到一个坑:即使调用两次,获取的缓存数据也是不同的。
缓存工作原理
常见开发语言中的缓存类型有很多,如 Redis、Memcached 等。它们都遵循了“键值对”模式,即任何写入缓存中的数据都被赋予一个唯一键,用于以后根据该键查找并取出缓存数据。
以读取为例,缓存服务器根据给定的键查询缓存项是否存在。若有,则直接返回对应的数据;若没有,则通过查询后端数据库获取所需数据,并以该键存储在缓存服务器中,再返回。
踩坑之旅
问题起因于编写一个小型缓存工具类,用于统一处理缓存的读写。该工具类似上述提到的缓存服务器,但数据只存在于内存中。为了实现简单的过期机制,工具类每次返回一个包含数据及过期时间的对象。
class Cache {
// 构造函数
constructor() {
this.cache = new Map();
}
// 设置缓存
set(key, value, expireSeconds) {
this.cache.set(key, {
value,
expireTime: Date.now() + expireSeconds * 1000,
});
}
// 获取缓存
get(key) {
const cachedItem = this.cache.get(key);
if (!cachedItem || Date.now() > cachedItem.expireTime) {
return null;
}
return cachedItem.value;
}
}
然而,在使用这个工具类时,我们发现从缓存中获取数据时,每次返回的竟是同一对象,而不是数据本身。这让我们百思不得其解:缓存明明是每次都创建新的数据项,为何读取到的是同一对象引用呢?
经过一番思考,我们终于发现了问题的根源。返回的数据不是一个基础类型值(如数字或字符串),而是包含数据和过期时间的一个对象。JavaScript 中的函数在返回非基础类型值时,返回的其实是一个引用。因此,每次调用 get()
方法时,得到的都是指向同一个缓存对象的引用,所以修改该对象时,会直接影响缓存中的数据。
const cache = new Cache();
cache.set("name", "John Doe", 60); // 设置缓存
const cachedName = cache.get("name"); // 获取缓存
// 修改缓存对象中的数据
cachedName.value = "Jane Doe";
console.log(cache.get("name")); // 输出:"Jane Doe"
显然,这并不是我们想要的。缓存数据应该始终保持一致,但现在却可以被随意修改。
优化解决方案
这个问题很好解决,只需要在返回时克隆缓存对象即可。
// ... 省略其他代码 ...
// 获取缓存
get(key) {
const cachedItem = this.cache.get(key);
if (!cachedItem || Date.now() > cachedItem.expireTime) {
return null;
}
// 返回缓存数据克隆
return {...cachedItem.value};
}
现在,再次获取缓存数据时,每次得到的都是一个新的对象,而不是同一个对象的引用。
一个有趣的玩具
利用这个有趣的结论,还可以制造一个实时更新待办列表。只要把待办列表数据缓存在浏览器中,每次修改列表中的内容,都可以通过修改缓存数据来实时更新列表。
const cache = new Cache();
const todoList = cache.get("todo-list") || [];
// 增加一项待办
todoList.push("Learn JavaScript");
// 修改一项待办
todoList[0] = "Learn React";
// 删除一项待办
todoList.pop();
// 更新缓存
cache.set("todo-list", todoList, 60 * 60); // 缓存一小时
这样,只要打开浏览器,就可以实时查看待办列表的最新状态。当然,这只是一个简单的示例,在实际项目中需要考虑更多因素,如数据安全、并发控制等。