微信公众号搜"智元新知"关注
微信扫一扫可直接关注哦!

使用 HashMap 问题在存储库中实现缓存

如何解决使用 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)MessageRepositoryfinal 意味着它是不可变的,所以它不能有一个修改的缓存。

(AFAIK HashMap 是可变的,它被组合成 MessageRepository 所以这不会成为问题)。

2) 字段 cachefinal 意味着它是不可变的,所以它不能被 put 修改

final 并不意味着不可变,所以这也不是问题)

3) 字段 cachestatic 意味着每次构建 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 举报,一经查实,本站将立刻删除。