美文网首页PHP经验分享PHP实战
PHP Db类强制读主库(master)的设计

PHP Db类强制读主库(master)的设计

作者: Brown_ | 来源:发表于2019-02-28 17:06 被阅读4次

    这段时间Db不给力,经常出现主从同步延迟或者挂掉的情况,导致很多业务出现异常,大家就讨论怎么样让程序强制读master,关于这个方面的讨论比较激烈,主要为两种。

    • 底层DB类不应该关注主从的抉择,应该交于业务侧的用户抉择,这样业务层使用起来比较灵活。
    • 业务层的用户不应该关注主从的抉择,应该交给DB层解决,因为如果业务层人为不小心把强制读master的代码上到了压力大的线上,会对Db造成很大的压力,会出现很多不可控的因素。

    我本人持第二种观点,程序中的bug,大部分都是人的失误造成的,在编程的世界里,人才是最大的bug,不应该把业务的稳定性依赖于人。

    但是现在业务上主从的问题要解决,并且再快的主从同步也会出现延迟,在不依赖其他的存储工具的情况下,使用强制读master是必须要实现的功能,那么怎么才能设计出来一个安全性高一些的操作方式呢?

    最初的想法,通过在调用查询函数时加入强制读master参数。

    $daoCity = new \dao\City();
    
    $ret = $daoCity->findAllBySql("select * from city limit 1",$useMaster=true);
    

    这个想法很快被淘汰了,原因如下。

    • 所有要修改的读取函数太多,太麻烦。
    • 使用方法不优美,忍不了。
    • 如果出现不了解的程序员直接拷贝代码,将其上线到线上环境,可能出现事故。

    接着出现了第二版的设计,通过一个函数来开启master读取,再主动关闭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();
    

    第二版的想法比第一版好了一些,但是还不是我想要的,否定的原因如下。

    • 需要额外的函数。
    • 大概率忘记关掉master查询。
    • 解决方法还是不够优雅,不能忍。

    仔细思考,衍生出了第三版的设计。通过链式调用实现强制读master。

    $daoCity = new \dao\City(false);
    /**
     * 自动主从
     */
    $ret = $daoCity->findAllBySql("select * from city limit 1");
    
    /**
     * 强制master
     */
    $ret = $daoCity->forceMaster()->findAllBySql("select * from city limit 1");
    
    
    • 解决方式对老代码几乎没有入侵。
    • 解决方法优雅。
    • 通过直接的函数名可以有效的提示使用者,这是master操作。
    • 无需主动关闭,程序实现隔离。

    那么接下来我们看下怎么实现这个链式调用呢?并且无需关注主从的切换。

    这是未修改过的代码。

    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);
          
        }
    
    
        /**
         * 根据sql查询多条数据
         *
         * @param $sql
         * @return mixed
         */
        public function findAllBySql($sql)
        {
            return $this->tqtMysqli->queryRows($sql);
        }
    
    

    代码逻辑不用细讲了,就是一个简单的Db操作父类。子类集成后实现对个表的操作。

    下面加入对这个类的第一次修改。

    class TqtDaoBase
    {
        protected $dbConf;
        protected $table;
        protected $pk = "id";
        public $tqtMysqli; //将这个属性改成 公开的
    
        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一个新的自己出来,将其中的Db 连接指向master,这样就可以实现链式调用了,是不是感觉大功告成了。
    慢着,这里有个安全问题,或者说是漏洞,将原有的
    protected $tqtMysqli; 改成了 public $tqtMysqli;
    带来了几个问题

    • 将不应该暴露给使用者的属性暴露出去了
    • 用户可以绕过我的 forceMaster()方法直接使用 $daoCity->tqtMysqli->forceMaster();操作master,这样我们的设计很可能被偷懒的程序员绕过。
    • 这样不优雅,不能忍。

    接下来思考了各种方法,从对象复制clone中找到了灵感。
    先看下php的文档的描述。

    对象复制可以通过 clone 关键字来完成(如果可能,这将调用对象的 __clone() 方法)。对象中的 __clone() 方法不能被直接调用。

    通过这个方法的帮助,做出以下设计

    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);
        }
    
        /**
         * 发生clone 新建一个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 在对象被复制的时候可以修改复制的对象属性,符合我们new一个类,做master操作。
    • __clone 函数不能被直接调用,保证了强制读master的操作权限收敛,避免人为的绕过
    • 解决方式优雅。

    以上就是我对Db加入强制读master的一个设计。任何一个核心功能的添加都不能随意,都要深思熟虑,尽量的收敛权限,对使用者一定要报以不信任。代码设计尽量的优雅简洁。
    如果有问题可以留言沟通。

    相关文章

      网友评论

        本文标题:PHP Db类强制读主库(master)的设计

        本文链接:https://www.haomeiwen.com/subject/vnfkuqtx.html