返回

缓存类踩坑:解析“为什么从缓存中获取相同数据的两次结果不同”?

前端

当我们处理缓存类时,缓存数据应该始终保持一致。但开发中常会踩到一个坑:即使调用两次,获取的缓存数据也是不同的。

缓存工作原理

常见开发语言中的缓存类型有很多,如 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); // 缓存一小时

这样,只要打开浏览器,就可以实时查看待办列表的最新状态。当然,这只是一个简单的示例,在实际项目中需要考虑更多因素,如数据安全、并发控制等。