【问题标题】:Global in-memory counter that is thread-safe and flushes to mysql every x increments线程安全的全局内存计数器,每 x 增量刷新到 mysql
【发布时间】:2011-12-12 16:20:27
【问题描述】:

是否可以创建一个所有 servlet 都将使用的内存计数器?

此全局计数器将跟踪 Web 应用程序的综合浏览量,并且该计数器将特定于当前登录的用户。即集合将为每个用户提供一个密钥。

globalCounterMap[userId].incrementCounter += 1;

在某个时间间隔或浏览量计数,我想将当前计数保存到 mysql(插入新行)例如:

table_pageviews [id, userId, pageview_count, date]

因此,此计数器将在刷新后重置为 0。

如果我有一个所有 servlet 都将从中继承的 BaseServlet,我将如何定义该字段? (最终的,静态的?)

ConcurrentHashMap 合适吗?也许我可以为每个条目的值存储一个 AtomicLong。

在刷新期间,我可以通过设置为 0 来使用原子 long 的 getAndSet,并保存我“获取”的值:http://docs.oracle.com/javase/1.5.0/docs/api/java/util/concurrent/atomic/AtomicLong.html

在刷新到 mysql 过程中是否必须同步? (假设我每 1K 页面浏览量执行一次)

更新

因此,即使我有 10 台服务器,每台服务器都有自己的内存计数器,事情仍然会正常工作,因为它们最终都会将它们的计数刷新到数据库,然后我将简单地聚合行以获得最终计数。

【问题讨论】:

  • 为什么不拥有 MemcacheD 服务器,并且每小时或每分钟将其刷新到数据库? MemcachedD IO 速度足够快,并且它始终会保存用户的总页面浏览量。

标签: java servlets concurrency


【解决方案1】:

正如 Konstantin 所说,类似 redis 的东西可能是更好的解决方案。 Cassandra 计数器也是做这种事情的好方法。

如果你想用 java 来做这件事,这里有一些代码可以安全地增加计数,而不会阻塞,

class Counter {

    private final ConcurrentHashMap<String, AtomicInteger> counts = new ConcurrentHashMap<String, AtomicInteger>();

    //increment the count for the user
    public void increment(String user) {
        while(true) {
            AtomicInteger current = counts.get(user);
            if(current == null) {
                //new user, initialize the count
                counts.putIfAbsent(user, new AtomicInteger());
                continue;
            }

            int value = current.incrementAndGet();
            if(value > 0) {
                //we have incremented the counter
                break;
            } else {
                //someone is flushing this key, remove it
                //so we can increment on our next iteration
                counts.replace(user, current, new AtomicInteger());
            }

        }
    }

    //call this periodically to flush keys to the database
    //this will empty the counts map so that users who
    //are not active do not take up space
    public void flush() {
        Map<String, Integer> toFlush = new HashMap<String, Integer>();

        for(Map.Entry<String, AtomicInteger> entry : counts.entrySet()) {
            String user = entry.getKey();
            AtomicInteger currentCount = entry.getValue();
            //stop incrementing this count
            counts.remove(user, currentCount);
            //if someone is trying to increment this AtomicInteger after
            //we remove it, they will see a -ve value from incrementAndGet, and 
            //will know their increment did not succeed
            Integer count = currentCount.getAndSet(Integer.MIN_VALUE);
            toFlush.put(user, count);
        }

        for(Map.Entry<String, Integer> clearedEntry : toFlush.entrySet()) {
            writeToDb(clearedEntry.getKey(), clearedEntry.getValue());
        }

    }

    public void writeToDb(String user, int count) {
        //do something with the count here
    }


}

代码相当复杂,正如 Peter Lawrey 所说,使用 synchronized 关键字保护的简单地图可能会表现得足够好并且更容易维护。

【讨论】:

  • 对于刷新,我不能复制现有地图,然后重置实时地图,然后刷新副本吗?并确保进程发生在单个线程上。
  • 如果重置的意思是调用 map.clear(),那么不,这不是线程安全的。您需要知道从地图中删除的 AtomicInteger,才能将它们的值设置为 MIN_VALUE。如果重置后的意思是 counters = new ConcurrentHashMap() 之类的东西,那么是的,但是计数器必须是易变的,而且它并没有真正给你买任何东西
  • 实际上,如果你使用 counters = new ConcurrentHashMap(),那也可能不是线程安全的
  • 那么将刷新包装在同步中可能是值得的吗?如果有 10K 个项目,那么可能只有 1 毫秒 :) 除非由于某种原因这不起作用?
  • 竞争不会在不同的刷新调用之间,竞争是在刷新和增量之间
【解决方案2】:

我会使用同步开始,因为这是最简单的方法。否则,您可能会在收集数据和刷新结果之间使用页面时遇到问题。您可以使用 getAndSet() 但如果您希望它是 128 而现在是 130,您会怎么做?

另一种选择是不用担心它是完全线程安全的,并且会丢失一些页面更新,不用担心。

另一种选择是保持到目前为止写入数据库的总数,并且只将差异发送到 eh 数据库。这样,就不需要重新设置数字(但是快照可能不是一次全部)

【讨论】:

    【解决方案3】:

    这是可能的;但不可取。使用我的超能力,我推断您尝试实现一些统计数据收集工具,并且您希望在每个时间间隔内为每个用户累积统计数据。

    您可以使用带有 servlet 过滤器和同步方法的方法,它会不时更新数据库 但是您会遇到以下问题: - 多台服务器上的应用程序集群 - 管理数据库连接和事务 (如果你不需要实时统计,你不会开发这个工具,否则你可以坚持每24小时处理一次日志)

    最好使用 Redis 等 NoSQL 数据库和某些键的值的原子增量来完成。只需使用“userid:startOfIntervalInMisllisecondsSince1970”作为键,并增加这个值。 - 它很快 - 原子的 - 数据始终是安全的 - 无需在负载平衡集群或容器中的多个线程之间共享任何内容和同步。

    【讨论】:

    • 好吧,我正在向数据库中插入一个新行,因此其他服务器是否执行相同操作并不重要,因为最终值将来自所有行的总和,不是吗?跨度>
    • 从您的数据模型中,我可以推断您最终会得到大量具有相同用户 ID 和时间但不同 ID 的条目。您可以稍后运行聚合查询,但这可能证明不适合您的数据大小
    【解决方案4】:

    在这种情况下,无需准确了解计数器。

    我会选择ConcurrentHashMap,但AtomicInteger 可能不是必需的。假设客户端的请求在同一台服务器上传递,您可以计算该客户端访问页面的数量,并具有宽松的线程安全性。您可能希望排除资源请求的计数(例如样式表等)并仅计算内容页面。因此,虽然 same 客户端的两个并发请求可能会发生冲突并且更新丢失,但这种情况很少发生,宽松的方案就足够了。也就是说,使用AtomicInteger 实现线程安全方案可能不会影响性能,因为锁定是在很短的时间内获取的(对任何人进行基准测试?)。

    问题是计数器的地图是全局,因此如果您有多个服务器,则无法扩展。

    • 处理此问题的一种方法是设置粘性会话(无论如何都是一个好习惯)。这样,客户端的请求总是到达同一个服务器。我们基本上和之前的情况一样。

    • 处理此问题的另一个选项是计算客户端访问的页面数。您的浏览器不时请求服务器保存该值(可能发送一个值和一个时间戳)。您可以在用户离开页面/站点时使用 javascript 进行拦截,以确保将数据刷新到服务器(请参阅onBeforeUnload)。

    【讨论】:

      猜你喜欢
      • 2018-08-02
      • 2014-12-24
      • 2011-01-30
      • 2017-02-25
      • 1970-01-01
      • 2017-04-14
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多