实战一:list类型 最简单的消息队列
首先得明白什么时候会用到消息队列?什么是消息队列? 举个简单的应用场景:下单 用户 ----> 订单API —>订单数据入库 商品服务模块 —>减库存操作 这可以是两个单独的模块 也可以理解成是两个服务 现在不都讲究服务化嘛? 正常的业务逻辑就是用户通过api然后订单数据入库 然后再通过商品服务模块去减少库存或者用mysql里面的事务 订单入库和库存减少操作同时进行 但是对于用户而言呢 他们只关注的是我是否下单成功 我不管你后边库存是否减少了 所以在完成订单入库之后我们就可以给用户返回成功的提示 然后通过队列的形式将用户的标记和订单号写入队列里面去 保持有序性 左进右出 或者 右进左出 然后有个定时任务或者守护进程不断去读取队列里面的数据进行库存减少的操作! 这样有什么好处呢? 提高了响应速度 订单入库成功即返回成功提示 实现了解耦 下单入库和减库存分开 有序性 谁先买的谁先得到处理 异步性 另外一个很大的好处就是针对秒杀的场景 秒杀一瞬间并发量很大 利用redis的事务+乐观锁机制 然后配合上队列技术 可以很好地实现秒杀功能 避免库存变负数的情况发生 将秒杀成功的用户写入redis的队列里面 然后秒杀完了之后再进行处理 ! 有个图你是不是很清晰了:

利用redis生成订单号 redis队列处理订单_php


我们可以起多个进程去处理list队列里面的数据 提高处理速度


利用redis生成订单号 redis队列处理订单_利用redis生成订单号_02

redis里面的list类型 天然可以用来实现一定程度上的消息队列 对于一般的商城系统或者其他应用场景来说都是可以满足的
但是如何对于超级大的系统比如天猫 京东 拼多多这样的系统来说 就显得力不从心了 那就得要用到MQ以及集群化部署 后边我们再讲

队列当中常用的命令:

lpush orders pn001                               从左侧往队列里面塞入元素
lpush orders pn002
lpush orders pn003
lpush orders pn004 pn005 pn006                   从左侧批量往队列里面塞入元素
rpush order rn001                                从右侧往队列里面塞入元素
lrange orders 0 -1                               查询orders队列里面所有的元素
brpop orders 10                                  从右侧弹出orders队列里面的元素  10表示如果没有的时候的等待时间 如果10s之后还没有则返回false
blpop orders 10                                  和从右侧一个道理 这是从左侧弹出orders队列里面的元素 10s也是等待时间

传统的做法:
需要两个程序
一个程序往队列里面插入值 不管是左进右出 还是右进左出的哈 得有一个程序往队列里面塞入元素啊
代码就不提供了哈
另一个程序就是死循环 对消息队列反复进行弹值
来看一下死循环的案例代码吧:

function start(redis $redis_client)
{
    while(true) {
    //这个地方不能设置无限期的弹  设置上再没有得到数据的时候等待个10s的时间 这个10s只有在orders队列空了的时候才会起作用 所以10s也显得很有必要了!orders队列里面没有元素那就等上个10s呗!
        $res = $redis_client->brPop(["orders"],10);
        if($res && $res[0])
        {
            echo "order_no=".$res[1]." done".PHP_EOL;
            usleep(500*1000); //休眠500毫秒
            echo "restart ".PHP_EOL;
        }
        else
            continue;
 }
如何提高队列的处理速度? 有时候我们需要快速去处理redis队列里面的元素 为了提高处理速度 那么我们可以开多个消费端
每个消费端都是一个死循环读取redis队列里面的元素 谁先抢到谁就处理呗 万一其中一个消费端处理的过程当中卡顿了
其他的消费端就会争抢redis队列里面的元素进行处理 开的消费端越多那么处理的速度也就越快!

利用redis生成订单号 redis队列处理订单_php_03

实战二:消费者出现异常时候的补救方案

上边我们讲过 需要两个程序 一个是生产者 往队列里面写数据
另一个程序是消费者或者多个消费者
从redis里面读取数据出来进行相关业务逻辑的处理 那么就是这些消费者在进行读取redis队列里面数据的时候发生了错误 比如brpop的时候 pn004订单处理的时候服务器宕机了 或者出现了其他的不可想象的错误
那么redis队列里面的这个pn004元素就被弹出了
就会造成redis队列里面元素的丢失!因为谁也不能保证服务器不宕机不停电或者你写的程序有问题!
咋办呢?

首先来说一下用到的相关命令:

brpoplpush orders orders_bf 10    从orders队列当中取出最后一个元素并插入到另一个队列orders_bf队列的头部  如果orders队列没有元素则会阻塞列表知道等到超时或者发现可弹出元素为止
只提供思路: a.死循环程序在处理redis当中的orders1队列的时候 也往另一个队列比如orders2队列当中从左侧插入元素 你可以把orders2队列理解成orders1队列的备份队列 这样就算在处理orders1队列里面元素的时候突然断电但是备份队列当中也是存在正在处理的元素的! b.当启动死循环程序的时候 首先会去备份队列当中查是否有元素存在 如果有 进行处理 处理完备份队列里面的元素之后再走正常的处理orders1队列的业务 就是我们上边写的 通过brpoplpush的命令 将要处理的元素写到orders2备份队列当中去 然后正常业务执行完毕之后删除orders2备份队列里面的对应的元素 这样的话只有在突然断电或者宕机的情况下才会出现orders1队列里面的元素被弹出了 然后断电了 orders2备份队列里面存在的情况! c.如果在正常业务处理过程当中发生了不可预知的程序上的错误 比如内存溢出 cpu爆满等无法处理的情况 当发生此类问题的时候可以将元素回写进redis队列orders1当中去 当然是从右侧进入了 这样的好处是 你这个死循环处理不了 但是别的死循环获取到了它可以把它处理掉进而走正常的业务逻辑!

利用redis生成订单号 redis队列处理订单_php_04

他妈的简单说吧就是


死循环处理原始队列的同时往备份队列里面写数据 死循环处理完原始队列里面的元素走了正常的处理逻辑之后 删除备份队列里面的元素


如果死循环处理原始队列里面元素的时候发生错误则回写到原始队列里面去 通过brpush 放在原始队列的最后一个 就这么个逻辑!

别以为这样就完事大吉了! 那假设在处理备份队列的时候 倒霉突然宕机断电了 那就完蛋啦!你总不能再去给备份队列再弄个备份队列去吧!
所以如果业务复杂 直接上MQ吧!这个只适合普通的消息队列 不怕数据丢失的情况下 你用它就行 但是丢失的几率还是比较小的哈 就怕赶上了!

伪代码 屡屡思路可以:

<?php
require("core/functions.php");
require("core/MyException.php");

if($argc!=2) //譬如php abc.php  那么argc就是1(自己算一个参数)   php abc.php aaa ,那么argc就是2    用argv[1] 可以取到 参数值
    exit("please set server id~~~");
$myid=$argv[1];//这是启动时自己设置的 唯一id

$client=$redis->client();

function start(redis $redis_client)
{
    global $myid;
    //先处理 上次没有处理的 key
    echo "find baklist".PHP_EOL;
    while(true) // 应该使用swoole 等 开一个异步任务或者进程,专门对自己的 备份list做长期监听
    {
        $bak_res = $redis_client->brPop(["orders".$myid],1); //从自己的备份队列中获取 ,过期时间设置短一些
        if($bak_res && $bak_res[0])
        {
            doJob($bak_res[1],$redis_client,true);
            usleep(500*1000); //休眠500毫秒
        }
        else //如果没取到值 说明 备份队列中 木有 了,则跳出循环
            break;
    }
    echo "baklist done".PHP_EOL;
    echo "begin orders ".PHP_EOL;
    //接下来开始继续
    while(true) {
        $res = $redis_client->brpoplpush("orders","orders".$myid,10); //注意,这个函数直接返回的是值
        if($res)
        {
            doJob($res,$redis_client);
            usleep(500*1000); //休眠500毫秒

        }
        else
            continue;


    }

}
function doJob($orderNo,redis $redis_client,$isBak=false) //isBak决定了 处理过程是否是 备份队列处理 ,如果是 有些步骤不需要执行
{
    global $myid;
    try{
        if($orderNo=="pn023") throw new MyException("error");
        sleep(3); //假装 干的很耗时,很辛苦
        if($isBak) //如果是 备份队列处理,显示不一样的字符,仅此而已
            echo "backjob order_no=";
        else
        echo "order_no=";

        echo $orderNo." done".PHP_EOL;
        if(!$isBak)
            $redis_client->lPop("orders".$myid);   //注意这里,处理完后,要删掉 备份列表左边的第一个元素
    }
    catch (MyException $myException)
    {
        if(!$isBak){
            //这里要判断 是什么类型的exception ,譬如超时等才能塞回去,这个机制是自己定的
            $redis_client->rPush("orders",$orderNo);//塞回原队列
            $redis_client->lPop("orders".$myid);
            echo "push back ".PHP_EOL;
            sleep(3);//休眠3秒,让其他 死循环程序来获取
        }

    }
    catch (Exception $ex)
    {

        echo "err".$ex->getMessage().PHP_EOL;



    }

}
start($client);
实战三:新闻延迟发布案例
新闻延迟发布的场景是什么? 比如我是今日头条的工作人员 在下班前我编辑好新闻 规定在明天早上7点发布 这就是新闻延迟发布。当然实现这个功能的方式有很多种 比如利用mysql数据库也可以实现。但是如果让你利用redis来搞 又是如何实现的呢? 我们来看一下基本的流程图:

利用redis生成订单号 redis队列处理订单_php_05

用户提交数据入库 其中有个字段是ispub是否发布的字段 前台根据这个字段展示新闻 如果为false则不展示
在入库之后我们需要将要发布该条新闻的那个时间戳和新闻ID写入到redis当中去 我们利用的是redis当中的有序集合
因为里面有score值 这个score值就是要发布新闻时候的时间戳 对应的值就是id 然后另外再去起一个死循环程序
定时的从zset中去取出score小于等于当前时间戳的值有哪些 然后去更新数据库的ispub为true 这样就实现了新闻的延迟发布!
其实这和利用有序集合实现订单的自动过期是一样的思路!

用到的有序集合的命令:

zrangebyscore newspub -inf  当前时间戳值 withscores limit 0 10  //代表取小于等于 当前时间戳的 所有新闻,取10条
实战四:如何实现订单过期自动取消的功能

1.利用有序集合实现:

利用redis生成订单号 redis队列处理订单_php_06


2.利用redis的发布订阅功能 自动监听过期事件实现

利用redis生成订单号 redis队列处理订单_php_07

实战五:熔断器介绍和简单实现原理

假设有个不断在运行的主程序 在这个主程序的某个点去调用函数A ,假设A函数抛出了异常,明知道A函数有异常 那么是否反复去调用呢? 答案是:会
因为程序不是人脑 没那么智能! 如何让它变的智能呢?那就可以使用熔断器!

利用redis生成订单号 redis队列处理订单_死循环_08


主程序不直接调用A,而是让熔断器调用A A函数如果有异常,熔断器记录 超过一定次数,则熔断器打开 下次则调用备用函数


如果函数A的bug改好了,怎么恢复?我们以后再说 先来看php当中如何来实现这么一个熔断器的机制?

我们先来看关键的非常重要的那个函数A的代码:

<?php

require "CircuitBreaker.php";
//class myclass{}当中的test就是那个非常重要的A函数 你就把它理解成A函数  反正很重要吧!
class myclass{
    public function test($str){
        return file_get_contents("aaaa");
    }
}
$c=new myclass();
//echo $c->test("abc");

//我们调用如此重要的A函数的使用通过熔断器机制去调用  而不是直接主程序当中运行  谁是熔断器呢?就是这里的CircuitBreaker{}类里面的invoke()函数 这里就是调用的地方了
//我们需要把实例化好的类和类里面的方法名称以及参数和当发生异常之后的回调函数扔到熔断器里面去 
//然后我们再去看熔断器里面是怎么搞的 往下看代码
echo (new CircuitBreaker())->invoke($c,"test",['bcd'],function(){ return "fallback";});

熔断器的具体代码实现:

<?php
require("./../core/functions.php");

class CircuitBreaker{
    private $zSetKey="circuit";

    public function invoke(object $class,string $method,array $params,callable $fallback){
        global $redis;
        //得使用try catch  易于捕获A重要函数里面发生的异常信息
        try{
        //这里可以判断一下redis里面的有序集合里面的score的值 如果超过3次则直接调用回调函数 相当于熔断器打开
        {
          if(读取有序集合里面score的值 >= 3){
               return $fallback();
			  }
        }
        //这里没啥 就是直接掉核心的函数 执行正常的逻辑 也就用了个解包
           return $class->$method(...$params);
        }
        catch (Throwable $ex){
        //Throwable 是 PHP 7 中可以用作任何对象抛出声明的基本接口,包括 Expection (异常)和 Error (错误)。可以捕获致命的错误信息 https://www.jianshu.com/p/dbca816a9af5
        //获取到类和方法 拼接成字符串
            $member=get_class($class)."_".$method;
            //调用类里面的函数异常 写到redis的有序集合当中去 次数每次加1
            $redis->client()->zIncrBy($this->zSetKey,1,$member);
            //调用A重要函数异常的话  直接进行函数降级  调用备用函数$fallback()
            return $fallback();//函数降级
        }
    }
}

function cbHandler(Exception $ex){
  throw new Exception($ex->getMessage());
}

//将错误信息  包含致命错误信息 调用cbHandler回调函数 在回调函数当中将错误信息写到异常里面去  那么上边的try catch 当中就可以获取到异常
set_error_handler("cbHandler",E_ALL);

这样就实现了调用A重要函数发生异常或者错误 那么直接进行函数降级 去调用备用的函数的机制!实现了熔断器的作用以及函数的降级 在这个过程当中我们还是使用上了redis里面的有序集合

这样虽然实现了熔断器以及函数降级功能 但是重要的A函数从此不会再被调用 因为调用次数失败超过3次了 并且这个值始终存储到了redis的有序集合里面 当我们修复好A函数之后 它还是会找备用函数去 这就不大好了,咋地也得给A函数改错的机会吧!咋办呢? 先不写了 再往后写就比较复杂了 我还没想好!