队列顺序性引发的思考
队列的顺序性
小贴士:我们这里讨论的是多进程单队列这种数据类型,顺序性主要是针对消费者消费数据一方。
前言
在我们日常的开发中,经常会使用队列来处理一些非及时性业务,队列具有解耦、异步、削峰等特性,但是同步也会业务要满足幂等性、顺序性等要求。对于单进程来消费队列的数据是不存在顺序性的问题的,因为程序是串联执行的。但是我们为了加快队列的消费速度一般都是会使用多进程来消费(比如说 Laravel 的队列)。
多进程消费问题
有这样一个应用场景,redis 队列中有 100 条数据。我们为了提供消费数据的速度以多进程的形式去消费队列的数据,然后写入文件中(假设消费很费力哦),我们简单使用代码来模拟一下整个过程。
往队列中写入数据
// 写入数据 $redis = new Redis(); $redis->connect('127.0.0.1', 6379); for($i=3;$i<100;$i++) { $redis->Rpush('we_queue','data is '. $i); }
消费者消费数据脚本
// 消费数据 $redis = new Redis(); $redis->connect('127.0.0.1', 6379); $pid = getmypid(); while(1) { if ($data = $redis->Lpop('we_queue')) { file_put_contents('1.log', "[$pid]".$data. PHP_EOL, 8); } }
supervisor启动四个进程消费
# 队列进程 [program:helloword] process_name=%(program_name)s_%(process_num)02d command=php 1.php numprocs=4 directory=/data/cmd
代码执行完成我们来看一下最后输出到
1.log
的文件内容(数据是从 3 开始的哦)#cat /data/cmd/1.log [1465]data is3 [1463]data is4 [1465]data is5 [1463]data is7 [1465]data is8 [1464]data is6 [1466]data is9 [1463]data is10 [1465]data is11 [1464]data is12 [1463]data is14 [1465]data is15 [1464]data is16 [1466]data is17 [1465]data is18 [1463]data is19 [1466]data is21 [1464]data is20 [1465]data is22 [1463]data is23 [1466]data is24 [1463]data is27 [1465]data is26 [1464]data is25 [1466]data is29 [1464]data is31 [1465]data is30 [1464]data is34 [1463]data is32 [1465]data is35 [1466]data is33 [1464]data is36 [1463]data is37 [1465]data is38 [1466]data is39 [1464]data is40 [1465]data is42 [1464]data is44 [1463]data is41 [1466]data is43 [1465]data is45 [1466]data is48 [1463]data is47 [1464]data is46 [1463]data is51 [1465]data is49 [1466]data is50 [1463]data is53 [1465]data is55 [1466]data is54 [1464]data is56 [1465]data is58 [1464]data is60 [1466]data is59 [1463]data is57 [1465]data is61 [1464]data is62 [1463]data is64 [1466]data is65 [1463]data is68 [1464]data is67 [1463]data is71 [1465]data is70 [1464]data is72 [1466]data is69 [1463]data is73 [1466]data is76 [1465]data is74 [1464]data is75 [1463]data is77 [1466]data is78 [1464]data is80 [1465]data is79 [1464]data is83 [1466]data is82 [1463]data is81 [1465]data is84 [1464]data is85 [1463]data is87 [1465]data is88 [1464]data is89 [1466]data is86 [1465]data is91 [1464]data is92 [1463]data is90 [1466]data is93 [1463]data is96 [1465]data is94 [1464]data is95 [1466]data is97 [1463]data is98 [1465]data is99
可以简单的看一下上面输出的文件,和我们想象的文件内容不一样,主要有以下几个现象:
数据并不是按照我们想的那样是按照顺序输出的 3-99。
这就是队列顺序性的表现,并没有按我们所预想的一样,究其原因就是消费者消费数据的业务处理的时候并没有保证顺序性,入上面的例子所示,也就是我们写入文件并没有顺序性执行
进程并不是想我们想的那样4个进程应该平均拿到前4条数据。
根本原因是由于我们的测试数据只有100条。每个进程都有不确定的因素导致并没有真正的四个进程都启动了(比如说网络连接等),当我们把数据测试到 100000 的时候时候,数据基本上都是每一个进程都能获取到(这不是我们今天的主角,大家知道就行了)。
以上我们可知,要想满足顺序性多进程消费,必须消费者的业务处理来保证顺序性。可是我们也知道业务处理许多不确认的因素(有网络请求等等),没办法保证强顺序性。
强顺序性
前面我们说了,业务的不确定性因素无法保证强顺序性,走的远了我们不能忘了我们为了什么出发,前面我们说了单进程消费数据的时候没有顺序性问题,为了提供队列消费数据的速度使用多进程引来了顺序性的问题。换句话说就是我们可以对需要顺序性执行的数据使用一个进程来消费。
上面这句话大家可能会有些不懂,我们以如下图的应用场景描述,我们队列中有如下的 6 条数据,id
代表的商品主键,action
代表了对商品的操作。我们有 4 个进程去消费数据。这样就像我们前面所说的那样,无法保证顺序性,也就是说对于 id=1
的商品来说有可能会出现先 delete
在 update
,好像这样没问题问题,反正最终都是删除了。但是对于 id=2
的商品可没有那么幸运了,它不能出现先update
在 add
。
舞台已经布置好了,我们来具体描述一下 需要顺序性执行的数据使用一个进程来消费 的含义。我们只需要是的 id=1
的商品在同一个进程执行,id=2
的商品在同一个进程执行,依此类推就好了。对于这个应用场景来说相同 id 的消息必须顺序执行。是的,我们只需要把 id 相同的消息分发到一个进程就好了。这样我们就能满足强顺序性的要求。
我们看一下修改的图,我们加入了一个中间层它的职责就是需要顺序性执行的数据使用一个进程来消费。这样我们就达到了顺序性也能使用多进程来加快去消费数据。也无需去关心业务的那些可变因素导致无法满足顺序性。
项目应用
对于我们平常使用的 Laravel 队列来说,我们与第三方通过消息中间件来对接的时候,我们分为以下两种情况给大家描述:
不需要强顺序性业务
对于这样的业务,多进程消费的时候我们业务无需太关注顺序性的问题,我们一般都会把数据持久化,一般持久化的软件都给这种情况有所考虑(其实这就是解决并发问题,Mysql 通过排他锁来解决,Elasticsearch 可以通过版本号来解决)。
必须强顺序性业务
对于必须强顺序性的业务,我们只需要本着需要顺序性执行的数据使用一个进程来消费目的就能解决顺序性问题。可有如下几种方案:
- 多队列单进程,用多队列数来加快消费数据速度,用单进程来解决顺序性
- 实现一个队列进行分发(master-work类型,多 work 加快消费数据速度,master进行有规则分发解决顺序性,规则的依旧就是需要顺序性执行的数据使用一个进程来消费,就是就是我们上面画的图)
每种方案都有各自的优缺点,应结合项目本身去选择合理的方案。
本作品采用《CC 协议》,转载必须注明作者和本文链接
实现一个队列进行分发
由master取消息分发worker的时候 如果master挂了岂不是worker都不工作了压力全在master进程上了
类似rabbitmq这种消息队列,一个交换机绑定多个队列。 1.保证一个订单的操作必须一个进程独立消费,只需要保证一个订单的消息只分发到一个队列 2.对订单号 hash 取摸之后, 路由到指定的队列
laravel 队列的任务链可以保证任务执行的先后顺序
希望能出一个laravel 的解决方案demo或者相关实现的链接
顺序执行多任务的队列设计应该是A任务结束以后才生成B任务,不是一口气把AB任务都丢到队列里面。