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

Dynamic Database Configuration in Yii 2 Starter Kit for Multi-Tenant Systems

检查数据库连接组件、RBAC组件是否被注册,如果已经被注册,则无需覆盖,编辑 \common\components\db\ActiveRecord.php

1. A multi-tenant system contains multiple tenants, and each tenant has its own database configuration. The current requirement is to connect to the database by calling the multi-tenant system’s API and using the hostname, username, and password from the response, rather than configuring it as an application component, as shown in Figure 1.

A multi-tenant system contains multiple tenants, and each tenant has its own database configuration. The current requirement is to connect to the database by calling the multi-tenant system's API and using the hostname, username, and password from the response, rather than configuring it as an application component.
Figure 1

2. Currently, it is configured as an application component in \common\config\base.php, as shown in Figure 2.

Currently, it is configured as an application component in \common\config\base.php
Figure 2

3. Enable Debug and check the Log Messages. The properties of the db component must be initialized (by calling the multi-tenant system’s API) before yii\db\Connection::open is executed, as shown in Figure 3.

Enable Debug and check the Log Messages. The properties of the db component must be initialized (by calling the multi-tenant system's API) before yii\db\Connection::open is executed.
Figure 3

4. Edit \common\config\base.php to configure the db component’s class as common\components\db\Connection, and comment out all environment variables.

PHP

        '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. Create \common\components\db\Connection.php, which extends \yii\db\Connection.

PHP
<?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);
    }

}

6. Repeat step 3. The properties of the db component have already been initialized before yii\db\Connection::open is executed (host=localhost changed to host=127.0.0.1), as shown in Figure 4.

Repeat step 3. The properties of the db component have already been initialized before yii\db\Connection::open is executed (host=localhost changed to host=127.0.0.1).
Figure 4

7. Now we need to call the multi-tenant system’s API and reassign $tenantConfig based on its response (Recommendation: This approach is only suitable for a single tenant. If there are multiple tenants, it is recommended that each tenant corresponds to a different connection component named based on the tenant ID, rather than mapping to the db connection component), as shown in Figure 5.

Now we need to call the multi-tenant system's API and reassign $tenantConfig based on its response (Recommendation: This approach is only suitable for a single tenant. If there are multiple tenants, it is recommended that each tenant corresponds to a different connection component named based on the tenant ID, rather than mapping to the db connection component).
Figure 5

8. Now revert all changes and prepare to implement the simultaneous use of multiple databases (where each tenant corresponds to a different connection component named based on the tenant ID). Reference URL: https://github.com/yiichina/yii2/blob/master/docs/guide-zh-CN/db-active-record.md , as shown in Figure 6.

Now revert all changes and prepare to implement the simultaneous use of multiple databases (where each tenant corresponds to a different connection component named based on the tenant ID). Reference URL: https://github.com/yiichina/yii2/blob/master/docs/guide-zh-CN/db-active-record.md
Figure 6

9. Create \common\components\db\ActiveRecord.php, which extends \yii\db\ActiveRecord, and override the [[yii\db\ActiveRecord::getDb()|getDb()]] method.

PHP
<?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();
    }

}

10. Edit \common\models\KeyStorageItem.php to extend common\components\db\ActiveRecord.

PHP
<?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
{
}

11. Edit \common\config\base.php and comment out the db component. Later, it is recommended to move the db component to the development environment to facilitate the use of Gii.

PHP

        /*
        '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. Open http://frontend.cmcp-api.localhost/ , and an error occurs: Failed to instantiate component or class “db”. The reason is that the log target is a database table, as shown in Figure 7.

Open http://frontend.cmcp-api.localhost/ , and an error occurs: Failed to instantiate component or class "db". The reason is that the log target is a database table.
Figure 7

13. View the log component configuration in \common\config\base.php.

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. The log component must be loaded during bootstrap. Therefore, if the database connection is dynamically configured, it is already too late for the bootstrap phase. Edit the log component configuration in \common\config\base.php to save log messages to a file.

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. Open http://frontend.cmcp-api.localhost/ , and an error occurs: Unknown component ID: db, as shown in Figure 8.

Open http://frontend.cmcp-api.localhost/ , and an error occurs: Unknown component ID: db
Figure 8

16. In all model files under \common\models, replace use yii\db\ActiveRecord; with use common\components\db\ActiveRecord;, as shown in Figure 9.

Plaintext
Replace use yii\db\ActiveRecord; with use common\components\db\ActiveRecord;
Replace \yii\db\ActiveRecord with \common\components\db\ActiveRecord

In all model files under \common\models, replace use yii\db\ActiveRecord; with use common\components\db\ActiveRecord;
Figure 9

17. Open http://frontend.cmcp-api.localhost/ , and an error occurs: Failed to instantiate component or class “db”. The reason is that the RBAC component uses a database table to store data, as shown in Figure 10.

Open http://frontend.cmcp-api.localhost/ , and an error occurs: Failed to instantiate component or class "db". The reason is that the RBAC component uses a database table to store data.
Figure 10

18. Edit \common\config\base.php and comment out the RBAC component. Because its db property defaults to db, it needs to be dynamically adjusted to the database connection component corresponding to the tenant ID.

PHP

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

19. Edit \common\components\db\ActiveRecord.php to register the database connection component and RBAC component via the [[yii\di\ServiceLocator::setComponents()]] method.

PHP
<?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;
    }
}

20. Repeat step 3. The properties of the db component have already been initialized before yii\db\Connection::open is executed (host=localhost changed to host=127.0.0.1), as shown in Figure 11.

Repeat step 3. The properties of the db component have already been initialized before yii\db\Connection::open is executed (host=localhost changed to host=127.0.0.1).
Figure 11

21. In Postman, send a GET request to http://www.cmcp-api.localhost/v1/pages , and receive a 200 response, as shown in Figure 12.

In Postman, send a GET request to http://www.cmcp-api.localhost/v1/pages , and receive a 200 response.
Figure 12

22. Open the URL: http://backend.cmcp-api.localhost/ , and an error occurs: Unknown component ID: db, as shown in Figure 13.

Open the URL: http://backend.cmcp-api.localhost/ , and an error occurs: Unknown component ID: db
Figure 13

23. Edit \backend\models\SystemLog.php to extend \common\components\db\ActiveRecord, as shown in Figure 14.

Plaintext
Replace \yii\db\ActiveRecord with \common\components\db\ActiveRecord

Edit \backend\models\SystemLog.php to extend \common\components\db\ActiveRecord
Figure 14

24. Open the URL: http://backend.cmcp-api.localhost/ , and receive a 200 response.

25. Now we need to call the multi-tenant system’s API and use its response to directly initialize the database connection and RBAC, based on the already installed Yii 2 HTTP client extension, as shown in Figure 15.

Now we need to call the multi-tenant system's API and use its response to directly initialize the database connection and RBAC, based on the already installed Yii 2 HTTP client extension.
Figure 15

26. Edit \.env.dist and \.env to add the multi-tenant related configuration.

Plaintext
# Multi-tenant
# ----
TENANT_HOST_INFO       = http://wjdev.chinamcloud.com:8600 # HOME URL
TENANT_BASE_URL        = /interface # BASE URL
TENANT_APP_NAME        = cmcpapi # Module English name
TENANT_SECRET          = 94030d307b160f04b88592cb9bebdd4c # Module Secret

27. Edit \common\config\bootstrap.php to set aliases.

PHP

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

28. Configure the client via application components. Edit \common\config\web.php, as shown in Figure 16.

PHP

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

Configure the client via application components. Edit \common\config\web.php
Figure 16

29. Log the HTTP requests sent and analyze their execution by configuring the log. Edit \common\config\base.php, as shown in Figure 17.

PHP

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

Log the HTTP requests sent and analyze their execution by configuring the log. Edit \common\config\base.php
Figure 17

30. The yii2 HTTP client extension provides a debug panel that integrates with the yii debug module and displays executed HTTP requests. Enable the debug panel, as shown in Figure 18.

PHP

    $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',
            ],
        ],
    ];

The yii2 HTTP client extension provides a debug panel that integrates with the yii debug module and displays executed HTTP requests. Enable the debug panel.
Figure 18

31. After refreshing the page, check the log panel. A new HTTP Client section appears, as shown in Figure 19.

After refreshing the page, check the log panel. A new HTTP Client section appears.
Figure 19

32. Create \common\logics\http\tenant\Env.php. The http directory indicates that the model data within this directory originates from HTTP requests; the tenant directory indicates models related to the multi-tenant system; and Env.php represents the environment configuration model.

PHP
<?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()])));
        }

    }

}

33. Create language pack files: \common\messages\en\model\http\tenant\env.php and \common\messages\zh\model\http\tenant\env.php, as shown in Figure 20.
\common\messages\en\model\http\tenant\env.php

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

PHP

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

Create language pack files: \common\messages\en\model\http\tenant\env.php and \common\messages\zh\model\http\tenant\env.php
Figure 20

34. Edit the language pack files \api\messages\en\error.php and \api\messages\zh\error.php.
\api\messages\en\error.php

PHP

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

\api\messages\zh\error.php

PHP

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

35. Copy \api\messages\en\app.php, \api\messages\en\error.php, \api\messages\zh\app.php, and \api\messages\zh\error.php to \common\messages\en\app.php, \common\messages\en\error.php, \common\messages\zh\app.php, and \common\messages\zh\error.php. Edit:
\common\messages\zh\error.php

PHP

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

36. Edit \common\components\db\ActiveRecord.php. The database configuration information is sourced from the tenant environment configuration API.

PHP
<?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;
    }
}

37. In Postman, send a GET request to http://www.cmcp-api.localhost/v1/pages , and receive a 200 response, as shown in Figure 21.

In Postman, send a GET request to http://www.cmcp-api.localhost/v1/pages , and receive a 200 response.
Figure 21

38. In Postman, send a GET request to the multi-tenant environment configuration API with one incorrect parameter, as shown in Figure 22.

In Postman, send a GET request to the multi-tenant environment configuration API with one incorrect parameter.
Figure 22

39. Edit \.env. The value of TENANT_APP_NAME is incorrect.

Plaintext
TENANT_APP_NAME        = cmcpapi1 # Module English name

40. In Postman, send a GET request to http://www.cmcp-api.localhost/v1/pages , and receive a 500 response, as shown in Figure 23.

In Postman, send a GET request to http://www.cmcp-api.localhost/v1/pages , and receive a 500 response.
Figure 23

41. Checking the logs, I found that the API application and the backend application made 8 and 139 requests to the multi-tenant API, respectively. The backend reported an error: Unable to send log via yii\log\FileTarget: Exception (Database Exception) ‘yii\db\Exception’ with message ‘SQLSTATE[HY000] [1040] Too many connections’ , as shown in Figures 24 and 25.

Checking the logs, I found that the API application and the backend application made 8 and 139 requests to the multi-tenant API, respectively.
Figure 24
The backend reported an error: Unable to send log via yii\log\FileTarget: Exception (Database Exception) 'yii\db\Exception' with message 'SQLSTATE[HY000] [1040] Too many connections'
Figure 25

42. Check whether the database connection component and RBAC component have already been registered; if they have, there is no need to overwrite them. Edit \common\components\db\ActiveRecord.php, as shown in Figure 26.

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,
                ],
                'authManager' => [
                    'class' => 'yii\rbac\DbManager',
                    'db' => $tenantDb,
                    'itemTable' => '{{%rbac_auth_item}}',
                    'itemChildTable' => '{{%rbac_auth_item_child}}',
                    'assignmentTable' => '{{%rbac_auth_assignment}}',
                    'ruleTable' => '{{%rbac_auth_rule}}'
                ],
            ]);
        }

Check whether the database connection component and RBAC component have already been registered; if they have, there is no need to overwrite them. Edit \common\components\db\ActiveRecord.php
Figure 26

43. The backend application error ‘SQLSTATE[HY000] [1040] Too many connections’ has been resolved, as shown in Figure 27.

The backend application error 'SQLSTATE[HY000] [1040] Too many connections' has been resolved.
Figure 27

44. At this point, the dynamic configuration of the database connection—with configuration properties sourced from the multi-tenant system—has been largely implemented. Next, I should implement caching for the multi-tenant system’s responses, because a single request can trigger up to hundreds of HTTP requests to the multi-tenant system, with each HTTP request taking an average of about 100ms, as shown in Figure 28.

At this point, the dynamic configuration of the database connection—with configuration properties sourced from the multi-tenant system—has been largely implemented. Next, I should implement caching for the multi-tenant system's responses, because a single request can trigger up to hundreds of HTTP requests to the multi-tenant system, with each HTTP request taking an average of about 100ms.
Figure 28

45. Enable Schema caching (only in the production environment) and cache it in Redis. Edit \.env.dist and \.env, as shown in Figure 29.

Plaintext
YII_ENV     = prod

# Redis
# ----
REDIS_HOSTNAME         = localhost # Hostname/IP address
REDIS_PORT             = 6379 # Port
#REDIS_PASSWORD        =  # Password
REDIS_DATABASE         = 0 # Database

# Redis cache
# ----
REDIS_CACHE_KEY_PREFIX = ca: # Unique key prefix

Enable Schema caching (only in the production environment) and cache it in Redis. Edit \.env.dist and \.env
Figure 29

46. Edit \common\config\base.php to configure the Redis connection and the cache component within the application configuration.

PHP

        '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. Enable Schema caching in the database connection. Edit \common\components\db\ActiveRecord.php.

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. After running the backend application, check the data in Redis. The structure of the database tables has been cached, as shown in Figure 30.

After running the backend application, check the data in Redis. The structure of the database tables has been cached.
Figure 30

49. Implement caching for the multi-tenant system’s responses. Edit \common\logics\http\tenant\Env.php. After running the backend application, check the data in Redis. The response data from the multi-tenant system has been cached, as shown in Figure 31.

PHP
<?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;
        }

    }

}

Implement caching for the multi-tenant system's responses. Edit \common\logics\http\tenant\Env.php. After running the backend application, check the data in Redis. The response data from the multi-tenant system has been cached.
Figure 31

50. After clearing Redis, a single request only triggers 1 HTTP request to the multi-tenant system, as shown in Figure 32.

A single request only triggers 1 HTTP request to the multi-tenant system.
Figure 32

51. By default, data in the cache will persist permanently unless it is forcibly removed by certain cache eviction policies (for example, if the cache space is full, the oldest data will be removed). Next, I plan to implement an interface to clear the corresponding cached data, so that when the multi-tenant system’s data changes, the interface can be called to clear the relevant cache.

关于作者 · 持续学习与技术实践

我是一名拥有 15 年以上开发经验的技术从业者,自 2013 年起持续运营个人技术博客,记录工作与个人项目中遇到的真实问题,以及解决问题过程中的探索、实践和思考。

除了技术写作,我也在持续开发和维护自己的独立项目,探索新技术和新工具在实际场景中的应用。我始终相信,没有不值得去解决的问题,也没有不值得去学习的技术。

如果这篇文章对你有所帮助,欢迎浏览博客中的其他文章,也欢迎交流技术经验与想法。

关于我  ·  GitHub  ·  邮件联系