高并发系统架构设计之实战篇35:计数系统设计之未读数系统

文章目录
引言
上一节课中我们了解了如何设计一套支撑高并发访问和存储大数据量的通用计数系统 , 我们通过缓存技术、消息队列技术以及对于 Redis 的深度改造 , 就能够支撑万亿级计数数据存储以及每秒百万级别读取请求了 。然而有一类特殊的计数并不能完全使用我们提到的方案 , 那就是未读数 。
未读数也是系统中一个常见的模块 , 以微博系统为例 , 你可看到有多个未读计数的场景 , 比如:当有人 @你、评论你、给你的博文点赞或者给你发送私信的时候 , 你会收到相应的未读提醒;在早期的微博版本中有系统通知的功能 , 也就是系统会给全部用户发送消息 , 通知用户有新的版本或者有一些好玩的运营活动 , 如果用户没有看 , 系统就会给他展示有多少条未读的提醒 。我们在浏览信息流的时候 , 如果长时间没有刷新页面 , 那么信息流上方就会提示你在这段时间有多少条信息没有看 。
那当你遇到第一个需求时 , 要如何记录未读数呢?其实 , 这个需求可以用上节课提到的通用计数系统来实现 , 因为二者的场景非常相似 。你可以在计数系统中增加一块儿内存区域 , 以用户 ID 为 Key 存储多个未读数 , 当有人 @你时 , 增加你的未读 @的计数;当有人评论你时 , 增加你的未读评论的计数 , 以此类推 。当你点击了未读数字进入通知页面 , 查看 @ 你或者评论你的消息时 , 重置这些未读计数为零 。相信通过上一节课的学习 , 你已经非常熟悉这一类系统的设计了 , 所以我不再赘述 。那么系统通知的未读数是如何实现的呢?我们能用通用计数系统实现吗?答案是不能的 , 因为会出现一些问题 。
一、系统通知设计方案
【高并发系统架构设计之实战篇35:计数系统设计之未读数系统】来看具体的例子 。假如你的系统中只有 A、B、C 三个用户 , 那么你可以在通用计数系统中增加一块儿内存区域 , 并且以用户 ID 为 Key 来存储这三个用户的未读通知数据 , 当系统发送一个新的通知时 , 我们会循环给每一个用户的未读数加 1 , 这个处理逻辑的伪代码就像下面这样
1 List userIds = getAllUserIds(); 2 for(Long id : userIds) { 3incrUnreadCount(id); 4 }
这样看来 , 似乎简单可行 , 但随着系统中的用户越来越多 , 这个方案存在两个致命的问题 。
首先 , 获取全量用户就是一个比较耗时的操作 , 相当于对用户库做一次全表的扫描 , 这不仅会对数据库造成很大的压力 , 而且查询全量用户数据的响应时间是很长的 , 对于在线业务来说是难以接受的 。如果你的用户库已经做了分库分表 , 那么就要扫描所有的库表 , 响应时间就更长了 。不过有一个折中的方法 ,  那就是在发送系统通知之前 , 先从线下的数据仓库中获取全量的用户 ID , 并且存储在一个本地的文件中 , 然后再轮询所有的用户 ID , 给这些用户增加未读计数 。这似乎是一个可行的技术方案 。
另外 , 它给所有人增加未读计数 , 会消耗非常长的时间 。你计算一下 , 假如你的系统中有一个亿的用户 , 给一个用户增加未读数需要消耗 1ms , 那么给所有人都增加未读计数就需要* 1 /1000 =秒 , 也就是超过一天的时间;即使你启动 100 个线程并发的设置 , 也需要十几分钟的时间才能完成 , 而用户很难接受这么长的延迟时间 。