即时消息技术剖析与实战
袁武林
微博研发中心技术专家
立即订阅
6503 人已学习
课程目录
已完结 24 讲
0/4登录后,你可以任选4讲全文学习。
开篇词 (1讲)
开篇词 | 搞懂“实时交互”的IM技术,将会有什么新机遇?
免费
基础篇 (8讲)
01 | 架构与特性:一个完整的IM系统是怎样的?
02 | 消息收发架构:为你的App,加上实时通信功能
03 | 轮询与长连接:如何解决消息的实时到达问题?
04 | ACK机制:如何保证消息的可靠投递?
05 | 消息序号生成器:如何保证你的消息不会乱序?
06 | HttpDNS和TLS:你的消息聊天真的安全吗?
07 | 分布式锁和原子性:你看到的未读消息提醒是真的吗?
08 | 智能心跳机制:解决网络的不确定性
场景篇 (4讲)
09 | 分布式一致性:让你的消息支持多终端漫游
10 | 自动智能扩缩容:直播互动场景中峰值流量的应对
11 | 期中实战:动手写一个简易版的IM系统
12 | 服务高可用:保证核心链路稳定性的流控和熔断机制
进阶篇 (10讲)
13 | HTTP Tunnel:复杂网络下消息通道高可用设计的思考
14 | 分片上传:如何让你的图片、音视频消息发送得更快?
15 | CDN加速:如何让你的图片、视频、语音消息浏览播放不卡?
16 | APNs:聊一聊第三方系统级消息通道的事
17 | Cache:多级缓存架构在消息系统中的应用
18 | Docker容器化:说一说IM系统中模块水平扩展的实现
19 | 端到端Trace:消息收发链路的监控体系搭建
20 | 存储和并发:万人群聊系统设计中的几个难点
21 | 期末实战:为你的简约版IM系统,加上功能
22 | 答疑解惑:不同即时消息场景下架构实现上的异同
结束语 (1讲)
结束语 | 真正的高贵,不是优于别人,而是优于过去的自己
即时消息技术剖析与实战
登录|注册

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

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

消息和未读不一致的原因

那么在即时消息场景中,究竟会有哪些情况导致消息和未读数出现“不一致”的情况呢?要搞清楚这个问题,我们要先了解两个涉及未读数的概念:“总未读”与“会话未读”。我们分别来看看以下两个概念。
取消
完成
0/1000字
划线
笔记
复制
© 版权归极客邦科技所有,未经许可不得传播售卖。 页面已增加防盗追踪,如有侵权极客邦将依法追究其法律责任。
该试读文章来自付费专栏《即时消息技术剖析与实战》,如需阅读全部文章,
请订阅文章所属专栏。
立即订阅
登录 后留言

精选留言(21)

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

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

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

    作者回复: 示例挺多的哈,给一个redis官网的例子:

    local current
    current = redis.call("incr",KEYS[1])
    if tonumber(current) == 1 then
        redis.call("expire",KEYS[1],1)
    end

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

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

    2019-09-11
    3
  • null
    老师您好,有几个问题,请教一下。

    re:比如微博的消息栏总未读不仅包括即时消息相关的消息数,还包括其他一些业务通知的未读数,所以通过消息推送到达后的累加来计算总未读,并不是很准确,而是换了另外一种方式,通过轮询来同步总未读。

    没太理解上面这一小段:
    1. 为什么通过消息推送到达,(谁?)累加计算未读数,不是很准确?能举个例子么?

    文章提到,服务端聚合所有会话未读数,得到总未读数,存在不准确的问题,如获取某个会话未读数失败时。

    但是在客户端统计总未读数,这时客户端的会话未读数,不应该是准确的么,从而所统计的总未读数,也是准确的?

    2. 为啥通过轮询来同步总未读是准确的?这个准确,是否需要一个前提:会话未读和总未读,在服务端单独维护?

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

    2019-10-03
    1
  • Darcy
    redis cluster集群模式lua脚本如果操作的两个key不在同一个节点,好像会报异常

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

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

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

    2019-09-11
    2
    1
  • A:春哥大魔王
    老师请教个问题,针对于高频修改场景,频繁的一个字段状态变更,为了解决一个操作一次请求的问题可以采用客户端缓存一段时间聊天记录,批量发送,或者服务端分区批量发送以减少网络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
  • GeekAmI
    老师您好,再请教下,假如一个用户的离线消息非常多比如说一万条,那么等用户上线的时候是全量同步到本地呢(类似微信)还是说只同步一部分消息剩下的等用户下拉的时候再懒加载比较老的消息呢(类似QQ)? 如果使用后者的话,当用户点击聊天对话框的未读数字时需要立即锚点到最老的那个未读消息,这个是怎么做到的呢

    作者回复: 这个看场景吧,一般支持多终端消息同步的话建议采用推拉结合的方式,因为消息会在服务端存储,按会话维度拉取也比较方便。另外,第二个问题,一般点未读是直接展示最新的消息呀,不会跳到最旧的。

    2019-10-05
  • 怡红公子
    老师我有疑问,会话未读和消息总未读由服务端存储的,那么在客户端收到离线推送过程之后,并没有在线查看相关消息内容,而在离线条件下查看相关会话,此时会话未读更新是不是仅仅是客户端的逻辑,不需要客户端发送信令到服务端?还是说,服务端只负责将离线消息成功推给客户端后,就将相关会话和总未读清零了?这样的话客户端和服务端是不是需要有各自的会话未读管理啊?而且两者不是在一个纬度上的。服务端的会话未读管理是离线过程中新消息条数,客户端的会话管理,是新消息是否查看?还请老师解答一下,自己想有点乱套了

    作者回复: 服务端接收到查看会话的请求时,除了返回会话内容,还会在服务端进行未读清理。另外课程中说的离线消息下推是指用户由断连再重新上线后的过程,不是指用户网络离线。所以收到离线消息并不会清未读。

    2019-10-04
  • GeekAmI
    老师您好,请问下会话里的未读数怎么同步给客户端的?

    作者回复: 跟着消息带下去,或者建连后从服务端同步一次然后端上通过接收的消息进行本地累加。

    2019-10-03
  • 盘尼西林
    redis 的multi 多条操作,前面几条操作成功,最后一条失败,无法做已经执行成功语句的 回滚。比如
    127.0.0.1:6379> get locked
    "1"
    127.0.0.1:6379> watch locked
    OK
    127.0.0.1:6379> multi
    OK
    127.0.0.1:6379> incr locked
    QUEUED
    127.0.0.1:6379> lpop list_key
    QUEUED
    127.0.0.1:6379> incr locked
    QUEUED
    127.0.0.1:6379> exec
    1) (integer) 2
    2) (nil)
    3) (integer) 3
    127.0.0.1:6379> get locked
    "3"
    上面的语句 incr 会执行成功,但是lpop 是失败的

    作者回复: 是的,redis的事务实际上需要使用方自行处理失败的后续操作。

    2019-10-01
  • 天天向上
    请问,redis嵌入的lua脚本都是原子化执行的吗?
    还是有特殊的声明,或则什么语法结构?

    作者回复: 是的,redis嵌入的lua脚本是会原子化执行的。具体使用可以参考redis官网示例。

    2019-10-01
  • 天天平安
    我想在自己的app中直接引用收费的聊天产品,老师推荐用哪个比较好?
    2019-09-25
  • qijj
    老师,群聊的未读消息业务应该如何设计,在数据库表设计时建立一个关系表,一条消息和群聊中的每个人的对应关系都存入这表,是不是效率低呢,如果群里有500人,这个关系表里就会插入500条记录,很快这个表中的数据量就会很大很大了,是不是会严重影响系统的性能?应该如何设计能够提高系统的效率。请帮忙解答下,这个问题困扰我很久了,谢谢

    作者回复: 对于群聊业务来说,由于消息扇出问题,一般处理上和点对点聊天不太一样。群聊消息一般采用”读扩散“的方式,一个群的消息只会记录一份,这个群里的每个用户在需要查看数据的时候都从会获取这个群的消息记录。未读数是每一个用户单独一份的,和消息不一样。

    2019-09-19
  • 墙角儿的花
    老师 对于im服务器集群,客户端的socket均布在各个服务器,目标socket不在同一个服务器上时,服务器间需要转发消息,这个场景需要低延迟无需持久化,服务器间用redis的发布订阅,因其走内存较快,即使断电还可以走库。im服务器和入库服务间用其他mq解耦,因为这个环节需要持久化,所以选rocketmq或kafka,但kafka会延迟批量发布消息 所以选rocketmq,这两个环节的mq选型可行吗。

    作者回复: 可以的,长连接入层和后面的业务层之间可以通过redis的pub sub来降低消息延迟,消息发送的api层和具体的持久化层出于成本考虑可以通过其他非内存型来实现,kafka由于是顺序的读写,写入和读取的性能有系统的PageCache来加速,所以性能上不会差。不知道你这里说的延迟批量发布消息具体是什么原因呢?

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

    作者回复: 分布式锁需要能拿到锁就能保证同一时间只有拿到锁的进程才行执行操作,因为会话未读和总未读变更是在一个进程里,所以理论上是可以保证原子性的。但如果像你所说,第二条加未读的命令一直执行失败还是会出现不一致的情况,这种情况一个是重试,另外就是回滚第一个操作。

    lua脚本这个可以考虑在脚本中增加一些修复机制,比如会话数比较少的情况下聚合一次未读来覆盖总未读。

    2019-09-11
  • 卫江
    首先,我认为redis的脚本化提供了类似于事务的功能,只是功能上面更强大,也更便捷。但是同redis的事务一样,对于事务的ACID支持并不完善。老师提的问题,执行过程中掉电,首先这个的执行事务肯定是失败的,即使开启持久化也没有办法修复,同时客户端也会收到断线回复,所以,就可以当做失败处理,而针对于失败,业务可以通过重试来进行容错,但是感觉这里需要特别的设计,比如针对于某个玩家的消息未读等信息的更新和读取需要一直依赖某一条连接,这样才能保证针对于该玩家的消息的顺序性,不知道想的对不对?

    作者回复: 是的,redis对于脚本执行并没有做到真正的事务性,lua脚本在redis中的执行只能保证多条命令会原子执行,整体执行完成才会同步给从库并写入aof,所以如果执行过程中掉电,会直接导致被中断的后面部分的脚本得不到执行。lua脚本中可以增加一些修复机制,比如会话比较少的话就聚合一次会话来覆盖总未读。

    2019-09-11
  • bbpatience
    掉电会出现问题,在redis做主从拷贝时,锁信息有可能正好没有同步到从,这些从在切为主时,没有锁信息。可以用zk来解决分布式锁问题,它能保证掉电后再选举成功的节点,一定包含锁信息

    作者回复: 好像和提的问题不大对的上哈,redis对于脚本的执行问题在于并不能保证执行过程掉电后从库或者aof能够感知到来用于恢复。

    2019-09-11
收起评论
21
返回
顶部