07 | 分布式锁和原子性:你看到的未读消息提醒是真的吗?
07 | 分布式锁和原子性:你看到的未读消息提醒是真的吗?
讲述:袁武林
时长13:05大小11.98M
消息和未读不一致的原因
会话未读和总未读单独维护
未读数的一致性问题
保证未读更新的原子性
分布式锁
支持事务功能的资源
原子化嵌入脚本
小结
赞 6
提建议
精选留言(29)
- Darcy2019-09-28redis cluster集群模式lua脚本如果操作的两个key不在同一个节点,好像会报异常
作者回复: 是的,对于需要使用lua的数据需要确保两个key能hash到一个节点。
共 2 条评论19 - 王棕生2019-09-12对于老师本节讲述的未读数不一致的问题,我想是否可以通过下面的方法来解决: 1 用户的未读数是在用户离线的时候,其他用户给他发消息的时候产生的,所以,只需要维护用户会话未读数即可; 2 当用户登录的时候,发送一个消息到MQ,由MQ触发维护用户总的未读数的操作,即将用户所有的会话未读数相加后的数值放入总未读数字段中。 这样的设计的好处时,降低维护用户总未读数的压力,只在用户登录的时候进行维护即可,不用每次收到一条消息就维护一次。 然后用户在线期间,收到的消息的未读数由前端来进行维护,不用服务端进行操作了。展开
作者回复: 思路是好的哈,不过很多时候不仅仅是用户登录的时候才需要总未读,比如每来一条消息需要进行系统推送时,由于苹果的APNs不支持角标未读的累加,只能每次获取总未读带下去。另外,客户端维护总未读这个也需要考虑比如离线消息太多,需要推拉结合获取时,到达客户端的消息数不一定是真正的未读消息数。
14 - leslie2019-09-11Redis不是特别熟悉:其实老师今天的问题和另外一个问题有点类似;既然问题是"执行过程中掉电是否会出现问题"这个极端场景:那么我就用极端场景解释,老师看看是否有理或者可能啊。 我的答案是会:尤其是极端场景下会,虽然概念很小;其实老师今天的问题是李玥老师的消息队列课程中前几天的期中考题,"数据写入PageCache后未做刷盘,那种情况下数据会丢失“当时的答案就是断电。 其实老师在提掉电时:未提及一个前提条件;掉电后硬件是否正常?如果掉电后硬件损坏了呢,那么数据肯定就丢失了,线上最新的数据都没了,数据肯定就丢了。因为问题是极端场景,回答就只能是极端场景,希望老师不介意;这就像云服务器厂商几乎都会某个区域出现一次事故,Amaze云已经连续多年有次事情,异地灾备做的好当然不受影响;一旦异地灾备没做直接的后果就是数据丢失,这种事情相信老师自己同样听到同行提及或者转载过。 故而这道题目的现实场景非常重要:Redis的异地多副本做了-可能不会;多副本没做且硬件刚好因为掉电导致出现了无法恢复的损坏-肯定丢失。谢谢老师的分享。展开
作者回复: 没关系哈,互相探讨的过程希望大家不要拘谨。正如你所说,redis在执行lua脚本过程中如果发生掉电,是可能会导致两个未读不一致的,因为lua脚本在redis中的执行只能保证多条命令会原子执行,整体执行完成才会同步给从库并写入aof,所以如果执行过程中掉电,会直接导致被中断的后面部分的脚本得不到执行。当然, 实际情况中这种概率非常小。作为兜底的方案,可以在未读变更时如果会话比较少,可以获取一次全量的会话未读来覆盖总未读,从而有机会能得到最终一致。
9 - 钢2019-09-11原子化嵌入式脚本有例子介绍吗
作者回复: 示例挺多的哈,给一个redis官网的例子: local current current = redis.call("incr",KEYS[1]) if tonumber(current) == 1 then redis.call("expire",KEYS[1],1) end
8 - romantic_PK2019-09-11老师你好,我想请教一个问题,如何实现微信打开聊天窗口后,点击未读数定位到第一条未读消息的位置,请指点迷津,谢谢。
作者回复: 这个应该是客户端逻辑哈,点击未读数实际上是把最新的一条消息id带进去了,端上在已有的消息里查询这条消息就可以了。这也是为什么最近联系人需要带上最新的一条消息了。
共 4 条评论2 - null2019-10-03老师您好,有几个问题,请教一下。 re:比如微博的消息栏总未读不仅包括即时消息相关的消息数,还包括其他一些业务通知的未读数,所以通过消息推送到达后的累加来计算总未读,并不是很准确,而是换了另外一种方式,通过轮询来同步总未读。 没太理解上面这一小段: 1. 为什么通过消息推送到达,(谁?)累加计算未读数,不是很准确?能举个例子么? 文章提到,服务端聚合所有会话未读数,得到总未读数,存在不准确的问题,如获取某个会话未读数失败时。 但是在客户端统计总未读数,这时客户端的会话未读数,不应该是准确的么,从而所统计的总未读数,也是准确的? 2. 为啥通过轮询来同步总未读是准确的?这个准确,是否需要一个前提:会话未读和总未读,在服务端单独维护?展开
作者回复: 1. 其实就是有一些纳入到总未读里的消息不一定会进行消息下推。 客户端统计总未读的情况如果是需要多终端同步或者离线消息下推采用推拉结合的,不一定会话会话就是全量的,这种情况计算总未读就会有误差。 2. 不需要这个前提,理论上只需要会话未读就可以保证准确,增加总未读是为了提升读取性能。
1 - 小祺2019-09-11首先,如果修改“会话未读数“和“总未读数”是放在一个数据库事务中肯定是可以保证原子性的,但是数据库没法满足高并发的需求,所以通常可以使用Redis来解决高并发问题,为了保证Redis多条命令的原子性老师给出了3个方案。 分布式锁:我认为分布式锁只能解决并发问题,因为第一条命令成功第二条命令失败的情况依然可能发生,怎么办呢?只能不断的重试第二条命令吗? watch机制:与分布式锁有想同的问题 lua脚本机制:确实是原子操作没有问题,但是由于redis主从异步同步,掉电时slave在没同步到最新数据的情况下提升为master,客户端就可能读到错误的未读数。有什么解决方案吗? 请老师分别解答一下展开
作者回复: 分布式锁需要能拿到锁就能保证同一时间只有拿到锁的进程才行执行操作,因为会话未读和总未读变更是在一个进程里,所以理论上是可以保证原子性的。但如果像你所说,第二条加未读的命令一直执行失败还是会出现不一致的情况,这种情况一个是重试,另外就是回滚第一个操作。 lua脚本这个可以考虑在脚本中增加一些修复机制,比如会话数比较少的情况下聚合一次未读来覆盖总未读。
1 - 大魔王汪汪2019-09-11老师请教个问题,针对于高频修改场景,频繁的一个字段状态变更,为了解决一个操作一次请求的问题可以采用客户端缓存一段时间聊天记录,批量发送,或者服务端分区批量发送以减少网络io或者db压力,但是两者都存在因为crash造成消息丢失的问题,请问这种情况有什么比较好的解决吗🙏
作者回复: buffer缓冲和强一致性本身就是两个比较对立的概念,所以要做到既能缓冲请求频率又保证强一致性是比较困难的。如果可以的话,尽量让不容易宕机的一方来进行buffer缓冲,比如:如果是客户端和服务端都能缓冲,那还是让服务端来缓冲可能比较可靠一些。
1 - 听水的湖2022-11-11 来自北京请教老师,MC的缩写是什么意思呢?MemCached么?
- 薛建辉2022-07-19关于三种原子化方案,请教下老师,主流大厂是使用哪种?谢谢。
- Geek_LeonSZ2021-11-05麻烦问一下未读数是怎么存储的? 第一节课讲了三个表, 好像之后数据是怎么存储的, 存在database, 还是in-memory cache, 等等的, 就没有再涉及到了.
- 滩涂曳尾2021-01-16请问老师,未读数为啥不在app本地保存呢,而是要服务端维护?我能想到的一个场景是,服务端维护可以支持多终端登录后知道用户读取的状态,以及离线推送啥的,可以详细说说有哪些场景吗
- 慎独明强2020-07-10既然不能保证事务的强一致性,那么就保证事务的最终一致性
- Alice2020-05-25使用lua的数据需要确保两个key能hash到一个分片。那么如何将会话未读数和总未读数一起更新?会话未读数和总未读数没办法使用同一个category来保存吧共 1 条评论
- mickey2020-05-09在案例2中,为什么用户B查看了用户A的会话后,会将自己的总读数清零呢?
- piboye2020-05-07未读数最难的地方是在离线推送的时候,客户端的显示可以客户端维持。那么离线推送的时候,其实更怕计数器的误差累计,我的解决方案是,保存两个计数器,一个是客户端确认的未读数和确认到的消息id,一个是服务端从客户端的msgid到最近的msgid之间的新增未读数和消息id。
- 分清云淡2019-12-03p2p的方式可以用来同步消息么?
作者回复: 对于点对点聊天,直接通过p2p的方式不经过服务端来收发消息是可行的,市面上较早就已经有类似的软件了。
- 郑印2019-11-01这部分在我们的消息系统中设计的时候是使用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性能也是一个问题。具体看业务上是否够用哈
- GeekAmI2019-10-05老师您好,再请教下,假如一个用户的离线消息非常多比如说一万条,那么等用户上线的时候是全量同步到本地呢(类似微信)还是说只同步一部分消息剩下的等用户下拉的时候再懒加载比较老的消息呢(类似QQ)? 如果使用后者的话,当用户点击聊天对话框的未读数字时需要立即锚点到最老的那个未读消息,这个是怎么做到的呢
作者回复: 这个看场景吧,一般支持多终端消息同步的话建议采用推拉结合的方式,因为消息会在服务端存储,按会话维度拉取也比较方便。另外,第二个问题,一般点未读是直接展示最新的消息呀,不会跳到最旧的。
- 怡红公子2019-10-04老师我有疑问,会话未读和消息总未读由服务端存储的,那么在客户端收到离线推送过程之后,并没有在线查看相关消息内容,而在离线条件下查看相关会话,此时会话未读更新是不是仅仅是客户端的逻辑,不需要客户端发送信令到服务端?还是说,服务端只负责将离线消息成功推给客户端后,就将相关会话和总未读清零了?这样的话客户端和服务端是不是需要有各自的会话未读管理啊?而且两者不是在一个纬度上的。服务端的会话未读管理是离线过程中新消息条数,客户端的会话管理,是新消息是否查看?还请老师解答一下,自己想有点乱套了展开
作者回复: 服务端接收到查看会话的请求时,除了返回会话内容,还会在服务端进行未读清理。另外课程中说的离线消息下推是指用户由断连再重新上线后的过程,不是指用户网络离线。所以收到离线消息并不会清未读。