mysql 连接查询_Swoole 实战:MySQL 查询器的实现(协程连接池)
Swoole 實(shí)戰(zhàn):MySQL 查詢器的實(shí)現(xiàn)(協(xié)程連接池)
需求分析
本篇我們將通過 Swoole 實(shí)現(xiàn)一個(gè)自帶連接池的 MySQL 查詢器:
1. 支持通過鏈?zhǔn)秸{(diào)用構(gòu)造并執(zhí)行 SQL 語句;
2. 支持連接池技術(shù);
3. 支持多協(xié)程事務(wù)并發(fā)執(zhí)行(協(xié)程安全性);
4. 支持連接對(duì)象的健康檢測(cè);
5. 支持連接對(duì)象斷線重連;
6. 程序需要可擴(kuò)展,為未來的改造留好擴(kuò)展點(diǎn);
使用示例
查詢:
$query->select(['uid', 'name'])
->from('users u')
->join('auth_users au', "u.uid=au.uid")
->where(['uid' => $uid])
->groupBy("u.phone")
->having("count(u.phone)>1")
->orderBy("u.uid desc")
->limit(10, 0)
->list();
插入:
$query->insert('users')
->values(
[
[
'name' => 'linvanda',
'phone' => '18687664562',
'nickname' => '林子',
],
[
'name' => 'xiake',
'phone' => '18989876543',
'nickname' => '俠客',
],
])->execute();// 這里是批量插入,不需要批量插入的話,傳入一維數(shù)組即可
// 延遲插入$query->insert('users')
->delayed()
->values(
[
'name' => 'linvanda',
'phone' => '18687664562',
'nickname' => '林子',
])->execute();
更新:
$query->update('users u')
->join('auth_users au', "u.uid=au.uid")
->set(['u.name' => '粽子'])
->where("u.uid=:uid", ['uid' => 123])
->execute();
刪除:
$query->delete('users')
->where("uid=:uid", ['uid' => 123])
->execute();
事務(wù):
$query->begin();$query->update('users u')
->join('auth_users au', "u.uid=au.uid")
->set(['u.name' => '粽子'])
->where("u.uid=:uid", ['uid' => 123])
->execute();...$query->commit();
模塊設(shè)計(jì)
1. 查詢模塊:
查詢器(Query,入口)
SQL構(gòu)造器(Builder)
2. 事務(wù)模塊:
事務(wù)接口(ITransaction)
協(xié)程版事務(wù)類(CoTransaction)
協(xié)程上下文(TContext)
3. 連接池模塊:
連接池接口(IPool)
協(xié)程連接池類(CoPool)
4. 數(shù)據(jù)庫(kù)連接(驅(qū)動(dòng))模塊:
連接接口(IConnector)
連接生成器接口(IConnectorBuilder)
協(xié)程連接類(CoConnector)
協(xié)程連接生成器(CoConnectorBuilder)
數(shù)據(jù)庫(kù)連接配置類(DBConfig)
數(shù)據(jù)庫(kù)連接(統(tǒng)計(jì))信息類(ConnectorInfo)
我們希望通過統(tǒng)一的入口對(duì)外提供服務(wù),將復(fù)雜性隱藏在內(nèi)部。該統(tǒng)一入口由查詢模塊提供。該模塊由查詢器和SQL 構(gòu)造器構(gòu)成,其中查詢器作為外界唯一入口,而構(gòu)造器是一個(gè) Trait,因?yàn)檫@樣可以讓外界通過查詢器入口直接使用構(gòu)造器提供的 SQL 組裝功能。
查詢器通過事務(wù)模塊執(zhí)行 SQL。這里的事務(wù)有兩個(gè)層面含義:數(shù)據(jù)庫(kù)操作的事務(wù)性(顯式或隱式事務(wù),由 CoTransaction 類保障),以及多協(xié)程下的執(zhí)行環(huán)境隔離性(由 TContext 類保障)。
事務(wù)模塊需要通過數(shù)據(jù)庫(kù)連接對(duì)象執(zhí)行具體的 SQL。連接對(duì)象由連接池模塊提供。
連接池模塊維護(hù)(創(chuàng)建、回收、銷毀)數(shù)據(jù)庫(kù)連接對(duì)象,具體是通過數(shù)據(jù)庫(kù)連接模塊的連接生成器生成新數(shù)據(jù)庫(kù)連接。
模塊之間依賴于接口而非具體實(shí)現(xiàn):查詢模塊依賴事務(wù)模塊的 ITransaction 接口;事務(wù)模塊依賴連接池模塊的 IPool 接口和數(shù)據(jù)庫(kù)連接模塊的 IConnector 接口;連接池模塊依賴數(shù)據(jù)庫(kù)連接模塊的 IConnectorBuilder 接口。
UML 類圖
下面,我們分模塊具體講解。
入口
由查詢模塊對(duì)外提供統(tǒng)一的使用入口。查詢模塊由兩部分構(gòu)成:查詢器和 SQL 構(gòu)造器。為了讓調(diào)用方可以直接通過查詢器來構(gòu)造 SQL(而不用先實(shí)例化一個(gè)構(gòu)造器構(gòu)造 SQL 然后傳給查詢器),我將構(gòu)造器設(shè)計(jì)成 Trait 供查詢器 Query 使用。
該入口類做了以下幾件事情:
· 提供 list()、one()、page()、execute() 等方法執(zhí)行 SQL 語句,其內(nèi)部是通過 transaction 實(shí)現(xiàn)的;
· 通過 Builder 這個(gè) Trait 對(duì)外提供 SQL 構(gòu)造功能;
· 委托 transaction 實(shí)現(xiàn)事務(wù)功能;
構(gòu)造器主要提供和 SQL 子句對(duì)應(yīng)的方法來構(gòu)造和編譯 SQL,并提供對(duì)原生 SQL 的支持。
該構(gòu)造器并未對(duì)所有的 SQL 語句做方法上的實(shí)現(xiàn)(比如子查詢),只對(duì)最常用的功能提供了支持,復(fù)雜的 SQL 建議直接寫 SQL 語句(一些框架對(duì)復(fù)雜 SQL 構(gòu)造也提供了方法級(jí)別的支持,但這其實(shí)會(huì)帶來使用和維護(hù)上的復(fù)雜性,它導(dǎo)致 SQL 不夠直觀)。
完整的查詢模塊代碼
事務(wù)
事務(wù)是集中管理 SQL 執(zhí)行上下文的地方,所有的 SQL 都是在事務(wù)中執(zhí)行的(沒有調(diào) begin() 則是隱式事務(wù))。
我們的查詢器是協(xié)程安全的,即一個(gè) Query 實(shí)例可以在多個(gè)協(xié)程中并發(fā)執(zhí)行事務(wù)而不會(huì)相互影響。協(xié)程安全性是通過事務(wù)模塊保證的,這里需要處理兩個(gè)維度的“事務(wù)”:數(shù)據(jù)庫(kù)維度和協(xié)程維度。不但需要保證數(shù)據(jù)庫(kù)事務(wù)的完整執(zhí)行,還要保證多個(gè)協(xié)程間的 SQL 執(zhí)行不會(huì)相互影響。
我們先看一個(gè)多協(xié)程并發(fā)執(zhí)行事務(wù)的例子(在兩個(gè)子協(xié)程中使用同一個(gè) Query 實(shí)例執(zhí)行事務(wù):先從數(shù)據(jù)庫(kù)查詢用戶信息,然后更新姓名):
$query = new Query(...);
for ($i = 0; $i < 2; $i++) {
go(function () use ($query) {
$query->begin();
$user = $query->select("uid,name")->from("users")->where("phone=:phone", ["phone" => "13908987654"])->one();
$query->update('users')->set(['name' => "李四"])->where("uid=:uid", ['uid' => $user['uid']])->execute();
$query->commit();
});}
上面代碼執(zhí)行步驟如圖:
在上圖兩個(gè)協(xié)程不斷切換過程中,各自的事務(wù)是在獨(dú)立執(zhí)行的,互不影響。
現(xiàn)實(shí)中,我們會(huì)在倉(cāng)儲(chǔ)中使用查詢器,每個(gè)倉(cāng)儲(chǔ)持有一個(gè)查詢器實(shí)例,而倉(cāng)儲(chǔ)是單例模式,多協(xié)程共享的,因而查詢器也是多協(xié)程共享的。如下:
/**
* MySQL 倉(cāng)儲(chǔ)基類
* 倉(cāng)儲(chǔ)是單例模式(通過容器實(shí)現(xiàn)單例),多協(xié)程會(huì)共享同一個(gè)倉(cāng)儲(chǔ)實(shí)例
*/abstract class MySQLRepository extends Repository implements ITransactional{
/**
* 查詢器
*/
protected $query;
public function __construct()
{
if (!$this->dbAlias()) {
throw new Exception('dbName can not be null');
}
// 通過工廠創(chuàng)建查詢器實(shí)例
$this->query = MySQLFactory::build($this->dbAlias());
}
...}
事務(wù)模塊是如何實(shí)現(xiàn)協(xié)程并發(fā)事務(wù)的隔離性呢?我們用協(xié)程上下文 TContext 類實(shí)現(xiàn)協(xié)程間數(shù)據(jù)的隔離,事務(wù)類 CoTransaction 持有 TContext 實(shí)例,事務(wù)中所有的狀態(tài)信息都通過 TContext 存取,以實(shí)現(xiàn)協(xié)程間狀態(tài)數(shù)據(jù)互不影響。
我們先看看協(xié)程上下文類:
class TContext implements ArrayAccess{
private $container = [];
...
public function offsetGet($offset)
{
if (!isset($this->container[Co::getuid()])) {
return null;
}
return $this->container[Co::getuid()][$offset] ?? null;
}
public function offsetSet($offset, $value)
{
$cuid = Co::getuid();
if (!isset($this->container[$cuid])) {
$this->init();
}
$this->container[$cuid][$offset] = $value;
}
private function init()
{
$this->container[Co::getuid()] = [];
// 協(xié)程退出時(shí)需要清理當(dāng)前協(xié)程上下文
Co::defer(function () {
unset($this->container[Co::getuid()]);
});
}}
協(xié)程上下文內(nèi)部通過 $container 數(shù)組維護(hù)每個(gè)協(xié)程的數(shù)據(jù)。該類實(shí)現(xiàn)了 ArrayAccess 接口,可以通過下標(biāo)訪問,如:
// 創(chuàng)建上下文實(shí)例$context = new TContext();// 設(shè)置當(dāng)前協(xié)程的數(shù)據(jù)$context["model"] = "write";// 訪問當(dāng)前協(xié)程的數(shù)據(jù)$context["model"];
再看看事務(wù)。
事務(wù)接口定義:
interface ITransaction{
public function begin(string $model = 'write', bool $isImplicit = false): bool;
/**
* 發(fā)送 SQL 指令
*/
public function command(string $preSql, array $params = []);
/**
* 提交事務(wù)
* @param bool $isImplicit 是否隱式事務(wù),隱式事務(wù)不會(huì)向 MySQL 提交 commit (要求數(shù)據(jù)庫(kù)服務(wù)器開啟了自動(dòng)提交的配置)
* @return bool
* @throws Exception
*/
public function commit(bool $isImplicit = false): bool;
public function rollback(): bool;
/**
* 獲取或設(shè)置當(dāng)前事務(wù)執(zhí)行模式
* @param string 讀/寫模式 read/write
* @return string 當(dāng)前事務(wù)執(zhí)行模式
*/
public function model(?string $model = null): string;
...
/**
* 獲取一次事務(wù)中執(zhí)行的 SQL 列表
* @return array
*/
public function sql():array;}
上面接口定義了事務(wù)管理器的主要工作:開啟事務(wù)、執(zhí)行 SQL、提交/回滾事務(wù)以及和本次事務(wù)執(zhí)行相關(guān)的信息。
我們?cè)賮砜纯此膶?shí)現(xiàn)類 CoTransaction,該類是整個(gè)查詢器中最重要的類,我們把整個(gè)類的代碼完整貼出來:
/**
* 協(xié)程版事務(wù)管理器
* 注意:事務(wù)開啟直到提交/回滾的過程中會(huì)一直占用某個(gè) IConnector 實(shí)例,如果有很多長(zhǎng)事務(wù),則會(huì)很快耗完連接池資源
*/class CoTransaction implements ITransaction{
private $pool;
// 事務(wù)的所有狀態(tài)信息(運(yùn)行狀態(tài)、SQL、運(yùn)行模式、運(yùn)行結(jié)果等)都是存儲(chǔ)在上下文中
private $context;
/**
* 創(chuàng)建事務(wù)實(shí)例時(shí)需要提供連接池,并在內(nèi)部創(chuàng)建該事物的協(xié)程上下文實(shí)例
*/
public function __construct(IPool $pool)
{
$this->pool = $pool;
$this->context = new TContext();
}
public function __destruct()
{
// 如果事務(wù)沒有結(jié)束,則回滾
if ($this->isRunning()) {
$this->rollback();
}
}
/**
* 開啟事務(wù)
*/
public function begin(string $model = 'write', bool $isImplicit = false): bool
{
// 如果事務(wù)已經(jīng)開啟了,則直接返回
if ($this->isRunning()) {
return true;
}
// 事務(wù)模式(決定從讀連接池還是寫連接池拿連接對(duì)象)
$this->model($model);
// 設(shè)置事務(wù)運(yùn)行狀態(tài)
$this->isRunning(true);
// 獲取數(shù)據(jù)庫(kù)連接
try {
if (!($connector = $this->connector())) {
throw new ConnectException("獲取連接失敗");
}
} catch (Exception $exception) {
$this->isRunning(false);
throw new TransactionException($exception->getMessage(), $exception->getCode());
}
// 開啟新事務(wù)前,需要清除上一次事務(wù)的數(shù)據(jù)
$this->resetLastExecInfo();
$this->clearSQL();
// 調(diào)用數(shù)據(jù)庫(kù)連接對(duì)象的 begin 方法開始事務(wù)(如果是隱式事務(wù)則不調(diào)用)
return $isImplicit || $connector->begin();
}
/**
* 執(zhí)行 SQL 指令
* 如果是隱式事務(wù),則在該方法中自動(dòng)調(diào)用 begin 和 commit 方法
*/
public function command(string $preSql, array $params = [])
{
if (!$preSql) {
return false;
}
// 是否隱式事務(wù):外界沒有調(diào)用 begin 而是直接調(diào)用 command 則為隱式事務(wù)
$isImplicit = !$this->isRunning();
// 如果是隱式事務(wù),則需要自動(dòng)開啟事務(wù)
if ($isImplicit && !$this->begin($this->calcModelFromSQL($preSql), true)) {
return false;
}
// 執(zhí)行 SQL
$result = $this->exec([$preSql, $params]);
// 隱式事務(wù)需要及時(shí)提交
if ($isImplicit && !$this->commit($isImplicit)) {
return false;
}
return $result;
}
/**
* 提交事務(wù)
*/
public function commit(bool $isImplicit = false): bool
{
if (!$this->isRunning()) {
return true;
}
$result = true;
if (!$isImplicit) {
// 顯式事務(wù)才需要真正提交到 MySQL 服務(wù)器
if ($conn = $this->connector(false)) {
$result = $conn->commit();
if ($result === false) {
// 執(zhí)行失敗,試圖回滾
$this->rollback();
return false;
}
} else {
return false;
}
}
// 釋放事務(wù)占用的資源
$this->releaseTransResource();
return $result;
}
/**
* 回滾事務(wù)
* 無論是提交還是回滾,都需要釋放本次事務(wù)占用的資源
*/
public function rollback(): bool
{
if (!$this->isRunning()) {
return true;
}
if ($conn = $this->connector(false)) {
$conn->rollback();
}
$this->releaseTransResource();
return true;
}
/**
* 獲取或設(shè)置當(dāng)前事務(wù)執(zhí)行模式
*/
public function model(?string $model = null): string
{
// 事務(wù)處于開啟狀態(tài)時(shí)不允許切換運(yùn)行模式
if (!isset($model) || $this->isRunning()) {
return $this->context['model'];
}
$this->context['model'] = $model === 'read' ? 'read' : 'write';
return $model;
}
public function lastInsertId()
{
return $this->getLastExecInfo('insert_id');
}
public function affectedRows()
{
return $this->getLastExecInfo('affected_rows');
}
public function lastError()
{
return $this->getLastExecInfo('error');
}
public function lastErrorNo()
{
return $this->getLastExecInfo('error_no');
}
/**
* 本次事務(wù)執(zhí)行的所有 SQL
* 該版本并沒有做記錄
*/
public function sql(): array
{
return $this->context['sql'] ?? [];
}
/**
* 釋放當(dāng)前事務(wù)占用的資源
*/
private function releaseTransResource()
{
// 保存本次事務(wù)相關(guān)執(zhí)行結(jié)果供外界查詢使用
$this->saveLastExecInfo();
// 歸還連接資源
$this->giveBackConnector();
unset($this->context['model']);
$this->isRunning(false);
}
/**
* 保存事務(wù)最終執(zhí)行的一些信息
*/
private function saveLastExecInfo()
{
if ($conn = $this->connector(false)) {
$this->context['last_exec_info'] = [
'insert_id' => $conn->insertId(),
'error' => $conn->lastError(),
'error_no' => $conn->lastErrorNo(),
'affected_rows' => $conn->affectedRows(),
];
} else {
$this->context['last_exec_info'] = [];
}
}
private function resetLastExecInfo()
{
unset($this->context['last_exec_info']);
}
private function getLastExecInfo(string $key)
{
return isset($this->context['last_exec_info']) ? $this->context['last_exec_info'][$key] : '';
}
/**
* 執(zhí)行指令池中的指令
* @param $sqlInfo
* @return mixed
* @throws
*/
private function exec(array $sqlInfo)
{
if (!$sqlInfo || !$this->isRunning()) {
return true;
}
return $this->connector()->query($sqlInfo[0], $sqlInfo[1]);
}
private function clearSQL()
{
unset($this->context['sql']);
}
private function calcModelFromSQL(string $sql): string
{
if (preg_match('/^(update|replace|delete|insert|drop|grant|truncate|alter|create)s/i', trim($sql))) {
return 'write';
}
return 'read';
}
/**
* 獲取連接資源
*/
private function connector(bool $usePool = true)
{
if ($connector = $this->context['connector']) {
return $connector;
}
if (!$usePool) {
return null;
}
$this->context['connector'] = $this->pool->getConnector($this->model());
return $this->context['connector'];
}
/**
* 歸還連接資源
*/
private function giveBackConnector()
{
if ($this->context['connector']) {
$this->pool->pushConnector($this->context['connector']);
}
unset($this->context['connector']);
}
private function isRunning(?bool $val = null)
{
if (isset($val)) {
$this->context['is_running'] = $val;
} else {
return $this->context['is_running'] ?? false;
}
}}
該類中,一次 SQL 執(zhí)行(無論是顯式事務(wù)還是隱式事務(wù))的步驟:
begin -> exec -> commit/rollback
1. begin:
判斷是否可開啟新事務(wù)(如果已有事務(wù)在運(yùn)行,則不可開啟);
設(shè)置事務(wù)執(zhí)行模式(read/write);
將當(dāng)前事務(wù)狀態(tài)設(shè)置為 running;
獲取連接對(duì)象;
清理本事務(wù)實(shí)例中上次事務(wù)的痕跡(上下文、SQL);
調(diào)連接對(duì)象的 begin 啟動(dòng)數(shù)據(jù)庫(kù)事務(wù);
2. exec:
調(diào)用連接對(duì)象的 query 方法執(zhí)行 SQL(prepare 模式);
3. commit:
判斷當(dāng)前狀態(tài)是否可提交(running 狀態(tài)才可以提交);
調(diào)用連接對(duì)象的 commit 方法提交數(shù)據(jù)庫(kù)事務(wù)(如果失敗則走回滾);
釋放本次事務(wù)占用的資源(保存本次事務(wù)執(zhí)行的相關(guān)信息、歸還連接對(duì)象、清除上下文里面相關(guān)信息)
4. rollback:
判斷當(dāng)前狀態(tài)是否可回滾;
調(diào)用連接對(duì)象的 rollback 回滾數(shù)據(jù)庫(kù)事務(wù);
釋放本次事務(wù)占用的資源(同上);
優(yōu)化:
類 CoTransaction 依賴 IPool 連接池,這種設(shè)計(jì)并不合理(違反了迪米特法則)。從邏輯上說,事務(wù)管理類真正依賴的是連接對(duì)象,而非連接池對(duì)象,因而事務(wù)模塊應(yīng)該依賴連接模塊而不是連接池模塊。讓事務(wù)管理類依賴連接池,一方面向事務(wù)模塊暴露了連接管理的細(xì)節(jié), 另一方面意味著如果使用該事務(wù)管理類,就必須使用連接池技術(shù)。
一種優(yōu)化方案是,在連接模塊提供一個(gè)連接管理類供外部(事務(wù)模塊)取還連接:
interface IConnectorManager{
public function getConnector() IConnector;
public function giveBackConnector(IConnector $conn);}
將 IConnectorManager 注入到 CoTransaction 中:
class CoTransaction implements ITransaction{
...
public function __construct(IConnectorManager $connMgr)
{
...
}}
連接管理器 IConnectorManager 承擔(dān)了工廠方法角色,至此,事務(wù)模塊僅依賴連接模塊,而不用依賴連接池。
連接池
連接池模塊由 IPool 接口和 CoPool 實(shí)現(xiàn)類組成。
連接池模塊和連接模塊之間的關(guān)系比較巧妙(上面優(yōu)化后的方案)。從高層(接口層面)來說,連接池模塊依賴連接模塊:連接池操作(取還)IConnector 的實(shí)例;從實(shí)現(xiàn)上來說,連接模塊同時(shí)又依賴連接池模塊:PoolConnectorManager(使用連接池技術(shù)的連接管理器)依賴連接池模塊來操作連接對(duì)象(由于該依賴是實(shí)現(xiàn)層面的而非接口層面,因而它不是必然的,如果連接管理器不使用連接池技術(shù)則不需要依賴連接池模塊)。“連接管理器”這個(gè)角色很重要:它對(duì)外(事務(wù)模塊)屏蔽了連接池模塊的存在,代價(jià)是在內(nèi)部引入了對(duì)連接池模塊的依賴(也就是用內(nèi)部依賴換外部依賴)。
經(jīng)過上面的分析我們得出,連接池模塊和連接模塊具有較強(qiáng)的耦合性,連接模塊可以對(duì)外屏蔽掉連接池模塊的存在,因而在設(shè)計(jì)上我們可以將這兩個(gè)模塊看成一個(gè)大模塊放在一個(gè)目錄下面,在該目錄下再細(xì)分成兩個(gè)內(nèi)部模塊即可。
我們可以先去看看連接池接口:
這里有幾點(diǎn)需要注意:
1. 連接池使用的是偽單例模式,同一個(gè)生成器對(duì)應(yīng)的是同一個(gè)連接池實(shí)例;
2. 連接池內(nèi)部維護(hù)了讀寫兩個(gè)池子,生成器生成的讀寫連接對(duì)象分別放入對(duì)應(yīng)的池子里面;
3. 從連接池取連接對(duì)象的時(shí)候,如果連接池為空,則根據(jù)情況決定是創(chuàng)建新連接還是等待。此處并非是在池子滿了的情況下就等待,而是會(huì)超額創(chuàng)建,為的是應(yīng)對(duì)峰值等異常情況。當(dāng)然一個(gè)優(yōu)化點(diǎn)是,將溢出比例做成可配置的,由具體的項(xiàng)目決定溢出多少。另外,如果創(chuàng)建新連接的時(shí)候數(shù)據(jù)庫(kù)服務(wù)器報(bào)連接過多的錯(cuò)誤,也需要轉(zhuǎn)為等待連接歸還;
4. 如果多次等待連接失敗(超時(shí)),則后面的請(qǐng)求會(huì)直接拋出異常(直到池子不為空)。這里有個(gè)優(yōu)化點(diǎn):目前的實(shí)現(xiàn)沒有區(qū)分是讀池子超時(shí)還是寫池子超時(shí);
5. 歸還連接時(shí),如果池子滿了,或者連接壽命到期了,則直接關(guān)閉連接;
后面在連接模塊會(huì)講解連接生成器,到時(shí)我們會(huì)知道一個(gè)連接池實(shí)例到底維護(hù)的是哪些連接對(duì)象。
連接
連接模塊負(fù)責(zé)和數(shù)據(jù)庫(kù)建立連接并發(fā)出 SQL 請(qǐng)求,其底層使用 Swoole 的 MySQL 驅(qū)動(dòng)。連接模塊由連接對(duì)象和連接生成器構(gòu)成,對(duì)外暴露 IConnector 和 IConnectorBuilder 接口。
(在我們的優(yōu)化版本中,一方面引入了連接管理器 IConnectorManager,另一方面將連接模塊和連接池模塊合并成一個(gè)大模塊,因而整個(gè)連接模塊對(duì)外暴露的是 IConnectorManager 和 IConnector 兩個(gè)接口。)
連接對(duì)象的實(shí)現(xiàn)比較簡(jiǎn)單,我們重點(diǎn)看下 CoConnector 里面查詢的處理:
該生成器是針對(duì)一主多從數(shù)據(jù)庫(kù)架構(gòu)的(包括未走讀寫分離的),如果使用是是其他數(shù)據(jù)庫(kù)架構(gòu)(如多主架構(gòu)),則創(chuàng)建其他生成器即可。
同一套讀寫配置使用同一個(gè)生成器,對(duì)應(yīng)的連接池也是同一個(gè)。
DBConfig 是一個(gè) DTO 對(duì)象,不再闡述。
查詢器的組裝
使用工廠組裝查詢器實(shí)例:
class MySQLFactory{
/**
* @param string $dbAlias 數(shù)據(jù)庫(kù)配置別名,對(duì)應(yīng)配置文件中數(shù)據(jù)庫(kù)配置的 key
*/
public static function build(string $dbAlias): Query
{
// 從配置文件獲取數(shù)據(jù)庫(kù)配置
$dbConf = Config::getInstance()->getConf("mysql.$dbAlias");
if (!$dbConf) {
throw new ConfigNotFoundException("mysql." . $dbAlias);
}
if (!isset($dbConf['read']) && !isset($dbConf['write'])) {
$writeConf = $dbConf;
$readConfs = [$writeConf];
} else {
$writeConf = $dbConf['write'] ?? [];
$readConfs = $dbConf['read'] ?? [$writeConf];
}
$writeConfObj = self::createConfObj($writeConf);
$readConfObjs = [];
foreach ($readConfs as $readConf) {
$readConfObjs[] = self::createConfObj($readConf);
}
// 創(chuàng)建生成器、連接池、事務(wù)管理器
// 在優(yōu)化后版本中,用連接管理器代替連接池的位置即可
$mySQLBuilder = CoConnectorBuilder::instance($writeConfObj, $readConfObjs);
$pool = CoPool::instance($mySQLBuilder, $dbConf['pool']['size'] ?? 30);
$transaction = new CoTransaction($pool);
return new Query($transaction);
}
private static function createConfObj(array $config): DBConfig
{
if (!$config) {
throw new Exception("config is null");
}
return new DBConfig(
$config['host'],
$config['user'],
$config['password'],
$config['database'],
$config['port'] ?? 3306,
$config['timeout'] ?? 3,
$config['charset'] ?? 'utf8'
);
}}
至此,整個(gè)查詢器的編寫、創(chuàng)建和使用就完成了。
總結(jié)
1. 項(xiàng)目的開發(fā)需要?jiǎng)澐帜K,模塊之間盡量減少耦合,通過接口通信(模塊之間依賴接口而不是實(shí)現(xiàn));
2. 如果兩個(gè)模塊之間具有強(qiáng)耦合性,則往往意味著兩者本身應(yīng)該歸并到同一個(gè)模塊中,在其內(nèi)部劃分子模塊,對(duì)外屏蔽內(nèi)部細(xì)節(jié),如本項(xiàng)目的連接模塊和連接池模塊;
3. 如果模塊之間存在不合常理的依賴關(guān)系,則意味著模塊劃分有問題,如本項(xiàng)目中的事務(wù)模塊依賴連接池模塊;
4. 有問題的模塊劃分往往違反第一點(diǎn)(也就是迪米特法則),會(huì)造成模塊暴露細(xì)節(jié)、過多的依賴關(guān)系,影響設(shè)計(jì)的靈活性、可擴(kuò)展性,如本項(xiàng)目中事務(wù)模塊依賴連接池模塊(雖然是實(shí)現(xiàn)層面的依賴而非接口層面),造成要使用 CoTransaction 時(shí)必須同時(shí)使用連接池;
5. 編寫生產(chǎn)可用的項(xiàng)目時(shí)需要注意處理異常場(chǎng)景,如本項(xiàng)目中從連接池獲取連接對(duì)象,以及在連接對(duì)象上執(zhí)行 SQL 時(shí)的斷線重連;
6. 設(shè)計(jì)本身是迭代式的,并非一蹴而就、一次性設(shè)計(jì)即可完成的,本項(xiàng)目在開發(fā)過程中已經(jīng)經(jīng)歷過幾次小重構(gòu),在本次分析時(shí)仍然發(fā)現(xiàn)一些設(shè)計(jì)上的缺陷。重構(gòu)屬于項(xiàng)目開發(fā)的一部分;
優(yōu)化版 UML 圖:
以上內(nèi)容希望幫助到大家,很多PHPer在進(jìn)階的時(shí)候總會(huì)遇到一些問題和瓶頸,業(yè)務(wù)代碼寫多了沒有方向感,不知道該從那里入手去提升,對(duì)此我整理了一些資料,包括但不限于:分布式架構(gòu)、高可擴(kuò)展、高性能、高并發(fā)、服務(wù)器性能調(diào)優(yōu)、TP6,laravel,YII2,Redis,Swoole、Swoft、Kafka、Mysql優(yōu)化、shell腳本、Docker、微服務(wù)、Nginx等多個(gè)知識(shí)點(diǎn)高級(jí)進(jìn)階干貨需要的可以免費(fèi)分享給大家,PHP進(jìn)階學(xué)習(xí)交流群
關(guān)注:架構(gòu)師學(xué)習(xí)路線圖,每日更新互聯(lián)網(wǎng)最新技術(shù)文章與你不斷前行,實(shí)戰(zhàn)資料,筆試面試。
總結(jié)
以上是生活随笔為你收集整理的mysql 连接查询_Swoole 实战:MySQL 查询器的实现(协程连接池)的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: python爬取jsp网页_帮MM用py
- 下一篇: mysql 安装_源码安装mysql