這段時(shí)間Db不給力,經(jīng)常出現(xiàn)主從同步延遲或者掛掉的情況,導(dǎo)致很多業(yè)務(wù)出現(xiàn)異常,大家就討論怎么樣讓程序強(qiáng)制讀master,關(guān)于這個(gè)方面的討論比較激烈,主要為兩種。
- 底層DB類不應(yīng)該關(guān)注主從的抉擇,應(yīng)該交于業(yè)務(wù)側(cè)的用戶抉擇,這樣業(yè)務(wù)層使用起來比較靈活。
- 業(yè)務(wù)層的用戶不應(yīng)該關(guān)注主從的抉擇,應(yīng)該交給DB層解決,因?yàn)槿绻麡I(yè)務(wù)層人為不小心把強(qiáng)制讀master的代碼上到了壓力大的線上,會(huì)對(duì)Db造成很大的壓力,會(huì)出現(xiàn)很多不可控的因素。
我本人持第二種觀點(diǎn),程序中的bug,大部分都是人的失誤造成的,在編程的世界里,人才是最大的bug,不應(yīng)該把業(yè)務(wù)的穩(wěn)定性依賴于人。
但是現(xiàn)在業(yè)務(wù)上主從的問題要解決,并且再快的主從同步也會(huì)出現(xiàn)延遲,在不依賴其他的存儲(chǔ)工具的情況下,使用強(qiáng)制讀master是必須要實(shí)現(xiàn)的功能,那么怎么才能設(shè)計(jì)出來一個(gè)安全性高一些的操作方式呢?
最初的想法,通過在調(diào)用查詢函數(shù)時(shí)加入強(qiáng)制讀master參數(shù)。
$daoCity = new \dao\City();
$ret = $daoCity->findAllBySql("select * from city limit 1",$useMaster=true);
這個(gè)想法很快被淘汰了,原因如下。
- 所有要修改的讀取函數(shù)太多,太麻煩。
- 使用方法不優(yōu)美,忍不了。
- 如果出現(xiàn)不了解的程序員直接拷貝代碼,將其上線到線上環(huán)境,可能出現(xiàn)事故。
接著出現(xiàn)了第二版的設(shè)計(jì),通過一個(gè)函數(shù)來開啟master讀取,再主動(dòng)關(guān)閉master讀取。
$daoCity = new \dao\City();
$daoCity->forceMaster();
$ret = $daoCity->findAllBySql("select * from city limit 1");
$ret = $daoCity->findAllBySql("select * from city limit 1");
$ret = $daoCity->findAllBySql("select * from city limit 1";
$ret = $daoCity->findAllBySql("select * from city limit 1");
$daoCity->forceMasterOver();
第二版的想法比第一版好了一些,但是還不是我想要的,否定的原因如下。
- 需要額外的函數(shù)。
- 大概率忘記關(guān)掉master查詢。
- 解決方法還是不夠優(yōu)雅,不能忍。
仔細(xì)思考,衍生出了第三版的設(shè)計(jì)。通過鏈?zhǔn)秸{(diào)用實(shí)現(xiàn)強(qiáng)制讀master。
$daoCity = new \dao\City(false);
/**
* 自動(dòng)主從
*/
$ret = $daoCity->findAllBySql("select * from city limit 1");
/**
* 強(qiáng)制master
*/
$ret = $daoCity->forceMaster()->findAllBySql("select * from city limit 1");
- 解決方式對(duì)老代碼幾乎沒有入侵。
- 解決方法優(yōu)雅。
- 通過直接的函數(shù)名可以有效的提示使用者,這是master操作。
- 無需主動(dòng)關(guān)閉,程序?qū)崿F(xiàn)隔離。
那么接下來我們看下怎么實(shí)現(xiàn)這個(gè)鏈?zhǔn)秸{(diào)用呢?并且無需關(guān)注主從的切換。
這是未修改過的代碼。
class TqtDaoBase
{
protected $dbConf;
protected $table;
protected $pk = "id";
protected $tqtMysqli;
/**
* TqtDaoBase constructor.
*
* @param bool $needReconnect
* @throws \Exception
*/
public function __construct()
{
if (empty($this->dbConf)) {
throw new \Exception("db config is null", 1);
}
$this->tqtMysqli = TqtMysqli::getInstance($this->dbConf);
}
/**
* 根據(jù)sql查詢多條數(shù)據(jù)
*
* @param $sql
* @return mixed
*/
public function findAllBySql($sql)
{
return $this->tqtMysqli->queryRows($sql);
}
代碼邏輯不用細(xì)講了,就是一個(gè)簡(jiǎn)單的Db操作父類。子類集成后實(shí)現(xiàn)對(duì)個(gè)表的操作。
下面加入對(duì)這個(gè)類的第一次修改。
class TqtDaoBase
{
protected $dbConf;
protected $table;
protected $pk = "id";
public $tqtMysqli; //將這個(gè)屬性改成 公開的
protected static $MASTER_INSTANCE;
/**
* TqtDaoBase constructor.
*
* @param bool $needReconnect
* @throws \Exception
*/
public function __construct()
{
if (empty($this->dbConf)) {
throw new \Exception("db config is null", 1);
}
$this->tqtMysqli = TqtMysqli::getInstance($this->dbConf);
}
/**
* @return TqtDaoBase
*/
public function forceMaster()
{
if (empty(self::$MASTER_INSTANCE)) {
$className = get_called_class();
$dbLink = new $className;
$dbLink->tqtMysqli->forceMaster();
self::$MASTER_INSTANCE = $dbLink;
}
return self::$MASTER_INSTANCE;
}
代碼的邏輯主要就是new一個(gè)新的自己出來,將其中的Db 連接指向master,這樣就可以實(shí)現(xiàn)鏈?zhǔn)秸{(diào)用了,是不是感覺大功告成了。
慢著,這里有個(gè)安全問題,或者說是漏洞,將原有的
protected $tqtMysqli; 改成了 public $tqtMysqli;
帶來了幾個(gè)問題
- 將不應(yīng)該暴露給使用者的屬性暴露出去了
- 用戶可以繞過我的
forceMaster()方法直接使用$daoCity->tqtMysqli->forceMaster();操作master,這樣我們的設(shè)計(jì)很可能被偷懶的程序員繞過。 - 這樣不優(yōu)雅,不能忍。
接下來思考了各種方法,從對(duì)象復(fù)制clone中找到了靈感。
先看下php的文檔的描述。
對(duì)象復(fù)制可以通過 clone 關(guān)鍵字來完成(如果可能,這將調(diào)用對(duì)象的 __clone() 方法)。對(duì)象中的 __clone() 方法不能被直接調(diào)用。
通過這個(gè)方法的幫助,做出以下設(shè)計(jì)
class TqtDaoBase
{
protected $dbConf;
protected $table;
protected $pk = "id";
protected $tqtMysqli;
protected static $MASTER_INSTANCE;
/**
* TqtDaoBase constructor.
*
* @throws \Exception
*/
public function __construct()
{
if (empty($this->dbConf)) {
throw new \Exception("db config is null", 1);
}
$this->tqtMysqli = TqtMysqli::getInstance($this->dbConf);
}
/**
* 發(fā)生clone 新建一個(gè)Db連接,并指向master
*/
public function __clone()
{
$this->tqtMysqli = TqtMysqli::getInstance($this->dbConf);
$this->tqtMysqli->forceMaster();
}
/**
* @return TqtDaoBase
*/
public function forceMaster()
{
if (empty(self::$MASTER_INSTANCE)) {
self::$MASTER_INSTANCE = clone $this;
}
return self::$MASTER_INSTANCE;
}
- __clone 在對(duì)象被復(fù)制的時(shí)候可以修改復(fù)制的對(duì)象屬性,符合我們new一個(gè)類,做master操作。
- __clone 函數(shù)不能被直接調(diào)用,保證了強(qiáng)制讀master的操作權(quán)限收斂,避免人為的繞過
- 解決方式優(yōu)雅。
以上就是我對(duì)Db加入強(qiáng)制讀master的一個(gè)設(shè)計(jì)。任何一個(gè)核心功能的添加都不能隨意,都要深思熟慮,盡量的收斂權(quán)限,對(duì)使用者一定要報(bào)以不信任。代碼設(shè)計(jì)盡量的優(yōu)雅簡(jiǎn)潔。
如果有問題可以留言溝通。