如何以线程安全的方式在DAO中缓存信息

我经常需要为一些不经常变化的参考数据实现DAO。 我有时会在DAO的集合字段中缓存它 – 因此它只加载一次并在需要时显式更新。

但是,这会带来许多并发问题 – 如果另一个线程在加载或更新时尝试访问数据会怎样。

显然,这可以通过使数据的getter和setter同步来处理 – 但对于大型Web应用程序来说,这是一个相当大的开销。

我已经包含了一个琐碎的例子,说明了我作为一个稻草人所需要的东西。 请建议其他方法来实现这一点。

public class LocationDAOImpl implements LocationDAO { private List locations = null; public List getAllLocations() { if(locations == null) { loadAllLocations(); } return locations; } 

有关更多信息,我正在使用Hibernate和Spring,但此要求适用于许多技术。

进一步的想法:

这根本不应该在代码中处理 – 而是让ehcache或类似处理吗? 我缺少一个共同的模式吗? 显然有很多方法可以实现,但我从未找到过简单易维护的模式。

提前致谢!

如果您只想快速推出自己的缓存解决方案,请查看关于JavaSpecialist的这篇文章, 这篇文章是Brian Goetz撰写的Java Concurrency in Practice一书的评论。

它讨论了使用FutureTask和ConcurrentHashMap实现基本线程安全缓存。

这样做的方式确保只有一个并发线程触发长时间运行的计算(在您的情况下,您的DAO中的数据库调用)。

如果需要,您必须修改此解决方案以添加缓存过期。

关于自己缓存的另一个想法是垃圾收集。 如果不使用WeakHashMap作为缓存,那么如果需要,GC将无法释放缓存使用的内存。 如果您缓存不常访问的数据(但由于难以计算,仍然值得缓存的数据),那么您可能希望在使用WeakHashMap运行内存不足时帮助垃圾收集器。

最简单和安全的方法是在项目中包含ehcache库并使用它来设置缓存。 这些人已经解决了您可能遇到的所有问题,他们尽可能快地建立了图书馆。

在我推出自己的参考数据缓存的情况下,我通常使用ReadWriteLock来减少线程争用。 然后,我的每个访问者都采用以下forms:

 public PersistedUser getUser(String userName) throws MissingReferenceDataException { PersistedUser ret; rwLock.readLock().lock(); try { ret = usersByName.get(userName); if (ret == null) { throw new MissingReferenceDataException(String.format("Invalid user name: %s.", userName)); } } finally { rwLock.readLock().unlock(); } return ret; } 

取出写锁定的唯一方法是refresh() ,我通常通过MBean公开它:

 public void refresh() { logger.info("Refreshing reference data."); rwLock.writeLock().lock(); try { usersById.clear(); usersByName.clear(); // Refresh data from underlying data source. } finally { rwLock.writeLock().unlock(); } } 

顺便说一句,我选择实现自己的缓存,因为:

  • 我的参考数据集很小,所以我总是可以将它们全部存储在内存中。
  • 我的应用程序需要简单/快速; 我希望尽可能少依赖外部库。
  • 数据很少更新,当它是对refresh()的调用相当快。 因此,我急切地初始化我的缓存(与你的稻草人示例不同),这意味着访问者永远不需要取出写锁。

如果你的参考数据是不可变的,那么hibernate的二级缓存可能是一个合理的解决方案。

显然,这可以通过使数据的getter和setter同步来处理 – 但对于大型Web应用程序来说,这是一个相当大的开销。

我已经包含了一个琐碎的例子,说明了我作为一个稻草人所需要的东西。 请建议其他方法来实现这一点。

虽然这可能有点真实,但您应该注意,您提供的示例代码当然需要进行同步,以避免在延迟加载locations时出现任何并发问题。 如果该访问者未同步,那么您将拥有:

  • 多个线程同时访问loadAllLocations()方法
  • 有些线程可能会在另一个线程完成方法并将结果分配给locations后输入loadAllLocations() – 在Java Memory Model下,无法保证其他线程在没有同步的情况下会看到变量中的更改。

使用延迟加载/初始化时要小心,这似乎是一个简单的性能提升,但它可能会导致许多讨厌的线程问题。

我认为最好不要自己动手,因为做正确是一件非常困难的事情。 使用EhCache或OSCache与Hibernate和Spring是一个好主意。

此外,它使你的DAO有状态,这可能是有问题的。 除了Spring为您管理的连接,工厂或模板对象之外,您应该没有任何状态。

更新:如果你的参考数据不是太大,而且真的永远不会改变,那么另一种设计可能就是创建枚举并完全省去数据库。 没有缓存,没有Hibernate,没有后顾之忧。 也许oxbow_lakes的观点值得考虑:也许它可能是一个非常简单的系统。