推荐去重机制探索
背景
目前公司首页等瀑布流推荐中,用户已读文章历史是存放在Redis的List列表中,用户每天1个List。Redis控制其过期时间
代码可以控制取最近X天的已读历史。
取出X天的List已读历史后, 将其合并转化为集合, 判断候选池中M千篇文章是否在此集合中,
如果在集合中,则过滤,如果不在,则保留。
当前方案存在的问题
- redis中明文存储历史文章ID, 占用空间大,用户日活增多,内存增长显著
- 从List取出历史文章,需要转化为集合,这里存在转化效率问题,文章越多,效率越低
总结,即需要降低内存占用,提高查询效率
解决方案探讨
bloomfilter
算法在去重场景有很好的应用, 网上该算法的文章比较多,这里不详细介绍。
总结特点如下:
- 占用空间小, 利用bitmap的0,1状态表述是否存在,比明文存储能节约很大空间
- 执行快, 预先使用HASH函数处理文章ID, 比较操作是O(1)
其存在几个关键的 变量
- 需预设去重集数量大小, 数量越大,占用空间越大
- 需预设准确率, 比如0.01, 允许1%的假阳性,即将不在数量集合中的 元素 误判为 在集合中, 导致多过滤, 如果需要的准确率越高, 则需要占用更多的空间
- hash函数个数, 此计算为cpu计算密集类型, hash函数越多,则占cpu越多,相应的,对占用空间则越不敏感
如果要将bloomfiler使用最大化,则需考虑以上1,2,3限制条件
本机压测分析
本机写了demo压测方案, 来评估1,2,3限制条件
这里使用JAVA程序自带的bloomfilter类库, 并没有使用redis的bloomfilter插件
因为redis并不擅长充分使用CPU多核,而bloomfilter恰好是计算密集型, 如果并发稍微过高,会把CPU打高
预设数据:
- 记录最近14天的用户历史数据
- 1个用户1天最多存储1000篇历史文章
- 待遍历的用户ID: 200个用户ID
- 候选池文章数: 2000篇文章
存在以下四种情况
- 方案1: 每天1个Redis List记录历史数据, 查询总共耗时: 3782ms
此方案为当前线上方案
- 方案2: 全部历史文章使用1个bloomfilter持久化, 查询总耗时: 385ms
此方案需要预设较大文章数, 占用较大的内存空间,使用不灵活,但是较List查询,耗时减少: 80%
- 方案3: 每天1个Redis key记录序列化后的bloomfilter对象, 查询总共耗时: 3417ms
改动较小,把list调整为存储bloomfilter对象, 耗时减少:10%
- 方案4: 使用10个bloomfiter对象,每个记录1400条历史数据,记录最多14000条数据, 查询总耗时, 2588ms, 耗时减少:30%
这种方案,调整了产品策略:
- 原来产品策略: 是说取最近14天的历史数据
- 现在的策略: 取用户最近浏览的 1.4万条历史数据。如果最近xx天没有新的历史数据,则删除原有历史数据
这里bloomfilter对象动态创建,如果最近只有1000条历史数据,则使用1个bloomfilter对象记录,则更能方便的控制其占用内存大小。
线上实际情况是,大部分用户很可能只有<1000条的历史数据,使用效果会更好
但其写入逻辑较复杂
其他
同事提供了另外一种思路:
今天的明文list + 昨天的明文list + (定时作业写入前: 前天N条bloomfiler对象 或者 定时作业写入后: 昨天N条bloomfiler对象)
但是这里
总结
- 方案四最佳,能做到省内存,提升查询效率,使用bloomfilter确实能有效提高查询效率, 但需要更好的和业务结合, 即不能预设太大条目,需要做到能动态扩展。
相关demo代码地址: https://github.com/l1905/bloom_demo
参考文章
- https://cloud.tencent.com/developer/article/1474247