无分片的高并发计数器

14 投票
2 回答
1977 浏览
提问于 2025-04-15 22:21

这个问题涉及到两种计数器的实现方式,它们的设计目的是在不进行分片的情况下进行扩展(但可能在某些情况下会少算):

  1. http://appengine-cookbook.appspot.com/recipe/high-concurrency-counters-without-sharding/(评论中的代码)
  2. http://blog.notdot.net/2010/04/High-concurrency-counters-without-sharding

我的问题:

  • 关于第一个实现:在一个延迟的、事务性的任务中运行 memcache.decr() 似乎有点过头了。如果 memcache.decr() 在事务外执行,我认为最糟糕的情况是事务失败,我们会错过计数我们减少的值。我是不是忽略了其他可能出现的问题?
  • 这两种实现之间有什么重要的权衡?

我看到的权衡:

  • 第二种实现不需要数据存储的事务。

  • 要获取计数器的值,第二种实现需要从数据存储中获取,而第一种实现通常只需要执行 memcache.get()memcache.add()
  • 在增加计数器时,两者都会调用 memcache.incr()。定期地,第二种实现会将任务添加到任务队列,而第一种实现则是通过事务性地执行数据存储的获取和存储。第一种实现还总是执行 memcache.add()(以测试是否该将计数器持久化到数据存储中)。

结论

(没有实际运行任何性能测试):

  • 第一种实现通常在获取计数器时会更快(第一种是memcache,第二种是数据存储)。不过第一种实现还需要多执行一次 memcache.add()

  • 然而,第二种实现在更新计数器时应该更快(第一种是数据存储的获取和存储,第二种是将任务加入队列)。
  • 另一方面,使用第一种实现时,你需要对更新的间隔更加小心,因为任务队列的配额几乎比数据存储或memcache的API小100倍。

2 个回答

-2

如果Memcache被清空了,你的计数器就会丢失,真是让人心痛。使用mysql数据库或者NOSQL解决方案可以解决这个问题,不过可能会影响性能。而像Redis、Tokyotyrant、MongoDB这些工具,可能就不会有这样的性能问题。

记住,你可能想要做两件事:

  1. 为了高性能,可以保留一个Memcache计数器。
  2. 同时保留一个日志,这样可以从中获取更准确的数据。
1

去数据存储的成本通常比通过内存缓存(memcache)要高。要不然,内存缓存就没什么用处了 :-)

我建议你选择第一种方案。

如果你的请求频率还不错,其实可以更简单地实现:

1) update the value in memcache
2) if the returned updated value is evenly divisible by N
2.1) add N to the datastore counter
2.2) decrement memcache by N

这假设你能给内存缓存设置一个足够长的超时时间,让它在连续事件之间保持活跃。不过,如果事件发生得太稀疏,以至于内存缓存超时了,那你可能根本不需要一个“高并发”的计数器 :-)

对于大型网站来说,依赖一个单独的内存缓存来统计总页面访问量可能会出问题;在这种情况下,你确实需要将内存缓存分片,并更新一个随机的计数器实例;计数器的总和会通过数据库更新来完成。

不过在使用内存缓存时,要注意一些客户端的API会认为一秒的超时时间意味着值不存在。如果发送到内存缓存实例的TCP SYN数据包丢失了,那么你的请求就会错误地认为数据不存在。(类似的问题也可能在使用UDP时发生)

撰写回答