之前,统计每篇博文的阅读数的方式是经过筛选去重之后直接更新数据库,并发压力直接传导到数据库,假设1秒有1000个并发请求,传统方案会在1秒内并发进行1000次数据库更新操作。
为了降低数据库的并发压力,需要重新设计统计服务。思路是即使1秒有1万个并发请求,也只是依次更新数据库,对数据库没有并发压力。
统计服务要做的事情很专一:去重+计数
去重的业务根据具体的需要来设计规则,例如一个用户1个小时内所有访问都只计算一次,没有用户信息的按 IP 地址或者浏览器标识去统计。去重就是把这些标志去重,有多种实现方法,Hash
过滤,数据库唯一性等。
这里我们采用 Redis 的 HyperLogLog
,简称HLL
,它是一个高效的结构,内存占用极小,能快速统计出所有不一样的元素。有三个方法:
PFADD
:向结构中增加一个元素,其实 HLL
并没有存储这个元素,而是按照概率论的算法进行统计,所以 12K 内存就能统计 2^64 个数据,返回值为1表示该元素被统计了,反之则没有;
PFMERGE
:可以把合并两个 HLL
;
PFCOUNT
:获取统计数,这个算法虽然高效,但是也有弊端,就是存在误差,在 1% 以下,只要不是非常精确的业务基本上也是可以忽略的。
我们的业务逻辑实现比较简单,可以用博文和时间作Key
,hll_{postId}_{yyyymmddhh}
,再把访问博文的用户标志按照规则生成一个字符串,name_{userName}
ip_{ipAddress}
,用PFADD
它添加进去,HLL
会判断是否重复,重复的就不会统计,然后把不重复的也就是返回值为 1 的 Key 存储到集合 SET
中,记录下来方便遍历。
经过去重之后我们就要统计总数并持久化到数据库中,每篇博文在 Redis
中对应至少一个 HLL
结构,创建观察者服务不停地Pop
SET
中所有的 hll 的 Key
,然后再通过PFCOUNT
得到对应的博文的统计数。 拿到统计数之后再发送给持久化服务处理,或者通过负载均衡交给多个持久化服务处理。
如上图所示,部署多个 Counter web服务负责接收请求,一个 redis 服务或者集群负责统计阅读数,多个 watcher 服务负责把统计结果取出来,交给多个数据存储服务去持久化。
我们线上用的是 docker-swarm 集群,它本身就有负载均衡作用,所以可以省略负载均衡。
这里之所以用SET.POP()
,是因为它支持并发访问的,不会锁 Redis。如果直接遍历所有 HLL Key
,就只能用 SCAN
全局查找,虽然也不会锁住 Redis,但是它不支持并行操作,对扩展不够友好。
这样架构的优点就是可以横向扩展,任何地方出现性能瓶颈都能通过扩展解决。