一 原由
公司要对以前的pv,uv统计进行重构,原先的不许,并且查询速度很慢。前端
经调研发现这绝对是一个坑,pv、uv统计存在的设计看起来简单,可是瞬间流量大,特别是有抢购等功能时,设计不良会致使数据库访问压力大,还存在被用心不良者利用等状况。redis
系统原先设计是将用户的请求放到redis中去,然后天天晚上一次将数据同步到数据库,在redis中并无保存每一个用户的访问时间,而只是保存的是每分钟有多少pv、uv。
这种设计存在这样一些问题:sql
- 只统计到分钟,并不统计每一个访问的具体时间,数据参考价值有限
- 对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查询就能很是方便的按照分钟,小时,天来分组了oracle
- 请求进来后不直接入库,也不直接入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量降下来后再处理