Redis在用户数据统计中的简单应用

前言

在项目的基本功能开发完成之后,有一个对用户的数据的统计需求。用于分析和查看应用的相关数据。正好之前看到过有用Redis统计的例子,便开始慢慢摸索。(可能本篇文章内的并不是最佳实践,总要踩坑,优化嘛。。。)

Redis

Redis是一个优秀的K-V数据库,Redis的所有数据都是存放在内存中的,

Redis是用C语言实现的,一般来说C语言实现的程序“距离”操作系统更近,执行速度相对会更快。

Redis使用了单线程架构,预防了多线程可能产生的竞争问题。

Redis有一些非常实用的数据结构,如:String,Set,Hash,List,Zset,方便使用。

统计的项目需求

  • 每日新注册用户
  • 每日登录用户
  • 每个时段的在线人数
  • 平均在线
  • 最高在线
  • 充值人数
  • 新增充值人数
  • 充值金额
  • N日留存
  • N日ARPU
    等。。

Redis设计

基于上面的一些需求,在数据类型上可以分成两类

  1. 累计类型
  2. 新增数据(涉及到集合的运算)
    在数据统计上,也可以分成两类
  3. 每日的数据
  4. 从开始起就统计的数据

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设计

  1. 对于累加类型的数据,可以使用String,比如:每日消耗的金币,每日注册人数等

    1
    2
    3
    function *incrbyKey(key, num) {
    cache.incrby(key, num)
    }
  2. 对于需要计算唯一性的数据,比如登录用户,充值人数等,不能继续使用 String,可以选用的有Set,Hashbitmap等。
    考虑到平均下来每个服务器分担的用户可能只在万级左右,并且在线人数统计要频繁地统计长度,最后选择了使Set来对这些数据统计。

    1
    2
    3
    cache.sadd(uid, uid) //用户上线
    cache.srem(uid) //用户下线
    cache.scard(key)//这个key的长度

在计算每小时平均在线的时候,使用String来存储,后续只要对字符串进行格式化,即可方便地计算出每小时平均在线,每小时最高在线,整天的数据。

1
2
3
4
5
6
7
8
9
function *calOnlinePlayerNum() {
let hour = New Date().getHours()
let num = yield cache.scard(key1)
cache.append(key2, `-${hour}:${num}`)
}

schedule(
co(calOnlinePlayerNum)
)

  1. 新增的数据计算
    这一类要记录的数据可能比较多,因为涉及到集合的交集,并集计算,一开始想到的就是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
2
3
4
5
6
cache.setbit(key, uid, 1)
//====================//
cache.bitop('and' , destKey, key1, key2) //计算交集
let a = cache.bitcount(key1) || 1
let b = cache.bitcount(destKey) // 交集的长度
per = b/a

持久化数据

虽然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)