每日一面 - Redis程序设计中,上百万的新闻,如何实时展示最热点的top10条呢

2,026 阅读3分钟

假设可以使用 MySQLredis本地缓存以及MQ

用户量级千万,新闻数据百万用户数比新闻数还多。用户的操作包括:

  • 关注某个新闻
  • 获取某个新闻的关注数量
  • 获取 top10 热点新闻
  • 查询自己关注的新闻。

可以推测,获取 top10 热点新闻请求会远大于关注某个新闻的请求。这些请求都不能直接压入数据库,数据库受不了。

首先想到的是 Redis 中的 Zset,所有的新闻id作为key放入同一个zset中,用户关注某个新闻,使用 zincrby 给这个新闻分数 +1。读取 top 10的时候,用zrevrange.

并且,在实际业务上(例如微博热点话题,知乎热点话题,都是每过一段时间才更新的),top10 热点新闻并不是实时更新的,可以接受一点延迟,可以通过客户端实例的本地缓存,将读取到的 top 10 存在本地缓存一段时间,过了这段时间自动失效。

但是这样也会很快遇到性能瓶颈

1.zset 在很大时可能不满足我们对于性能的要求: Redis 的 Zset 在数量够大的时候底层基于 skiplist:

image

skiplist 实现简单,插入、删除、查找的复杂度均为O(logN)。zincrby 实际上是一个查找+删除+插入(当然由于score只加了1,所以删除插入只修改相邻节点,这个有优化)

我们的场景是首先插入的新闻分数都是0,之后增长这个分数,在新闻很多,并且并不能确定某些新闻是热点的时候,zincrby 导致的节点变动很频繁。这个通过业务设计可以优化,例如新闻分级,不同级别的新闻初始分数不同

2.放入同一个 zset,对于单实例 redis 性能瓶颈时,扩展不友好

用户千万量级,更新很频繁,如果都更新同一个 zset,很快会遇到性能瓶颈。读取还好说,可以通过本地缓存,因为展示最热点的top10的实时性要求并没有那么高。这时考虑 redis 集群,redis分片,但是如果放在同一个 zset,无法分摊压力。

那么我们可以换一种思路:redis 中,每个新闻id作为key,关注数作为value,存储简单的键值对。用户关注了某个新闻:

同步事务更新数据库中的用户关注新闻表,这个每个用户会均摊行锁压力 缓存新闻id key + 1(注意catch住缓存不可用的异常) 写入 MQ,之后返回 MQ 消费更新数据库这个新闻的关注数量,这样不会有性能瓶颈,同时针对新闻id做queue以及线程分区(就是同一个新闻总是对应特定的queue以及线程,尽量每一个行锁一个线程更新,避免数据库 lock wait timeout)

怎样获取 top 10:定时任务扫描数据库,按照新闻关注数量排序获取top10,直接放入缓存。用户请求都是读取这个缓存。虽然实时性差,但是能满足需求。

读取某个新闻的关注数量:这个就读缓存,缓存不可用,读取数据库。

获取某个用户关注的新闻列表:这个读取数据库,如果感觉也有性能瓶颈,对于每个用户id添加缓存保存关注的新闻列表即可。这个很简单

每日一刷,轻松提升技术,斩获各种offer:

image