基于redis-stream类型实现商品的异步发布

基于redis-stream类型实现商品的异步发布

简介

​ Redis5.0带来了Stream类型。从字面上看是流类型,但其实从功能上看,应该是Redis对消息队列(MQ,Message Queue)的完善实现。用过Redis做消息队列的都了解,基于Reids的消息队列实现有很多种,例如:

  • PUB/SUB,订阅/发布模式
  • 基于List的 LPUSH+BRPOP 的实现
  • 基于Sorted-Set的实现

​ Redis5.0中发布的Stream类型,也用来实现典型的消息队列。该Stream类型的出现,几乎满足了消息队列具备的全部内容,包括但不限于:

  • 消息ID的序列化生成
  • 消息遍历
  • 消息的阻塞和非阻塞读取
  • 消息的分组消费
  • 未完成消息的处理
  • 消息队列监控

参考文档

https://zhuanlan.zhihu.com/p/60501638

https://www.cnblogs.com/jing1208/p/14201069.html

实现商品的异步发布

1、创建 Jobs派遣任务

php artisan make:job Goods/AddGoodsJob

2、初始化生产者 , 创建消费者组,并且在 handle()中执行消费

<?php

namespace App\Jobs\Goods;

use App\Services\Goods\AddGoodsService;
use App\Services\Goods\AddGoodsStreamService;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldBeUnique;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
use Illuminate\Support\Facades\Log;

class AddGoodsJob implements ShouldQueue
{
    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;

    public $data;

    protected $queue_name = 'backend::goods::queue';

    /**
     * Create a new job instance.
     *
     * @return void
     */
    public function __construct($goods_info)
    {
        try{
            $this->data = $goods_info;
			
            //消息的盐标识
            $this->data['salt'] = $this->data['store_id'] .'_'. substr(uniqid(),-7);

            //监听消费队列的标识
            $this->connection = "redis";

            $addGoodsStreamService = new AddGoodsStreamService();

            //生产者数据写入队列
            $addGoodsStreamService->push($this->queue_name,$this->data);
        }catch (\Exception $e){
            Log::info('生产者异常捕获:'.$e->getMessage());
        }
    }

    /**
     * Execute the job.
     *
     * @return void
     */
    public function handle()
    {
        $addGoodsStreamService = new AddGoodsStreamService();

        //消费者消费信息
        $addGoodsStreamService->pop($this->queue_name,function ($message,$xread_key) use (&$addGoodsStreamService){
            //从消费队列中获取到详细的信息
            $goods_info = $message[$this->queue_name][$xread_key[0]];

            try{
                //执行商品操作
                $addGoodsStreamService->doEditGoods($goods_info);
            }catch (\Exception $e){
                throw  new  BadRequestHttpException($e->getMessage());
            }
        });
    }
}

2 . 1 监听队列,进行消息消费:
 php artisan queue:work redis

3、创建service实现生产者,消费者组,消费者消费消息的功能(AddGoodsStreamService.php)

​ 消费者在消费数据,ack后redis并不会释放资源,需要手动释放资源(xdel)

​ 消息消费了但未ack的消息可以通过(xpending)查看未ack的消息

​ 消息消费失败的数据的数据会存储在死讯队列中,可以通过死讯队列(xrange)重新获取消息重新消费(由定时任务完成即可)

<?php


namespace App\Services\Goods;

use App\Dao\GoodsDao;
use Illuminate\Support\Facades\Log;
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;

class AddGoodsStreamService
{
    protected $redis;

    protected $group_name = 'goods_group';

    protected $cons_name  = 'consumerA';

    protected $queue_name = 'backend::goods::queue';

    public function __construct()
    {
        $this->redis = app('redis.connection');
    }

    /**
     * 生产者数据写入队列,与创建消费者组
     * @param $queue
     * @param $messageBody
     */
    public function push($queue,$messageBody){
        //生产者写入消息队列
        $goods_queue = $this->redis->xadd($queue,'*',$messageBody);

        if(!$goods_queue){
            Log::info('添加商品消息写入失败');

            return false;
        }

        //如果没有创建消费者组,则创建消费者组
        if(!$this->groups($queue)){
            Log::info('创建消费者异常');

            return false;
        }
    }

    /**
     * 创建消费者组
     * @param $queue
     * @return bool
     */
    public function groups($queue){
        //查看队列信息
        $queue_info = $this->redis->xinfo('stream',$queue);

        //消费者组不存在时,创建消费者组
        if($queue_info['groups'] <= 0){
            //添加消费者组
            $create_group = $this->redis->xGroup('create',$queue,$this->group_name,0);

            if(!$create_group)  return false;
        }

        return true;
    }

    /**
     * 消费者消费信息
     * @param $queue
     * @param $callback
     */
    public function pop($queue,$callback){
        try{
            //消费者消费信息
            $goods_read = $this->redis->xreadGroup($this->group_name,$this->cons_name,[$queue => '>'],1);

            if(!$goods_read){
                Log::info('暂无信息消费');

                return false;
            }

            //获取消费id标识
            $xread_key = array_keys($goods_read[$queue]);

            //执行添加商品的操作
            $resout = $callback($goods_read,$xread_key);

            if($resout){
                Log::info('消息消费失败~');

                return false;
            }

            //ack确认消费,消费成功后删除redis的资源,如果在死信队列中存在消息,则是消费失败没有ack的消息
            //如果重新消费无法消费成功,则认为数据是异常的,xdel消息的同时ack掉数据
            $this->redis->xack($queue,$this->group_name,$xread_key);

            //ack后手动释放资源,不然一直存在redis中
            $this->redis->xdel($queue,$xread_key);

            Log::info('消息消费完成~');


        }catch (\Exception $e){
            Log::info('消费者消费数据异常:'.$e->getMessage());
        }
    }

    /**
     * 获取未ack的消息
     * @param $xread_key
     */
    public function getXrangeByUnAck(){
        $msg_info = $this->redis->xRange($this->queue_name,'-','+');

        return $msg_info;
    }

    /**
     * 消息重新消费
     * @return bool
     */
    public function doAgainAckByTack(){
        try{
            //从死信队列获取未ack的消息(存在数据已经消费,但未ack的消息,会造成重复消费)
            $goods_list = $this->getXrangeByUnAck();

            if(!count($goods_list)) return false;

            $goodsDao = new GoodsDao();

            foreach ($goods_list as $xread_key => $item){

                //执行商品操作
                $resout = $this->doEditGoods($item,$goodsDao,1);

                if($resout){
                    $this->redis->xack($this->queue_name,$this->group_name,[$xread_key]);

                    //ack后手动释放资源,不然一直存在redis中
                    $this->redis->xdel($this->queue_name,[$xread_key]);
                }
            }

            Log::info('消息重新消费完成~');
        }catch (\Exception $e){
            Log::info('消费者重新消费数据异常:'.$e->getMessage());
        }
    }

    /**
     * 执行商品操作
     * @param $goods_info
     * @param null $goodsDao
     * @param int $type
     * @return bool
     */
    public function doEditGoods($goods_info,$goodsDao = null,$type = 0){
        try{
            $addGoodsService = new AddGoodsService();

            //若有商品id则为编辑商品
            if(isset($goods_info['goods_commonid'])){
                //编辑商品
                $addGoodsService->editGoodsByStore($goods_info['goods_commonid'],$goods_info);
            }else{
                //如果是死信队列重新消费,则需要检查消息是消费成功,只是ack失败,则重新ack即可
                if($type == 1){
                    $has = $goodsDao->hasGoodsCommBySalt($goods_info['salt']);

                    if($has)    return true;
                }

                //添加商品
                $addGoodsService->addGoodsByStore($goods_info);
            }
            return true;
        }catch (\Exception $e){
            throw  new  BadRequestHttpException($e->getMessage());
        }
    }
}

4、创建触发的功能

<?php


namespace App\Services\Goods;

use App\Http\Controllers\Controller;
use App\Jobs\Goods\AddGoodsJob;

class DoTaskService extends Controller
{
    
    /**
     * Stream异步发布商品
     * @param $data
     */
    public function doStreamByGoods($data){
        //任务派遣
        $stream = new AddGoodsJob($data);

        $this->dispatch($stream);
    }
}

5、创建swoole定时任务,重新消费消息

<?php


namespace App\Jobs\Goods;

use App\Services\Goods\GoodsTimedTask;
use Hhxsv5\LaravelS\Swoole\Timer\CronJob;

class GoodsAckJob  extends CronJob
{
    protected $goodsTimedTask;

    public function __construct()
    {
        $this->goodsTimedTask = new GoodsTimedTask();
    }

    protected $i = 0;

    /**
     * 该方法可类比为 Swoole 定时器中的回调方法
     */
    public function run(){
        //消息重新消费
        $this->goodsTimedTask->doAgainAckByTack();
    }

    /**
     * 每隔 1000ms 执行一次任务
     * @return int
     */
    public function interval(){
        // 定时器间隔,单位为 ms
        return 60000;
    }

    /**
     * 是否在设置之后立即触发 run 方法执行
     * @return bool
     */
    public function isImmediate(){
        return false;
    }
}

6、定时触发重新消费任务

​ 目前设定重新消费一次,消费后无论是否消费成功都判定为无效数据,释放资源(实际应用中应该转移当前消息到其他消费者,当超过指定的消费次数或者超过资源存储的时间时,在释放资源)

<?php


namespace App\Services\Goods;

use Illuminate\Support\Facades\Log;

class GoodsTimedTask
{

    protected $addGoodsStreamService;

    public function __construct()
    {

        $this->addGoodsStreamService = new AddGoodsStreamService();
    }


    /**
     * 消息重新消费
     */
    public function doAgainAckByTack(){
        try{
            $this->addGoodsStreamService->doAgainAckByTack();

            Log::info('ACK :: 定时消费成功');
        }catch (\Exception $e){
            Log::info('ACK :: 定时消费失败:'.$e->getMessage());
        }
    }

}

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值