Laravel讀寫分離原理

SnDragon發表於2021-01-20

無論是使用原生SQL查詢,查詢構建器還是Eloquent模型,執行資料庫操作可以大致分為四步:

  1. 生成連線物件(5.5之後的版本在這一步不會真正建立資料庫連線)
  2. 構造SQL
  3. 選擇讀連線還是寫連線,建立與資料庫的連線
  4. 執行SQL

1. 生成連線物件

無論是哪種方式,最後都會執行到Illuminate\Database\Connectors\ConnectionFactory::make方法

    public function make(array $config, $name = null)
    {
        // 解析和準備DB配置
        $config = $this->parseConfig($config, $name);
        // 如果有配置讀寫分離則呼叫createReadWriteConnection方法建立兩個連線
        // 這裡用到了懶載入到思想,不會真正建立連線,見下文
        if (isset($config['read'])) {
            return $this->createReadWriteConnection($config);
        }
        // 沒有配置讀寫分離則呼叫createSingleConnection方法建立單個連線
        return $this->createSingleConnection($config);
    }

createReadWriteConnection方法:

    protected function createReadWriteConnection(array $config)
    {
        // 建立\Illuminate\Database\Connection物件
        $connection = $this->createSingleConnection($this->getWriteConfig($config));
        // 建立獲取讀PDO的閉包並賦值給$connection的readPdo屬性
        return $connection->setReadPdo($this->createReadPdo($config));
    }

    protected function createSingleConnection(array $config)
    {
        // 這裡的pdo是一個閉包,執行閉包才會真正建立連線獲取到寫PDO
        $pdo = $this->createPdoResolver($config);
        /**
         * 根據不同的資料庫驅動(mysql,pgsql,sqlite等)建立不同的連線
         * 把$pdo閉包物件作為對應的建構函式引數傳進去
        **/
        return $this->createConnection(
            $config['driver'], $pdo, $config['database'], $config['prefix'], $config
        );
    }

    // 實際實現還會根據database是否配置了host選項去做不同解析,這裡簡化了
    protected function createPdoResolver(array $config)
    {
        return function () use ($config) {
            return $this->createConnector($config)->connect($config);
        };
    }

值得注意的一點是,這裡的pdo是一個閉包物件實際上是做了最佳化了,這裡建立Connection物件用到的$pdo/$readPdo物件是透過createPdoResolver獲取到的閉包,也就是不會真正去建立PDO物件。

對比5.1:

    protected function createSingleConnection(array $config)
    {
        // 真正的建立連線
        $pdo = $this->createConnector($config)->connect($config);

        return $this->createConnection($config['driver'], $pdo, $config['database'], $config['prefix'], $config);
    }

5.1在createReadWriteConnection這一步就會同時建立讀連線和寫連線,假如一個請求是隻讀的,這意味著寫連線是不必要的,而且建立連線後需要等到請求結束才會釋放,對於某些耗時的請求意味著寫連線被白白佔用,甚至導致資料庫連線過多的錯誤,而新版本透過引入閉包實現了懶載入,解決了這個問題。

2. 構造SQL

主要是根據鏈式呼叫拼接SQL語句,繫結引數等,不是本文重點,這裡不展開

3. 選擇讀連線還是寫連線

最新版本會在這一步選擇讀連線還是寫連線並真正與資料庫建立連線

select語句:

Illuminate\Database\Query\Builder.php

    protected function runSelect()
    {
        // 呼叫第一步獲取到的Connection物件去執行查詢
        // 這裡會傳useWritePdo屬性指定是使用讀連線還是寫連線(預設是false,即使用讀連線)
        // 可以在查詢之前呼叫useWritePdo方法,手動指定用寫連線查詢
        // select ... for update也會設定useWritePdo=true,參考lockForUpdate方法
        return $this->connection->select($this->toSql(), $this->getBindings(), ! $this->useWritePdo);
    }

Illuminate\Database\Connection.php

    public function select($query, $bindings = [], $useReadPdo = true)
    {
        return $this->run($query, $bindings, function ($me, $query, $bindings) use ($useReadPdo) {
            if ($me->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->getPdoForSelect($useReadPdo)->prepare($query);

            $statement->execute($me->prepareBindings($bindings));

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

可以看到決定使用讀連線還是寫連線的關鍵程式碼是這一句:

$this->getPdoForSelect($useReadPdo)

具體實現:

    protected function getPdoForSelect($useReadPdo = true)
    {
        // 根據$useReadPdo去決定呼叫getReadPdo()還是getPdo()
        return $useReadPdo ? $this->getReadPdo() : $this->getPdo();
    }

    // getPdo很簡單,如果$this->pdo是閉包(第一次執行的時候),則執行閉包建立與資料庫的連線並重新賦值,否則直接返回
    public function getPdo()
    {
        if ($this->pdo instanceof Closure) {
            return $this->pdo = call_user_func($this->pdo);
        }

        return $this->pdo;
    }

    // 獲取讀Pdo,不一定就使用讀連線
    public function getReadPdo()
    {
        if ($this->transactions >= 1) {
            return $this->getPdo();
        }

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

        // 跟getPdo()類似,用閉包實現懶載入
        if ($this->readPdo instanceof Closure) {
            return $this->readPdo = call_user_func($this->readPdo);
        }

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

可以看到就算呼叫了getReadPdo()方法,最終也不一定是使用讀連線,以下幾種情況會使用寫連線:

  • 當前活躍事務數>0
  • 配置了sticky屬性為true(Laravel5.5引入)且$recordsModified為true(曾經執行過修改語句)
  • readPdo為空時(例如沒有配置讀寫分離時不會初始化readPdo)

修改語句

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


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

            // 透過getPdo方法使用寫Pdo
            $statement = $me->getPdo()->prepare($query);

            $statement->execute($me->prepareBindings($bindings));

            // 記錄修改過
            $this->recordsHaveBeenModified(
                ($count = $statement->rowCount()) > 0
            );

            return $count;
        });
    }

    // insert或其他語句會執行到statement這裡
    public function statement($query, $bindings = [])
    {
        return $this->run($query, $bindings, function ($me, $query, $bindings) {
            if ($me->pretending()) {
                return true;
            }

            $bindings = $me->prepareBindings($bindings);

            $this->recordsHaveBeenModified();

            return $me->getPdo()->prepare($query)->execute($bindings);
        });
    }

4. 執行SQL

這一步就是去調PHP原生的PDO相關方法去執行SQL語句,然後封裝結果,這裡不再贅述。

本作品採用《CC 協議》,轉載必須註明作者和本文連結

相關文章