使用拆分"read"和"write"数据库连接时 Laravel 中的争用条件



我有一个Laravel应用程序,它使用了很多AJAX POST和GET请求(单页应用程序)。通过 POST 保存项目后,将发送 GET 请求以重新加载页面的某些部分并获取任何新数据。

使用 Laravel 连接配置启用拆分读写数据库连接后,应用程序运行速度非常快(从未想过这会是一个问题!它保存然后请求的速度如此之快,以至于 RO 数据库(仅报告落后 22 毫秒)没有机会更新,我最终得到了旧信息。

我已经在数据库配置中启用了sticky参数,我认为这可以缓解问题,但 POST 和 GET 请求是分开的,因此粘性丢失了。

我可以重写大部分应用程序 POST 请求使用正确的数据响应,但这不适用于一次重新加载许多组件,并且是一项艰巨的工作,因此我认为这是最后的手段。

我的另一个想法是修改getReadPdo(){...}方法和$recordsModified数据库Connection类中的值,以便粘性在用户会话中保存长达 1 秒。我不确定这是否会导致速度或会话加载过多的任何进一步问题,从而导致更多问题。

以前有没有人经历过这种情况,或者对如何解决这个问题有任何想法?

提前谢谢。

以为我会更新并回答这个问题,以防其他人遇到同样的问题。

这不是一个完美的解决方案,但在过去一周左右的时间里效果很好。

AppServiceProviderboot()方法中,我添加了以下内容

DB::listen(function ($query) {
if (strpos($query->sql, 'select') !== FALSE) {
if (time() < session('force_pdo_write_until')) {
DB::connection()->recordsHaveBeenModified(true);
}
} else {
session(['force_pdo_write_until' => time() + 1]);
}
});

简而言之,这会侦听每个数据库查询。如果当前查询是SELECT(数据库读取),我们会检查用户会话中的"force_pdo_write_until"键是否具有大于当前时间的时间戳。如果是,我们利用recordsHaveBeenModified()方法诱使当前数据库连接使用 ReadPDO - 这就是通常检测核心 Laravel 粘性会话的方式

如果当前查询不是SELECT(很可能是数据库写入),我们将将来将"force_pdo_write_until"的会话变量设置为 1 秒。

每当发送 POST 请求时,如果下一个 GET 请求在上一个查询的 1 秒内,我们可以确定当前用户将使用 RW DB 连接并获得正确的结果。


更新(09/12/19):

事实证明,上面的解决方案实际上根本没有修改数据库连接,它只是为任何请求增加了几毫秒的处理时间,所以看起来它大约工作了 75% 的时间(因为数据库副本滞后根据负载波动)。

最后,我决定更深入一点,直接覆盖数据库连接类并修改相关函数。我的Laravel实例使用MySQL,所以我覆盖了IlluminateDatabaseMySqlConnection类。这个新类是通过新的服务提供程序注册的,而服务提供程序又通过配置加载。

我复制了下面使用的配置和文件,以便任何新开发人员更容易理解。如果要直接复制这些,请确保还将"sticky_by_session"标志添加到连接配置中。

配置/数据库.php

'connections' => [
'mysql' => [
'sticky' => true,
'sticky_by_session' => true,
...
],
],

配置/应用.php

'providers' => [
AppProvidersDatabaseServiceProvider::class
...
],

app/Providers/DatabaseServiceProvider.php

<?php
namespace AppProviders;
use AppDatabaseMySqlConnection;
use IlluminateDatabaseConnection;
use IlluminateSupportServiceProvider;
class DatabaseServiceProvider extends ServiceProvider
{
/**
* Register the service provider.
*
* @return void
*/
public function register()
{
if (config('database.connections.mysql.sticky_by_session')) {
Connection::resolverFor('mysql', function ($connection, $database, $prefix, $config) {
return new MySqlConnection($connection, $database, $prefix, $config);
});
}
}
}

app/Database/MySqlConnection.php

<?php
namespace AppDatabase;
use IlluminateDatabaseMySqlConnection as BaseMysqlConnection;
class MySqlConnection extends BaseMysqlConnection
{
public function recordsHaveBeenModified($value = true)
{
session(['force_pdo_write_until' => time() + 1]);
parent::recordsHaveBeenModified($value);
}
public function select($query, $bindings = [], $useReadPdo = true)
{
if (time() < session('force_pdo_write_until')) {
return parent::select($query, $bindings, false);
}
return parent::select($query, $bindings, $useReadPdo);
}
}

recordsHaveBeenModified()中,我们只是添加一个会话变量供以后使用。如前所述,此方法由正常的Laravel粘性会话检测使用。

select()内部,我们检查会话变量是否在不到一秒前设置。如果是这样,我们会手动强制请求使用 RW 连接,否则照常继续。

现在我们直接修改请求,我还没有看到副本滞后的任何 RO 竞争条件或影响。

我已经作为一个软件包发布了!

  • mpyw/laravel-cached-database-stickiness:保证同一用户连续请求的数据库粘性

安装

composer require mpyw/laravel-cached-database-stickiness

默认实现由ConnectionServiceProvider提供,但是,包发现不可用。 请注意,您必须自己在config/app.php中注册。

<?php
return [
/* ... */
'providers' => [
/* ... */
MpywLaravelCachedDatabaseStickinessConnectionServiceProvider::class,
/* ... */
],
/* ... */
];

仅此而已!所有问题都将得到解决。

最新更新