使用Spring/EHCache加载时刷新缓存



我在一个带有数据库后端和基于EHCache的缓存的Spring多线程web服务上遇到了缓存问题。该服务有许多客户端,所有客户端都一次又一次地请求同一对象,每秒有几十个请求。只有几个对象被频繁地请求,而大量其他对象被不频繁地请求。对象可以每隔几分钟更改一次,因此缓存的TTL设置为一分钟。从数据库加载对象的速度很慢,至少需要几秒钟的时间。

起初,我使用了一个简单的实现来获得对象:

  1. 检查对象是否在缓存中
  2. 如果是,则从缓存中返回
  3. 如果没有,请从数据库中加载它,将其放入缓存并返回

当最初在本地测试时,它运行良好。但是,在速度更快的服务器上进行的性能测试显示,每当缓存中一个更频繁请求的对象过期时,就会出现一些非常糟糕的负载峰值。当这种情况发生时,在接下来的10秒内,对该对象的所有请求都将导致数据库加载,直到第一个线程完成数据库加载并将新对象放入缓存。结果是数据库上的负载很短,但非常高,很多用户需要等待请求完成。

我当前的实现通过跟踪当前是否正在加载哪个对象来提高数据库负载:

  1. 检查对象是否已缓存
  2. 如果是,则从缓存中返回
  3. 如果没有,请检查当前是否正在加载对象
  4. 如果是,请等待其他线程的加载完成,从缓存中获取新对象并返回
  5. 如果没有,则将对象放入加载对象列表中,完成后将其放入缓存并返回

使用此实现,即使对象过期,也只有一个数据库操作。而且,由于数据库负载较低,它也会更快地完成。但这仍然意味着所有在对象加载期间请求该对象的用户都需要等待。

我真正想要的是,只有第一个线程等待数据库加载,而所有其他线程在加载对象时只返回"过期"对象。对我来说,响应时间比对象太旧几秒更重要。

或者,当我注意到一个对象将在几秒钟后过期时,我可以异步刷新缓存。这更接近于EHCache的单个TTL模型,这意味着没有人需要等待数据库加载

我真正的问题是:在我重新发明轮子之前,是否有任何现有的框架已经实现了这样的东西(在Spring/EHCache环境中)?或者可能在Spring/EHCache中已经存在对此的支持,而我只是找不到正确的选项?

有两种Ehcache提供的构造可以帮助您:

  1. 提前刷新
  2. 计划的刷新

两者都需要更改与缓存交互的方式,因为它们需要配置CacheLoader

不幸的是,我找不到显示第二个选项示例的在线文档。它允许使用Quartz来刷新缓存条目。它还可以基于密钥生成器只刷新密钥的子集。查看包net.sf.ehcache.constructs.scheduledrefresh 中的类

您的设计是有缺陷的,因为第二个线程无法从缓存中获取任何"过期"对象,因为没有(根据步骤#2:当对象在缓存中时立即返回)。

解决方法:

  1. 加载单个对象的10秒钟太长了。检查您的SQL并尝试优化它。

  2. 将对象缓存更长时间,并运行更新线程,以查询数据库中对象的新状态。这意味着线程#1只是触发一些后台工作,最终刷新缓存中的对象。缺点:缓存必须足够大,以便始终将大多数对象保存在内存中。否则,"第一次加载对象"将过于可见。

  3. 在不加载对象的情况下显示网页,并在后台使用AJAX请求加载它们。对象可用时更新网页。根据你的网站在不是所有东西都准备好的情况下有多有用,这可能是响应性和准确性之间的良好平衡。

  4. 改进对象的加载。创建"视图"表,其中包含在每行中显示单个对象所需的所有数据。在更改"真实"(规范化)对象时更新这些行。"视图缓存"仅从此表填充。这使得加载对象的速度非常快,而牺牲了对数据模型的更改。有关极端解决方案,请参阅"命令-查询分离"。

  5. 尝试稍微取消数据模型的规范化,以减少加载单个对象所需的联接数量。或者,缓存一些通常会加入的对象,并在web服务器上进行筛选/聚合。

  6. 更新对象时,触发缓存刷新。很可能很快就会有人想看到这个物体。当人们手动编辑对象时,这种方法效果最好,而当外部系统(新闻行情、股票报价等)随机触发更改时,效果最差。

  7. 如果您只需要大量联接来显示所有详细信息,请尝试加载概述,然后使用第二个缓存来获取详细信息,然后可以在第二个线程中加载。与AJAX一起,您可以快速显示对象的概述,这将为您争取一些等待细节的善意。

最新更新