关于保证用户级的队列数据按顺序消费且能扩容的思考

 队列中的一条数据,在文中都称为一个“任务” 

对于指定的用户uid_a,假设它的异步任务数据都入队列queue_a, 但是有N个消费线程从queue_a pop数据去消费,那么每条任务的执行顺序是未知的,假设任务A、B、C 执行业务分别为insert,update,delete,一旦顺序无法保障,假设变成delete,insert,update,产生的结果截然不同,本该删除的数据,但是缺保留了下来。还有很多类似的场景,对于同一个用户的不同任务间有顺序依赖性的场景。 本文就是探讨该场景下的解决方案

 

解决思路: 

1. 考虑加锁:用户级加锁,当从redis队列rpop一个任务时,先获取锁,当成功获取锁时执行任务,当获取锁失败时,重新放回队列(repush)。这种思路仔细思考会发现问题很多,如何保证获取锁按顺序?可能后来的任务先抢到了锁,这是问题1,对于没拿到锁的线程,数据应该放回队列的哪个位置? 无论lush还是rpush都无法保证这个任务时按原来的队列顺序放回去的,也就是无法保证repush时严格按照之前的任务顺序,这是问题2 。如果获取锁失败时,让线程sleep呢?那也有可能产生多个线程sleep,谁先获取到锁还不知道呢?

结论:加锁保证顺序难以实现,即使实现了性能和稳定性也很难保障

 2.  像redis一样单线程去处理,这样就肯定按顺序执行,也就是一个队列一个消费线程,且同一个用户的任务必须只入一个队列。但是这样也带来了编码和运维上的一些不便:

当消费速度跟不上生产速度时,队列会堆积,此时只能增加消费线程或者消费进程数,由于一一对应的关系,所以队列个数也必须同步增加。

总结:使用方案2:一个队列对应一个消费者的模型。本文以php为例, 假设有4台集群server,每个server开4个消费进程,每个进程都必须是单线程同步模型,否则并发了,就乱序了。

对于生产端伪代码如下:

<?php

 $server_count=4; //server总数

$workers = 4; //单台server的消费进程总数。

$user_id = 1234;

$queue_key= "rds_key_" . ($user_id % $server_count) . ":" . ($user_id % $workers ). ":hash_by_{$server_count}_{$workers}";

$task_data= ["user_id"=>1234, "age"=> 666];

$redis->lpush($queue_key, $task_data);

?>

对于第一台server的消费端:

<?php

$server_count=4; //server总数

$workers = 4; //单台server的消费进程总数。

// 服务器编号,从0开始计算,因为生产端是根据server_count取模的,所以server_id = 0~server_count-1。其实server_id应该是放入一个生产环境专用的配置

//此处为了方便理解,硬编码在代码中。

$server_id =0 ;

$worker_no= 3; //当前worker进程所在编号,也是从0开始计算,0~3

$queue_key = "rds_key_{$server_id}:{$worker_no}:hash_by_{$server_count}_{$workers}";

$data = $redis->rpop($queue_key);

// 此处开始单线程同步模式消费任务...

?>

当集群加了一台机器之后,新增的机器手动加配置:server_id=5。

此时生产端往全新的N个队列中lush任务,因为队列的key中包含:hash_by_{$server_count}_{$workers},当server_count变化之后,

新的队列名称和之前的一批都不一样了。所以此时需要干一些体力活,即:监控旧的队列消费情况,当所有的旧队列数据都消费完成之后,重启队列进程加载最新的hash分组算法。

所以整个扩容过程就是:

1、队列生产端修改代码:机器总数:server_count = 5,并发布到线上,此时所有worker进程reload,所有任务入新的一批队列中

2、消费进程组先不动,但是监控rehash之前的旧队列消费情况,当所有的旧队列都消费完成时,新增的机器添加配置server_id = 4, 并修改消费端代码,把server_count设置成5

发布消费端代码且reload所有消费进程。

 

也许有人会思考:动态去监控server_count的数量,实现server_count从redis动态加载。但是由于动态监控也是需要各个server上报自己的ip到注册中心的,比如每个server onWorkerStart启动时,把自己的ip注册到redis中(sadd,zadd,hset),但是当业务代码去get这个server_count时,无法保证哪一刻获取的server_count是正确的,因为注册过程是挨个上报,无法确定什么时候,所有server都上报完成。就算动态监控实现了,消费端是必须保证旧队列数据消费完成,才能使用新的任务hash分组策略的,只有解决了这些问题,动态控制才是可靠的。

 

 

      

  

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值