laravel配置mysql主从及使用注意

司徒元明
2023-12-01

关于主从数据库,有两种方式。
1、自己搭建主从数据库,然后在代码层面上将请求定向到主或从。
2、使用类似阿里云的服务,一种是把主从连接都配置上,这就和普通的主从一样了。另一种就是你可以在代码里只配置一个数据库(主库)的连接,然后阿里云根据你的SQL判断将请求定向到主或从,在代码层面你看不出是主从配置,这种操作使你可以很灵活的上下从库,而代码不需要做任何改动,但是弊端就是不能从代码层面决定去主或从(当然,你可以通过开启事务的方式强制到主库)。

无论是哪种方式,开启了主从之后,在编码过程中需要注意的是主从延迟是必然会出现的,不可能消除!!所以,对于查询操作,如果没查到,并不是真的不存在,有可能是没同步过来。

1、在做数据一致性校验的时候,请读主库;
2、在同一个会话中先写再读,请读主库;
3、实时计算或统计的时候,请读主库;
。。。。。。

强调以上三点是因为我最近踩过的坑,公司用的就是阿里云的主从,而在Laravel的配置中是看不到的(只配了一个连接),所以我没意识到是主从,在统计的时候偶尔会出现偏差,在排查完代码之后才怀疑到可能是数据没同步的原因,继而才知道是配置了主从,并且无法将读定向到主库(除非开启事务),所以不得不重写该功能。这也提醒了我在编码的时候要以主从来考虑问题,即使项目暂时没上主从,如果以后业务需要上了主从,你要能确保自己的功能不会出问题。


Laravel配置主从

文档地址:https://learnku.com/docs/laravel/5.7/database/2288

有时候你希望 SELECT 语句使用一个数据库连接,而 INSERT,UPDATE,和 DELETE 语句使用另一个数据库连接。在 Laravel 中,无论你是使用原生查询,查询构造器,或者是 Eloquent ORM,都能轻松的实现。

配置文件 config/database.php

1、一主一从配置

'mysql' => [
    'read' => [
        'host' => env('DB_READ1_HOST', ''),
        'username'=>env('DB_READ1_USERNAME', ''),
        'password'=>env('DB_READ1_PASSWORD', ''),
    ],
    'write' => [
        'host' => env('DB_WRITE1_HOST', ''),
        'username'=>env('DB_WRITE1_USERNAME', ''),
        'password'=>env('DB_WRITE1_PASSWORD', ''),
    ],
    'sticky'    => true,
    'driver'    => 'mysql',
    'database'  => 'database',
    'charset'   => 'utf8mb4',
    'collation' => 'utf8mb4_unicode_ci',
    'prefix'    => '',
],

2、一主多从

'mysql' => [
    'read' => [
        [
	        'host' => env('DB_READ1_HOST', ''),
	        'username'=>env('DB_READ1_USERNAME', ''),
	        'password'=>env('DB_READ1_PASSWORD', ''),
        ],
        [
	        'host' => env('DB_READ2_HOST', ''),
	        'username'=>env('DB_READ2_USERNAME', ''),
	        'password'=>env('DB_READ2_PASSWORD', ''),
        ],
    ],
    'write' => [
        'host' => env('DB_WRITE1_HOST', ''),
        'username'=>env('DB_WRITE1_USERNAME', ''),
        'password'=>env('DB_WRITE1_PASSWORD', ''),
    ],
    'sticky'    => true,
    'driver'    => 'mysql',
    'database'  => 'database',
    'charset'   => 'utf8mb4',
    'collation' => 'utf8mb4_unicode_ci',
    'prefix'    => '',
],

3、多主多从

'mysql' => [
    'read' => [
        [
	        'host' => env('DB_READ1_HOST', ''),
	        'username'=>env('DB_READ1_USERNAME', ''),
	        'password'=>env('DB_READ1_PASSWORD', ''),
        ],
        [
	        'host' => env('DB_READ2_HOST', ''),
	        'username'=>env('DB_READ2_USERNAME', ''),
	        'password'=>env('DB_READ2_PASSWORD', ''),
        ],
    ],
    'write' => [
        [
	        'host' => env('DB_WRITE1_HOST', ''),
	        'username'=>env('DB_WRITE1_USERNAME', ''),
	        'password'=>env('DB_WRITE1_PASSWORD', ''),
        ],
        [
	        'host' => env('DB_WRITE2_HOST', ''),
	        'username'=>env('DB_WRITE2_USERNAME', ''),
	        'password'=>env('DB_WRITE2_PASSWORD', ''),
        ],
    ],
    'sticky'    => true,
    'driver'    => 'mysql',
    'database'  => 'database',
    'charset'   => 'utf8mb4',
    'collation' => 'utf8mb4_unicode_ci',
    'prefix'    => '',
],

4、关于 sticky 配置项

sticky 是一个 可选值,它可用于立即读取在当前请求周期内已写入数据库的记录。若 sticky 选项被启用,并且当前请求周期内执行过 「写」 操作,那么任何 「读」 操作都将使用 「写」 连接。这样可确保同一个请求周期内写入的数据可以被立即读取到,从而避免主从延迟导致数据不一致的问题。不过是否启用它,取决于应用程序的需求。

意思就是,如果 sticky 为 true ,那么只要当前连接发生了“写”操作,那么此连接后续的所有SQL都定向到主库了,其实就是为了防止同一个连接中先写再读数据延迟的问题。

如果你的项目刚开始没用使用主从,现在才加上主从,那么应该设置 sticky 为 true ,因为你无法知道项目中是否存在“同一个连接中先写再读”的情况;实际上 sticky 为 true 会增加主库的压力,使主从发挥的效果降低了。

5、读操作定向到主库

很多时候为了保证数据及时性,你需要将读操作定向到主库。

Laravel5读写分离主要有两个过程:
第一步,根据database.php配置,创建写库和读库的连接connection。
第二步,调用select时先判断使用读库还是写库,而insert / update / delete / statement统一使用写库。

第一步分析:Illuminate/Database/Connectors/ConnectionFactory.php

public function make(array $config, $name = null)
{
    $config = $this->parseConfig($config, $name);

    if (isset($config['read'])) {
    	// 开启了读写分离
        return $this->createReadWriteConnection($config);
    }
	
    return $this->createSingleConnection($config);
}
// 创建read连接,write连接
protected function createReadWriteConnection(array $config)
{
	// 创建write连接
    $connection = $this->createSingleConnection($this->getWriteConfig($config));
	// 创建read连接
    return $connection->setReadPdo($this->createReadPdo($config));
}

// 创建write连接,因为默认就是非主从的,只有一个库,切实主库
protected function createSingleConnection(array $config)
{
    $pdo = $this->createPdoResolver($config);

    return $this->createConnection(
        $config['driver'], $pdo, $config['database'], $config['prefix'], $config
    );
}
// 随机取一个连接配置
protected function getReadWriteConfig(array $config, $type)
{
    return isset($config[$type][0])
                    ? Arr::random($config[$type])
                    : $config[$type];
}

由以上源码分析可知,开启了主从之后,laravel会预先创建一个read连接和一个write连接,如果是多主多从,则从多主中随机选择一个配置,从多从中随机选择一个配置来建立连接。

第二部分析:Illuminate/Database/Connection.php

一、对于select操作
1、可根据第三个输入参数判断使用读库还是写库,默认读库。
2、开启了事务,则一律去写库。
3、开启了 sticky 且发生了写操作,则一律去写库。

public function select($query, $bindings = [], $useReadPdo = true)
{
    return $this->run($query, $bindings, function ($query, $bindings) use ($useReadPdo) {
        if ($this->pretending()) {
            return [];
        }

        // For select statements, we'll simply execute the query and return an array
        // of the database result set. Each element in the array will be a single
        // row from the database table, and will either be an array or objects.
        $statement = $this->prepared($this->getPdoForSelect($useReadPdo)
                          ->prepare($query));

        $this->bindValues($statement, $this->prepareBindings($bindings));

        $statement->execute();

        return $statement->fetchAll();
    });
}

protected function getPdoForSelect($useReadPdo = true)
{
    return $useReadPdo ? $this->getReadPdo() : $this->getPdo();
}

/*  获取读库连接
	1、开启了事务,则一律走写库
	2、sticky 为 true,并且发生了写操作,则一律走写库
*/
public function getReadPdo()
{
    if ($this->transactions > 0) {
        return $this->getPdo();
    }

    if ($this->recordsModified && $this->getConfig('sticky')) {
        return $this->getPdo();
    }

    if ($this->readPdo instanceof Closure) {
        return $this->readPdo = call_user_func($this->readPdo);
    }

    return $this->readPdo ?: $this->getPdo();
}

// 获取写库连接
public function getPdo()
{
    if ($this->pdo instanceof Closure) {
        return $this->pdo = call_user_func($this->pdo);
    }

    return $this->pdo;
}

那么如何设置 select 的第三个参数呢?
1、使用原生SQL时

DB::select('select * from users where active = ?', [1], false);
或者
DB::selectFromWriteConnection('select * from users where active = ?', [1]);

源码文件:Illuminate/Database/Connection.php

public function selectFromWriteConnection($query, $bindings = [])
{
    return $this->select($query, $bindings, false);
}

2、使用模型时

User::onWriteConnection()->find($id);

源码文件:Illuminate/Database/Eloquent/Model

public static function onWriteConnection()
{
    $instance = new static;
    // query builder 指定使用写库
    return $instance->newQuery()->useWritePdo();
}

看看query builder如何指定使用写库 主要文件:Illuminate/Database/Query/Builder


/**
 * Use the write pdo for query.
 *
 * @return $this
 */
public function useWritePdo()
{
    $this->useWritePdo = true;
 
    return $this;
}
 
/**
 * Run the query as a "select" statement against the connection.
 *
 * @return array
 */
protected function runSelect()
{
    return $this->connection->select($this->toSql(), $this->getBindings(), ! $this->useWritePdo);
}

二、对于 insert / delete / update / statement 操作,一律走写库

public function insert($query, $bindings = [])
{
    return $this->statement($query, $bindings);
}

public function update($query, $bindings = [])
{
    return $this->affectingStatement($query, $bindings);
}

public function delete($query, $bindings = [])
{
    return $this->affectingStatement($query, $bindings);
}

public function statement($query, $bindings = [])
{
    return $this->run($query, $bindings, function ($query, $bindings) {
        if ($this->pretending()) {
            return true;
        }

        $statement = $this->getPdo()->prepare($query);

        $this->bindValues($statement, $this->prepareBindings($bindings));

        $this->recordsHaveBeenModified();

        return $statement->execute();
    });
}

public function affectingStatement($query, $bindings = [])
{
    return $this->run($query, $bindings, function ($query, $bindings) {
        if ($this->pretending()) {
            return 0;
        }

        // For update or delete statements, we want to get the number of rows affected
        // by the statement and return that back to the developer. We'll first need
        // to execute the statement and then we'll use PDO to fetch the affected.
        $statement = $this->getPdo()->prepare($query);

        $this->bindValues($statement, $this->prepareBindings($bindings));

        $statement->execute();

        $this->recordsHaveBeenModified(
            ($count = $statement->rowCount()) > 0
        );

        return $count;
    });
}
 类似资料: