没有不值得去解决的问题,也没有不值得去学习的技术!

在 Yii 2 Starter Kit 中实现数据库连接时的动态配置,配置属性来源于多租户系统

检查数据库连接组件、RBAC组件是否被注册,如果已经被注册,则无需覆盖,编辑 \common\components\db\ActiveRecord.php
1、多租户系统中包含多个租户,每个租户均有其自有的数据库配置信息,现有的需求是通过调用多租户系统的接口,基于响应的主机名、用户名和密码来连接数据库,而不是以应用组件的方式来配置,如图1
多租户系统中包含多个租户,每个租户均有其自有的数据库配置信息,现有的需求是通过调用多租户系统的接口,基于响应的主机名、用户名和密码来连接数据库,而不是以应用组件的方式来配置
图1
2、现在是以应用组件的方式来配置,\common\config\base.php,如图2
现在是以应用组件的方式来配置,\common\config\base.php
图2
3、打开 Dubug,然后查看 Log Messages,db 组件的属性需要在 yii\db\Connection::open 之前完成初始化定义(调用多租户系统的接口),如图3
打开 Dubug,然后查看 Log Messages,db 组件的属性需要在 yii\db\Connection::open 之前完成初始化定义(调用多租户系统的接口)
图3
4、编辑 \common\config\base.php,配置 db 组件的类:common\components\db\Connection,将所有 env 变量注释掉


        'db'=>[
            'class'=>'common\components\db\Connection',
            // 'dsn' => env('DB_DSN'),
            // 'username' => env('DB_USERNAME'),
            // 'password' => env('DB_PASSWORD'),
            // 'tablePrefix' => env('DB_TABLE_PREFIX'),
            'charset' => 'utf8',
            'enableSchemaCache' => YII_ENV_PROD,
        ],


5、新建 \common\components\db\Connection.php,继承至 \yii\db\Connection
<pre class="wp-block-syntaxhighlighter-code">

<?php
/**
 * Created by PhpStorm.
 * User: Administrator
 * Date: 2018/01/15
 * Time: 16:18
 */

namespace common\components\db;

use Yii;
use yii\helpers\ArrayHelper;

/**
 * 获取租户模块环境配置信息,存储至Redis,注册db组件
 *
 * @author Qiang Wang <shuijingwanwq@163.com>
 * @since 1.0
 */

class Connection extends \yii\db\Connection
{
    public function __construct($config = [])
    {
        // ... 配置生效前的初始化过程
        $tenantConfig = [
            'dsn' => 'mysql:host=127.0.0.1;port=3306;dbname=cmcp-api',
            'username' => env('DB_USERNAME'),
            'password' => env('DB_PASSWORD'),
            'tablePrefix' => env('DB_TABLE_PREFIX'),
        ];

        if (!empty($config)) {
            $config = ArrayHelper::merge($config, $tenantConfig);
        }

        parent::__construct($config);
    }

}

</pre>
6、重复第3步骤,db 组件的属性已经在 yii\db\Connection::open 之前完成初始化定义(host=localhost 变化为 host=127.0.0.1),如图4
重复第3步骤,db 组件的属性已经在 yii\db\Connection::open 之前完成初始化定义(host=localhost 变化为 host=127.0.0.1)
图4
7、现在需要调用多租户系统的接口,以其响应重新赋值 $tenantConfig(建议:此方案仅适用于存在一个租户的情况,如果存在多个租户,建议每个租户对应不同的连接组件,连接组件的命名基于租户ID,而不是对应 db 连接组件),如图5
现在需要调用多租户系统的接口,以其响应重新赋值 $tenantConfig(建议:此方案仅适用于存在一个租户的情况,如果存在多个租户,建议每个租户对应不同的连接组件,连接组件的命名基于租户ID,而不是对应 db 连接组件)
图5
8、现在还原所有修改,准备实现同时使用多个数据库(每个租户对应不同的连接组件,连接组件的命名基于租户ID),参考网址:https://github.com/yiichina/yii2/blob/master/docs/guide-zh-CN/db-active-record.md ,如图6
现在还原所有修改,准备实现同时使用多个数据库(每个租户对应不同的连接组件,连接组件的命名基于租户ID),参考网址:https://github.com/yiichina/yii2/blob/master/docs/guide-zh-CN/db-active-record.md
图6
9、新建 \common\components\db\ActiveRecord.php,继承至 \yii\db\ActiveRecord,重写 [[yii\db\ActiveRecord::getDb()|getDb()]] 方法
<pre class="wp-block-syntaxhighlighter-code">

<?php
/**
 * Created by PhpStorm.
 * User: Administrator
 * Date: 2018/01/16
 * Time: 10:31
 */

namespace common\components\db;

use Yii;

/**
 * 获取租户模块环境配置信息,存储至Redis,注册数据库连接组件
 *
 * @author Qiang Wang <shuijingwanwq@163.com>
 * @since 1.0
 */

class ActiveRecord extends \yii\db\ActiveRecord
{
    /**
     * Returns the database connection used by this AR class.
     * By default, the "db" application component is used as the database connection.
     * You may override this method if you want to use a different database connection.
     * @return Connection the database connection used by this AR class.
     */
    public static function getDb()
    {
        $tenantDb = new \yii\db\Connection([
            'dsn' => 'mysql:host=127.0.0.1;port=3306;dbname=cmcp-api',
            'username' => env('DB_USERNAME'),
            'password' => env('DB_PASSWORD'),
            'tablePrefix' => env('DB_TABLE_PREFIX'),
            'charset' => 'utf8',
            'enableSchemaCache' => YII_ENV_PROD,
        ]);
        return $tenantDb;
        // return Yii::$app->getDb();
    }

}

</pre>
10、编辑 \common\models\KeyStorageItem.php,继承至 common\components\db\ActiveRecord
<pre class="wp-block-syntaxhighlighter-code">

<?php

namespace common\models;

use Yii;
use yii\behaviors\TimestampBehavior;
use common\components\db\ActiveRecord;

/**
 * This is the model class for table "key_storage_item".
 *
 * @property integer $key
 * @property integer $value
 */
class KeyStorageItem extends ActiveRecord
{
}

</pre>
11、编辑 \common\config\base.php,注释掉 db 组件,后期建议将 db 组件移至开发环境,以方便于 Gii 的使用


        /*
        'db'=>[
            'class'=>'yii\db\Connection',
            'dsn' => env('DB_DSN'),
            'username' => env('DB_USERNAME'),
            'password' => env('DB_PASSWORD'),
            'tablePrefix' => env('DB_TABLE_PREFIX'),
            'charset' => 'utf8',
            'enableSchemaCache' => YII_ENV_PROD,
        ],
        */


12、打开:http://frontend.cmcp-api.localhost/ ,报错:Failed to instantiate component or class “db”.,原因在于日志目标为数据库表,如图7
打开:http://frontend.cmcp-api.localhost/ ,报错:Failed to instantiate component or class "db".,原因在于日志目标为数据库表
图7
13、查看日志组件配置,\common\config\base.php


        'log' => [
            'traceLevel' => YII_DEBUG ? 3 : 0,
            'targets' => [
                'db'=>[
                    'class' => 'yii\log\DbTarget',
                    'levels' => ['error', 'warning'],
                    'except'=>['yii\web\HttpException:*', 'yii\i18n\I18N\*'],
                    'prefix'=>function () {
                        $url = !Yii::$app->request->isConsoleRequest ? Yii::$app->request->getUrl() : null;
                        return sprintf('[%s][%s]', Yii::$app->id, $url);
                    },
                    'logVars'=>[],
                    'logTable'=>'{{%system_log}}'
                ]
            ],
        ],


14、日志组件必须在引导期间就被加载,因此如果数据库连接是动态配置的,已经晚于引导阶段了,编辑日志组件配置,\common\config\base.php,保存日志消息到文件中


        'log' => [
            'traceLevel' => YII_DEBUG ? 3 : 0,
            'targets' => [
                'file'=>[
                    'class' => 'yii\log\FileTarget',
                    'levels' => ['error', 'warning'],
                    'except' => ['yii\web\HttpException:*', 'yii\i18n\I18N\*'],
                    'prefix' => function () {
                        $url = !Yii::$app->request->isConsoleRequest ? Yii::$app->request->getUrl() : null;
                        return sprintf('[%s][%s]', Yii::$app->id, $url);
                    },
                    'logVars'=>[],
                ]
            ],
        ],


15、打开:http://frontend.cmcp-api.localhost/ ,报错:Unknown component ID: db,如图8
打开:http://frontend.cmcp-api.localhost/ ,报错:Unknown component ID: db
图8
16、将 \common\models 下的所有模型文件,use yii\db\ActiveRecord; 替换为 use common\components\db\ActiveRecord;,如图9


use yii\db\ActiveRecord; 替换为 use common\components\db\ActiveRecord;
\yii\db\ActiveRecord 替换为 \common\components\db\ActiveRecord


将 \common\models 下的所有模型文件,use yii\db\ActiveRecord; 替换为 use common\components\db\ActiveRecord;
图9
17、打开:http://frontend.cmcp-api.localhost/ ,报错:Failed to instantiate component or class “db”.,原因在于 RBAC 组件使用数据库表存放数据,如图10
打开:http://frontend.cmcp-api.localhost/ ,报错:Failed to instantiate component or class "db".,原因在于 RBAC 组件使用数据库表存放数据
图10
18、编辑 \common\config\base.php,注释掉 RBAC 组件,因为其 db 属性默认值为 db,需要动态调整为租户ID所对应的数据库连接组件


        /*
        'authManager' => [
            'class' => 'yii\rbac\DbManager',
            'itemTable' => '{{%rbac_auth_item}}',
            'itemChildTable' => '{{%rbac_auth_item_child}}',
            'assignmentTable' => '{{%rbac_auth_assignment}}',
            'ruleTable' => '{{%rbac_auth_rule}}'
        ],
        */


19、编辑 \common\components\db\ActiveRecord.php,通过 [[yii\di\ServiceLocator::setComponents()]] 方法注册数据库连接组件、RBAC组件
<pre class="wp-block-syntaxhighlighter-code">

<?php
/**
 * Created by PhpStorm.
 * User: Administrator
 * Date: 2018/01/16
 * Time: 10:31
 */

namespace common\components\db;

use Yii;

/**
 * 获取租户模块环境配置信息,存储至Redis,注册数据库连接组件
 *
 * @author Qiang Wang <shuijingwanwq@163.com>
 * @since 1.0
 */

class ActiveRecord extends \yii\db\ActiveRecord
{
    /**
     * Returns the database connection used by this AR class.
     * By default, the "db" application component is used as the database connection.
     * You may override this method if you want to use a different database connection.
     * @return Connection the database connection used by this AR class.
     */
    public static function getDb()
    {
        $tenantDb = 'defaultDb';
        // 注册数据库连接组件、RBAC组件
        Yii::$app->setComponents([
            $tenantDb => [
                'class' => 'yii\db\Connection',
                'dsn' => 'mysql:host=127.0.0.1;port=3306;dbname=cmcp-api',
                'username' => env('DB_USERNAME'),
                'password' => env('DB_PASSWORD'),
                'tablePrefix' => env('DB_TABLE_PREFIX'),
                'charset' => 'utf8',
                'enableSchemaCache' => YII_ENV_PROD,
            ],
            'authManager' => [
                'class' => 'yii\rbac\DbManager',
                'db' => $tenantDb,
                'itemTable' => '{{%rbac_auth_item}}',
                'itemChildTable' => '{{%rbac_auth_item_child}}',
                'assignmentTable' => '{{%rbac_auth_assignment}}',
                'ruleTable' => '{{%rbac_auth_rule}}'
            ],
        ]);

        return Yii::$app->$tenantDb;
    }
}

</pre>
20、重复第3步骤,db 组件的属性已经在 yii\db\Connection::open 之前完成初始化定义(host=localhost 变化为 host=127.0.0.1),如图11
重复第3步骤,db 组件的属性已经在 yii\db\Connection::open 之前完成初始化定义(host=localhost 变化为 host=127.0.0.1)
图11
21、在 Postman 中,GET http://www.cmcp-api.localhost/v1/pages ,200响应,如图12
在 Postman 中,GET http://www.cmcp-api.localhost/v1/pages ,200响应
图12
22、打开网址:http://backend.cmcp-api.localhost/ ,报错:Unknown component ID: db,如图13
打开网址:http://backend.cmcp-api.localhost/ ,报错:Unknown component ID: db
图13
23、编辑 \backend\models\SystemLog.php,继承至 \common\components\db\ActiveRecord,如图14


\yii\db\ActiveRecord 替换为 \common\components\db\ActiveRecord


编辑 \backend\models\SystemLog.php,继承至 \common\components\db\ActiveRecord
图14
24、打开网址:http://backend.cmcp-api.localhost/ ,200响应 25、现在需要调用多租户系统的接口,以其响应直接初始化使用数据库连接、RBAC,基于已经安装的 Yii 2 的 HTTP 客户端扩展,如图15
现在需要调用多租户系统的接口,以其响应直接初始化使用数据库连接、RBAC,基于已经安装的 Yii 2 的 HTTP 客户端扩展
图15
26、编辑 \.env.dist、\.env,新增多租户的相关配置


# 多租户
# ----
TENANT_HOST_INFO       = http://wjdev.chinamcloud.com:8600 # HOME URL
TENANT_BASE_URL        = /interface # BASE URL
TENANT_APP_NAME        = cmcpapi # 模块英文名称
TENANT_SECRET          = 94030d307b160f04b88592cb9bebdd4c # 模块Secret


27、编辑 \common\config\bootstrap.php,设置别名


Yii::setAlias('@tenantUrl', env('TENANT_HOST_INFO') . env('TENANT_BASE_URL'));


28、通过应用组件配置客户端,编辑 \common\config\web.php,如图16


        'tenantHttp' => [
            'class' => 'yii\httpclient\Client',
            'baseUrl' => Yii::getAlias('@tenantUrl'),
            'transport' => 'yii\httpclient\CurlTransport'
        ],


通过应用组件配置客户端,编辑 \common\config\web.php
图16
29、通过配置日志记录 HTTP 发送的请求并分析其执行情况,编辑 \common\config\base.php,如图17


                'httpRequest'=>[
                    'class' => 'yii\log\FileTarget',
                    'logFile' => '@runtime/logs/http-request.log',
                    'categories' => ['yii\httpclient\*'],
                ]


通过配置日志记录 HTTP 发送的请求并分析其执行情况,编辑 \common\config\base.php
图17
30、yii2 HTTP 客户端扩展提供了一个可以与 yii 调试模块集成的调试面板,并显示已执行的HTTP 请求,启用调试面板,如图18


    $config['modules']['debug'] = [
        'class' => 'yii\debug\Module',
        'allowedIPs' => ['127.0.0.1', '::1', '192.168.33.1', '172.17.42.1', '172.17.0.1', '192.168.99.1'],
        'panels' => [
            'httpclient' => [
                'class' => 'yii\httpclient\debug\HttpClientPanel',
            ],
        ],
    ];


yii2 HTTP 客户端扩展提供了一个可以与 yii 调试模块集成的调试面板,并显示已执行的HTTP 请求,启用调试面板
图18
31、刷新页面之后,查看日志面板,新增 HTTP Client,如图19
刷新页面之后,查看日志面板,新增 HTTP Client
图19
32、新建 \common\logics\http\tenant\Env.php,http目录表示此目录下的模型数据源自于HTTP请求,tenant目录表示多租户系统相关的模型,Env.php表示环境配置模型
<pre class="wp-block-syntaxhighlighter-code">

<?php
/**
 * Created by PhpStorm.
 * User: Administrator
 * Date: 2018/01/17
 * Time: 15:20
 */

namespace common\logics\http\tenant;

use Yii;
use yii\base\Model;
use yii\web\ServerErrorHttpException;

/**
 * 多租户的模块环境配置
 *
 * @author Qiang Wang <shuijingwanwq@163.com>
 * @since 1.0
 */
class Env extends Model
{
    public $app_name;
    public $secret;
    public $tenant_id;

    public function attributeLabels()
    {
        return [
            'app_name' => \Yii::t('model/http/tenant/env', 'App Name'),
            'secret' => \Yii::t('model/http/tenant/env', 'Secret'),
            'tenant_id' => \Yii::t('model/http/tenant/env', 'Tenant ID'),
        ];
    }

    /**
     * 返回租户模块环境配置信息
     *
     * @return array|false
     *
     * 格式如下:
     *
     * 租户模块环境配置信息
     * [
     *     'message' => '', //说明
     *     'data' => [], //数据
     * ]
     *
     * 失败(将错误保存在 [[yii\base\Model::errors]] 属性中)
     * false
     *
     * @throws ServerErrorHttpException 如果响应状态码不等于20x
     */
    public function getTenantEnv()
    {
        $this->app_name = env('TENANT_APP_NAME');
        $this->secret = env('TENANT_SECRET');
        /* 租户ID后续从请求参数中获取 */
        $this->tenant_id = 'default';
        $response = Yii::$app->tenantHttp->createRequest()
            ->setMethod('get')
            ->setUrl('getTenantEnvs')
            ->setData([
                'appname' => $this->app_name,
                'secret' => $this->secret,
                'tenantid1' => $this->tenant_id,
            ])
            ->send();
        // 检查响应状态码是否等于20x
        if ($response->isOk) {
            // 检查业务逻辑是否成功
            if ($response->data['returnCode'] === 0) {
                return ['message' => $response->data['returnDesc'], 'data' => $response->data['returnData']];
            } else {
                $this->addError('tenant_id', $response->data['returnDesc']);
                return false;
            }
        } else {
            throw new ServerErrorHttpException(Yii::t('error', Yii::t('error', Yii::t('error', '20005'), ['statusCode' => $response->getStatusCode()])));
        }

    }

}

</pre>
33、新建语言包文件,\common\messages\en\model\http\tenant\env.php、\common\messages\zh\model\http\tenant\env.php,如图20 \common\messages\en\model\http\tenant\env.php


return [
    'App Name' => 'English name of the module',
    'Secret' => 'Secret module in multi-tenant system',
    'Tenant ID' => 'Tenant ID',
];


\common\messages\zh\model\http\tenant\env.php


return [
    'App Name' => '模块英文名称',
    'Secret' => '模块Secret',
    'Tenant ID' => '租户ID',
];


新建语言包文件,\common\messages\en\model\http\tenant\env.php、\common\messages\zh\model\http\tenant\env.php
图20
34、编辑语言包文件,\api\messages\en\error.php、\api\messages\zh\error.php \api\messages\en\error.php


    20005 => 'Multitenant HTTP request failed with status code: {statusCode}',
    20006 => 'Multitenant HTTP request failed: {firstErrors}',


\api\messages\zh\error.php


    20005 => '多租户HTTP请求失败,状态码:{statusCode}',
    20006 => '多租户HTTP请求失败:{firstErrors}',


35、复制 \api\messages\en\app.php、\api\messages\en\error.php、\api\messages\zh\app.php、\api\messages\zh\error.php 至 \common\messages\en\app.php、\common\messages\en\error.php、\common\messages\zh\app.php、\common\messages\zh\error.php,编辑 \common\messages\zh\error.php


return [
    20000 => 'error',
    20005 => '多租户HTTP请求失败,状态码:{statusCode}',
    20006 => '多租户HTTP请求失败:{firstErrors}',
];


36、编辑 \common\components\db\ActiveRecord.php,数据库配置信息源自于租户环境配置接口
<pre class="wp-block-syntaxhighlighter-code">

<?php
/**
 * Created by PhpStorm.
 * User: Administrator
 * Date: 2018/01/16
 * Time: 10:31
 */

namespace common\components\db;

use Yii;
use common\logics\http\tenant\Env;
use yii\web\ServerErrorHttpException;

/**
 * 获取租户模块环境配置信息,存储至Redis,注册数据库连接组件
 *
 * @author Qiang Wang <shuijingwanwq@163.com>
 * @since 1.0
 */

class ActiveRecord extends \yii\db\ActiveRecord
{
    /**
     * Returns the database connection used by this AR class.
     * By default, the "db" application component is used as the database connection.
     * You may override this method if you want to use a different database connection.
     * @return Connection the database connection used by this AR class.
     */
    public static function getDb()
    {
        $env = new Env();
        $tenantEnv = $env->getTenantEnv();

        if ($tenantEnv === false) {
            if ($env->hasErrors()) {
                foreach ($env->getFirstErrors() as $message) {
                    $firstErrors = $message;
                }
                throw new ServerErrorHttpException(Yii::t('error', Yii::t('error', Yii::t('error', '20006'), ['firstErrors' => $firstErrors])));
            } elseif (!$env->hasErrors()) {
                throw new ServerErrorHttpException('Multi-tenant HTTP requests fail for unknown reasons.');
            }
        }

        $tenantDb = $tenantEnv['data']['tenantid'] . 'Db';
        // 注册数据库连接组件、RBAC组件
        Yii::$app->setComponents([
            $tenantDb => [
                'class' => 'yii\db\Connection',
                'dsn' => 'mysql:host=' .$tenantEnv['data']['db_info']['host'] . ';port=3306;dbname=' . $tenantEnv['data']['db_info']['database'] . '',
                'username' => $tenantEnv['data']['db_info']['login'],
                'password' => $tenantEnv['data']['db_info']['password'],
                'tablePrefix' => $tenantEnv['data']['db_info']['prefix'],
                'charset' => 'utf8',
                'enableSchemaCache' => YII_ENV_PROD,
            ],
            'authManager' => [
                'class' => 'yii\rbac\DbManager',
                'db' => $tenantDb,
                'itemTable' => '{{%rbac_auth_item}}',
                'itemChildTable' => '{{%rbac_auth_item_child}}',
                'assignmentTable' => '{{%rbac_auth_assignment}}',
                'ruleTable' => '{{%rbac_auth_rule}}'
            ],
        ]);
        
        return Yii::$app->$tenantDb;
    }
}

</pre>
37、在 Postman 中 GET http://www.cmcp-api.localhost/v1/pages ,200响应,如图21
在 Postman 中 GET http://www.cmcp-api.localhost/v1/pages ,200响应
图21
38、在 Postman 中 GET 多租户的环境配置接口,一个参数是错误的,如图22
在 Postman 中 GET 多租户的环境配置接口,一个参数是错误的
图22
39、编辑 \.env,TENANT_APP_NAME的值是错误的


TENANT_APP_NAME        = cmcpapi1 # 模块英文名称


40、在 Postman 中 GET http://www.cmcp-api.localhost/v1/pages ,500响应,如图23
在 Postman 中 GET http://www.cmcp-api.localhost/v1/pages ,500响应
图23
41、查看日志,发现接口应用与后台应用分别请求多租户接口的次数为8、139次,后台报错:Unable to send log via yii\log\FileTarget: Exception (Database Exception) ‘yii\db\Exception’ with message ‘SQLSTATE[HY000] [1040] Too many connections’ ,如图24、25
查看日志,发现接口应用与后台应用分别请求多租户接口的次数为8、139次
图24
后台报错:Unable to send log via yii\log\FileTarget: Exception (Database Exception) 'yii\db\Exception' with message 'SQLSTATE[HY000] [1040] Too many connections'
图25
42、检查数据库连接组件、RBAC组件是否被注册,如果已经被注册,则无需覆盖,编辑 \common\components\db\ActiveRecord.php,如图26


        // 检查数据库连接组件、RBAC组件是否被注册
        if (!(Yii::$app->has($tenantDb) && Yii::$app->has('authManager'))) {
            // 注册数据库连接组件、RBAC组件
            Yii::$app->setComponents([
                $tenantDb => [
                    'class' => 'yii\db\Connection',
                    'dsn' => 'mysql:host=' . $tenantEnv['data']['db_info']['host'] . ';port=3306;dbname=' . $tenantEnv['data']['db_info']['database'] . '',
                    'username' => $tenantEnv['data']['db_info']['login'],
                    'password' => $tenantEnv['data']['db_info']['password'],
                    'tablePrefix' => $tenantEnv['data']['db_info']['prefix'],
                    'charset' => 'utf8',
                    'enableSchemaCache' => YII_ENV_PROD,
                ],
                'authManager' => [
                    'class' => 'yii\rbac\DbManager',
                    'db' => $tenantDb,
                    'itemTable' => '{{%rbac_auth_item}}',
                    'itemChildTable' => '{{%rbac_auth_item_child}}',
                    'assignmentTable' => '{{%rbac_auth_assignment}}',
                    'ruleTable' => '{{%rbac_auth_rule}}'
                ],
            ]);
        }


检查数据库连接组件、RBAC组件是否被注册,如果已经被注册,则无需覆盖,编辑 \common\components\db\ActiveRecord.php
图26
43、后台应用报错:’SQLSTATE[HY000] [1040] Too many connections’ 已经解决,如图27
后台应用报错:'SQLSTATE[HY000] [1040] Too many connections' 已经解决
图27
44、至此,数据库连接时的动态配置,配置属性来源于多租户系统已经基本上实现了,后续应该会实现缓存(多租户系统的响应),因为一次请求中,对于多租户系统的HTTP请求多达上百次,而每次HTTP请求平均耗时为100ms左右,如图28
至此,数据库连接时的动态配置,配置属性来源于多租户系统已经基本上实现了,后续应该会实现缓存(多租户系统的响应),因为一次请求中,对于多租户系统的HTTP请求多达上百次,而每次HTTP请求平均耗时为100ms左右
图28
45、开启 Schema 缓存(仅在生产环境中开启),且缓存至 Redis 中,编辑 \.env.dist、\.env,如图29


YII_ENV     = prod

# Redis
# ----
REDIS_HOSTNAME         = localhost # 主机名/IP地址
REDIS_PORT             = 6379 # 端口
#REDIS_PASSWORD        =  # 密码
REDIS_DATABASE         = 0 # 数据库

# Redis cache
# ----
REDIS_CACHE_KEY_PREFIX = ca: # 唯一键前缀


开启 Schema 缓存(仅在生产环境中开启),且缓存至 Redis 中,编辑 \.env.dist、\.env
图29
46、编辑 \common\config\base.php,在应用程序配置中配置 Redis 连接,且配置缓存组件


        'redisCache' => [
            'class' => 'yii\redis\Cache',
            'keyPrefix' => env('REDIS_CACHE_KEY_PREFIX'), // 唯一键前缀
        ],

        'redis' => [
            'class' => 'yii\redis\Connection',
            'hostname' => env('REDIS_HOSTNAME'),
            'port' => env('REDIS_PORT'),
            'password' => env('REDIS_PASSWORD'),
            'database' => env('REDIS_DATABASE'),
        ],


47、在数据库连接中开启 Schema 缓存,编辑 \common\components\db\ActiveRecord.php


        // 检查数据库连接组件、RBAC组件是否被注册
        if (!(Yii::$app->has($tenantDb) && Yii::$app->has('authManager'))) {
            // 注册数据库连接组件、RBAC组件
            Yii::$app->setComponents([
                $tenantDb => [
                    'class' => 'yii\db\Connection',
                    'dsn' => 'mysql:host=' . $tenantEnv['data']['db_info']['host'] . ';port=3306;dbname=' . $tenantEnv['data']['db_info']['database'] . '',
                    'username' => $tenantEnv['data']['db_info']['login'],
                    'password' => $tenantEnv['data']['db_info']['password'],
                    'tablePrefix' => $tenantEnv['data']['db_info']['prefix'],
                    'charset' => 'utf8',
                    'enableSchemaCache' => YII_ENV_PROD,
                    'schemaCache' => 'redisCache',
                ],
                'authManager' => [
                    'class' => 'yii\rbac\DbManager',
                    'db' => $tenantDb,
                    'itemTable' => '{{%rbac_auth_item}}',
                    'itemChildTable' => '{{%rbac_auth_item_child}}',
                    'assignmentTable' => '{{%rbac_auth_assignment}}',
                    'ruleTable' => '{{%rbac_auth_rule}}'
                ],
            ]);
        }


48、运行后台应用后,查看 Redis 中的数据,数据表的结构已经被缓存了,如图30
运行后台应用后,查看 Redis 中的数据,数据表的结构已经被缓存了
图30
49、实现缓存(多租户系统的响应),编辑 \common\logics\http\tenant\Env.php,运行后台应用后,查看 Redis 中的数据,多租户系统的响应数据已经被缓存了,如图31
<pre class="wp-block-syntaxhighlighter-code">

<?php
/**
 * Created by PhpStorm.
 * User: Administrator
 * Date: 2018/01/17
 * Time: 15:20
 */

namespace common\logics\http\tenant;

use Yii;
use yii\base\Model;
use yii\web\ServerErrorHttpException;

/**
 * 多租户的模块环境配置
 *
 * @author Qiang Wang <shuijingwanwq@163.com>
 * @since 1.0
 */
class Env extends Model
{
    public $app_name;
    public $secret;
    public $tenant_id;

    public function attributeLabels()
    {
        return [
            'app_name' => \Yii::t('model/http/tenant/env', 'App Name'),
            'secret' => \Yii::t('model/http/tenant/env', 'Secret'),
            'tenant_id' => \Yii::t('model/http/tenant/env', 'Tenant ID'),
        ];
    }

    /**
     * 返回租户模块环境配置信息
     *
     * @return array|false
     *
     * 格式如下:
     *
     * 租户模块环境配置信息
     * [
     *     'message' => '', //说明
     *     'data' => [], //数据
     * ]
     *
     * 失败(将错误保存在 [[yii\base\Model::errors]] 属性中)
     * false
     *
     * @throws ServerErrorHttpException 如果响应状态码不等于20x
     */
    public function getTenantEnv()
    {
        /* 租户ID后续从请求参数中获取 */
        $this->tenant_id = 'default';

        // 设置多租户数据的缓存键
        $redisCache = Yii::$app->redisCache;
        $tenantKey = 'tenant:' . $this->tenant_id;

        // 从缓存中取回多租户数据
        $tenantData = $redisCache[$tenantKey];

        if ($tenantData === false) {
            $this->app_name = env('TENANT_APP_NAME');
            $this->secret = env('TENANT_SECRET');

            $response = Yii::$app->tenantHttp->createRequest()
                ->setMethod('get')
                ->setUrl('getTenantEnv')
                ->setData([
                    'appname' => $this->app_name,
                    'secret' => $this->secret,
                    'tenantid' => $this->tenant_id,
                ])
                ->send();
            // 检查响应状态码是否等于20x
            if ($response->isOk) {
                // 检查业务逻辑是否成功
                if ($response->data['returnCode'] === 0) {
                    $tenantData = ['message' => $response->data['returnDesc'], 'data' => $response->data['returnData']];
                    // 将多租户数据存放到缓存供下次使用
                    $redisCache[$tenantKey] = $tenantData;
                    return $tenantData;
                } else {
                    $this->addError('tenant_id', $response->data['returnDesc']);
                    return false;
                }
            } else {
                throw new ServerErrorHttpException(Yii::t('error', Yii::t('error', Yii::t('error', '20005'), ['statusCode' => $response->getStatusCode()])));
            }
        } else {
            return $tenantData;
        }

    }

}

</pre>
实现缓存(多租户系统的响应),编辑 \common\logics\http\tenant\Env.php,运行后台应用后,查看 Redis 中的数据,多租户系统的响应数据已经被缓存了
图31
50、清空Redis,一次请求中,对于多租户系统的HTTP请求仅有1次,如图32
一次请求中,对于多租户系统的HTTP请求仅有1次
图32
51、默认情况下,缓存中的数据会永久存留,除非它被某些缓存策略强制移除(例如:缓存空间已满,最老的数据会被移除),后续准备实现清除对应缓存数据的接口,以便于多租户系统的数据发生变化时,可以调用清除对应缓存数据的接口

PHP / Laravel / Yii2 老项目维护与长期技术支持

如果你的 PHP / Laravel / Yii2 项目已经上线,但遇到原开发离职、Bug 长期无人修复、接口不稳定、性能下降、代码难以接手等问题,可以联系我做一次远程技术排查。

适合以下情况:
✅ 老旧 PHP 系统无人维护
✅ Laravel / Yii2 项目 Bug 修复
✅ 后台管理系统小功能迭代
✅ RESTful API 接口排查
✅ MySQL / Redis / Nginx 性能问题
✅ 长期远程兼职维护

可先从一次小问题开始:
✅ 线上报错排查
✅ 接口异常分析
✅ 慢查询与性能瓶颈定位
✅ 代码结构初步评估
✅ 部署环境与日志检查

如需咨询,请联系我,并注明:PHP 维护咨询

联系方式:
Telegram:@shuijingwan
微信:13980074657
邮箱:shuijingwanwq@gmail.com

评论

2 条对“在 Yii 2 Starter Kit 中实现数据库连接时的动态配置,配置属性来源于多租户系统”的回复

  1. […] 在 Yii 2 Starter Kit 中数据库迁移的多租户实现 – 永夜 – 水井湾发表在《在 Yii 2 Starter Kit 中实现数据库连接时的动态配置,配置属性来源于多租户系…》 […]

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

这个站点使用 Akismet 来减少垃圾评论。了解你的评论数据如何被处理