即时消息技术剖析与实战
袁武林
微博研发中心技术专家
24187 人已学习
新⼈⾸单¥59
登录后,你可以任选4讲全文学习
课程目录
已完结/共 25 讲
即时消息技术剖析与实战
15
15
1.0x
00:00/00:00
登录|注册

07 | 分布式锁和原子性:你看到的未读消息提醒是真的吗?

小结
保证未读更新的原子性
未读数的一致性问题
会话未读和总未读单独维护
消息和未读不一致的原因
消息未读数对用户使用体验的影响
分布式锁和原子性

该思维导图由 AI 生成,仅供参考

你好,我是袁武林。
在前面几节课程中,我着重把即时消息场景中几个核心的特性,进行了较为详细的讲解。在实际用户场景下,除了实时性、可靠性、一致性、安全性这些刚需外,还有很多功能对用户体验的影响也是很大的,比如今天我要讲的“消息未读数”。
消息未读数对用户使用体验影响很大,这是因为“未读数”是一种强提醒方式,它通过 App 角标,或者 App 内部 Tab 的数字标签,来告诉用户收到了新的消息。
对于在多个社交 App 来回切换的重度用户来说,基本上都是靠“未读数”来获取新消息事件,如果“未读数”不准确,会对用户造成不必要的困扰。
比如,我们看到某个 App 有一条“未读消息提醒”,点进去事件却没有,这种情况对于“强迫症患者”实在属于不可接受;或者本来有了新的消息,但未读数错误,导致没有提醒到用户,这种情况可能会导致用户错过一些重要的消息,严重降低用户的使用体验。所以,从这里我们可以看出“消息未读数”在整个消息触达用户路径中的重要地位。

消息和未读不一致的原因

那么在即时消息场景中,究竟会有哪些情况导致消息和未读数出现“不一致”的情况呢?要搞清楚这个问题,我们要先了解两个涉及未读数的概念:“总未读”与“会话未读”。我们分别来看看以下两个概念。
确认放弃笔记?
放弃后所记笔记将不保留。
新功能上线,你的历史笔记已初始化为私密笔记,是否一键批量公开?
批量公开的笔记不会为你同步至部落
公开
同步至部落
取消
完成
0/2000
荧光笔
直线
曲线
笔记
复制
AI
  • 深入了解
  • 翻译
    • 英语
    • 中文简体
    • 中文繁体
    • 法语
    • 德语
    • 日语
    • 韩语
    • 俄语
    • 西班牙语
    • 阿拉伯语
  • 解释
  • 总结

在即时消息场景中,保证消息未读数的一致性是至关重要的。本文介绍了会话未读和总未读的概念,以及它们单独维护的原因,并讨论了未读数的一致性问题。针对这一问题,文章提出了保证未读更新原子性的重要性,并介绍了分布式锁、支持事务功能的资源以及原子化嵌入脚本三种解决方案。通过对这三种方案的比较,读者可以了解在分布式场景下如何保证消息未读数的一致性,以及不同解决方案的特点和适用场景。 其中,原子化嵌入脚本是一种性能不错且支持“原子变更”的方案,通过Redis的Lua脚本实现了总未读和会话未读的原子化变更,同时还能实现复杂的未读变更逻辑。相对于分布式锁和支持事务功能的资源,原子化嵌入脚本在执行性能上更胜一筹,但需要注意避免长时间悬挂导致资源不可用。此外,本文还指出这些解决方案不仅可以用来解决未读数的问题,对于分布式场景下需要保证多个操作的原子性或者事务性的情况也是可选方案。 总的来说,本文通过深入分析了保证消息未读数一致性的关键技术,为读者提供了在分布式场景下保证消息未读数一致性的解决方案和思路。读者可以从中了解到不同解决方案的优缺点,以及如何选择适合自身场景的方案。

仅可试看部分内容,如需阅读全部内容,请付费购买文章所属专栏
《即时消息技术剖析与实战》
新⼈⾸单¥59
立即购买
登录 后留言

全部留言(31)

  • 最新
  • 精选
  • Darcy
    redis cluster集群模式lua脚本如果操作的两个key不在同一个节点,好像会报异常

    作者回复: 是的,对于需要使用lua的数据需要确保两个key能hash到一个节点。

    2019-09-28
    2
    21
  • 王棕生
    对于老师本节讲述的未读数不一致的问题,我想是否可以通过下面的方法来解决: 1 用户的未读数是在用户离线的时候,其他用户给他发消息的时候产生的,所以,只需要维护用户会话未读数即可; 2 当用户登录的时候,发送一个消息到MQ,由MQ触发维护用户总的未读数的操作,即将用户所有的会话未读数相加后的数值放入总未读数字段中。 这样的设计的好处时,降低维护用户总未读数的压力,只在用户登录的时候进行维护即可,不用每次收到一条消息就维护一次。 然后用户在线期间,收到的消息的未读数由前端来进行维护,不用服务端进行操作了。

    作者回复: 思路是好的哈,不过很多时候不仅仅是用户登录的时候才需要总未读,比如每来一条消息需要进行系统推送时,由于苹果的APNs不支持角标未读的累加,只能每次获取总未读带下去。另外,客户端维护总未读这个也需要考虑比如离线消息太多,需要推拉结合获取时,到达客户端的消息数不一定是真正的未读消息数。

    2019-09-12
    15
  • leslie
    Redis不是特别熟悉:其实老师今天的问题和另外一个问题有点类似;既然问题是"执行过程中掉电是否会出现问题"这个极端场景:那么我就用极端场景解释,老师看看是否有理或者可能啊。 我的答案是会:尤其是极端场景下会,虽然概念很小;其实老师今天的问题是李玥老师的消息队列课程中前几天的期中考题,"数据写入PageCache后未做刷盘,那种情况下数据会丢失“当时的答案就是断电。 其实老师在提掉电时:未提及一个前提条件;掉电后硬件是否正常?如果掉电后硬件损坏了呢,那么数据肯定就丢失了,线上最新的数据都没了,数据肯定就丢了。因为问题是极端场景,回答就只能是极端场景,希望老师不介意;这就像云服务器厂商几乎都会某个区域出现一次事故,Amaze云已经连续多年有次事情,异地灾备做的好当然不受影响;一旦异地灾备没做直接的后果就是数据丢失,这种事情相信老师自己同样听到同行提及或者转载过。 故而这道题目的现实场景非常重要:Redis的异地多副本做了-可能不会;多副本没做且硬件刚好因为掉电导致出现了无法恢复的损坏-肯定丢失。谢谢老师的分享。

    作者回复: 没关系哈,互相探讨的过程希望大家不要拘谨。正如你所说,redis在执行lua脚本过程中如果发生掉电,是可能会导致两个未读不一致的,因为lua脚本在redis中的执行只能保证多条命令会原子执行,整体执行完成才会同步给从库并写入aof,所以如果执行过程中掉电,会直接导致被中断的后面部分的脚本得不到执行。当然, 实际情况中这种概率非常小。作为兜底的方案,可以在未读变更时如果会话比较少,可以获取一次全量的会话未读来覆盖总未读,从而有机会能得到最终一致。

    2019-09-11
    10
  • 原子化嵌入式脚本有例子介绍吗

    作者回复: 示例挺多的哈,给一个redis官网的例子: local current current = redis.call("incr",KEYS[1]) if tonumber(current) == 1 then redis.call("expire",KEYS[1],1) end

    2019-09-11
    8
  • romantic_PK
    老师你好,我想请教一个问题,如何实现微信打开聊天窗口后,点击未读数定位到第一条未读消息的位置,请指点迷津,谢谢。

    作者回复: 这个应该是客户端逻辑哈,点击未读数实际上是把最新的一条消息id带进去了,端上在已有的消息里查询这条消息就可以了。这也是为什么最近联系人需要带上最新的一条消息了。

    2019-09-11
    5
    2
  • null
    老师您好,有几个问题,请教一下。 re:比如微博的消息栏总未读不仅包括即时消息相关的消息数,还包括其他一些业务通知的未读数,所以通过消息推送到达后的累加来计算总未读,并不是很准确,而是换了另外一种方式,通过轮询来同步总未读。 没太理解上面这一小段: 1. 为什么通过消息推送到达,(谁?)累加计算未读数,不是很准确?能举个例子么? 文章提到,服务端聚合所有会话未读数,得到总未读数,存在不准确的问题,如获取某个会话未读数失败时。 但是在客户端统计总未读数,这时客户端的会话未读数,不应该是准确的么,从而所统计的总未读数,也是准确的? 2. 为啥通过轮询来同步总未读是准确的?这个准确,是否需要一个前提:会话未读和总未读,在服务端单独维护?

    作者回复: 1. 其实就是有一些纳入到总未读里的消息不一定会进行消息下推。 客户端统计总未读的情况如果是需要多终端同步或者离线消息下推采用推拉结合的,不一定会话会话就是全量的,这种情况计算总未读就会有误差。 2. 不需要这个前提,理论上只需要会话未读就可以保证准确,增加总未读是为了提升读取性能。

    2019-10-03
    1
  • 小祺
    首先,如果修改“会话未读数“和“总未读数”是放在一个数据库事务中肯定是可以保证原子性的,但是数据库没法满足高并发的需求,所以通常可以使用Redis来解决高并发问题,为了保证Redis多条命令的原子性老师给出了3个方案。 分布式锁:我认为分布式锁只能解决并发问题,因为第一条命令成功第二条命令失败的情况依然可能发生,怎么办呢?只能不断的重试第二条命令吗? watch机制:与分布式锁有想同的问题 lua脚本机制:确实是原子操作没有问题,但是由于redis主从异步同步,掉电时slave在没同步到最新数据的情况下提升为master,客户端就可能读到错误的未读数。有什么解决方案吗? 请老师分别解答一下

    作者回复: 分布式锁需要能拿到锁就能保证同一时间只有拿到锁的进程才行执行操作,因为会话未读和总未读变更是在一个进程里,所以理论上是可以保证原子性的。但如果像你所说,第二条加未读的命令一直执行失败还是会出现不一致的情况,这种情况一个是重试,另外就是回滚第一个操作。 lua脚本这个可以考虑在脚本中增加一些修复机制,比如会话数比较少的情况下聚合一次未读来覆盖总未读。

    2019-09-11
    1
  • 大魔王汪汪
    老师请教个问题,针对于高频修改场景,频繁的一个字段状态变更,为了解决一个操作一次请求的问题可以采用客户端缓存一段时间聊天记录,批量发送,或者服务端分区批量发送以减少网络io或者db压力,但是两者都存在因为crash造成消息丢失的问题,请问这种情况有什么比较好的解决吗🙏

    作者回复: buffer缓冲和强一致性本身就是两个比较对立的概念,所以要做到既能缓冲请求频率又保证强一致性是比较困难的。如果可以的话,尽量让不容易宕机的一方来进行buffer缓冲,比如:如果是客户端和服务端都能缓冲,那还是让服务端来缓冲可能比较可靠一些。

    2019-09-11
    1
  • 分清云淡
    p2p的方式可以用来同步消息么?

    作者回复: 对于点对点聊天,直接通过p2p的方式不经过服务端来收发消息是可行的,市面上较早就已经有类似的软件了。

    2019-12-03
  • 郑印
    这部分在我们的消息系统中设计的时候是使用Redis hash 来实现的 结构如下: UNREAD_${userId} messageId contactId 写入未读: hset UNREAD_${userId} messageId contactId 获取总的未读数 hlen UNREAD_${userId} 获取会话的未读数,取出所有的未读消息,然后在程序里进行过滤,类似下面的代码 getUnreadMessages(userId) .values() .stream() .filter(v -> v == contactId) .count(); 这样实现不用能够平衡两者的读取,也不用使用原子操作,目前已知的问题是当某个用户的未读数多一会,在获取会话的未读数时,会比较慢,但是获取会话未读不是高频操作,且这样的用户基本属于长时间不使用才会导致未读数堆积。 目前这样的方式,不知道有没什么考虑不足的?

    作者回复: 未读数这个实际上访问量不大的话实现会灵活很多,上面这种实现实际上得看消息ID是否需要用到,不需要的话不用存储消息ID,否则对存储是一种浪费;另外会话未读的获取并发大的时候hgetall性能也是一个问题。具体看业务上是否够用哈

    2019-11-01
收起评论
显示
设置
留言
31
收藏
沉浸
阅读
分享
手机端
快捷键
回顶部