laravel的mysql的分表及分表联查解决方案

0X00 问题初现

php的laravel如何实现水平分表?最近手里有个项目,框架使用php的laravel7框架,由于数据量激增,当时为了快速迭代开发的详情表现在一晚上就干出30万行的数据量,主表已经一千两百万的数据。因为这只是用来存储记录用户视频详情,所以决定使用按月进行水平分表。

初期直接使用union all进行联表,导致sql没有走索引,早上六点客户疯狂发消息说生产挂了,赶紧排查发现所有sql都在全表扫描,把SQL服务器打死了,语句如下

select * from (select * from video_time_info_details union all select * from video_time_info_details_10_1) where xxxx

因为xxx条件是在Union之后的,所以不会走索引,所以就挂了,因此对分表这一块进行重构

0x01 重构分表逻辑

1. model插入、更新分表

更新的分表很简单,直接在__construct构造方法里对表新增月数后缀,判断下表是否存在,不存在则创建,存在则直接插入。这里因为属于详情表,调用非常频繁,所以进行缓存,避免每次都查询数据库判断是否存在表,代码如下


class VideoTimeInfoDetails extends Model
{
    use SerializeDate, SplitTableTrait;

    protected $table;

    public function __construct(array $attributes = [])
    {
        parent::__construct($attributes);
        // 手动实现分表,按月分表
        $this->table = 'video_time_info_details_' . date('Y_m') . "_1"; // 现在分表一个月存一次。后缀加_1是为了以后进行按日分表时,可以快速避免按月分表造成的无法联表的问题,避免给以后留坑
        $hasTable = Cache::get('has_' . $this->table, "0"); // 对当前表进行缓存
        if ($hasTable == "0") { // 如果不存在缓存
            if (!Schema::hasTable($this->table)) { // 如果不存在表,则创建表
                DB::update('create table ' . DB::getTablePrefix() . $this->table . ' like ' . DB::getTablePrefix() . 'video_time_info_details');
            }
            Cache::set('has_' . $this->table, "1");
        }
    }
}

这样我们对模型model进行正常的save、update、insert就会自动进行分表

2.model的查询

经过文档研究,准备使用laravel的globalScope对所有目标model进行监听,针对需要进行分表查询的业务流程新增makeUnionQuery方法用于返回model的queryBuilder,并且对sql的表名留下一个特殊的标识,这样我们在我们自己封装的globalScope中监听这个标识,然后把sql和where条件重新拼装,就可以构造带条件的union语句。

(1)对model的处理

对model的boot方法新增一个全局scope,代码如下:

    public static function boot()
    {
        parent::boot();

        static::addGlobalScope(new SplitTableScope); // 该方法在下方
    }

对model新增一个makeUnionQuery方法替换原来的query方法,这样以前的VideoTimeInfoDetail::query()就可以替换为VideoTimeInfoDetail::makeUnionQuery()代码如下:


    /**
     * 构造联表UNION
     */
    public static function makeUnionQuery($startTime,$endTime = null)
    {
        $queryList = static::getSubTablesByMonth($startTime,$endTime); // 要查询的表,该方法在下面
        // 构造联表union
        $queries = collect();
        // 开始循环
        $unionTable = $queries->push("video_time_info_details");
        foreach ($queryList as $suffix) {
            $tempTable = 'video_time_info_details_' . $suffix;
            $hasTable = Cache::get('has_' . $tempTable, "0");
            if ($hasTable == "0") {
                if (!Schema::hasTable($tempTable)) {
                    DB::update('create table ' . DB::getTablePrefix() . $tempTable . ' like ' . DB::getTablePrefix() . 'video_time_info_details');
                }
                Cache::set('has_' . $tempTable, "1");
            }

            if ($hasTable == "1") {
                $queries->push($tempTable);
            }
        }

        $self = new self; // 返回model而不是raw query builder
        $unionTableSql = implode('{SPLIT_TABLE_FLAG}', $unionTable->toArray());

        return $self->setTable(DB::raw("{SPLIT_TABLE}$unionTableSql{SPLIT_TABLE}"));
    }

其中,getSubTablesByMonth方法为封装的用来获取,从制定开始日期到目标结束日期里,根据月数构建表名的方法,代码如下

/**
     * 按月分表
     */
    public static function getSubTablesByMonth($startTime, $endTime = null)
    {
        $endTime = empty($endTime) ? time() : $endTime;
        if ($endTime instanceof \Illuminate\Support\Carbon) {
            $endTime = $endTime->timestamp;
        }
        $now = strtotime(date("Y-m-1", $endTime)); // 当前1号的时间戳
        $indexTime = empty($startTime) ? time() : $startTime;
        if ($indexTime instanceof \Illuminate\Support\Carbon) {
            $indexTime = $indexTime->timestamp;
        }
        $indexTime = strtotime(date("Y-m-1", $indexTime)); // 开始时间的1号时间戳
        $queryList = [];
        while ($indexTime <= $now) {
            $queryList[] = date('Y_m', $indexTime) . "_1";
            $indexTime = strtotime("+1 month", $indexTime);
        }
        $queryList = array_unique($queryList); // 要查询的表
        return $queryList;
    }

接下来,创建类SplitTableScope,实现在执行mysql前对结果进行监听、替换,代码如下


namespace App\Scopes;

use Illuminate\Database\Eloquent\Scope;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\ScopeInterface;
use Illuminate\Support\Facades\DB;

class SplitTableScope implements Scope
{
    /**
     * Apply the scope to a given Eloquent query builder.
     *
     * @param  \Illuminate\Database\Eloquent\Builder  $builder
     * @param  \Illuminate\Database\Eloquent\Model  $model
     * @return void
     */
    public function apply(Builder $builder, Model $model)
    {
        $query = $builder->getQuery();
        $from = $query->from;
        $query->from = null;
        if (strpos($from, "{SPLIT_TABLE}") === false) {
            return;
        }
        $splitFrom = str_replace("{SPLIT_TABLE}", "", $from);
        $splitTables = explode("{SPLIT_TABLE_FLAG}", $splitFrom);

        // 处理where
        $wheres = (array)$query->wheres;

        $columns = empty($query->columns) ? "*" : $query->columns;
        $bindings = empty($query->bindings) ? [] : $query->bindings;
        $myBindings = []; // 记录bindings否则后面会出现?与参数不一致的情况
        // 简单拼一下
        $queries = collect();
        foreach ($splitTables as $table) {
            $tempDb = DB::table($table);
            $tempBindings = [];
            foreach ($wheres as $key => $where) {
                $tempDb->where($where['column'], $where['operator'], $where['value'], $where['boolean']);
                $tempBindings[] = $where['value'];
            }
            $myBindings = array_merge($myBindings, $tempBindings);
            $queries->push($tempDb->select($columns));
        }

        $firstQuery = $queries->shift();

        $queries->each(function ($item, $key) use ($firstQuery) {
            $firstQuery->unionAll($item);
        });

        $bindings = array_merge($myBindings, $bindings);
        $query->bindings = $bindings;
        $query->from = DB::raw("({$firstQuery->toSql()}) as video_time_info_details_all");

    }
}

以上,就对我们原有的sql的from进行了一次替换,并且在原有的基础上将bindings拼接上了,之后执行的wherelimit都将进行自动拼接新sql,而且会走索引,速度大大提高。具体的调用方法如下

VideoTimeInfoDetails::makeUnionQuery($LearnTime->created_at,$LearnTime->updated_at)->where('video_time_info_id',$LearnTime['id'])->orderByDesc('start_play_time')->first();

0x02 生产现状

因为还有一个端是使用的go语言,所以go的分表查询在后面再赘述吧。看下现在生产的表行数量:

1.表数量

在这里插入图片描述
因为是在10月份使用的分表,所以为了防止之前月份出现问题,在创建的时候也会根据时间对之前的日期进行创建

2.主表数量

在这里插入图片描述
1800万的时候进行分表

3.10月份表数量

在这里插入图片描述
也到了400万了,后面还得细分

0x03 结束语

因为这个项目的detail必须存,而且要保存3年,当时没想到数据量会增长的如此之快,早知如此当时该全部存到mongodb之类的日志数据库里了。不过好在甲方爸爸有钞能力,这样也可以顶住压力~

智能网联汽车的安全员高级考试涉及多个方面的专业知识,包括但不限于自动驾驶技术原理、车辆传感器融合、网络安全防护以及法律法规等内容。以下是针对该主题的些核心知识点解析: ### 关于智能网联车安全员高级考试的核心内容 #### 1. 自动驾驶分级标准 国际自动机工程师学会(SAE International)定义了六个级别的自动驾驶等级,从L0到L5[^1]。其中,L3及以上级别需要安全员具备更高的应急处理能力。 #### 2. 车辆感知系统的组成与功能 智能网联车通常配备多种传感器,如激光雷达、毫米波雷达、摄像头和超声波传感器等。这些设备协同工作以实现环境感知、障碍物检测等功能[^2]。 #### 3. 数据通信与网络安全 智能网联车依赖V2X(Vehicle-to-Everything)技术进行数据交换,在此过程中需防范潜在的网络攻击风险,例如中间人攻击或恶意软件入侵[^3]。 #### 4. 法律法规要求 不同国家和地区对于无人驾驶测试及运营有着严格的规定,考生应熟悉当地交通法典中有关自动化驾驶部分的具体条款[^4]。 ```python # 示例代码:模拟简单决策逻辑 def decide_action(sensor_data): if sensor_data['obstacle'] and not sensor_data['emergency']: return 'slow_down' elif sensor_data['pedestrian_crossing']: return 'stop_and_yield' else: return 'continue_driving' example_input = {'obstacle': True, 'emergency': False, 'pedestrian_crossing': False} action = decide_action(example_input) print(f"Action to take: {action}") ``` 需要注意的是,“橙点同学”作为特定平台上的学习资源名称,并不提供官方认证的标准答案集;建议通过正规渠道获取教材并参加培训课程来准备此类资格认证考试。
评论 7
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Framework大林

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值