为什么80%的码农都做不了架构师?>>>
一、需求:
最近做一个统计项目,项目要求统计不同店铺的当前在线用户数、平均访问时长、今日在线用户最高数等需求
二、准备工作:###
在开始实现具体功能之前,首先得弄清楚这些概念以及计算公式(大家有哪里不认同的,欢迎指正,微笑)
- 当前在线用户数:这个很好理解,即当前浏览该店铺的所有用户数,也就是大家经常讨论的uv
- 平均访问时长:所有用户访问时长之和/总的用户数
- 今日在线用户最高数以及相应的时间,精确到分钟:这个也很好理解,即今天同时浏览该店铺的最大用户数
- 最近15min浏览次数:即店铺在最近15min被打开页面的次数,也就是大家经常讨论的pv
- 近30日平均浏览次数、平均访问时长:该店铺30日总的浏览次数(总的pv)/ 30,平均访问时长=30总的访问时长/总的用户数
三、数据采集:
好了,现在概念应该都明白了,开始进入正题。首先聊下数据采集,目前,我采用的是埋点(包括心跳通知)的方法进行数据采集,一方面使用埋点对所收集的数据比较准确,另外对数据收集相对比较简单。我们使用了两个数据收集接口,第一个即每次打开一个页面都会调用一次的pv收集接口,该接口主要收集当前页面的id、url、标题、上一个页面的地址等信息,该接口会返回一个唯一的pvId,如果用户第一次访问,则会服务端会生成一个唯一的sessionid存放到cookie里面,这个sessionid就用于区分不同用户(这种统计存在一定的偏差:比如同一个用户用不同浏览器打开,会统计成多个用户,暂时忽略这种偏差),另一个接口就是心跳通知接口,即打开某个页面后(成功调用第一个接口后),每10s请求一次服务端(带上上一个接口的pvId),通过该接口服务端就知道用户仍然保留该页面。
四、现在来开始看具体的统计:
-
当前在线用户数:目前很多实时数据,我是通过redis来实现的,包括当前在线用户数。每当有新的用户访问店铺时,服务端会生成唯一的sessionid返回给浏览器,浏览器会将其存放到cookie中(下次不论调用pv收集接口还是心跳通知接口,都会携带这个sessionid请求服务端),并且服务端会将这个sessionid存放到一个redis的set数据集合中,这个集合的key为:xxx_shopId(如果xxx_shopId这个key不存在,会新建一个), 值为sessionid,因为店铺的shopId唯一,所以,有用户访问店铺都有一个唯一的key与之对应;另外,如果用户如果离开(即关闭浏览器或者用户关闭店铺的所有页面并且在一定时间内未调用心跳通知接口,我目前定义为1min),会将该该sessionid从set中移除(那么问题来了,怎么知道用户离开?下面一段会具体说明)。那么,如果统计当前在线用户数就很简单了,就是这个set的长度啦(为什么使用set,读者可以考虑下)。具体代码如下(java代码,我使用的是stringRedisTemplate客户端):
// 获取set的长度 stringRedisTemplate.opsForSet().size(xxx_shopId); // 将sessionid从set中移除 stringRedisTemplate.opsForSet().remove(xxx_shopId, sessionid); // 将sessionid添加到set中 stringRedisTemplate.opsForSet().add(xxx_shopId, sessionid);
现在来说明下如何知道用户离开?这个当然也是比较关键的地方,我使用了redis的一个特性:可以设置key的失效事件,使用代码监听redis这个key失效的事件。所以,问题就很简单了,在收集接口,如果是新用户(第一次访问)调用,我会将其sessionid作为key保存到redis里面,并且设置它的失效事件(暂且设为1min),如果不是新用户,直接更新这个key即可;更新接口做一样操作,直接更新这个key,这样,只要有心跳通知,或者收集接口,这个key会一直存在,也就认为这个用户在线;如果超过一分钟未更新,那么key的失效事件会被触发,同时将sessionid从xxx_shopId的set集合中移除即可。具体如何实现监听,读者可以自己搜索下,本人下周末会具体说明,如何使用java代码监听redis的key的失效事件。
* **平均访问时长**:根据上述公式,我们需要知道用户总的访问时长,以及总的用户数;先说今日用户总的访问时长,其实我们只需要知道每一个用户开始进入店铺的时间,和用户最后离开的时间即可,刚开始进入的时间,即sessionid生成的时间,离开的时间就是上述sessionid失效的时间,所以,有了上面的铺垫,基本上迎刃而解。我们目前存储方案使用的是mongodb,就是会记录用户每一天开始访问店铺时间和离开时间,都会记录到mongodb中,结构如下:
```
{
"sessionid":"xxx",
"shopId":"xxx",
"startTime":xxx,
"endTime":xxx,
"onlineTime":xxx,
"date":"yyyy-MM-hh"
}
// onlineTime = endTime-startTime, 是冗余字段,方便后续统计用
这样,总的访问时长就是根据shopId和date条件,对onlineTime进行求和即可,总的用户数,即记录数,是不是很easy了。
-
今日在线用户最高数以及相应的时间,精确到分钟:这个需求其实也和第一个需求“当前在线用户数”有关系,我目前方案是使用任务来实现,即每分钟计算一次所有有用户访问的店铺的当前在线用户数,并保存到mongodb中,使用这个方案原因是redis的有较快的读速度(redis官网测试读写能到每秒10万左右),写到mongo采用批量写入。这个方案的弊端就是如果有大量店铺的话(百万级别),可能mongo在一分钟写入不了那么多数据。对于目前的业务情况,是完全没问题的。相应的mongodb的数据结构如下:
{ "shopId" : "xxx", "onlineCount" : NumberLong(xxx), //在线用户数 "dateTime" : "2017-07-29 05:18", "date" : "2017-07-29" }
这样,只用找出最大的onlineCount即可。具体mongo查询代码,下周末同样会贴出。
* **最近15min浏览次数**:这个相对比较简单,我在第一个接口即pv收集接口,都会将相关的pv数据保存到mongo中,只需要根据条件查出最近15min的条数即可。mongodb的数据结构如下:
```
{
"domain" : "xxx",
"url" : "xxx",
"pageTitle" : "home",
"pageId" : "index",
"referrer" : "xxx",
"sessionid" : "xxx",
"viewDate" : NumberLong(1500948078152),
"shopId" : "xxx",
"ip" : "xxx",
"date" : "2017-07-25"
}
-
近30日平均浏览次数、平均访问时长:这个我采用的方案是使用任务,即每天00:00:00开始统计前一天总的浏览次数(pv)以及总的访问时长,以及总的用户数(uv),总的浏览次数根据4中mongodb保存数据,即可统计出来,总的访问时长和总的用户数通过2的mongodb保存数据统计出来,那么计算也手到擒来了。数据保存结构如下:
{ "shopId" : "xxx", "date" : "2017-07-25", "pv" : NumberLong(51), "uv" : NumberLong(5), "ip" : NumberLong(3), "uvTime" : NumberLong(60270921) }
### 五、总结:
个人认为,实现这次统计,有几个关键点:redis key的失效事件监听,然后就是mongodb一些查询,这些会在下周贴上具体代码。另外就是有两个地方使用任务处理,个人认为不是最佳方案,对于是否要用任务处理,或者有更好更合适的方案,欢迎大家讨论!