【问题标题】:How to cache information in a DAO in a threadsafe manner如何以线程安全的方式在 DAO 中缓存信息
【发布时间】:2009-06-26 09:38:54
【问题描述】:

我经常需要为一些不经常更改的参考数据实施 DAO。我有时将它缓存在 DAO 的集合字段中 - 这样它只加载一次并在需要时显式更新。

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

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

我已经包含了一个微不足道的有缺陷的示例,说明我作为稻草人需要什么。请提出替代方法来实现这一点。

public class LocationDAOImpl implements LocationDAO {

private List<Location> locations = null;

public List<Location> getAllLocations() {
    if(locations == null) {
        loadAllLocations();
    }
    return locations;
}

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

一些进一步的想法:

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

提前致谢!

【问题讨论】:

  • 感谢大家的精彩回答。每个人都添加了一些东西。

标签: java caching concurrency dao


【解决方案1】:

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

【讨论】:

    【解决方案2】:

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

    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() 时相当快。因此,我急切地初始化了我的缓存(与您的稻草人示例不同),这意味着访问者永远不需要取出写锁。

    【讨论】:

      【解决方案3】:

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

      它谈到了使用FutureTaskConcurrentHashMap 实现基本的线程安全缓存。

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

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

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

      【讨论】:

      • 接受了,因为我从中学到了最多的东西,并且在某些情况下使用开箱即用的缓存解决方案是不够的。
      【解决方案4】:

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

      【讨论】:

        【解决方案5】:

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

        我已经包含了一个微不足道的有缺陷的示例,说明我作为稻草人需要什么。请提出替代方法来实现这一点。

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

        • 多个线程同时访问loadAllLocations()方法
        • 即使在另一个线程完成该方法并将结果分配给locations 之后,某些线程仍可能进入loadAllLocations() - 在Java 内存模型下,不能保证其他线程会在没有同步的情况下看到变量中的更改。

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

        【讨论】:

        • 谢谢马特 - 我意识到它已经坏了,这就是为什么我把它称为有缺陷的。
        【解决方案6】:

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

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

        更新:如果您的参考数据不是太大,并且真的永远不会改变,也许另一种设计是创建枚举并完全放弃数据库。没有缓存,没有休眠,不用担心。也许 oxbow_lakes 的观点值得考虑:也许它可以是一个非常简单的系统。

        【讨论】:

        • 对于一个可能非常简单的系统,您为什么还要费心使用 ehcache 和 hibernate 之类的东西呢?在我看来,添加依赖项和这样的重量级框架(如 Hibernate)是一个重大决定。我学到了这种现成的方法可能会反过来咬你的艰难方法
        • 他说他已经在使用 Hibernate,所以使用 EhCache 似乎比自己编写更好。是否使用 Spring 或 Hibernate 与自己​​编写是另一个问题。
        猜你喜欢
        • 2020-06-05
        • 2019-05-29
        • 2012-04-04
        • 1970-01-01
        • 2019-10-25
        • 1970-01-01
        • 2012-07-11
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多