背景
最近换了新工作,一切都在适应中。想起来好久没写博客了,今天提起精神写点什么。就来谈谈最近碰到的一些项目问题及解决思路与优化点吧。
最近碰到碰到两个问题,第一个是投放策略引擎对接人群包的问题,第二个是rta的redis优化问题。
问题一 人群包
需求
圈选平台做出圈选(一个人群包8000万),人群包id给到投放策略管理平台。rta投放时,要能判断是否命中人群包,走对应的投放策略。
技术方面的要求如下,rta查人群包命中情况3ms以内(rta站内整体10ms),qps满足25万。基于此要求,及结合现有业务场景。所有数据要缓存到redis中。
方案
圈选平台mq数据是cid对应所有uid,如果以cid作为key,会出现热key问题。即使将cid做hash,也会有可能碰到热key。得到结论,以uid作为key。
以uid做key,cid映射的自增id做bitmap的位。rta侧,通过get拿到字符串,程序中转2进制解析。示例如下:
res, err := cache.NewRedisTools(ctx, mediaType).Get(cosnts.GetUserCircleKey(userId))
if err != nil { // 注意 redis.Nil
return nil, err
}
buffer := &bytes.Buffer{}
for _,v := range res {
item := strconv.FormatUint(uint(v), 2)
itemLen := len(item)
for i := 0; i < 8 - itemLen; i++ {
buffer.WriteString("0")
}
buffer.WriteString(item)
}
newBit := buffer.String()
ciecleMap := make(map[uint64]int)
for i, val := range newBit {
if val == 1 {
circleMap[uint(i)] == 1
}
}
return circleMap, nil
目前已有uid做key的hash结构,实现的频控逻辑。不增加新的key,节省redis内存。hash代码示例省略。
我们都知道,redis的key其实是个对象,里面包括了value的type、encoding、strlen。type可以快速判断redis方法是否正确,encoding分int和raw,可以快速判断是否可以加减 (incr操作)。
redis的hash底层有两种结构,当field数量<512个,value<64字节时,是ziplist结构,省内存。当不满足条件,是hashtable结构。
到这里你以为结束了吗,重头戏才开始~~
圈选推送是全量推送,每天10点左右人群包数据重新生成(T+1)。有两点问题需解决:
- 减少的数据不推,但redis要清掉(不清除影响投放效果,浪费钱,降低roi)
- 一个人群包数据全量8000万,数十个人群包,写redis太多会影响rta侧读的时间(目前99线在3ms内),导致rta超时。redis fork
使用离线计算,将数据差集写入redis。如老包减新包为减,新包减老包为加。现在文件焦点就在如何快速的算差结。我们知道位运算是最快的,最先想到的是bitmap。但是由于历史原因,uid 目前为12位(以后可能会到13甚至14位),且最小为1,中间有大量断层不可知。bitmap会有大量无效位,且无法去除,12位需要1.1G,13位需要11G,内存消耗不可控。有没有一种针对bitmap的压缩算法了,有的。roaringbitmap,具体原理就不介绍了,你可以去查。rbm是一种优秀的、目前使用最广泛的位压缩算法,在大数据领域有广泛应用,官网。
状态流转图
问题二 redis优化
到这里其实优点累了,想留到明天写。算了还是总结下吧。
背景
媒体传过来设备号,我们要判断是否站内用户,走召回策略。安卓设备号有三个 oaid imei andorodid,优先级依次从前到后。现在方案是,一次网络io把三个数据都拿多来,在程序中根据优先级判断使用哪个。这么做用句高级的话就是数据向计算移动,消耗网络io,且redis工作线程时间复杂度O(3)。
优化点
使用lua脚本实现,根据设别号优先级,第1优先级能拿到,就直接返回
计算向数据移动,消耗redis的cpu减少内存io和网络带宽占用
示例
client := initRedis()
luaScript := redis.NewScript(`
local a = redis.call("get", KEYS[1])
if a ~= nil then
return a
else
return redis.call("get", KEYS[2])
end
`)
// lua脚本预加载到redis,此处放到程序启动阶段执行
sha, err := luaScript.Load(client).Result()
if err != nil {
panic(err)
}
cmd := client.EvalSha(sha, []string{"k1", "k2"}, 2)
res, err = cmd.Result()
if err != nil {
panic(err)
}
fmt.Println(res.(string))