火影推荐程序连载39-laravel关于错误和异常的部分源码

第一部分:laravel关于错误和异常的部分源码

第二部分:自定义异常的使用(结合serviceprovider monolog elasticsearch)

过程中涉及到的重要函数请自行查看手册

error_reporting set_error_handler set_exception_handler register_shutdown_function error_get_last

laravel v6.18.40

源码部分

我们来到http kernel文件,处理请求部分

可以看到执行我们业务逻辑的sendRequestThroughRouter方法被try_catch包裹的严严实实

public function handle($request)
{
    try {
        $request->enableHttpMethodParameterOverride();

        $response = $this->sendRequestThroughRouter($request);
    } catch (Exception $e) {
        $this->reportException($e);

        $response = $this->renderException($request, $e);
    } catch (Throwable $e) {
        $this->reportException($e = new FatalThrowableError($e));

        $response = $this->renderException($request, $e);
    }

    $this->app['events']->dispatch(
        new RequestHandled($request, $response)
    );

    return $response;
}

捕获到异常后 框架做了哪些工作呢? reportException 记录了异常 renderException 响应了异常

/**
 * Report the exception to the exception handler.
 *
 * @param  \Exception  $e
 * @return void
 */
protected function reportException(Exception $e)
{	
    # 从容器中解析ExceptionHandler, 绑定位于bootstrap/app.php中
    # 执行的是App\Exceptions\Handler的report方法
    $this->app[ExceptionHandler::class]->report($e);
}

# 跳转到App\Exceptions\Handler的report方法
public function report(Exception $exception)
{	
    # 继续跳转到父类的report方法
    parent::report($exception);
}

# 只看核心代码
Illuminate\Foundation\Exceptions\Handler::report()
/**
 * Report or log an exception.
 *
 * @param  \Exception  $e
 * @return void
 *
 * @throws \Exception
 */
public function report(Exception $e)
{
    if ($this->shouldntReport($e)) {
        return;
    }
	
    # 判断传递进来的异常是否存在report方法,有就执行
    if (is_callable($reportCallable = [$e, 'report'])) {
        // 值得注意的是,此步骤通过容器调用 
        // 这意味着我们可以在自定义的异常类中肆无忌惮的向report方法中诸如依赖了!!!
        // 后面自定义异常类的使用会提及
        return $this->container->call($reportCallable);
    }
	
    # 从容器中解析log服务
    # Illuminate\Log\LogManager实例
    try {
        $logger = $this->container->make(LoggerInterface::class);
    } catch (Exception $ex) {
        throw $e;
    }
	
    # 记录日志 基于monolog 后面自定义日志服务会讲解monolog的使用
    $logger->error(
        $e->getMessage(),
        array_merge(
            $this->exceptionContext($e),
            $this->context(),
            ['exception' => $e]
        )
    );
}

# renderException方法请自行查看

通过上面的代码,我们自然而然的认为这样就可以捕获应用中产生的所有异常了,其实不然

下面我们来看框架引导阶段为处理错误和异常做的工作

逻辑同样位于框架的boot阶段

下面给出简要介绍

Illuminate\Foundation\Bootstrap\HandleExceptions::bootstrap()
public function bootstrap(Application $app)
{
    self::$reservedMemory = str_repeat('x', 10240);

    $this->app = $app;

    // 尽可能显示所有错误, 甚至包括将来 PHP 可能加入的新的错误级别和常量
    // -1 和 E_ALL | E_STRICT 的区别为是否向后兼容
    error_reporting(-1);

    // 设置执行逻辑部分出现错误的回调
    // 默认错误级别为 E_ALL | E_STRICT
    // 网上说此函数只有warning和notice级别的错误能够触发的说法不够准确
    // 个人拙见:可以触发回调的错误级别为运行时产生的错误
    // 直接中断脚本执行的错误不能触发此回调 因为回调还未注册
    // 为了更大范围的抓取错误,需要配合register_shutdown_function 和 error_get_last 处理
    set_error_handler([$this, 'handleError']);

    // 设置捕获执行逻辑部分未捕获的异常回调
    set_exception_handler([$this, 'handleException']);

    // 设置php脚本结束前最后执行的回调
    register_shutdown_function([$this, 'handleShutdown']);

    if (! $app->environment('testing')) {
        ini_set('display_errors', 'Off');
    }
}

# 将php错误转化为异常抛出
/**
 * Convert PHP errors to ErrorException instances.
 * @throws \ErrorException
 */
public function handleError($level, $message, $file = '', $line = 0, $context = [])
{   
    if (error_reporting() & $level) {
        throw new ErrorException($message, 0, $level, $file, $line);
    }
}

# 注释已经非常清晰 致命错误异常不能按照普通异常处理 在此处直接记录和返回响应
/**
 * Handle an uncaught exception from the application.
 *
 * Note: Most exceptions can be handled via the try / catch block in
 * the HTTP and Console kernels. But, fatal error exceptions must
 * be handled differently since they are not normal exceptions.
 *
 * @param  \Throwable  $e
 * @return void
 */
public function handleException($e)
{   
    if (! $e instanceof Exception) {
        $e = new FatalThrowableError($e);
    }

    try {
        self::$reservedMemory = null;

        $this->getExceptionHandler()->report($e);
    } catch (Exception $e) {
        //
    }

    if ($this->app->runningInConsole()) {
        $this->renderForConsole($e);
    } else {
        $this->renderHttpResponse($e);
    }
}

/**
 * Handle the PHP shutdown event.
 *
 * @return void
 */
public function handleShutdown()
{   
    // 生成的异常类是symfony封装的异常类
    // 例:可以在任意路由中来上一句不加分号的代码 看看测试效果
    if (! is_null($error = error_get_last()) && $this->isFatal($error['type'])) {
        $this->handleException($this->fatalExceptionFromError($error, 0));
    }
}

使用部分

自定义异常的使用

方式一:直接抛出一个异常 在中判断 进行自定义的处理

1 创建一个自定义异常类
php artisan make:exception CustomException
    
2 在业务逻辑中抛出异常
Route::get('ex', function () {
    throw new CustomException('thrown for caught');
});

3 扩展App\Exceptions\Handler类
public function report(Exception $exception)
{
    if ($exception instanceof CustomException) {
        Log::channel('daily')->error($exception->getMessage(), ['type' => 'myEx']);
    }
    parent::report($exception);
}
# 当然你也可以扩展render方法

4 访问路由
# 查看logs下的文件

以上方法显然不够优雅,当异常变多的时候,需要配合大量的instanceof判断,并且可能会记录两份相同内容的日志

所以还可以使用第二种方式进行自定义异常的使用,利用框架自动调用report和render方法的特性,实现记录和渲染异常响应

方式二:自定义一个exception 然后让框架自动调用report方法 不进行render

1 创建自定义并编辑自定义异常
php artisan make:exception MyException
class MyException extends Exception
{
    public function report()
    {
        Log::channel('daily')->error($this->getMessage(), array_merge($this->exceptionContext(), $this->context()));
    }
    
    // 其实是不必要的 这两个方法可以在Handler中进行重写,请自行查看Handler的父类,根据需要进行扩展重写
    public function exceptionContext()
    {   
        // return [$this->getTraceAsString()];
        return [];
    }

    // 其实是不必要的 这两个方法可以在Handler中进行重写
    public function context()
    {
        // return ['type' => 'myEx'];
        return ['exception' => $this];
    }

    public function render()
    {
        return 'whatever you like';
    }
}

2 抛出异常
Route::get('myex', function () {
    throw new MyException('thrown for caught');
});

3 执行并查看日志文件 是不是发现和laravel原生的异常记录长得很像呢

方式三:使用自定义的日志服务记录异常

上面提到异常实例的report是通过容器调用的,这意味着我们可以注入我们自定义的日志服务

这里使用神器monolog,因为laravel的日志系统基于monolog,框架已经包含了此库。如果使用其他框架请先确保安装monolog

这里使用elasticsearch作为日志handler之一,所以请确保安装了composer require elasticsearch/elasticsearch

使用monolog作为自定义日志服务实现的原因是因为monolog本身具有替换性和通用性,其他框架稍加改动也可以使用

1 创建契约
<?php

namespace App\Contracts;

interface ExceptionLog
{      
    // 记录异常
    public function recordEx(\Exception $e);
}

2 创建日志服务 并简单介绍monolog使用
# 关于monolog的更多使用方法请查看官方文档 https://github.com/Seldaek/monolog
<?php

namespace App\Services\Logs;

use App\Contracts\ExceptionLog;
use Monolog\Logger;
use Monolog\Handler\StreamHandler;
use Monolog\Handler\RotatingFileHandler;
use Monolog\Handler\ElasticsearchHandler;
use Elasticsearch\ClientBuilder;

class MonoException implements ExceptionLog
{
    protected $logger;

    public function __construct()
    {
        // 创建本地文件存储handlers
        $streamHandler = new StreamHandler(storage_path('logs/exception.log'), Logger::DEBUG);

        // 创建本地日期切分存储handler
        $rotateHandler = new RotatingFileHandler(storage_path('logs/exception.log'), Logger::DEBUG);

        // 创建es 客户端 为了减小难度 就不在这里注入elasticsearch客户端了 其实是我懒 开心撸码最重要
        $esClient = ClientBuilder::create()->setHosts(config('es.hosts'))->build();
        // es配置
        $options = [
            'index' => 'my_exception_index',
            'type'  => 'test_exception',
        ];

        // 创建远程elasticsearch日志存储
        $esHandler = new ElasticsearchHandler($esClient, $options);

        // 这里没有阻止handlers的堆栈冒泡,一条日志会逐个经过es、rotate、stream日志处理器
        // 更多的日志存储handler请查看文档(为了性能考量,monolog甚至为你提供了异步方式记录日志)

        // 创建logger 虽然叫logger但是他并没有记录日志的能力
        // 真正提供记录日志能力的是提前创建好的handlers
        // monolog提供非常多开箱即用的handler 请查看文档
        // 并没有设置processor等等 更多api请查看官方文档
        $logger = new Logger('exception', compact('streamHandler', 'rotateHandler', 'esHandler'));
        $this->logger = $logger;
    }

    public function recordEx(\Exception $e)
    {
        $this->logger->error($e->getMessage());
    }
}

3 创建服务提供者 因为我们创建的服务在异常中调用 所以使用单例和延迟绑定更加合适
php artisan make:provider LogServiceProvider
<?php

namespace App\Providers;

use Illuminate\Support\ServiceProvider;
use App\Contracts\ExceptionLog;
use App\Services\Logs\MonoException;
use Illuminate\Contracts\Support\DeferrableProvider;

class LogServiceProvider extends ServiceProvider implements DeferrableProvider
{
    /**
     * Register services.
     *
     * @return void
     */
    public function register()
    {
        $this->app->singleton(ExceptionLog::class, function () { 
            return new MonoException();
        });
    }

    /**
     * Bootstrap services.
     *
     * @return void
     */
    public function boot()
    {
        //
    }

    public function provides()
    {
        return [ExceptionLog::class];
    }
}

4 注册服务 config/app.php
...
App\Providers\AuthServiceProvider::class,
// App\Providers\BroadcastServiceProvider::class,
App\Providers\EventServiceProvider::class,
App\Providers\RouteServiceProvider::class,
// 注册自定义日志服务
App\Providers\LogServiceProvider::class,


4 创建自定义异常 并在其中使用自定义的日志服务
<?php

namespace App\Exceptions;

use Exception;
use App\Contracts\ExceptionLog;

class CustomException extends Exception
{	
    // 注入我们的日志服务
    public function report(ExceptionLog $logger)
    { 
        $logger->recordEx($this);
    }

    public function render()
    {
        return 'whatever you like';
    }
}

5 测试异常的使用
Route::get('cex', function () { 
    throw new CustomException('thrown for caught!!!');
});

6 检查es和storage/logs下是否存在我们的日志吧
# 我们实现的日志服务可以在应用的任何地方使用,这里只是使用在了异常记录的地方,希望大家能够理解

方式四:有的道友可能吐槽,laravel好好的日志服务不香吗?折腾啥啊?

好的,那就通过laravel自带的日志服务实现和上面同样的功能

config/logging.php
<?php

use Monolog\Handler\NullHandler;
use Monolog\Handler\StreamHandler;
use Monolog\Handler\SyslogUdpHandler;
use Monolog\Handler\ElasticsearchHandler;
use Elasticsearch\ClientBuilder;
use Monolog\Formatter\ElasticsearchFormatter;
  也许有的小伙伴对这个功能比较陌生,不太清楚这个功能是干什么的,没关系小黑哥先来举个例子。
  
  假设我们有这么一个业务场景,在网站下单支付以后,需要通知库存服务进行发货处理。
  
  上面业务实现不难,我们只要让库存服务提供给相关的给口,下单支付之后只要调用库存服务即可。
  
  后面如果又有新的业务,比如说积分服务,他需要获取下单支付的结果,然后增加用户的积分。
  
  这个实现也不难,让积分服务同样提供一个接口,下单支付之后只要调用库存服务即可。
  
  如果就两个业务需要获取下单支付的结果,那也还好,程序改造也快。可是随着业务不断的发展,越来越多的新业务说是要下单支付的结果。
  
  这时我们会发现上面这样的系统架构存在很多问题:
  
  第一,下单支付业务与其他业务重度耦合,每当有个新业务需要支付结果,就需要改动下单支付的业务。
  
  第二,如果调用业务过多,会导致下单支付接口响应时间变长。另外,如果有任一下游接口响应变慢,就会同步导致下单支付接口响应也变长。
  
  第三,如果任一下游接口失败,可能导致数据不一致的情况。比如说下图,先调用 A,成功之后再调用 B,最后再调用 C。
  
  如果在调用 B 接口的发生异常,此时可能就导致下单支付接口返回失败,但是此时 A 接口其实已经调用成功,这就代表它内部已经处理下单支付成功的结果。
  
  这样就会导致 A,B,C 三个下游接口,A 获取成功获取支付结果,但是 B,C 没有拿到,导致三者系统数据不一致的情况。
  
  其实我们仔细想一下,对于下单支付业务来讲,它其实不需要关心下游调用结果,只要有某种机制通知能通知到他们就可以了。
  
  讲到这里,这就需要引入今天需要介绍发布订阅机制。
  
  Redis 发布与订阅
  
  Redis 提供了基于「发布/订阅」模式的消息机制,在这种模式下,消息发布者与订阅者不需要进行直接通信。
  
  如上图所示,消息发布者只需要想指定的频道发布消息,订阅该频道的每个客户端都可以接受到到这个消息。
  
  使用 Redis 发布订阅这种机制,对于上面业务,下单支付业务只需要向支付结果这个频道发送消息,其他下游业务订阅支付结果这个频道,就能收相应消息,然后做出业务处理即可。
  
  这样就可以解耦系统上下游之间调用关系。
  
  接下来我们来看下,我们来看下如何使用 Redis 发布订阅功能。
  
  Redis 中提供了一组命令,可以用于发布消息,订阅频道,取消订阅以及按照模式订阅。
  
  首先我们来看下如何发布一条消息,其实很简单只要使用 publish 指令:
  
  publish channel message
  
  上图中,我们使用 publish 指令向 pay_result 这个频道发送了一条消息。我们可以看到 redis 向我们返回 0 ,这其实代表当前订阅者个数,由于此时没有订阅,所以返回结果为 0 。
  
  接下来我们使用 subscribe 订阅一个或多个频道
  
  subscribe channel [channel ...]
  
  如上图所示,我们订阅 pay_result 这个频道,当有其他客户端往这个频道发送消息,
  
  当前订阅者就会收到消息。
  
  我们子在使用订阅命令,需要主要几点:
  
  第一,客户端执行订阅指令之后,就会进入订阅状态,之后就只能接收 subscribe、psubscribe、unsubscribe、punsubscribe 这四个命令。
  
  第二,新订阅的客户端,是无法收到这个频道之前的消息,这是因为 Redis 并不会对发布的消息持久化的。
  
  相比于很多专业 MQ,比如 kafka、rocketmq 来说, redis 发布订阅功能就显得有点简陋了。不过 redis 发布订阅功能胜在简单,如果当前场景可以容忍这些缺点,还是可以选择使用的。
  
  除了上面的功能以外的,Redis 还支持模式匹配的订阅方式。简单来说,客户端可以订阅一个带 * 号的模式,如果某些频道的名字与这个模式匹配,那么当其他客户端发送给消息给这些频道时,订阅这个模式的客户端也将会到收到消息。
  
  使用 Redis 订阅模式,我们需要使用一个新的指令 psubscribe。
  
  我们执行下面这个指令:
  
  psubscribe pay.*
  
  那么一旦有其他客户端往 pay 开头的频道,比如 pay_result、pay_xxx,我们都可以收到消息。
  
  如果需要取消订阅模式,我们需要使用相应punsubscribe 指令,比如取消上面订阅的模式:
  
  punsubscribe pay.*
  
  Redis 客户端发布订阅使用方式
  
  基于 Jedis 开发发布/订阅
  
  聊完 Redis 发布订阅指令,我们来看下 Java Redis 客户端如何使用发布订阅。
  
  下面的例子主要基于 Jedis,maven 版本为:
  
  <dependency>
  
  <groupId>redis.clients</groupId>
  
  <artifactId>jedis</artifactId>
  
  <version>3.1.0</version>
  
  </dependency>
  
  其他 Redis 客户端大同小异。
  
  jedis 发布代码比较简单,只需要调用 Jedis 类的 publish 方法。
  
  // 生产环境千万不要这么使用哦,推荐使用 JedisPool 线程池的方式
  
  Jedis jedis = new Jedis("localhost", 6379);
  
  jedis.auth("xxxxx");
  
  jedis.publish("pay_result", "hello world");
  
  订阅的代码就相对复杂了,我们需要继承 JedisPubSub 实现里面的相关方法,一旦有其他客户端往订阅的频道上发送消息,将会调用 JedisPubSub 相应的方法。
  
  private static class MyListener extends JedisPubSub {
  
  @Override
  
  public void onMessage(String channel, String message) {
  
  System.out.println("收到订阅频道:" + channel + " 消息:" + message);
  
  }
  
  @Override
  
  public void onPMessage(String pattern, String channel, String message) {
  
  System.out.println("收到具体订阅频道:" + channel + "订阅模式:" + pattern + " 消息:" + message);
  
  <project xmlns="http://maven.apache.org/POM/4.0.0"
  
  xmlns:xsi="http://www.lanboyulezc.cn /2001/XMLSchema-instance"
  
  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://www.dongdongrji.cn maven.apache.org/xsd/maven-4.0.0.xsd">
  
  <artifactId>dubbo<www.xxinyoupt.cn /artifactId>
  
  <groupId>nacos</groupId>
  
  <version>0.0.1-SNAPSHOT<www.lanboyulezc.cn /version>
  
  <priority value="info"www.chuanchenpt.cn/ />
  
  <appender-ref ref=www.yixingxzc.cn"stdout"www.longtenghai2.com/>
  
  <appender-ref ref="fileout"www.baichuangyule.cn /
  
  换掉encoder的实现类或者换掉layout的实现类就可以了
  
  <?xml version="1.0"www.zhuyngyule.cn encoding=www.bhylzc.cn"UTF-8"?>
  
  <configuration debug=www.shicaiyulezc.cn"false"www.huachenguoj.com >
  
  <property name=www.51huayuzc.cn"APP_NAME" value="logtest"/>
  
  <property name=www.xinhuihpw.com "LOG_HOME" value="./logs" www.wanyaylezc.cn//>
  
  <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
  
  <!--替换成AspectLogbackEncoder-->
  
  <encoder class="www.shengrenyp.cn "com.yomahub.tlog.core.enhance.logback.AspectLogbackEncoder">
  
  <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{www.pinguo2yl.com} - %msg%n</pattern>
  
  <appender www.baishenjzc.cn name="FILE" www.fanhuagjqw.cn class="ch.qos.logback.core.rolling.RollingFileAppender">
  
  <File>${LOG_HOME}/${APP_www.jinliyld.cn NAME}.log<www.baihua178.cn /File>
  
  <rollingPolicy class="www".jintianxuesha.com"ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy"www.wanyaylezc.cn/>
  
  <FileNamePattern>www.yachengyl.cn ${LOG_HOME}/${APP_NAME}.log.%d{yyyy-MM-dd}.%i.log</FileNamePattern>
  
  <MaxHistory>30<www.51huayuzc.cn /MaxHistory>
  
  <maxFileSize>1000MB<www.jinliyld.cn /maxFileSize>
  
  <!--替换成AspectLogbackEncoder-www.qiaoheibpt.com->
  
  <encoder class="com.yomahub.tlog.core.enhance.logback.AspectLogbackEncoder">
  
  <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS}www.baishenjzc.cn [www.wanyaylezc.cn/thread] %-5level %logger{50} - %msg%n</pattern>
  
  其次我们需要调用 Jedis 类的 subscribe 方法:
  
  Jedis jedis = new Jedis("localhost", 6379);
  
  jedis.auth("xxx");
  
  jedis.subscribe(new MyListener(), "pay_result");
  
  当有其他客户端往 pay_result频道发送消息时,订阅将会收到消息。
  
  不过需要注意的是,jedis#subscribe 是一个阻塞方法,调用之后将会阻塞主线程的,所以如果需要在正式项目使用需要使用异步线程运行,这里就不演示具体的代码了。
  
  基于 Spring-Data-Redis 开发发布订阅
  
  原生 jedis 发布订阅操作,相对来说还是有点复杂。现在我们很多应用已经基于 SpringBoot 开发,使用 spring-boot-starter-data-redis ,可以简化发布订阅开发。
  
  首先我们需要引入相应的 startter 依赖:
  
  <dependency>
  
  <groupId>org.springframework.boot</groupId>
  
  <artifactId>spring-boot-starter-data-redis</artifactId>
  
  <exclusions>
  
  <exclusion>
  
  <artifactId>lettuce-core</artifactId>
  
  <groupId>io.lettuce</groupId>
  
  </exclusion>
  
  </exclusions>
  
  </dependency>
  
  <dependency>
  
  <groupId>redis.clients</groupId>
  
  <artifactId>jedis</artifactId>
  
  </dependency>
  
  这里我们使用 Jedis 当做底层连接客户端,所以需要排除 lettuce,然后引入 Jedis 依赖。
  
  然后我们需要创建一个消息接收类,里面需要有方法消费消息:
  
  @Slf4j
  
  public class Receiver {
  
  private AtomicInteger counter = new AtomicInteger();
  
  public void receiveMessage(String message) {
  
  log.info("Received <" + message + ">");
  
  counter.incrementAndGet();
  
  }
  
  public int getCount() {
  
  return counter.get();
  
  }
  
  }
  
  接着我们只需要注入 Spring- Redis 相关 Bean,比如:
  
  StringRedisTemplate,用来操作 Redis 命令
  
  MessageListenerAdapter ,消息监听器,可以在这个类注入我们上面创建消息接受类 Receiver
  
  RedisConnectionFactory, 创建 Redis 底层连接
  
  @Configuration
  
  public class MessageConfiguration {
  
  @Bean
  
  RedisMessageListenerContainer container(RedisConnectionFactory connectionFactory,
  
  MessageListenerAdapter listenerAdapter) {
  
  RedisMessageListenerContainer container = new RedisMessageListenerContainer();
  
  container.setConnectionFactory(connectionFactory);
  
  // 订阅指定频道使用 ChannelTopic
  
  // 订阅模式使用 PatternTopic
  
  container.addMessageListener(listenerAdapter, new ChannelTopic("pay_result"));
  
  return container;
  
  }
  
  @Bean
  
  MessageListenerAdapter listenerAdapter(Receiver receiver) {
  
  // 注入 Receiver,指定类中的接受方法
  
  return new MessageListenerAdapter(receiver, "receiveMessage");
  
  }
  
  @Bean
  
  Receiver receiver() {
  
  return new Receiver();
  
  }
  
  @Bean
  
  StringRedisTemplate template(RedisConnectionFactory connectionFactory) {
  
  return new StringRedisTemplate(connectionFactory);
  
  }
  
  }
  
  最后我们使用 StringRedisTemplate#convertAndSend 发送消息,同时 Receiver 将会收到一条消息。
  
  @SpringBootApplication
  
  public class MessagingRedisApplication {
  
  public static void main(String[] args) throws InterruptedException {
  
  ApplicationContext ctx = SpringApplication.run(MessagingRedisApplication.class, args);
  
  StringRedisTemplate template = ctx.getBean(StringRedisTemplate.class);
  
  Receiver receiver = ctx.getBean(Receiver.class);
  
  while (receiver.getCount() == 0) {
  
  template.convertAndSend("pay_result", "Hello from Redis!");
  
  Thread.sleep(500L);
  
  }
  
  System.exit(0);
  
  }
  
  }
  
  Redis 发布订阅实际应用
  
  Redis Sentinel 节点发现
  
  Redis Sentinel 是 Redis 一套高可用方案,可以在主节点故障的时候,自动将从节点提升为主节点,从而转移故障。
  
  今天这里我们不详细解释 Redis Sentinel 详细原理,主要来看下 Redis Sentinel 如何使用发布订阅机制。
  
  Redis Sentinel 节点主要使用发布订阅机制,实现新节点的发现,以及交换主节点的之间的状态。
  
  如下所示,每一个 Sentinel 节点将会定时向 _sentinel_:hello 频道发送消息,并且每个 Sentinel 都会订阅这个节点。
  
  这样一旦有节点往这个频道发送消息,其他节点就可以立刻收到消息。
  
  这样一旦有的新节点加入,它往这个频道发送消息,其他节点收到之后,判断本地列表并没有这个节点,于是就可以当做新的节点加入本地节点列表。
  
  除此之外,每次往这个频道发送消息内容可以包含节点的状态信息,这样可以作为后面 Sentinel 领导者选举的依据。
  
  以上都是对于 Redis 服务端来讲,对于客户端来讲,我们也可以用到发布订阅机制。
  
  当 Redis Sentinel 进行主节点故障转移,这个过程各个阶段会通过发布订阅对外提供。
  
  对于我们客户端来讲,比较关心切换之后的主节点,这样我们及时切换主节点的连接(旧节点此时已故障,不能再接受操作指令),
  
  客户端可以订阅 +switch-master频道,一旦 Redis Sentinel 结束了对主节点的故障转移就会发布主节点的的消息。
  
  redission 分布式锁
  
  redission 开源框架提供一些便捷操作 Redis 的方法,其中比较出名的 redission 基于 Redis 的实现分布式锁。
  
  今天我们来看下 Redis 的实现分布式锁中如何使用 Redis 发布订阅机制,提高加锁的性能。
  
  PS:redission 分布式锁实现原理,可以参考之前写过的文章:
  
  可重入分布式锁的实现方式
  
  Redis 分布式锁,看似简单,其实真不简单
  
  首先我们来看下 redission 加锁的方法:
  
  Redisson redisson = ....
  
  RLock redissonLock = redisson.getLock("xxxx");
  
  redissonLock.lock();
  
  RLock 继承自 Java 标准的 Lock 接口,调用 lock 方法,如果当前锁已被其他客户端获取,那么当前加锁的线程将会被阻塞,直到其他客户端释放这把锁。
  
  这里其实有个问题,当前阻塞的线程如何感知分布式锁已被释放呢?
  
  这里其实有两种实现方法:
  
  第一钟,定时查询分布时锁的状态,一旦查到锁已被释放(Redis 中不存在这个键值),那么就去加锁。
  
  实现伪码如下:
  
  while (true) {
  
  boolean result=lock();
  
  if (!result) {
  
  Thread.sleep(N);
  
  }
  
  }
  
  这种方式实现起来起来简单,不过缺点也比较多。
  
  如果定时任务时间过短,将会导致查询次数过多,其实这些都是无效查询。
  
  如果定时任务休眠时间过长,那又会导致加锁时间过长,导致加锁性能不好。
  
  那么第二种实现方案,就是采用服务通知的机制,当分布式锁被释放之后,客户端可以收到锁释放的消息,然后第一时间再去加锁。
  
  这个服务通知的机制我们可以使用 Redis 发布订阅模式。
  
  当线程加锁失败之后,线程将会订阅 redisson_lock__channel_xxx(xx 代表锁的名称) 频道,使用异步线程监听消息,然后利用 Java 中 Semaphore 使当前线程进入阻塞。
  
  一旦其他客户端进行解锁,redission 就会往这个redisson_lock__channel_xxx 发送解锁消息。
  
  等异步线程收到消息,将会调用 Semaphore 释放信号量,从而让当前被阻塞的线程唤醒去加锁。
  
  ps:这里只是简单描述了 redission 加锁部分原理,出于篇幅,这里就不再消息解析源码。
  
  感兴趣的小伙伴可以自己看下 redission 加锁的源码。
  
  通过发布订阅机制,被阻塞的线程可以及时被唤醒,减少无效的空转的查询,有效的提高的加锁的效率。
  
  ps: 这种方式,性能确实提高,但是实现起来的复杂度也很高,这部分源码有点东西,快看晕了。
  
  总结
  
  今天我们主要介绍 Redis 发布订阅功能,主要对应的 Redis 命令为:
  
  subscribe channel [channel ...] 订阅一个或多个频道
  
  unsubscribe channel 退订指定频道
  
  publish channel message 发送消息
  
  psubscribe pattern 订阅指定模式
  
  punsubscribe pattern 退订指定模式
  
  我们可以利用 Redis 发布订阅功能,实现的简单 MQ 功能,实现上下游的解耦。
  
  不过需要注意了,由于 Redis 发布的消息不会被持久化,这就会导致新订阅的客户端将不会收到历史消息。
  
  所以,如果当前的业务场景不能容忍这些缺点,那还是用专业 MQ 吧。
  
  最后介绍了两个使用 Redis 发布订阅功能使用场景供大家参考。
...
'stack' => [
    'driver' => 'stack',
    // 'channels' => ['single'],
    'channels' => ['daily', 'es'],
    'ignore_exceptions' => false,
],

// 自定义es日志处理器
// 一定要设置es的formatter!!!
'es' => [
    'driver' => 'monolog',
    'level' => 'debug',
    'handler' => ElasticsearchHandler::class,
    'formatter' => ElasticsearchFormatter::class,
    'formatter_with' => [
        'index' => 'lara_exception',
        'type' => 'lara_exception'
    ],
    'handler_with' => [
        'client' => ClientBuilder::create()->setHosts(config('es.hosts'))->build()
    ]
],

# 这样就添加了es作为日志记录的驱动了
# 测试一下吧 比如访问这个路由 查看你的本地文件和es吧
Route::get('cex', function () {
    // laravel会将其转换成一个symfony的致命异常
    aaa
});

其他方式:想要维持一个高性能的、功能强大的日志服务的话,可以考虑添加一个异步的日志handler,其实monolog也已经提供了开箱即用的handler

更多用法请查看laravel组件的日志部分文档,感兴趣的道友可以自行查看laravel log和monolog的源码

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值