meep php,PHP在TIDB上遇到的坑

PHP在TIDB上遇到的坑

原因不多说, 总之部署了两套TIDB的集群环境.

TIDB现在相当的火, 还去过很多次pingcap的在西小口的线下meepup, 各种牛人的分享.

今天主要是总结一些在tidb中遇到的坑, 有PHP的, 也有TIDB的, 但是可能是自己坑自己的.

主要问题如下:

自增ID

事务提交

prepare语句

目录:

自增ID

事务提交

prepare语句

在lavavel中使用TIDB

总结

自增ID

TIDB的自增ID不是按照时间增序的, 那么按照时间进行分页的需求就比较坑了.

比如有2台TIDB, t1 和 t2,

那么ID 1-5000 在t1, 5001-10000 在t2,

10001-15000又在t1, 15001-20000在t2.

如果新增两条纪录, 它们的ID会是1, 5001.

解决办法是在每张表新增一个字段, 然后到发号器去取一下这个ID.

而表的ID, TIDB建议使用UUID, 而不使用TIDB的自增ID.

个人觉得, 在要满足业务的条件是用UUID+发号器.

比如一个简单的实现为:

以表名在redis保存一个key, 每新增一条纪录, 使用incr命令取一下ID, 插入到表中.

事务提交

具体问题是PDO的commit()函数始终返回true, 所以会导致业务判断失误.

解决办法是使用rawSql, 其php模版如下:

$options = $options = [

PDO::ATTR_CASE => PDO::CASE_NATURAL,

PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,

PDO::ATTR_ORACLE_NULLS => PDO::NULL_NATURAL,

PDO::ATTR_STRINGIFY_FETCHES => false,

PDO::ATTR_EMULATE_PREPARES => true,

];

$pdo = new PDO('', '', '', $options);

$pdo->exec('START TRANSACTION'); // will throw an exception if failed

try {

// SQLs

// ...

$pdo->exec('COMMIT'); // will throw an exception

} catch (Exception $e) {

$ret = $pdo->exec('ROLLBACK');

throw $e;

} catch (Throwable $e) {

$ret = $pdo->exec('ROLLBACK');

throw $e;

}

而且commit()函数在返回true后, 虽然事实事务是失败的, 但是也没有设置errorCode,

所以办法只有使用rawSql.

TIDB还有一个事务问题, 是理论上能够成功提交的事务, 实际上多次提交也成功不了,

在我这里是一次都没成功过, 而且因为PHP-commit()函数的问题, 又会导致误判.

但是换了sql的参数后, 就能成功提交.

这个问题暂时无解, 但是业务上也不能出错才行, 可以通过rawSql-COMMIT暂时处理下.

prepare语句

用PDO的可能会使用prepare语句, 但是在TIDB中遇到的一个问题就是,

用prepare语句查出来的纪录有时会比理论的少几条.

具体的问题我也给TIDB提了一个issue.

解决办法就是将prepare语句转换成rawSql, 这个PHP已经帮我们做了,

用法如下:

$options = $options = [

PDO::ATTR_EMULATE_PREPARES => true,

];

$pdo = new PDO('', '', '', $options);

PDO::ATTR_EMULATE_PREPARES 启用或禁用预处理语句的模拟。

有些驱动不支持或有限度地支持本地预处理。

使用此设置强制PDO总是模拟预处理语句(如果为 TRUE ),

或试着使用本地预处理语句(如果为 FALSE)。

如果驱动不能成功预处理当前查询,

它将总是回到模拟预处理语句上。

需要 bool 类型。

在lavavel中使用TIDB

现在Lavavel应该是PHP中最火的框架了, 在我使用lavavel+TIDB遇到的问题,

基本就是上面提到的一些问题, 我给出的解决办法如下:

修改database配置, 使其不使用prepare语句

重点在mysql数组中增加options字段, 其实为

'options' => [

PDO::ATTR_EMULATE_PREPARES => true,

],

file: config/database.php

return [

'fetch' => PDO::FETCH_CLASS,

'default' => env('DB_CONNECTION', 'mysql'),

'connections' => [

'testing' => [

'driver' => 'sqlite',

'database' => ':memory:',

],

'sqlite' => [

'driver' => 'sqlite',

'database' => env('DB_DATABASE', storage_path('database.sqlite')),

'prefix' => env('DB_PREFIX', ''),

],

'mysql' => [

'read' => [

'host' => env('DB_READ_HOST','localhost'),

],

'write' => [

'host' => env('DB_WRITE_HOST','localhost'),

],

'driver' => 'mysql',

'port' => env('DB_PORT', 3306),

'database' => env('DB_DATABASE', 'forge'),

'username' => env('DB_USERNAME', 'forge'),

'password' => env('DB_PASSWORD', ''),

'charset' => 'utf8mb4',

'collation' => 'utf8mb4_unicode_ci',

'prefix' => env('DB_PREFIX', ''),

'timezone' => env('DB_TIMEZONE', '+08:00'),

'strict' => false,

'options' => [

PDO::ATTR_EMULATE_PREPARES => true,

],

],

'pgsql' => [

'driver' => 'pgsql',

'host' => env('REDSHIFT_HOST', 'localhost'),

'port' => env('REDSHIFT_PORT', 5432),

'database' => env('REDSHIFT_DATABASE', 'forge'),

'username' => env('REDSHIFT_USERNAME', 'forge'),

'password' => env('REDSHIFT_PASSWORD', ''),

'charset' => 'utf8',

'prefix' => env('DB_PREFIX', ''),

'schema' => 'public',

],

'sqlsrv' => [

'driver' => 'sqlsrv',

'host' => env('DB_HOST', 'localhost'),

'database' => env('DB_DATABASE', 'forge'),

'username' => env('DB_USERNAME', 'forge'),

'password' => env('DB_PASSWORD', ''),

'prefix' => env('DB_PREFIX', ''),

],

'mongodb' => [

'driver' => 'mongodb',

'host' => explode(',', env('MONGODB_HOST', 'localhost')),

'port' => env('MONGODB_PORT', 27017),

'username' => env('MONGODB_USERNAME', 'accountUser'),

'password' => env('MONGODB_PASSWORD', 'password'),

'database' => env('MONGODB_DATABASE', 'users'),

'options' => ['replicaSet' => env('MONGODB_RS_NAME')]

],

],

'migrations' => 'migrations',

'redis' => [

'cluster' => env('REDIS_CLUSTER', false),

'default' => [

'host' => env('REDIS_HOST', '127.0.0.1'),

'port' => env('REDIS_PORT', 6379),

'database' => intval(env('REDIS_DATABASE', 0)),

'password' => env('REDIS_PASSWORD', null)

]

],

];

添加自定义的TidbConnection, 使其使用SQL语句进行提交

新建文件"app/Database/TidbConnection.php", 其内容如下:

namespace App\Database;

use Closure;

use Exception;

use Throwable;

use Illuminate\Database\MySqlConnection;

class TidbConnection extends MySqlConnection

{

public function transaction(Closure $callback)

{

$this->beginTransaction();

// We'll simply execute the given callback within a try / catch block

// and if we catch any exception we can rollback the transaction

// so that none of the changes are persisted to the database.

try {

$result = $callback($this);

$this->commit();

}

// If we catch an exception, we will roll back so nothing gets messed

// up in the database. Then we'll re-throw the exception so it can

// be handled how the developer sees fit for their applications.

catch (Exception $e) {

$this->rollBack();

throw $e;

} catch (Throwable $e) {

$this->rollBack();

throw $e;

}

return $result;

}

/**

* Start a new database transaction.

*

* @return void

* @throws Exception

*/

public function beginTransaction()

{

++$this->transactions;

if ($this->transactions == 1) {

try {

$this->pdo->exec('START TRANSACTION');

} catch (Exception $e) {

--$this->transactions;

throw $e;

}

} elseif ($this->transactions > 1 && $this->queryGrammar->supportsSavepoints()) {

$this->pdo->exec(

$this->queryGrammar->compileSavepoint('trans'.$this->transactions)

);

}

$this->fireConnectionEvent('beganTransaction');

}

/**

* Commit the active database transaction.

*

* @return void

*/

public function commit()

{

if ($this->transactions == 1) {

$this->pdo->exec('COMMIT');

}

--$this->transactions;

$this->fireConnectionEvent('committed');

}

/**

* Rollback the active database transaction.

*

* @return void

*/

public function rollBack()

{

if ($this->transactions == 1) {

$this->pdo->exec('ROLLBACK');

} elseif ($this->transactions > 1 && $this->queryGrammar->supportsSavepoints()) {

$this->pdo->exec(

$this->queryGrammar->compileSavepointRollBack('trans'.$this->transactions)

);

}

$this->transactions = max(0, $this->transactions - 1);

$this->fireConnectionEvent('rollingBack');

}

}

然后注入到容器里面, 方法如下:

编辑"app/Providers/AppServiceProvider.php", 其内容如下:

namespace App\Providers;

use Illuminate\Support\ServiceProvider;

class AppServiceProvider extends ServiceProvider

{

public function register()

{

$this->app->bind('db.connection.mysql', function ($app, $parameters) {

$rc = new \ReflectionClass('App\Database\TidbConnection');

return $rc->newInstanceArgs($parameters);

});

}

}

最后确保框架引入了AppServiceProvider

Lumen需要编辑的文件"bootstrap/app.php"

$app->register(App\Providers\AppServiceProvider::class);

Laravel一般不需要修改内容, 它已经在"config/app.php"引入了"AppServiceProvider"类.

总结

虽然在TIDB听了他们开发者讲了很多细节,

但是离真正用好TIDB还有很长的一段距离.

目前遇到的问题, 都可以使用各种方式"解决",

但是或多或少都没有达到不修改代码直接迁移到TIDB的地步,

后续有什么其他的问题, 我也会一并整理到这篇文章.

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值