Featured image of post 在 Laravel 中当 MySQL 异常宕机时强制返回空数据

在 Laravel 中当 MySQL 异常宕机时强制返回空数据

数据库有时候挂了, 我们希望还能继续从 Redis 中查询数据

起因

  • 之前线上遇到一个问题, 就是当MySQL挂了, 然后导致整个服务崩塌, Redis在前面完全没分担任何压力.
  • 业务常规的查询逻辑如下:
    1. redis中获取数据, 有则返回
    2. 当第一步redis无数据, 去MySQL查询数据
    3. 把第二步查询到的数据写入redis
    4. 返回数据

问题分析

  • redis当然不会有问题, 问题是在第二步的时候
  • MySQL查询数据,数据库服务已经宕机, 这时候请求阻塞住
  • 阻塞超时,然后抛出异常,导致无法走到第三步
  • 下一次请求来, 又继续去连接MySQL,无限阻塞,把业务服务器也拖垮

解决方案

  • 这是我们的解决方案, 不一定适合所有业务. 当MySQL宕机强制缓存空数据到redis,允许部分页面为空.而不是无法提供服务

解决思路

  • 设置好合理的MySQL连接超时时间
    • mysqlnd.net_read_timeout = 3
    • 当数据库连接超时之后, 抛出异常
  • 新建一个基础模型BaseModel, 其它所有模型继承这个模型, 并重写newEloquentBuilder方法
<?php

namespace App\Models;


use Illuminate\Database\Eloquent\Model;

class BaseModel extends Model
{
    public function newEloquentBuilder($query)
    {
        return new MysqlCustomBuilder($query);
    }
}
  • 新建一个查询构造器类MysqlCustomBuilder
<?php

namespace App\Models\Database;

use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Str;

class MysqlCustomBuilder extends Builder
{
    public function get($columns = ['*'])
    {
        try {
            return parent::get($columns);
        } catch (\Exception $e) {

            // 根据 laravel 重连的错误码
            $message = $e->getMessage();
            if (Str::contains($message, [
                'server has gone away',
                'no connection to the server',
                'Lost connection',
                'is dead or not enabled',
                'Error while sending',
                'decryption failed or bad record mac',
                'server closed the connection unexpectedly',
                'SSL connection has been closed unexpectedly',
                'Error writing data to the connection',
                'Resource deadlock avoided',
            ])) {
                // 记录日志, 通知xxx
                // Log::error($e);
                // 强制返回空集合
                return Collection::make();
            }

            // 如果不在处理的范围内, 继续抛出异常
            throw $e;
        }
    }
}
  • 之后需要重点监控日志报错, 来确定页面为空是运营配置的问题还是数据库异常的问题