如何解决使用 HashMap 问题在存储库中实现缓存
我在一次采访中遇到了这个问题,我正在努力从中学习。
假设这个存储库在数据库中有数十亿条消息的并发上下文中使用。
public class MessageRepository {
public static final Map<String,Message> cache = new HashMap<>();
public Message findMessageById(String id) {
if(cache.containsKey(id)) {
return cache.get(id);
}
Message p = loadMessageFromDb(id);
cache.put(id,p);
return p;
}
Message loadMessageFromDb(String id) {
/* query DB and map row to a Message object */
}
}
这种方法可能存在哪些问题?
我能想到的是 HashMap
不是 Map
的线程安全实现。也许 ConcurrentHashMap
会更好。
我不确定其他任何可能的答案是:
1) 类 MessageRepository
是 final
意味着它是不可变的,所以它不能有一个可修改的缓存。
(AFAIK HashMap
是可变的,它被组合成 MessageRepository
所以这不会成为问题)。
2) 字段 cache
是 final
意味着它是不可变的,所以它不能被 put 修改。
(final
并不意味着不可变,所以这也不是问题)
3) 字段 cache
是 static
意味着每次构建 MessageRepository
的实例时都会重置它。
(cache
将被 MessageRepository
的所有实例共享,因此应该没有问题)
4) HashMap
是同步的,不同步性能可能会更好。
(我认为 SynchronizedHashMap
是同步实现)
5) HashMap
没有实现开箱即用的驱逐机制,可能会导致内存问题。
(什么样的问题?)
解决方法
我发现这个缓存有两个问题。如果 loadMessageFromDb()
是一个开销很大的操作,那么两个线程可以启动重复计算。即使您使用 ConcurrentHashMap
,这也不会减轻。避免这种情况的缓存的正确实现是:
public class MessageRepository {
private static final Map<String,Future<Message>> CACHE = new ConcurrentHashMap<>();
public Message findMessageById(String id) throws ExecutionException,InterruptedException {
Future<Message> messageFuture = CACHE.get(id);
if (null == messageFuture) {
FutureTask<Message> ft = new FutureTask<>(() -> loadMessageFromDb(id));
messageFuture = CACHE.putIfAbsent(id,ft);
if (null == messageFuture) {
messageFuture = ft;
ft.run();
}
}
return messageFuture.get();
}
}
(直接取自 Brian Goetz 等人的 JCIP)
在上面的缓存中,当一个线程开始计算时,它会将Future
放入缓存中,然后耐心等待直到计算完成。任何具有相同 id 的线程都会看到计算已经在进行中,并将再次等待相同的未来。如果两个线程完全同时调用,putIfAbsent
确保只有一个线程能够启动计算。
Java 没有任何 SynchronizedHashMap
类。您应该使用 ConcurrentHashMap
。您可以执行 Collections.synchronisedMap(new HashMap<>())
但它的性能非常差。
上述缓存的一个问题是它不会驱逐条目。 Java 提供了 LinkedHashMap
可以帮助您创建 LRU 缓存,但它不是同步的。如果您想要这两种功能,您应该尝试 Guava
缓存。
版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。