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

Posted

技术标签:

【中文标题】使用拆分“读取”和“写入”数据库连接时 Laravel 中的竞争条件【英文标题】:Race conditions in Laravel when using split "read" and "write" database connections 【发布时间】:2019-12-04 15:11:11 【问题描述】:

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

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

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

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

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

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

提前致谢。

【问题讨论】:

如果拆分或不拆分数据库连接,性能提升是多少?如果这是“毫秒”的问题,请不要浪费数小时的时间来解决这个问题。我会说在拆分之前恢复代码,因为不值得花时间编写一个奇怪的“解决方法”来应对可能的竞争条件在代码的其他地方也触发其他错误.. 通过拆分对整个应用程序的性能提升是巨大的。我们有相当多的高负载时段,并且拆分负载意味着我们可以轻松地水平扩展 RO 数据库,而 RW 不会不堪重负。我给出的毫秒数是 RW 和 RO 数据库之间的数据延迟,而不是速度增益。感谢您的评论! 水平缩放接缝在这里是一个错误的定义,也不确定在这种情况下 RO/RW 意味着什么......?但很可能我误解了您的应用程序/数据库服务器设置...您能否展示一些定义连接的 lavaral 代码以及您如何在应用程序中使用它。也许其他人也会获得更好的印象并且可能会看到更好方法或解决问题的方法.. ...如果我认为使用 mysql 数据库进行水平扩展...我想MySQL Cluster 这也可能是解决您问题的好方法.. 【参考方案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(DB 读取),我们检查用户会话中的“force_pdo_write_until”键是否具有大于当前时间的时间戳。如果是,我们通过recordsHaveBeenModified() 方法欺骗当前的数据库连接使用ReadPDO - 这是how the core Laravel sticky sessions are normally detected

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

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


更新(09/12/19):

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

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

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

config/database.php

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

配置/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 竞争条件或副本延迟的影响。

【讨论】:

【参考方案2】:

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

mpyw/laravel-cached-database-stickiness: Guarantee database stickiness over the same user's consecutive requests

安装

composer require mpyw/laravel-cached-database-stickiness

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

<?php

return [

    /* ... */

    'providers' => [

        /* ... */

        Mpyw\LaravelCachedDatabaseStickiness\ConnectionServiceProvider::class,

        /* ... */

    ],

    /* ... */
];

就是这样!一切问题都会迎刃而解。

【讨论】:

以上是关于使用拆分“读取”和“写入”数据库连接时 Laravel 中的竞争条件的主要内容,如果未能解决你的问题,请参考以下文章

如何使用套接字通道读取和写入数据并接受连接

使用 ADO 在 Excel 工作簿中读取和写入数据时未复制最后一个 Header 单元格?

无法从使用 mongo spark 连接器读取的 spark DF 中显示/写入。

ACCESS通过一个连接写入的数据,还有一个连接却读取不出来

使用 JDBC 连接器 5.1 从 Java 读取/写入 MySQL 中的 UTF-8 数据时出现问题

java能否读取csv文件的同时也写入数据?