返回

缓存浅拷贝:往坑里跳

后端

人们常常轻视缓存,他们想当然地认为在缓存失效后会重新从数据库取数据,而不会产生什么问题,这是非常错误的,浅拷贝导致的问题非常严重,稍不留神就容易踩坑。

浅拷贝在缓存中的使用和局限性

浅拷贝实现和作用

Java 中缓存常用的是 Map 结构,可以使用 Java 自带的 Map 来做缓存,也可以使用 Guava Cache。简单实现一个 Map 缓存如下:

Map<String, User> userCache = new ConcurrentHashMap<>();

使用时,可以将要缓存的数据直接 put 进 Map。如下所示:

userCache.put("tom", user);

此时,缓存的作用就是当从缓存中获取数据时,可以直接从 Map 中获取,而不需要访问数据库。

浅拷贝的局限性

从本质上讲,这种缓存只是将数据简单地 put 到 Map 中。需要注意的是,这种缓存方式是浅拷贝。对于对象类型的数据,浅拷贝只是将对象的引用复制到 Map 中,并没有复制对象本身。

如下图所示,当从数据库中获取到 user 对象后,将其 put 到缓存中,此时 user 对象的引用被复制到缓存中,但 user 对象的内容并没有复制到缓存中。

浅拷贝示意图

浅拷贝导致的问题在于,当缓存失效后,从缓存中获取数据时,实际上获取的是对象的引用,而不是对象的内容。此时,如果对象的内容被修改,那么从缓存中获取到的数据就和数据库中的数据不一致,这就是浅拷贝导致的问题。

例如,在上面的例子中,当从数据库中获取到 user 对象后,将其 put 到缓存中,此时 user 对象的引用被复制到缓存中。然后,对 user 对象的内容进行修改。此时,缓存中 user 对象的引用指向的内容和数据库中 user 对象的引用指向的内容不一致了。如果从缓存中获取 user 对象,那么获取到的就是旧的数据,这显然是不正确的。

如何在缓存中正确使用浅拷贝
想要安全地使用缓存,需要理解浅拷贝的局限性,并在缓存中正确地使用浅拷贝。

避免在缓存中存储对象引用

浅拷贝的本质是将对象的引用复制到缓存中,而不是复制对象的内容。因此,在缓存中存储对象引用时,需要特别注意。

例如,在上面的例子中,如果将 user 对象的引用直接存储到缓存中,那么当缓存失效后,从缓存中获取 user 对象时,获取到的就是对象的引用,而不是对象的内容。如果对象的内容被修改,那么从缓存中获取到的数据就和数据库中的数据不一致。

在缓存中存储对象副本

为了避免浅拷贝导致的问题,可以在缓存中存储对象副本。对象副本是指将对象的内容复制到另一个对象中,而不是复制对象的引用。

例如,在上面的例子中,可以将 user 对象的内容复制到一个新的 user 对象中,然后将新的 user 对象存储到缓存中。此时,从缓存中获取 user 对象时,获取到的就是对象的内容,而不是对象的引用。即使对象的内容被修改,从缓存中获取到的数据也不会受到影响。

使用深拷贝工具

如果想要在缓存中存储对象引用,可以使用深拷贝工具来实现。深拷贝工具可以将对象的内容复制到另一个对象中,而不是复制对象的引用。

例如,Java 中可以使用 Apache Commons Lang3 中的 BeanUtils 类来实现深拷贝。如下所示:

User userCopy = BeanUtils.cloneBean(user);

使用序列化和反序列化

如果想要在缓存中存储对象引用,可以使用序列化和反序列化来实现。序列化是指将对象转换为二进制数据,反序列化是指将二进制数据转换为对象。

例如,Java 中可以使用 ObjectOutputStream 和 ObjectInputStream 类来实现序列化和反序列化。如下所示:

ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos);
oos.writeObject(user);
byte[] bytes = baos.toByteArray();
ByteArrayInputStream bais = new ByteArrayInputStream(bytes);
ObjectInputStream ois = new ObjectInputStream(bais);
User userCopy = (User) ois.readObject();

结论
浅拷贝只复制对象的引用,不复制对象的内容,导致数据容易出错。在使用缓存时,需要对浅拷贝的局限性有充分的认识,并正确地使用浅拷贝,如果非要使用浅拷贝,需要在缓存中存储对象副本,或者使用深拷贝工具或序列化和反序列化来实现。