<?php
$redis = new Redis;
$redis->connect('127.0.0.1');
$key = "order:delayqueue";
function generateOrderNo()
{
return '订单号:' . mt_rand(100000, 999999);
}
// 一秒后执行
$redis->zAdd($key, ['NX'], time() + 1, json_encode(['orderNo' => generateOrderNo(), 'delay' => 1]));
// 两秒后执行
$redis->zAdd($key, ['NX'], time() + 2, json_encode(['orderNo' => generateOrderNo(), 'delay' => 2]));
// 三秒后执行
$redis->zAdd($key, ['NX'], time() + 3, json_encode(['orderNo' => generateOrderNo(), 'delay' => 3]));
// 在这一秒同时写入了三条,这是一个并发点,如果出现上万条数据的情况可能会在 zRevRangeByScore 的时候把内存打爆
$redis->zAdd($key, ['NX'], time() + 4, json_encode(['orderNo' => generateOrderNo(), 'delay' => 4]));
$redis->zAdd($key, ['NX'], time() + 4, json_encode(['orderNo' => generateOrderNo(), 'delay' => 4]));
$redis->zAdd($key, ['NX'], time() + 4, json_encode(['orderNo' => generateOrderNo(), 'delay' => 4]));
// 着重要改造的地方
//while (true) {
// // 当小于现在的数据都拿出来消费掉
// $rows = $redis->zRevRangeByScore($key, time(), 0);
// sleep(1); // 这里可以适当放开
//
// // 这里拿到所有的数据循环进行处理,但是这里有问题,数据过大可能导致此处内存爆掉
// foreach ($rows as $row) {
// $result = json_decode($row, true);
// echo "当前数据被消费了:订单号【{$result['orderNo']}】,延时{$result['delay']}秒后执行的" . PHP_EOL;
// $redis->zRem($key, $row);
// }
//}
// 1.改造方案一,单条获取执行,问题是并发条件要处理的记录过多的时候,会阻塞进程
while (true) {
// 我们每次只获取一条数据来执行
$rows = $redis->zRevRangeByScore($key, time(), 0, ['limit' => [0, 1]]);
sleep(1); // 这里可以适当放开
$row = $rows[0];
// 这里拿到所有的数据循环进行处理,但是这里有问题,数据过大可能导致此处内存爆掉
$result = json_decode($row, true);
echo "当前数据被消费了:订单号【{$result['orderNo']}】,延时{$result['delay']}秒后执行的" . PHP_EOL;
$redis->zRem($key, $row);
}
// 2.第二种方案,利用Shell或者其他语言唤起多个PHP消费进程,有点多此一举,同时会存在资源争抢的问题,所以
// zAdd的时候要设置第二个参数 ['NX']key不存在的时候才设置,对资源进行加锁
3.第三种方案,换语言实现,还是通过Redis来处理,比如Golang,大概思路是一样的,只不过在循环的时候可能阻塞的地方
使用Go关键字开启多个协程并发消费,不过同时也要注意锁的问题
假设taskChannel是redis zRevRangeByScore拿出来的数组集合,一次性有1000条,这种情况下如果单条任务出现拥塞会导致后面任务延时,严重堆积的时候业务就会出现问题,那么下面是golang的解决方案
func main() {
taskChannel := make(chan int, 1000)
resultChannel := make(chan int, 1000)
closeChannel := make(chan bool, 10)
//这里我们模拟一下,一次性从redis中取出了1000条数据
go func() {
for i := 0; i < 1000; i++ {
taskChannel <- i
}
close(taskChannel)
}()
// 开辟十个协程去消费数据
for i := 0; i < 10; i++ {
go Process(taskChannel, resultChannel, closeChannel)
}
for i := 0; i < 10; i++ {
<-closeChannel
}
close(closeChannel)
close(resultChannel)
for result := range resultChannel {
fmt.Println(result)
}
}
func Process(taskChannel chan int, resultChannel chan int, closeChannel chan bool) {
for task := range taskChannel {
// todo 在这里处理task的数据写入到结果中
resultChannel <- task
}
closeChannel <- true
}