一 起因
公司要对之前的pv,uv统计进行重构,原先的不准,而且查询速度很慢。
经调研发现这绝对是一个坑,pv、uv统计存在的设计看起来简单,但是瞬间流量大,特别是有抢购等功能时,设计不良会导致数据库访问压力大,还存在被用心不良者利用等情况。
系统原先设计是将用户的请求放到redis中去,而后每天晚上一次将数据同步到数据库,在redis中并没有保存每个用户的访问时间,而只是保存的是每分钟有多少pv、uv。
这种设计存在这样一些问题:
- 只统计到分钟,并不统计每个访问的具体时间,数据参考价值有限
- 对pv的查询会从redis中查一部分,从数据库中再查一部分,合并起来返回前端,开发实现上代码比较复杂。更别说在数据库中查询居然是用where min=xx来实现,1000分钟时间段的查询会查询1000次数据库,查询返回奇慢无比,能写出这个sql的简直是天才。
- 统计一次pv的redis操作要操作6次,数据结构的使用上存在问题。
redisTemplate.opsForValue().increment(nowMin,1);
int pv = redisTemplate.opsForValue().get(nowMin);
redisTemplate.opsForHash().put(PV_KEY, nowMin, pv);
redisTemplate.opsForSet().put(nowMin,uid);
int uv = redisTemplate.opsForSet().size(nowMin);
redisTemplate.opsForHash().put(UV_KEY, nowMin, pv);
原代码甚至要8次,这里无力吐槽,完全不把redis当资源,你知道如何能优化成一个redis操作么?
而且进行pv,uv统计肯定是要精确到用户的,这样才能看出什么用户进行了什么访问,方便后期的用户画像以及访问数统计。但如此一来带来的问题就是
- 数据库记录会急剧增长,以前只是统计分钟,一天也就1000+条数据,而如果粒度是细到用户的话,如果PV到千万级,即使每天同步也受不了
- 实时查询会从redis中取数据,redis中资源本来就稀缺,如果每天同步一次,意味着要从redis中取百万、甚至千万级数据,不仅同步会非常慢,而且无法满足实时查询的需求。
系统本身存在如下限制
- 必须使用oracle数据库,而且pv表与业务表就在同一个实例,要考虑不能有瞬时过大的流量影响到业务操作。
- 必须使用同一个微服务网关。
二 第一步
经思考实现了如下方案:
- 使用浏览器指纹来记录每一个用户,来记录uv,而不是使用用户id,将pv、uv统计与业务隔离。浏览器指纹是根据客户端的一些参数计算而成,业内已经有成熟解决方案,准确率能达到94%
- 优化入统计pv、uv时入redis的操作,重新设计redis的数据格式,将6次缩减为1次。
redisTemplate.opsForHash().put(bizKey, devFinger + dateTime);
只需一次redis操作, 落到数据库后用group by查询就能非常方便的按照分钟,小时,天来分组了
- 请求进来后不直接入库,也不直接入redis,而是放入mq,通过mq再入redis,起到削峰填谷的作用。测试环境redis存的速度大概能达到 3w/s, 3w的qps,taobao抢购系统恐怕都支撑起来了。
- 每分钟将redis的数据批量入数据库,而不是每天统计一次,因为千万级的数据统计不仅对oracle数据库,而且对redis都是巨大的压力,甚至很可能会导致读redis超时。同时将写数据库的操作分配到每分钟,降低数据库的压力
- 查询只从数据库中查,加上对应的索引,查询数据不会太慢。同时也降低了程序的复杂性,不用到redis中查了。
三 第二步
压测发现有两个问题
- mq挂了
- 同步到数据库时数据库也处理不过来。
我们mq是公用的,就是说所有的服务,而且不止我们的服务,都用到了mq,而pv,uv操作是一个超级大数量级别的操作,而且并非核心业务,所以不能把主要的资源都放到mq上,所以我们又继续进行了处理:
- 后端用了缓存队列,当pv满足10个的时候才发送,否则不发。此处要加synchonize,否则会出现异常的
- 数据库同步时做了柔性处理,当pv数据量过大的时候不处理,而是延后再做,等到pv量降下来后再处理
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。