前言
在项目的基本功能开发完成之后,有一个对用户的数据的统计需求。用于分析和查看应用的相关数据。正好之前看到过有用Redis统计的例子,便开始慢慢摸索。(可能本篇文章内的并不是最佳实践,总要踩坑,优化嘛。。。)
Redis
Redis是一个优秀的K-V数据库,Redis的所有数据都是存放在内存中的,
Redis是用C语言实现的,一般来说C语言实现的程序“距离”操作系统更近,执行速度相对会更快。
Redis使用了单线程架构,预防了多线程可能产生的竞争问题。
Redis有一些非常实用的数据结构,如:String,Set,Hash,List,Zset,方便使用。
统计的项目需求
- 每日新注册用户
- 每日登录用户
- 每个时段的在线人数
- 平均在线
- 最高在线
- 充值人数
- 新增充值人数
- 充值金额
- N日留存
- N日ARPU
等。。
Redis设计
基于上面的一些需求,在数据类型上可以分成两类
- 累计类型
- 新增数据(涉及到集合的运算)
在数据统计上,也可以分成两类 - 每日的数据
- 从开始起就统计的数据
Key设计
于是可以对键进行设计,Redis是内存数据库,所以对于键的长度也不能忽视,可以采用缩写
我这里采用 [areaName]:[date]:[type]的格式1
2
3
4
5
6
7
8
9
10
11
12
13/*数据类型*/
const ValueType = {
'newPlayer': 'np',
'chargeAmount': 'cA',...
}
/*生成Redis Key*/
function initRedisKey(area, type, time) {
let date = 'all'
if(time) {
date = moment().format('YYYYMMDD')
}
return `${area}:${date}:${ValueType[type]}`
}
Value设计
对于累加类型的数据,可以使用
String
,比如:每日消耗的金币,每日注册人数等1
2
3function *incrbyKey(key, num) {
cache.incrby(key, num)
}对于需要计算唯一性的数据,比如登录用户,充值人数等,不能继续使用
String
,可以选用的有Set
,Hash
,bitmap
等。
考虑到平均下来每个服务器分担的用户可能只在万级左右,并且在线人数统计要频繁地统计长度,最后选择了使Set
来对这些数据统计。1
2
3cache.sadd(uid, uid) //用户上线
cache.srem(uid) //用户下线
cache.scard(key)//这个key的长度
在计算每小时平均在线的时候,使用String
来存储,后续只要对字符串进行格式化,即可方便地计算出每小时平均在线,每小时最高在线,整天的数据。1
2
3
4
5
6
7
8
9function *calOnlinePlayerNum() {
let hour = New Date().getHours()
let num = yield cache.scard(key1)
cache.append(key2, `-${hour}:${num}`)
}
schedule(
co(calOnlinePlayerNum)
)
- 新增的数据计算
这一类要记录的数据可能比较多,因为涉及到集合的交集,并集计算,一开始想到的就是Set
,又考虑到内存的问题,便将目光转移到bitmap
上。bitmap
有点hash位的感觉,如1000 0001
,就可以表示uid为8,0 的用户今天登录过。
如果uid时1000000开始的,可以对其切割(减去初始uid)。在第一次setbit的时候,Redis要对其开一个较大的内存,可能会影响性能。
比如记录留存率:
记录每天的新注册的id。[area]:[date]:[type]
记录每天的登录用户id。[area]:[date]:[type]
A日的N日留存率=(A日之后第N天登录过游戏的并且这些用户是在A日新登录的用户数)/(A日新登录的玩家用户数)
1 | cache.setbit(key, uid, 1) |
持久化数据
虽然Redis本身也有持久化的功能,但显然这是不划算的,对于每天统计的保存在Redis中的数据,可能有些数据第二天就不需要继续存在Redis里面了,所以,可以选择数据库,将数据持久化到磁盘。
对于每天的数据,可以在统计完之后就删除,
对于N天的需要统计计算的数据,可以在最后一天进行删除操作。
本文使用MongoDB,MongoDB的一个优势就是对JSON的友好。在次日对Redis中的数据进行一定的分类,计算,对数据进行持久化。以date为索引,可以方便后台的以日期为单位进行查询。
小节
Redis因其提供的数据结构,使得使用起来非常方便,对于不同的需求,可以有多种的实现方式。
但是对于性能和内存的占用还是要有比较多的考虑,
本文只是结合一些资料的探索,可能还存在一些不足的地方,希望在实践中能够进行优化。
参考
- 《Redis开发与运维》
- 《Redis实战》
附: 常见的Redis的操作的复杂度
string
set o(1)
get o(1)
del o(n)
set
sadd o(1)
sismember o(1)
srem o(n) n is the number of members to be removed
smembers O(N) where N is the set cardinality
scard o(1)
zset
zadd O(log(N)) for each item added, where N is the number of elements in the sorted set.
zrange
O(log(N)+M) with N being the number of elements in the sorted set and M the number of elements returned.
zrem
O(M*log(N)) with N being the number of elements in the sorted set and M the number of elements to be removed.
zrangebyscore
O(log(N)+M) with N being the number of elements in the sorted set and M the number of elements being returned. If M is constant (e.g. always asking for the first 10 elements with LIMIT), you can consider it O(log(N)).
hash
hset o(1)
hget o(1)
bitmap
bitcount o(n)
bitop o(n)
setbit o(1)
getbit o(1)