队列中的一条数据,在文中都称为一个“任务”
对于指定的用户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分组策略的,只有解决了这些问题,动态控制才是可靠的。