具有过期功能的简单Java字符串缓存

3
我正在寻找一个带有过期功能的并发Set用于Java 1.5应用程序。它将被用作存储/缓存名称(即String值),在一定时间后会过期。
我要解决的问题是,两个线程不应该能够在某段时间内使用相同的名称值(因此这类似于黑名单,确保同一“名称”,它类似于消息引用,直到经过一定的时间段,另一个线程才能重新使用)。我无法控制名称生成本身,因此不能对实际名称/字符串进行强制唯一性的限制,而应该将其视为限制机制,以防止同一名称每秒钟被多次使用。
例如:线程#1执行cache.add("unique_string, 1),它将名称“unique_string”存储1秒钟。如果任何线程在1秒钟内通过cache.get("unique_string")查找“unique_string”,它将得到一个肯定的响应(项目存在),但之后该项目应该过期并从集合中删除。
容器将在某些时候处理50-100次插入/读取操作。
我已经仔细研究了不同的解决方案,但没有找到符合我的需求的东西。这似乎是一个简单的问题,但我发现所有的解决方案都过于复杂或过度设计。
一个简单的想法是使用ConcurrentHashMap对象,其键设置为“名称”,值设置为过期时间,然后运行每秒钟的线程并删除所有过期时间(即值)已经过去的元素,但我不确定这样做的效率如何?是否有我错过的更简单的解决方案?

为什么不使用完全不重复或在一定时间内不重复的自动生成的字符串ID呢?这样就不必跟踪以前的ID。 - Peter Lawrey
对不起,我意识到我在那个部分应该更清楚。我不能自己控制名称生成。例如,名称可以是IP地址、邮政地址或其他任何内容,我要使用我所要求的机制进行节流(即不允许每秒发送多于1个单位)。 - Nils
与你的问题类似,我希望对你的解决方案有所帮助。 https://dev59.com/LVPTa4cB1Zd3GeqPjn7P - Erdinç Taşkın
5个回答

5

Google 的 Guava 库包含了一个高速缓存(cache)工具:CacheBuilder


谢谢建议,看起来非常有帮助!实际上,仔细查看后我发现这种方法已经被弃用了,取而代之的是CacheBuilder,它还处于测试阶段。我现在会去看一下并将其与@Shawn提出的解决方案进行比较。我正在寻找的是一个“即插即用”的解决方案,不需要根据我打算进行的调用数量考虑内存管理,基于此,您认为这两种解决方案中有哪个更具优势? - Nils
@Nils 我检查了公司使用的guava版本 - 但是guava发展得太快了。我会更新答案,以免强迫未来的读者走冗长的路线;-) - Ivan Sopov
@Nils 关于使用ScheduledFuture手写解决方案的问题 - 我认为它在这个意义上不够健壮,因为它只能给你它所给出的东西,但是guava解决方案可以给你一整套可能的未来版本改进这个功能。如果你处于非常受限制的环境并且无法使用guava - 当然要使用它,但在其他情况下 - 考虑使用guava,只有在这种特殊情况下,但它在一般情况下是非常方便的库。 - Ivan Sopov
我同意采用Guava路线(在我的环境中没有问题)是首选的方式,但是我刚刚注意到CacheBuilder在Guava的版本r10中尚未发布:(所以现在要继续进行,我需要其他东西或需要依赖弃用的功能。我使用@Shawn的建议包装了一个快速工作示例,似乎做得不错,因此也许如果我将其抽象化,一旦Guava R10可用,我就可以简单地切换。但是,是否有人知道使用计划执行程序的建议方式会导致性能问题? - Nils
如果有经过试验和测试的开源解决方案,我绝不会主张编写自己的代码。但我也不会使用已弃用的方法或测试版代码。我的解决方案只是一个示例,展示了如何实现。您可以很容易地进行测试,而且这并不是一段难以理解的代码。自从1.5版本以来,线程变得更加安全,因此我认为这将是一个易于实现和良好测试的代码片段。我在一个金融平台中实现了类似的解决方案,它可以24/7运行,并且非常稳定。 - Shawn Vader
显示剩余2条评论

4
创建一个使用线程执行器的Map,以便对过期的项目进行处理。
//Declare your Map and executor service
final Map<String, ScheduledFuture<String>> cacheNames = new HashMap<String, ScheduledFuture<String>>();
ScheduledExecutorService executorService = Executors.newSingleThreadScheduledExecutor();

您可以编写一个方法,将缓存名称添加到集合中,并在其过期后自动删除,例如这个例子中是一秒钟。我知道代码看起来有点多,但这可以是一个非常优雅的解决方案,只需几个方法。

ScheduledFuture<String> task = executorService.schedule(new Callable<String>() {
  @Override
  public String call() {
    cacheNames.remove("unique_string");
    return "unique_string";
  }
}, 1, TimeUnit.SECONDS);
cacheNames.put("unique_string", task);

最终我选择了这个解决方案的一个版本。简单而优雅。非常感谢您宝贵的意见! - Nils

0
一个简单的独特字符串模式,不会重复。
private static final AtomicLong COUNTER = new AtomicLong(System.currentTimeMillis()*1000);
public static String generateId() {
     return Long.toString(COUNTER.getAndIncrement(), 36);
}

即使您重新启动应用程序,这也不会重复。

注意:在以下情况下将重复:

  • 您重新启动并且每秒生成超过一百万个ID。
  • 293年后。如果这不够长,您可以将1000减少到100,获得2930年。

抱歉彼得,但正如我上面所评论的,我不能自己控制名称生成,因此无法更改该部分。 - Nils

0
为什么不像Konoplianko所示,将密钥被列入黑名单的时间存储在映射中呢?
类似这样的方式:
private final Map<String, Long> _blacklist = new LinkedHashMap<String, Long>() {
   @Override
   protected boolean removeEldestEntry(Map.Entry<String, Long> eldest) {
      return size() > 1000;
   }
};

public boolean isBlacklisted(String key, long timeoutMs) {
   synchronized (_blacklist) {
      long now = System.currentTimeMillis();
      Long blacklistUntil = _blacklist.get(key);
      if (blacklistUntil != null && blacklistUntil >= now) {
         // still blacklisted
         return true;
      } else {
         // not blacklisted, or blacklisting has expired 
         _blacklist.put(key, now + timeoutMs);
         return false;
      }
   }
}

地图随着我处理的吞吐量水平而变得越来越大,我需要一些自动清理它的东西,这就是复杂性开始增长的地方,也是为什么我寻求关于经过测试的解决方案的建议,而不一定要自己开发,即使那并不太难。 - Nils
是的,具有有限条目数的LinkedHashMap更好。 - ante

0

这取决于 - 如果您需要时间的严格条件,还是软性的条件(例如1秒+/- 20ms)。 此外,如果您需要离散的缓存失效或“按调用”。

对于严格的条件,我建议添加一个独立的线程,每20毫秒使缓存无效一次。

另外,您可以在存储的密钥中包含时间戳并检查它是否过期。


时间条件并不是非常严格,应该是“大约”1秒。实际上,在这种情况下,我的严格要求是,在使用相同的唯一名称进行下一次尝试之前,至少必须经过一秒钟,因此,如果实现可以使其偏差在20-200毫秒之间,我完全可以将值配置为1.2秒,这不是问题。关于离散或按调用缓存失效,如果它解决了我的问题,我没有真正的要求,因为我可能每秒执行50-100个调用,尽管如果内存管理不得当,可能会出现问题? - Nils

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接