Loading [MathJax]/jax/output/CommonHTML/config.js
前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
社区首页 >专栏 >使用消息中间件时,如何保证消息仅仅被消费一次?

使用消息中间件时,如何保证消息仅仅被消费一次?

原创
作者头像
平头哥的技术博文
修改于 2020-03-12 02:23:36
修改于 2020-03-12 02:23:36
9920
举报

消息中间件使用广泛,常用来削峰填谷、系统解耦、异步处理。异步处理可能是使用的最多的场景了,比如现在的技术博客网站,都采用积分制,用户发表一篇文章后,可以获取想要的积分,为了提升系统的性能,给用户加积分的操作可以异步处理,并不需要放在同步流程中。

我们可以把用户ID,需要增加的积分封装成一条消息投递到消息系统中,异步处理加积分操作,由于这是发生在不同服务器之间,消息有可能投递失败、处理失败等问题,从而导致用户加积分失败,还有一种可能是消息重复投递,那么用户就有可能重复加积分,不管出现那种情况,都是不正常的情况。

要避免上面的两种情况,就需要我们尽量保证消息不丢失和消息只被消费一次,这篇文章抛开具体的消息中间件,从消息系统的通用层面上,谈谈如何避免这两种情况。

1、保证消息不丢失

一条消息从生产到消费这条链路中,有三个地方可能会造成消息丢失,分别如下:

  • 消息从生产者写入到消息队列的过程投递失败。
  • 消息在消息队列中,持久化失败。
  • 消息被消费者消费的过程出现异常。

1.1 在消息生产的过程中投递失败

消息生产者和消息系统一般都是独立部署在不同的服务器上,两台服务器之间要通信就要通过网络来完成,网络是不稳定,可能会发生抖动,那么数据就有可能丢失。网络发生抖动会有以下两种情况。

在消息生产的过程中丢失消息
在消息生产的过程中丢失消息

情景一:消息在传送给消息系统的过程中发生网络抖动,数据直接丢失。

情景二:消息已经到达消息系统,但是在消息系统给生产者服务器返回信息时,网络发生抖动,此时的数据不一定真正的丢失,很可能只是生产者认为数据丢失

针对消息在消息生产时丢失,可以采取重投机制,当程序检测到网络异常时,将消息再次投递到消息系统。但是重新投递在情景二情况下,可能造成数据重复,如何解决这个问题,在后面会提到。

1.2 在消息队列中持久化失败

消息系统是可以对消息进行持久化,一般都是将消息存储到本地磁盘中,当然也有少数消息中间件支持将数据持久化到数据库中,那么消息系统的性能可能就会下降。

如果你对 Redis 的持久化有一定的了解话,你会发现 Redis 在持久化数据时并不是每新增一条就立即存入到本地磁盘,而是会将数据先写入到操作系统的 Page Cache 中,当满足一定条件时,再将 Page Cache 中的数据刷入磁盘,因为这样可以减少对磁盘的随机 I/O 操作,我们知道随机 I/O 是非常耗时的,这样也提高了系统性能,消息中间件也不例外,在持久化时也是采用这种方式。

在某些极端情况下,可能会造成 Page Cache 中的数据丢失,比如突然停电或者机器异常重启操作。要解决 Page Cache 中数据丢失问题,可以采用集群部署的方式,来尽量保证数据不丢失。

1.3 在消费的过程中存在消息丢失

消息在消费过程中也是会发生丢失的,而且在消费过程中丢失的概率要比前两种情况大很多。一条消息消费过程大概分成三步:消费者拉取消息,消费者处理消息,消息系统更新消费进度。

图片描述
图片描述

第一步在拉取消息的时候可能发生网络抖动异常,第二步在处理消息的时候可能发生一些业务异常,而导致流程并没有走完,如果在第一步、第二步发生异常的情况下,通知消息系统更新消费进度,那么这条失败的消息就永远不会在被处理了,自然就丢失了,其实我们的业务并没有跑完。

要避免消息在消费时丢失的情况,可以在消息接收和处理完成之后才更新消费进度,但是在极端的情况下,会出现消息重复消费的问题,比如某一条消息在处理完成之后,消费者宕机了,这时还没有更新消费进度,消费者重启后,这条消息还是会被消费到。

2、如何保证消息只被消费一次

消息系统本身不能保证消息仅被消费一次,因为消费本身可能重复、下游系统启动拉取重复、失败重试带来的重复、补偿逻辑导致的重复都有可能造重复消息,要保证消息仅被消费一次可以利用等幂性来实现

等幂是数学上的一个概念,就是多次执行同一个操作和执行一次操作,最终得到的结果是相同的。

从等幂的概念上就可以看出来,就算消息执行多次也不会对系统造成影响,那么在使用消息系统时如何保证等幂性呢?因为生产者和消费者都有可能产生重复消息,所以要在生产者和消费者两端都保证等幂性。

保证生产者等幂性,在生产消息的时候,利用雪花算法给消息生成一个全局 ID,在消息系统中维护消息已 ID 映射关系,如果在映射表中已经存在相同 ID,这丢弃这条消息,虽然消息被投递了两次,但是实际上就保存了一条,避免了消息重复问题。

生产者等幂性跟所选者的消息中间件有关系,因为绝大数情况下消息系统不需要我们自己实现,所以等幂性是不太好控制的,消费者等幂性才是我们开发人员控制的重点方向

在消费者端可以从通用层和业务层两个方面来做等幂操作,取决于我们的业务要求。

在通用层面中,利用好消息生成是产生的全局唯一ID,消息被处理成功后,把这个全局 ID 存入到数据中,在处理下一条消息之前,先从数据库中查询这个全局 ID 是否存在,如果已经存在,则直接放弃该消息。

利用这个全局唯一ID就实现了消息等幂性,伪代码如下:

代码语言:txt
AI代码解释
复制
boolean isIDExisted = selectByID(ID); // 判断ID是否存在
if(isIDExisted) {
  return; //存在则直接返回
} else {
  process(message); //不存在,则处理消息
  saveID(ID);   //存储ID
}

但是在极端情况下,这种方式还是会出问题,如果消息在处理之后,还没来得及保存到数据库,消费者就宕机重启了,重启之后还会再次获取该消息,执行时查询该消息并未被消费过,还是会执行两次消费。可以引入数据库事务来解决这个问题,但是会降低系统性能。如果对消息重复消费没有特别严格要求的话,直接使用这种没有引入事务的通用方案就好了,毕竟这也是极小概率的事情。

在业务层面上,我们可选择性就变多了,比如乐观锁、悲观锁、内存去重(https://github.com/RoaringBitmap/RoaringBitmap)等方法。

我们拿乐观锁来举例,比如我们要给一个用户加积分,因为加积分操作并不需要放在主业务中,所以就可以使用消息系统来异步通知,要使用乐观锁,就需要给积分表添加一个版本号字段。并且在生产消息的时候先查询这个账号的版本号并且连同消息一起发送到消息系统中。

图片描述
图片描述

消费者拿到消息和版本号后,在执行更新积分操作的 SQL 时带上版本号,类似于:

代码语言:txt
AI代码解释
复制
update score set score = score + 20, version=version+1 where userId=1 and version=1;

这条消息消费成功后,version 就变成了 2,那么如果有重复的 version=1 的消息再次被消费者拉取到,SQL 语句并不会执行成功,从而保证了消息的幂等性。

要保证消息仅被消费一次,我们需要把重点放在消费者这一段,利用等幂性来保证消息被消费一次。

今天站在消息中间件的通用层面上,聊了聊如何保证数据不丢失和仅被消费一次,希望今天的文章对您的学习或者工作有所帮助,如果您认为文章有价值,欢迎点个赞,谢谢。

最后

目前互联网上很多大佬都有消息中间件相关文章,如有雷同,请多多包涵了。原创不易,码字不易,还希望大家多多支持。若文中有所错误之处,还望提出,谢谢。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
暂无评论
推荐阅读
编辑精选文章
换一批
在Linux中检查当前运行级别的五种方法?
运行级就是Linux操作系统当前正在运行的功能级别。存在七个运行级别,编号从0到6。系统可以引导到任何给定的运行级别。运行级别由数字标识。
用户8710643
2021/06/08
1.9K0
CentOS下如何更改默认的启动方式
版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/u011415782/article/details/78708355
泥豆芽儿 MT
2018/09/11
1.9K0
CentOS下如何更改默认的启动方式
正确理解Linux运行级别那点事儿
您可以将Linux运行级别视为操作系统运行的不同“模式”。每一种模式或运行级别都有自己的进程和服务列表,这些进程和服务要么被打开,要么被关闭。
用户6543014
2019/12/17
2.3K0
正确理解Linux运行级别那点事儿
第十六章.Linux系统管理-开机启动流程
-多年互联网运维工作经验,曾负责过大规模集群架构自动化运维管理工作。 -擅长Web集群架构与自动化运维,曾负责国内某大型金融公司运维工作。 -devops项目经理兼DBA。 -开发过一套自动化运维平台(功能如下): 1)整合了各个公有云API,自主创建云主机。 2)ELK自动化收集日志功能。 3)Saltstack自动化运维统一配置管理工具。 4)Git、Jenkins自动化代码上线及自动化测试平台。 5)堡垒机,连接Linux、Windows平台及日志审计。 6)SQL执行及审批流程。 7)慢查询日志分析web界面。
DriverZeng
2022/09/26
2.5K0
第十六章.Linux系统管理-开机启动流程
Linux之运行级别指令
文章目录 1. 指定运行级别 1.1 基本介绍 1.2 应用实例 1.3 CentOS7后运行级别说明 1. 指定运行级别 1.1 基本介绍 运行级别说明: 0 :关机 1 :单用户【找回丢失密码】 2:多用户状态没有网络服务 3:多用户状态有网络服务 4:系统未使用保留给用户 5:图形界面 6:系统重启 常用运行级别是3和5 ,也可以指定默认运行级别. 1.2 应用实例 命令: init [0123456]应用案例:通过init来切换不同的运行级别,比如动5-3,然后关机。 1.3 Cent
兮动人
2021/06/11
3.2K0
Linux之运行级别指令
Centos8种如何更改运行级别
如何在Systemd中查看当前targets(运行级别) 系统启动时,默认情况下,systemd激活default.target单元。它的主要工作是通过依赖关系来激活服务和其他单元。要查看默认目标,输入下面的 :
用户1685462
2021/09/05
9760
Linux系统安全-Linux启动流程和服务管理(init和systemd)
GRUB2相较于GRUB一代的提升:更健壮、可移植、更强大。支持BIOS、EFI和OpenFirmware,支持GPT和MBR分区表。支持非Linux系统,如苹果HFS文件系统和Windows的NTFS文件系统。
谢公子
2022/01/19
2.3K0
Linux系统安全-Linux启动流程和服务管理(init和systemd)
011.Linux目录结构以及重要系统文件
作用:DNS(Domain Name System)配置文件,DNS主要负责将网站域名解析为对应的IP地址,从域名到IP的解析过程,称作A记录,即Address Record
CoderJed
2020/05/09
1K0
011.Linux目录结构以及重要系统文件
Linux学习笔记之Centos7安装GNOME桌面环境
最小化安装Centos7,系统默认是命令行界面,如果像我一样有特殊需求,这时就需要我们手动来安装用户图形界面了。 
Jetpropelledsnake21
2018/12/06
9.5K2
Linux之service命令基本使用
文章目录 服务(service)管理 1. 介绍 2. service 管理指令 3. service 管理指令案例 4. 查看服务名的方式 5. 服务的运行级别(runlevel): 6. Cen
兮动人
2021/06/11
1K0
Linux之service命令基本使用
centos 中安装与使用genome
3.在centos7里面的默认运行级别可以查看到有两种:multi-user.target和graphical.target。
Fanssi
2020/07/01
7310
Linux之service命令基本使用
1) service 服务名 [start | stop | restart | reload | status]
兮动人
2021/05/31
1.4K0
Linux之service命令基本使用
CentOS7下Systemctl详解
Systemd是由红帽公司的一名叫做Lennart Poettering的员工开发,systemd是Linux系统中最新的初始化系统(init),它主要的设计目的是克服Sys V 固有的缺点,提高系统的启动速度,systemd和upstart是竞争对手,ubantu上使用的是upstart的启动方式,centos7上使用systemd替换了Sys V,Systemd目录是要取代Unix时代依赖一直在使用的init系统,兼容SysV和LSB的启动脚本,而且能够在进程启动中更有效地引导加载服务。 system:系统启动和服务器守护进程管理器,负责在系统启动或运行时,激活系统资源,服务器进程和其他进程,根据管理,字母d是守护进程(daemon)的缩写,systemd这个名字的含义就是它要守护整个系统。
用户5807183
2019/08/02
1.4K0
centos systemctl_正在不使用中
CentOS 7.x开始,CentOS开始使用systemd服务来代替daemon,原来管理系统启动和管理系统服务的相关命令全部由systemctl命令来代替。
全栈程序员站长
2022/09/30
3470
linux selenium chrome 加载用户配置文件
需要安装linux桌面环境(系统版本: CentOS Linux release 7.6.1810 (Core))
py3study
2020/05/14
4K0
Systemd 入门教程:命令篇
Systemd 是 Linux 系统工具,用来启动守护进程,已成为大多数发行版的标准配置。
咻一咻
2020/05/29
1.1K0
linux基础命令介绍十三:启动流程
固件(firmware)是指设备最底层的,让设备得以运行的程序代码。简单理解就是:固定在硬件上的软件。计算机中的许多设备都拥有固件(如硬盘、鼠标、光驱、U盘等),在计算机启动过程中,最先读取的就是位于主板上的固件,这个固件当前有两种类型:传统的BIOS和新的通用性更强的UEFI。
用户5030870
2019/04/11
2.1K0
CentOS 8启动流程
​Basic Input Output System的缩写,翻译过来就是“基本输入输出系统”,是一种业界标准的固件接口,第一次出现在1975年,是计算机启动时加载的第一个程序,主要功能是检测和设置计算机硬件,引导系统启动。
星哥玩云
2022/09/15
2.3K0
CentOS 8启动流程
KVM基础7
现在可以像一台普通服务器一样进行使用了 后面的篇章中再对虚拟机的管理进行详细演示 ---- 修改运行级别 查看当前运行级别 [root@docker ~]# cat /etc/inittab # inittab is no longer used when using systemd. # # ADDING CONFIGURATION HERE WILL HAVE NO EFFECT ON YOUR SYSTEM. # # Ctrl-Alt-Delete is handled by /usr/lib/sy
franket
2022/01/19
3470
centos 7部署图形化和VNC详解
[root@localhost ~]# systemctl get-default
乡村小男孩
2020/01/27
2.4K1
centos 7部署图形化和VNC详解
相关推荐
在Linux中检查当前运行级别的五种方法?
更多 >
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档
查看详情【社区公告】 技术创作特训营有奖征文