分享一个 MySQL 分库分表类

当一个表数据记录过大时就会出现性能瓶颈,而一般对应的解决办法是要么做分区表,要么分表,分区表就不说了,分表又分为垂直分割和水平分割,具体区别请自行搜索。一般而言,分库分表属于水平分割,按照一定的规则将数据插入到不同的表中去。而分库则可以很方便的转移数据库的压力,比如将一个很大库的分别放在不同的服务器上。

下面是我写的一个分库分表的实现:

<?php
/**
 * User: jing.liu
 * Date: 14-8-12
 * Time: 下午3:16
 */

namespace App\Model\Database;

class Config
{
    public $dsn;
    public $user;
    public $password;
    /**
     * @var string 分库分表后得到的数据库名
     */
    public $dbname;
    /**
     * @var string 分库分表后得到的表名
     */
    public $table;

    /**
     * @var array MySQL 配置数组
     */
    private static $config;

    /**
     * @var string 配置文件路径
     */
    private static $configFile = 'mysql.php';

    public function __construct($dbname, $table, $id = 0)
    {
        if (is_null(static::$config)) {
            $config = include(static::$configFile);
            static::$config = $config;
        }

        $config = static::$config;
        if (isset($config['shared']) && isset($config['shared'][$dbname])) {
            $dbconfig = $config['shared'][$dbname];
            $id = is_numeric($id) ? (int)$id : crc32($id);
            $database_id = ($id / $dbconfig['database_split'][0]) % $dbconfig['database_split'][1];
            $table_id = ($id / $dbconfig['table_split'][0]) % $dbconfig['table_split'][1];

            foreach ($dbconfig['host'] as $key => $conf) {
                list($from, $to) = explode('-', $key);
                if ($from <= $database_id && $database_id <= $to) {
                    $the_config = $conf;
                }
            }

            $this->dbname = $dbname . '_' . $database_id;
            $this->table = $table . '_' . $table_id;
        } else {
            $this->dbname = $dbname;
            $this->table = $table;
            $the_config = $config['db'][$dbname];
        }
        $c = $the_config;
        if (isset($c['unix_socket']) && $c['unix_socket']) {
            $this->dsn = sprintf('mysql:dbname=%s;unix_socket=%s', $this->dbname, $c['unix_socket']);
        } else {
            $this->dsn = sprintf('mysql:dbname=%s;host=%s;port=%s', $this->dbname, $c['host'], $c['port']);
        }
        $this->user = $c['user'];
        $this->password = $c['password'];
    }

}

Config 类就做一个事情,根据配置文件,去拿到对应的库和表的链接配置,然后客户可以根据 dsn 去链接对应的数据库。对应的配置文件如下:

<?php
/**
 * User: jing.liu
 * Date: 14-8-6
 * Time: 上午11:19
 */

$default = array(
    'unix_socket' => null,
    'host' => 'localhost',
    'port' => '3306',
    'user' => 'root',
    'password' => '',
);

$config = array(
    // 不进行分库分表的数据库
    'db' => array(
        'my_site' => $default,
    ),
    // 分库分表
    'shared' => array(
        'user' => array(
            'host' => array(
                /**
                 * 编号为 0 到 10 的库使用的链接配置
                 */
                '0-10' => $default,
                /**
                 * 编号为 11 到 28 的库使用的链接配置
                 */
                '11-28' => $default,
                /**
                 * 编号为 29 到 99 的库使用的链接配置
                 */
                '29-99' => $default,

            ),

            // 分库分表规则
            /**
             * 下面的配置对应百库百表
             * 如果根据 uid 进行分表,假设 uid 为 543234678,对应的库表为:
             *  (543234678 / 1) % 100 = 78 为编号为 78 的库
             *  (543234678 / 100) % 100 = 46 为编号为 46 的表
             */
            'database_split' => array(1, 100),
            'table_split' => array(100, 100),
        ),
    ),
);


return $config;

给出一个使用这个分库分表的例子:

<?php
/**
 * User: jing.liu
 * Date: 14-8-6
 * Time: 上午10:23
 */

namespace App\Model;

use App\Model\Database\Config;
use \PDO;

abstract class Model
{
    /**
     * @var Config
     */
    public $config;

    /**
     * @var PDO
     */
    public $connection;

    protected $dbnamePrefix;
    protected $tablePrefix;

    /**
     * @var string 分库分表后对应的表
     */
    protected $table;

    public function __construct($id)
    {
        $this->config = new Config($this->dbnamePrefix, $this->tablePrefix, $id);
        $this->connection = new Pdo($this->config->dsn, $this->config->user, $this->config->password);
        $this->table = $this->config->table;
    }

    public function update(array $data, array $where = array())
    {

    }

    public function select(array $where)
    {

    }

    public function insert(array $data)
    {

    }

    public function query($sql)
    {
        return $this->connection->query($sql);
    }
}

下面这个例子展示了如何使用上述的 Model 类:

<?php
/**
 * User: jing.liu
 * Date: 14-8-12
 * Time: 下午4:06
 */

require 'Config.php';
require 'Model.php';

use App\Model\Model;

class User extends Model
{
    protected $dbnamePrefix = 'user';
    protected $tablePrefix = 'userinfo';
}

$user = new User(4455345345);

print_r($user);

如果看官您有任何疑问或者有更好的实现,欢迎交流~

PHP 取一个整数的十位上的数值可能出现的 bug

问:如何取到一个整数的十位上的数值?

答:先除以 10 再对 10 取余。代码如下:

echo ($number / 10) % 10;

很简单是不是?但这段代码有 bug。你可以运行下面这段代码:

echo PHP_INT_MAX;
echo PHP_EOL; // 输出换行 
echo (PHP_INT_MAX / 10) % 10;
echo PHP_EOL;

输出如下:

buggggggggggeeeeee

可以看到整数 9223372036854775807 的十位数上是 0 ,而我们确得到了 2 ,这是怎么一回事呢?

我们执行下如下代码:

$a = PHP_INT_MAX;
var_dump(gettype($a), $a);
$b = $a / 10;
var_dump(gettype($b), $b);
$c = $b % 10;
var_dump(gettype($c), $c);

输出为:

RTX截图未命名

可以看到, 9223372036854775807 不能被 10 整除,$a / 10 后得到的是一个浮点数,丢失了精度,这个时候再对 10 取模得到的值就不准了。

请时刻记住 PHP 变量的类型

我们都知道 PHP 变量类型是弱类型,这样很方便,当你需要一个数字时,变量就表现得像个 Int,当你需要字符串时,变量就表现得像个 String。但方便的同时,如果不注意也会带来坑,并且这种情况带来的 bug 还很不好排查。今天我就遇到了一个。

游戏新注册用户的时候需要生成一个 uid,一般来说,在数据量小且低并发的时候直接使用 MySQL 的自增 ID 就可以生成得到唯一的 uid,但如果数据量大且并发很高,使用 MySQL 的自增 ID 就不能满足需求了。我们使用了一个叫 ukg 的东西,该服务的功能就一个,每次调用它,就给你返回一个唯一的数字,嗯,很好,刚好能满足我们生成 uid 的需求。

代码写完后,测试的过程中发现了一个 bug,设备第一次登录的时候会登录失败,第二次及以后就登录成功。排查了近 1 个小时才找到问题所在,设备第一次登录的时候,我们访问 ukg 得到一个数字,当做玩家的 uid,并且存入 MySQL,后来发现我们从 ukg 返回得到的实际上是一个 数字+空格+回车 组成的一个字符串,而空格和回车都是不可见字符串,很容易就忽略了,存入 MySQL 时,MySQL 进行了转换将空格和回车去掉然后正确存进了数据库。这样就能解释为什么第一次登录失败,第二次及以后的请求就没问题了。

定位 bug 后,解决就很容易了,从 ukg 取到数据后,将其强制转换为 Int 就 OK 了。

虽然,PHP 的语法并不要求一个变量强制其数据类型,但我们在写代码的过程中,还是需要时刻注意变量的数据类型。常见的错误还有 0 == false 相等导致的逻辑出错。如果不嫌麻烦的话,所有 == 的地方都改成 === 就不会出现这类型的 bug 了。

设计模式之装饰模式(修饰模式)

装饰模式也叫修饰模式,维基上对修饰模式的描述是:

修饰模式,是面向对象编程领域中,一种动态地往一个类中添加新的行为的设计模式。就功能而言,修饰模式相比生成子类更为灵活,这样可以给某个对象而不是整个类添加一些功能。

本文将以一个具体的例子说明为什么装饰模式比生成子类的方式更为灵活。

假设有一款城堡游戏,每个城堡有它的价值,我们有普通城堡,拥有钻石的城堡,被污染的城堡,假设普通的城堡价值为 10,拥有钻石的城堡比普通的城堡价值多 5,被污染的城堡比普通的城堡价值少 6,因此我们建了如下的类:

<?php
/**
 * 公用接口,所有物品都是有价值的
 */
abstract class Stuff
{
    abstract function getWealth();
}

class Castle extends Stuff
{
    public function getWealth()
    {
        return 10;
    }
}

class DiamondCastle extends Castle
{
    public function getWealth()
    {
        return parent::getWealth() + 5;
    }
}

class PollutedCastle extends Castle
{
    public function getWealth()
    {
        return parent::getWealth() - 6;
    }
}

看起来还不错,我们可以很方便获得钻石城堡对象也可以获得被污染的城堡对象,但是如果我们想获得即拥有钻石又被污染了的城堡对象该怎么做呢。当然可以新建一个类 DiamondPollutedCastle,但实现起来并不优雅,尤其是当后期又增加一些带有不同属性的城堡时,这种组合会更加的多,显然为每一种组合都创建一个类会使得整个系统类过多。这个时候使用装饰模式就比较合适了。

装饰模式使用组合和委托而不是只使用集成来解决功能变化的问题。Decorator 类会持有另外一个类的实例。Decorator 对象会实现与被调用对象的方法的队友的泪方法。用这种方法可以在运行是创建一系列的 Decorator 对象(《PHP 面向对象模式与实践》p167)。

下面是使用装饰模式重写的城堡游戏类:

<?php
/**
 * 公用接口,所有物品都是有价值的
 */
abstract class Stuff
{
    abstract function getWealth();
}

class Castle extends Stuff
{
    public function getWealth()
    {
        return 10;
    }
}

abstract class Decorator extends Stuff
{
    /**
     * @var Stuff
     */
    protected $stuff;
    public function __construct(Stuff $stuff)
    {
        $this->stuff = $stuff;
    }
}

class DiamondDecorator extends Decorator
{
    public function getWealth()
    {
        return $this->stuff->getWealth() + 5;
    }
}

class PollutedDecorator extends Decorator
{
    public function getWealth()
    {
        return $this->stuff->getWealth() - 6;
    }
}

我们这样来实例化城堡对象:

<?php
// 普通城堡
$generalCastle = new Castle();
// 钻石城堡
$diamondCastle = new DiamondDecorator(new Castle());
// 被污染的城堡
$pollutedCastle = new PollutedDecorator(new Castle());
// 被污染的钻石城堡
$diamondPollutedCastle = new PollutedDecorator(new DiamondDecorator(new Castle()));

通过像这样使用组合和委托,可以在运行时轻松地合并对象,对于初始化类调用的代码来说,并不需要内部是如何合并的,因为每个 Decorator 类都有 getWealth() 方法,所以无论是一个装饰器对象还是真正的 Castle 对象,调用端都可以使用 getWealth() 方法来获取一个装饰器城堡或者普通城堡的价值。