1. 程式人生 > >詳解yii2實現分庫分表的方案與思路

詳解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分配了資料庫地址,資料庫名稱,配置如下:

?

1

2

3

4

5

6

7

8

'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類裡看到。

?

1

2

3

4

5

6

7

8

9

10

11

12

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層使用。

?

1

2

3

4

5

6

7

8

9

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來決定表名是什麼。

?

1

2

3

4

5

6

7

8

9

10

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,於是理所應當的過載這個函式,提供一種動態變化的能力即可,例如:

?

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

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動態變化類似。

?

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

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()動態變化的。

?

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

'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()方法,根據庫下標返回不同的資料庫連線即可。

?

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

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造成的遷移量是很小的,這個可以自己算一下。

總結

以上就是關於yii2實現分庫分表的全部內容了,希望本文的內容對大家的學習或者工作能帶來一定的幫助,如果有疑問大家