使用拆分的“读取”和“写入”数据库连接时,Laravel中的竞争条件

时间:2019-07-26 09:38:04

标签: mysql laravel session laravel-5 pdo

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

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

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

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

我的另一个想法是修改数据库getReadPdo(){...}类中的$recordsModified方法和Connection值,以便将粘性保留在用户会话中最多1秒钟。我不确定这是否会导致速度或会话加载过多的其他问题,是否会引起更多问题。

是否有人曾经经历过此事或对如何解决问题有任何想法?

谢谢。

1 个答案:

答案 0 :(得分:1)

如果其他任何人遇到相同的问题,我会更新并回答这个问题。

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

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

            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-这是how the core Laravel sticky sessions are normally detected

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

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


更新(09/12/19):

事实证明,上面的解决方案实际上并没有真正修改数据库连接,它只是对任何请求增加了几毫秒的处理时间,因此看起来它在大约75%的时间里正常工作(因为数据库副本)滞后随负载而变化)。

最后,我决定更深入一点,直接覆盖DB连接类并修改相关功能。我的Laravel实例使用MySQL,因此我覆盖了Illuminate\Database\MySqlConnection类。这个新类是通过新的服务提供商注册的,该服务提供商又通过配置加载了该服务。

我已经复制了下面使用的配置和文件,以使新开发者都更容易理解。如果您要直接复制这些文件,请确保还将“ sticky_by_session”标志也添加到连接配置中。

  

config / database.php

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

config / app.php

    'providers' => [
        App\Providers\DatabaseServiceProvider::class
        ...
    ],
  

app / Providers / DatabaseServiceProvider.php

<?php

namespace App\Providers;

use App\Database\MySqlConnection;
use Illuminate\Database\Connection;
use Illuminate\Support\ServiceProvider;

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 App\Database;

use Illuminate\Database\MySqlConnection 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竞争条件或影响。