Codeigniter多租户SaaS应用:CLI定时任务动态数据库选择方案咨询
适配Web与CLI的CodeIgniter多租户动态数据库选择方案
我之前做CodeIgniter多租户SaaS的时候也碰到过这个坑——CLI模式下拿不到HTTP_HOST,导致没法自动切换租户数据库。给你几个亲测有效的方案,能完美兼顾Web和CLI场景:
方案1:CLI传参指定租户,Web用域名自动识别
这个方案适合需要单独触发某个租户定时任务的场景,比如只给某客户执行数据备份。
实现步骤:
CLI运行时传租户参数:
执行Cron任务时通过命令行参数指定租户域名,比如:php index.php cron_runner run --tenant=customer1.yourdomain.com修改数据库配置逻辑:
在database.php里判断当前是否为CLI模式,分别获取租户标识:$ci = get_instance(); // 区分Web和CLI模式获取租户标识 if (is_cli()) { // 从CLI参数读取租户域名 $tenant_host = $ci->input->get('tenant'); if (empty($tenant_host)) { die("Error: 请通过--tenant参数指定租户域名\n"); } } else { // Web模式下读取HTTP_HOST $tenant_host = $ci->input->server('HTTP_HOST'); } // 从主数据库查询该租户对应的数据库配置 // 假设主数据库有个`tenants`表,存储域名与数据库信息的映射 $ci->db->where('domain', $tenant_host); $tenant_config = $ci->db->get('tenants')->row(); // 配置租户数据库连接 $db['default'] = array( 'dsn' => '', 'hostname' => $tenant_config->db_host, 'username' => $tenant_config->db_username, 'password' => $tenant_config->db_password, 'database' => $tenant_config->db_name, 'dbdriver' => 'mysqli', 'dbprefix' => '', 'pconnect' => FALSE, 'db_debug' => (ENVIRONMENT !== 'production'), 'cache_on' => FALSE, 'cachedir' => '', 'char_set' => 'utf8', 'dbcollat' => 'utf8_general_ci', 'swap_pre' => '', 'encrypt' => FALSE, 'compress' => FALSE, 'stricton' => FALSE, 'failover' => array(), 'save_queries' => TRUE );
方案2:批量遍历所有活跃租户执行任务
如果你的定时任务需要给所有租户统一执行(比如每日数据统计),这个方案更高效——一次Cron调用处理所有租户,不用逐个传参。
实现步骤:
修改CronRunner逻辑:
先从主数据库获取所有活跃租户,然后循环切换数据库执行任务:class CronRunner { var $ci; var $ci_cron_db; var $ci_cron_table; // 新增主数据库连接属性,避免被租户连接覆盖 var $main_db; public function __construct() { $this->ci = & get_instance(); $this->ci->load->helper('url'); $this->ci->load->library('user_agent'); // CLI模式下不需要加载session,避免无效初始化 if (!is_cli()) { $this->ci->load->library('session'); } // 先连接主数据库(单独配置一个main连接,存储租户信息) $this->main_db = $this->ci->load->database('main', TRUE); $this->ci_cron_table = 'crn_crons'; } private function calculateNextRun($obj) { return (time() + $obj->interval_sec); } // 新增切换租户数据库的方法 private function switch_to_tenant_db($tenant) { $db_config = array( 'hostname' => $tenant->db_host, 'username' => $tenant->db_username, 'password' => $tenant->db_password, 'database' => $tenant->db_name, 'dbdriver' => 'mysqli', 'dbprefix' => '', 'pconnect' => FALSE, 'db_debug' => (ENVIRONMENT !== 'production'), 'char_set' => 'utf8', 'dbcollat' => 'utf8_general_ci' ); // 初始化租户数据库连接 $this->ci_cron_db = $this->ci->load->database($db_config, TRUE); } public function run() { // 从主数据库获取所有活跃租户 $this->main_db->where('is_active', 1); $tenants = $this->main_db->get('tenants')->result(); foreach ($tenants as $tenant) { // 切换到当前租户的数据库 $this->switch_to_tenant_db($tenant); // 查询当前租户需要执行的定时任务 $query = $this->ci_cron_db->where('is_active', 1) ->where('now() >= next_run_at OR next_run_at IS NULL', '', false) ->from($this->ci_cron_table) ->get(); if ($query->num_rows() > 0) { $env = getenv('CI_ENV'); foreach ($query->result() as $row) { $cmd = "export CI_ENV={$env} && {$row->command}"; // 更新下次执行时间 $this->ci_cron_db->set('next_run_at', 'FROM_UNIXTIME(' . $this->calculateNextRun($row) . ')', false) ->where('id', $row->id) ->update($this->ci_cron_table); // 执行任务 $output = shell_exec($cmd); // 更新最后执行时间 $this->ci_cron_db->set('last_run_at', 'now()', false) ->where('id', $row->id) ->update($this->ci_cron_table); } } } } }配置主数据库连接:
在database.php里新增一个main连接,专门用来存储租户信息:$db['main'] = array( 'dsn' => '', 'hostname' => '你的主数据库地址', 'username' => '主数据库用户名', 'password' => '主数据库密码', 'database' => '主数据库名称', 'dbdriver' => 'mysqli', // 其他默认配置... );
方案3:通过环境变量传递租户信息
如果不想用命令行参数,也可以用环境变量传递租户标识,更隐蔽。
实现步骤:
Cron表达式中设置环境变量:
* * * * * CI_TENANT=customer1.yourdomain.com php /path/to/your/app/index.php cron_runner run代码中读取环境变量:
在database.php里修改租户标识获取逻辑:$ci = get_instance(); if (is_cli()) { $tenant_host = getenv('CI_TENANT'); if (empty($tenant_host)) { die("Error: 请设置CI_TENANT环境变量指定租户域名\n"); } } else { $tenant_host = $ci->input->server('HTTP_HOST'); } // 后续查询租户配置、设置数据库连接的逻辑和方案1一致
额外注意事项:
- 主数据库连接隔离:一定要把存储租户信息的主数据库和租户业务数据库分开配置,避免切换租户时覆盖主连接。
- CLI模式下的无用库加载:像
session这类Web专属的库,在CLI模式下不要加载,避免出现初始化错误。 - 错误处理:不管用哪个方案,都要加参数/环境变量的校验,避免无租户标识时任务异常。
内容的提问来源于stack exchange,提问作者Daniel Felix
相关产品推荐
相关产品推荐

