yii2实现分库分表的方案与思路

大家可以从任何一个gii生成model类开始代码上溯,会发现:yii2的model层基于ActiveRecord实现DAO访问数据库的能力。

而ActiveRecord的继承链可以继续上溯,最终会发现model其实是一个component,而component是yii2做IOC的重要组成部分,提供了behaviors,event的能力供继承者扩展。

(IOC,component,behaviors,event等概念可以参考http://www.digpage.com/学习)

先不考虑上面的一堆概念,一个站点发展历程一般是1个库1个表,1个库N个表,M个库N个表这样走过来的,下面拿订单表为例,分别说说。

1)1库1表:yii2默认采用PDO连接mysql,框架默认会配置一个叫做db的component作为唯一的mysql连接对象,其中dsn分配了数据库地址,数据库名称,配置如下:

     'components' => [

'db' => [

'class' => 'yii\db\Connection',

'dsn' => 'mysql:host=10.10.10.10;port=4005;dbname=wordpress',

'username' => 'wp',

'password' => '123',

'charset' => 'utf8',

],

这就是yii2做IOC的一个典型事例,model层默认就会取这个db做为mysql连接对象,所以model访问都经过这个connection,可以从ActiveRecord类里看到。

class ActiveRecord extends BaseActiveRecord {

/**

* 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()

{

return Yii::$app->getDb();

}

追踪下去,最后会走yii2的ioc去创建名字叫做”db”的这个component返回给model层使用。

abstract class Application extends Module {

/**

* Returns the database connection component.

* @return \yii\db\Connection the database connection.

*/

public function getDb()

{

return $this->get('db');

}

yii2上述实现决定了只能连接了1台数据库服务器,选择了其中1个database,那么具体访问哪个表,是通过在Model里覆写tableName这个static方法实现的,ActiveRecord会基于覆写的tableName来决定表名是什么。

class OrderInfo extends \yii\db\ActiveRecord

{

/**

* @inheritdoc

* @return

*/

public static function tableName()

{

return 'order_info';

}

2)1库N表:因为orderInfo数据量变大,各方面性能指标有所下降,而单机硬件性能还有较大冗余,于是可以考虑分多张order_info表,均摊数据量。假设我们要份8张表,那么可以依据uid(用户ID)%8来决定订单存储在哪个表里。

然而1库1表的时候,tableName()返回是的order_info,于是理所应当的重载这个函数,提供一种动态变化的能力即可,例如:

class OrderInfo extends \yii\db\ActiveRecord

{

private static $partitionIndex_ = null; // 分表ID

/**

* 重置分区id

* @param unknown $uid

*/

private static function resetPartitionIndex($uid = null) {

$partitionCount = \Yii::$app->params['Order']['partitionCount'];

self::$partitionIndex_ = $uid % $partitionCount;

}

/**

* @inheritdoc

*/

public static function tableName()

{

return 'order_info' . self::$partitionIndex_;

}

提供一个resetParitionIndex($uid)函数,在每次操作model之前主动调用来标记分表的下标,并且重载tableName来为model层拼接生成本次操作的表名。

3)M库N表:1库N表逐渐发展,单机存储和性能达到瓶颈,只能将数据分散到多个服务器存储,于是提出了分库的需求。但是从”1库1表”的框架实现逻辑来看,model层默认取db配置作为mysql连接的话,是没有办法访问多个mysql实例的,所以必须解决这个问题。

一般产生这个需求,产品已经进入中期稳步发展阶段。有2个思路解决M库问题,1种是yii2通过改造直连多个地址进行访问多库,1种是yii2仍旧只连1个地址,而这个地址部署了dbproxy,由dbproxy根据你访问的库名代理连接多个库。

如果此前没有熟练的运维过dbproxy,并且php集群规模没有大到单个mysql实例客户端连接数过多拒绝服务的境地,那么第1种方案就可以解决了。否则,应该选择第2种方案。

无论选择哪种方案,我们都应该进一步改造tableName()函数,为database名称提供动态变化的能力,和table动态变化类似。

class OrderInfo extends \yii\db\ActiveRecord {

private static $databaseIndex_ = null; // 分库ID

private static $partitionIndex_ = null; // 分表ID

/**

* 重置分区id

* @param unknown $uid

*/

private static function resetPartitionIndex($uid = null) {

$databaseCount = \Yii::$app->params['Order']['databaseCount'];

$partitionCount = \Yii::$app->params['Order']['partitionCount'];

// 先决定分到哪一张表里

self::$partitionIndex_ = $uid % $partitionCount;

// 再根据表的下标决定分到哪个库里

self::$databaseIndex_ = intval(self::$partitionIndex_ / ($partitionCount / $databaseCount));

}

/**

* @inheritdoc

*/

public static function tableName()

{

$database = 'wordpress' . self::$databaseIndex_;

$table = 'order_info' . self::$partitionIndex_;

return $database . '.' . $table;

}

在分表逻辑基础上稍作改造,即可实现分库。假设分8张表,那么分别是00,01,02,03…07,然后决定分4个库,那么00,01表在00库,02,03表在01库,04,05表在02库,06,07表在03库,根据这个规律对应的计算代码如上。最终ActiveRecord生效的代码都会类似于”select * from wordpress0.order_info1″,这样就可以解决连接dbproxy访问多库的需求了。

那么yii直接访问多Mysql实例怎么做呢,其实类似tableName() ,我们只需要覆盖getDb()方法即可,同时要求我们首先配置好4个mysql实例,从而可以通过yii的application通过IOC设计来生成多个db连接,所有改动如下:

先配置好4个数据库,给予不同的component id以便区分,它们连接了不同的mysql实例,其中dsn里的dbname只要存在即可(防止PDO执行use database时候不存在报错),真实的库名是通过tableName()动态变化的。

'db0' => [

'class' => 'yii\db\Connection',

'dsn' => 'mysql:host=10.10.10.10;port=6184;dbname=wordpress0',

'username' => 'wp',

'password' => '123',

'charset' => 'utf8',

// 'tablePrefix' => 'ktv_',

],

'db1' => [

'class' => 'yii\db\Connection',

'dsn' => 'mysql:host=10.10.10.11;port=6184;dbname=wordpress2',

'username' => 'wp',

'password' => '123',

'charset' => 'utf8',

// 'tablePrefix' => 'ktv_',

],

'db2' => [

'class' => 'yii\db\Connection',

'dsn' => 'mysql:host=10.10.10.12;port=6184;dbname=wordpress4',

'username' => 'wp',

'password' => '123',

'charset' => 'utf8',

// 'tablePrefix' => 'ktv_',

],

'db3' => [

'class' => 'yii\db\Connection',

'dsn' => 'mysql:host=10.10.10.13;port=6184;dbname=wordpress6',

'username' => 'wp',

'password' => '123',

'charset' => 'utf8',

// 'tablePrefix' => 'ktv_',

],

覆写getDb()方法,根据库下标返回不同的数据库连接即可。

class OrderInfo extends \yii\db\ActiveRecord

{

private static $databaseIndex_ = null; // 分库ID

private static $partitionIndex_ = null; // 分表ID

/**

* 重置分区id

* @param unknown $uid

*/

private static function resetPartitionIndex($uid = null) {

$databaseCount = \Yii::$app->params['Order']['databaseCount'];

$partitionCount = \Yii::$app->params['Order']['partitionCount'];

// 先决定分到哪一张表里

self::$partitionIndex_ = $uid % $partitionCount;

// 再根据表的下标决定分到哪个库里

self::$databaseIndex_ = intval(self::$partitionIndex_ / ($partitionCount / $databaseCount));

}

/**

* 根据分库分表,返回库名.表名

*/

public static function tableName()

{

$database = 'wordpress' . self::$databaseIndex_;

$table = 'order_info' . self::$partitionIndex_;

return $database . '.' . $table;

}

/**

* 根据分库结果,返回不同的数据库连接

*/

public static function getDb()

{

return \Yii::$app->get('db' . self::$databaseIndex_);

}

这样,无论是yii连接多个mysql实例,还是yii连接1个dbproxy,都可以实现了。

网上有一些例子,试图通过component的event机制,通过在component的配置中指定onUpdate,onBeforeSave等自定义event去hook不同的DAO操作来隐式(自动)的变更database或者connection或者tablename的做法,都是基于model object才能实现的,如果直接使用model class的类似updateAll()方法的话,是绕过DAO直接走了PDO的,不会触发这些event,所以并不是完备的解决方案。

这样的方案原理简单,方案对框架无侵入,只是每次DB操作前都要显式的resetPartitionIndex($uid)调用。如果要做到用户无感知,那必须对ActiveRecord类进行继承,进一步覆盖所有class method的实现以便插入选库选表逻辑,代价过高。

补充:关于分库分表的一些实践细节,分表数量建议2^n,例如n=3的情况下分8张表,然后确定一下几个库,库数量是2^m,但要<=表数量,例如这里1个库,2个库,4个库,8个库都是可以的,表顺序坐落在这些库里即可。 为什么数量都是2指数,是因为如果面临扩容需求,数据的迁移将方便一些。假设分了2张表,数据按uid%2打散,要扩容成4张表,那么只需要把表0的部分数据迁移到表2,表1的部分数据迁移到表3,即可完成扩容,也就是uid%2和uid%4造成的迁移量是很小的,这个可以自己算一下。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,560评论 4 361
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,104评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,297评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,869评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,275评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,563评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,833评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,543评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,245评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,512评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,011评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,359评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,006评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,062评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,825评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,590评论 2 273
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,501评论 2 268

推荐阅读更多精彩内容