對(duì)于我們線上的運(yùn)行環(huán)境來說,經(jīng)常會(huì)有的一種情況就是需要主從分離。關(guān)于主從分離有什么好處,怎么配之類的內(nèi)容不是我們學(xué)習(xí)框架的重點(diǎn)。但是你要知道的是,Laravel 以及現(xiàn)代化的所有框架都是可以方便地配置主從分離的。另外,我們還要再回去 查詢構(gòu)造器 中,看一下我們的原生 SQL 語句的拼裝語法到底是如何生成的。
其實(shí)配置非常簡(jiǎn)單,我們先來簡(jiǎn)單的看一下。之后,我們?cè)偕钊朐创a,看看它是怎么做到寫入走主庫(kù),讀取走從庫(kù)的。
'mysql2' => [
'driver' => 'mysql',
'read' => [
'host'=>[
'192.168.56.101'
]
],
'write' => [
'host'=>[
env('DB_HOST', '127.0.0.1'),
]
],
'url' => env('DATABASE_URL'),
// 'host' => env('DB_HOST', '127.0.0.1'),
'port' => env('DB_PORT', '3306'),
'database' => env('DB_DATABASE', 'forge'),
'username' => env('DB_USERNAME', 'forge'),
'password' => env('DB_PASSWORD', ''),
'unix_socket' => env('DB_SOCKET', ''),
'sticky' => true,
'charset' => 'utf8mb4',
'collation' => 'utf8mb4_unicode_ci',
'prefix' => '',
'prefix_indexes' => true,
'strict' => true,
'engine' => null,
'options' => extension_loaded('pdo_mysql') ? array_filter([
PDO::MYSQL_ATTR_SSL_CA => env('MYSQL_ATTR_SSL_CA'),
]) : [],
],
我們這里是修改的 config/database.php 文件??梢钥吹?,和原始配置不同的是我們注釋掉了原來的 hosts ,然后增加了 read 和 write ,在這兩個(gè)屬性里面可以以數(shù)組的形式指定 hosts 。這樣,我們的查詢語句和增刪改語句就實(shí)現(xiàn)了分離,查詢語句會(huì)走 read 的配置,而其它語句則會(huì)走 write 的配置。同時(shí),我們還多增加了一個(gè) sticky 并設(shè)置為 true 。它的作用是,在同一次的請(qǐng)求中,如果執(zhí)行了增刪改的操作,那么緊接著的查詢也會(huì)走 write 也就是主庫(kù)的查詢。這也是因?yàn)槲覀冊(cè)谀承I(yè)務(wù)中,需要在操作完數(shù)據(jù)后馬上查詢,主從之間的延遲可能會(huì)導(dǎo)致查詢的從庫(kù)數(shù)據(jù)不正確(這在現(xiàn)實(shí)業(yè)務(wù)中很常見)。因此,在一次增刪改操作后如果緊接著有查詢的話,我們當(dāng)前的這個(gè)請(qǐng)求流程還是會(huì)繼續(xù)查詢主庫(kù)。
接下來,我們定義兩個(gè)路由來測(cè)試。
Route::get('ms/test/insert', function(){
\Illuminate\Support\Facades\DB::connection('mysql2')->table('db_test')->insert(['name'=>'Lily', 'sex'=>2]);
dd( \Illuminate\Support\Facades\DB::connection('mysql2')->table('db_test')->get()->toArray());
});
Route::get('ms/test/list', function(){
dd( \Illuminate\Support\Facades\DB::connection('mysql2')->table('db_test')->get()->toArray());
});
在執(zhí)行第一個(gè)路由之后,dd() 打印的數(shù)據(jù)中我們會(huì)看到新添加成功的數(shù)據(jù)。接著去請(qǐng)求第二個(gè)路由,會(huì)發(fā)現(xiàn)數(shù)據(jù)還是原來的,并沒有增加新的數(shù)據(jù)。因?yàn)槲覀儾]有在 MySQL 配置主從同步,這也是為了方便我們的調(diào)試查看。很明顯,第二個(gè)路由的查詢語句走的就是另一個(gè)數(shù)據(jù)庫(kù)了。
對(duì)于如何實(shí)現(xiàn)的讀寫分離,我們從 原生查詢 的 select() 方法來看。找到 laravel/framework/src/Illuminate/Database/Connection.php 中的 select() 方法,可以看到它還有第三個(gè)參數(shù)。
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();
}
$useReadPdo 這個(gè)參數(shù)默認(rèn)就是一個(gè) true 值,方法體內(nèi)部,getPdoForSelect() 方法使用了這個(gè)參數(shù)。我們繼續(xù)向下看。
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();
}
// $this->readPdo laravel/framework/src/Illuminate/Database/Connectors/ConnectionFactory.php createPdoResolverWithHosts
這個(gè)方法中,其實(shí)沒有做別的,最核心的就是使用 call_user_func() 去調(diào)用這個(gè) $this->readPdo 方法,從這里可以看出這個(gè) $this->readPdo 應(yīng)該是一個(gè)回調(diào)函數(shù)。打印出來可以看到,它返回的是 laravel/framework/src/Illuminate/Database/Connectors/ConnectionFactory.php 的 createPdoResolverWithHosts() 方法所生成的一個(gè)回調(diào)函數(shù)。而其它的代碼都是在判斷在什么情況下直接去使用主庫(kù)的 PDO 連接。那么 $this->readPdo 是在什么時(shí)候定義的呢?在當(dāng)前這個(gè)文件中,我們找不到答案,Connection.php 中只有一個(gè) setReadPdo() 方法,但沒有調(diào)用設(shè)置它的代碼。
public function setReadPdo($pdo)
{
$this->readPdo = $pdo;
return $this;
}
那么我們就向上追溯,直接去 laravel/framework/src/Illuminate/Database/Connectors/ConnectionFactory.php 連接工廠類看看,發(fā)現(xiàn) createReadWriteConnection() 這個(gè)方法中調(diào)用了 setReadPdo() 方法。
public function make(array $config, $name = null)
{
$config = $this->parseConfig($config, $name);
if (isset($config['read'])) {
return $this->createReadWriteConnection($config);
}
return $this->createSingleConnection($config);
}
protected function createReadWriteConnection(array $config)
{
$connection = $this->createSingleConnection($this->getWriteConfig($config));
return $connection->setReadPdo($this->createReadPdo($config));
}
protected function createReadWriteConnection(array $config)
{
$connection = $this->createSingleConnection($this->getWriteConfig($config));
return $connection->setReadPdo($this->createReadPdo($config));
}
protected function createReadPdo(array $config)
{
return $this->createPdoResolver($this->getReadConfig($config));
}
protected function createPdoResolver(array $config)
{
return array_key_exists('host', $config)
? $this->createPdoResolverWithHosts($config)
: $this->createPdoResolverWithoutHosts($config);
}
protected function getReadConfig(array $config)
{
return $this->mergeReadWriteConfig(
$config, $this->getReadWriteConfig($config, 'read')
);
}
protected function getReadWriteConfig(array $config, $type)
{
return isset($config[$type][0])
? Arr::random($config[$type])
: $config[$type];
}
很明顯,在創(chuàng)建連接時(shí),make() 方法體內(nèi)根據(jù)配置文件是否有 read 配置,來調(diào)用這個(gè) createReadWriteConnection() 方法。然后順著我貼出的代碼,可以一路看到就是如果有read 配置,那么就會(huì)先使用 write 配置創(chuàng)建一個(gè)主連接,接著調(diào)用這個(gè)主連接的 setReadPdo() 方法并根據(jù) read 配置又創(chuàng)建了一個(gè)從數(shù)據(jù)庫(kù)連接。主對(duì)象是我們的 write 連接對(duì)象,而 read 連接對(duì)象是它的一個(gè)子對(duì)象。
在 createPdoResolver() 方法中,我們看到了上面發(fā)現(xiàn)的那個(gè)生成回調(diào)函數(shù)的 createPdoResolverWithHosts() 方法的使用。這一下大家應(yīng)該就真相大白了吧。如果還沒弄清楚的同學(xué),可以自己設(shè)置一下斷點(diǎn)調(diào)試調(diào)試,畢竟代碼位置和文件都給出了。
從這里我們可以看出,Laravel 是根據(jù)參數(shù)來判斷是否使用從庫(kù)連接進(jìn)行查詢的,而我之前看過其它框架的源碼,是 Yii 還是 TP 什么來著,有根據(jù)查詢語句是否有 SELECT 字符來判斷走從庫(kù)去查詢的,也很有意思,大家可以自己去研究下哈。
講完連接了我們?cè)倩貋碇v講數(shù)據(jù)庫(kù)連接中非常重要的一個(gè)東西,那就是 SQL 語句是怎么生成的。這里使用的是 語法 這個(gè)高大上的詞匯,實(shí)際上簡(jiǎn)單的理解就是 查詢構(gòu)造器 是如何生成 SQL 語句的。原生查詢 就不用多說了,都是我們自己寫 SQL 語句讓 PDO 執(zhí)行就好了。但是 查詢構(gòu)造器 以及上層的 Eloquent ORM 都是之前講過的面向?qū)ο笫降逆準(zhǔn)缴蓪?duì)象之后完成數(shù)據(jù)庫(kù)查詢的,這其中,肯定有 SQL 語句的生成過程,這就是我們接下來要學(xué)習(xí)的內(nèi)容。
其實(shí)我們?cè)?查詢構(gòu)造器 那篇文章中就已經(jīng)看到過 Laravel 是如何生成 SQL 語句了,還記得我們分析的那個(gè) update() 方法嗎?如果不記得的小伙伴可以回去看一下 【Laravel系列4.2】查詢構(gòu)造器https://mp.weixin.qq.com/s/vUImsLTpEtELgdCTWI6k2A 。在執(zhí)行 update() 操作時(shí),我們最后進(jìn)入了 laravel/framework/src/Illuminate/Database/Query/Grammars/Grammar.php 這個(gè)對(duì)象中。從名稱就可以看出,這是一個(gè) 語法 對(duì)象。在這個(gè)對(duì)象中會(huì)負(fù)責(zé)拼接真正的 SQL 語句。比如我再來看一下 insert() 最終到達(dá)的 compileInsert() 方法。
public function compileInsert(Builder $query, array $values)
{
// Essentially we will force every insert to be treated as a batch insert which
// simply makes creating the SQL easier for us since we can utilize the same
// basic routine regardless of an amount of records given to us to insert.
$table = $this->wrapTable($query->from);
if (empty($values)) {
return "insert into {$table} default values";
}
if (! is_array(reset($values))) {
$values = [$values];
}
$columns = $this->columnize(array_keys(reset($values)));
// We need to build a list of parameter place-holders of values that are bound
// to the query. Each insert should have the exact same amount of parameter
// bindings so we will loop through the record and parameterize them all.
$parameters = collect($values)->map(function ($record) {
return '('.$this->parameterize($record).')';
})->implode(', ');
return "insert into $table ($columns) values $parameters";
}
最終返回的這個(gè) SQL 語句,會(huì)交給連接,也就是 laravel/framework/src/Illuminate/Database/Connection.php 中的 insert() 方法來執(zhí)行。這個(gè)就是我們最早學(xué)習(xí)使用過的那個(gè)原生查詢所調(diào)用的方法。接下來,我們?cè)倏匆幌?get() 方法,也就是獲得查詢結(jié)果集的方法。在 Builder 中,get() 方法會(huì)調(diào)用一個(gè) runSelect() 方法,這個(gè)方法里面會(huì)再調(diào)用一個(gè) toSql() 方法,就是獲得原始查詢語句的方法。
public function toSql()
{
return $this->grammar->compileSelect($this);
}
可以看到,toSql() 又到語法對(duì)象中調(diào)用了 compileSelect() 方法。
public function compileSelect(Builder $query)
{
if ($query->unions && $query->aggregate) {
return $this->compileUnionAggregate($query);
}
// If the query does not have any columns set, we'll set the columns to the
// * character to just get all of the columns from the database. Then we
// can build the query and concatenate all the pieces together as one.
$original = $query->columns;
if (is_null($query->columns)) {
$query->columns = ['*'];
}
// To compile the query, we'll spin through each component of the query and
// see if that component exists. If it does we'll just call the compiler
// function for the component which is responsible for making the SQL.
$sql = trim($this->concatenate(
$this->compileComponents($query))
);
if ($query->unions) {
$sql = $this->wrapUnion($sql).' '.$this->compileUnions($query);
}
$query->columns = $original;
return $sql;
}
其中,基礎(chǔ)的 SELECT 語句的拼接是在 compileComponents() 中完成的,我們繼續(xù)進(jìn)入這個(gè)方法。
protected function compileComponents(Builder $query)
{
$sql = [];
foreach ($this->selectComponents as $component) {
if (isset($query->$component)) {
$method = 'compile'.ucfirst($component);
$sql[$component] = $this->$method($query, $query->$component);
}
}
return $sql;
}
貌似有點(diǎn)看不明白呀?這一個(gè)循環(huán)是在干嘛?其實(shí),從代碼中我們可以看,它在遍歷一個(gè)本地屬性 selectComponents ,并根據(jù)這個(gè)屬性里面的內(nèi)容去調(diào)用自身的這些方法。我們查看 selectComponents 屬性會(huì)發(fā)現(xiàn)它就是一系列方法名的預(yù)備信息。
protected $selectComponents = [
'aggregate',
'columns',
'from',
'joins',
'wheres',
'groups',
'havings',
'orders',
'limit',
'offset',
'lock',
];
在循環(huán)中拼接的結(jié)果就是 compileAggregate() 、compileColumns() .... 這一系列方法,這堆方法在當(dāng)前的這個(gè)語法文件中我們都可以找到。每個(gè)方法需要的額外參數(shù)是通過 $query->$component 傳遞進(jìn)去的,這里我們也可以再回到 Builder 類中查看,在這個(gè)類中,有與 selectComponents 相對(duì)應(yīng)的各個(gè)屬性。在我們定義 查詢構(gòu)造器 的時(shí)候,這些對(duì)應(yīng)的屬性都會(huì)建立并賦值。這些 compile 方法執(zhí)行完成之后,再通過 concatenate() 方法將 compileComponents() 中獲得的那個(gè) $sql 數(shù)組轉(zhuǎn)換成一個(gè)字符串,查詢的 SQL 語句就拼接完成了。
protected function concatenate($segments)
{
return implode(' ', array_filter($segments, function ($value) {
return (string) $value !== '';
}));
}
你想要知道的 Where 條件、Join 語句是怎么拼接的,就全在這些 compileWheres()、compileJoins() 方法中了。這里我就不貼代碼了,剩下的東西就看大家自己怎么發(fā)掘咯!
今天的內(nèi)容其實(shí)相對(duì)來說輕松一些,畢竟關(guān)于 Laravel 數(shù)據(jù)庫(kù)方面的內(nèi)容重點(diǎn)在于之前學(xué)習(xí)過的 模型 和 查詢構(gòu)造器 上。對(duì)于主從數(shù)據(jù)庫(kù)來說,一般中大型的業(yè)務(wù)項(xiàng)目會(huì)應(yīng)用得比較廣泛,它實(shí)現(xiàn)的原理其實(shí)也并不復(fù)雜。而 語法生成 這里我們主要是看了一下查詢語句的語法生成,相比增刪改來說,查詢語句因?yàn)榇嬖?where/join/order by/group by 等功能,所以會(huì)更加的復(fù)雜一些。當(dāng)然,更復(fù)雜的東西其實(shí)還是在構(gòu)造器中,畢竟在語法生成這里其實(shí)是已經(jīng)到了最后的拼裝階段了。有興趣的同學(xué)可以多深入研究一下 Builder 對(duì)象中關(guān)于上述功能的方法實(shí)現(xiàn)。相信經(jīng)過這一系列的學(xué)習(xí),這個(gè)文件的內(nèi)容對(duì)你已經(jīng)不陌生了,也相信你已經(jīng)可以自己獨(dú)立的分析剩下的內(nèi)容了。后面我們還要再學(xué)習(xí)兩篇簡(jiǎn)單的和數(shù)據(jù)庫(kù)相關(guān)的內(nèi)容,分別是事務(wù)與PDO屬性設(shè)置,以及 Redis 的簡(jiǎn)單使用。
參考文檔:
https://learnku.com/docs/laravel/8.x/database/9400#e05dce
聯(lián)系客服