背景

最近换了新工作,一切都在适应中。想起来好久没写博客了,今天提起精神写点什么。就来谈谈最近碰到的一些项目问题及解决思路与优化点吧。

最近碰到碰到两个问题,第一个是投放策略引擎对接人群包的问题,第二个是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)。有两点问题需解决:

  1. 减少的数据不推,但redis要清掉(不清除影响投放效果,浪费钱,降低roi)
  2. 一个人群包数据全量8000万,数十个人群包,写redis太多会影响rta侧读的时间(目前99线在3ms内),导致rta超时。redis fork

使用离线计算,将数据差集写入redis。如老包减新包为减,新包减老包为加。现在文件焦点就在如何快速的算差结。我们知道位运算是最快的,最先想到的是bitmap。但是由于历史原因,uid 目前为12位(以后可能会到13甚至14位),且最小为1,中间有大量断层不可知。bitmap会有大量无效位,且无法去除,12位需要1.1G,13位需要11G,内存消耗不可控。有没有一种针对bitmap的压缩算法了,有的。roaringbitmap,具体原理就不介绍了,你可以去查。rbm是一种优秀的、目前使用最广泛的位压缩算法,在大数据领域有广泛应用,官网

状态流转图

image-20211203201531636

问题二 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))
Scroll to Top