PHP Db類強(qiáng)制讀主庫(kù)(master)的設(shè)計(jì)

這段時(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)潔。
如果有問題可以留言溝通。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容