Symfony Lock在从同一浏览器发出两个请求时不会锁定

31moq8wy  于 2022-11-16  发布在  其他
关注(0)|答案(1)|浏览(284)

我想通过使用Symfony Lock组件来防止用户两次发出相同的请求。因为现在用户可以两次单击一个链接(是否意外?),并创建重复的实体。我想使用唯一实体约束,该约束本身并不针对争用条件提供保护。
Symfony Lock组件似乎没有按预期工作。当我在页面开头创建一个锁,并同时打开页面两次时,两个请求都可以获得锁。当我在标准和匿名浏览器窗口中打开测试页时,第二个请求没有获得锁。但我在文档中找不到任何关于此操作与会话关联的信息。我在一个新的项目中创建了一个小的测试文件来隔离这个问题。

<?php

namespace App\Controller;

use Sensio\Bundle\FrameworkExtraBundle\Configuration\Template;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\Lock\LockFactory;
use Symfony\Component\Routing\Annotation\Route;

class LockTest extends AbstractController
{
    /**
     * @Route("/test")
     * @Template("lock/test.html.twig")
     */
    public function test(LockFactory $factory): array
    {
        $lock = $factory->createLock("test");

        $acquired = $lock->acquire();

        dump($lock, $acquired);

        sleep(2);

        dump($lock->isAcquired());

        return ["message" => "testing"];
    }
}
5hcedyr0

5hcedyr01#

我稍微重写了您的控制器,如下所示(使用symfony 5.4和php 8.1):

class LockTestController extends AbstractController
{
    #[Route("/test")]
    public function test(LockFactory $factory): JsonResponse
    {
        $lock = $factory->createLock("test");

        $t0 = microtime(true);
        $acquired = $lock->acquire(true);
        $acquireTime = microtime(true) - $t0;

        sleep(2);

        return new JsonResponse(["acquired" => $acquired, "acquireTime" => $acquireTime]);
    }
}

它等待释放锁,并计算控制器等待获取锁的时间。
我用curl在caddy服务器上运行了两个请求。

curl -k 'https://localhost/test' & curl -k 'https://localhost/test'

输出确认一个请求被延迟,而第一个请求使用获取的锁休眠。

{"acquired":true,"acquireTime":0.0006971359252929688}
{"acquired":true,"acquireTime":2.087146043777466}

因此,锁的作用是防止并发请求。
如果锁未阻塞:

$acquired = $lock->acquire(false);

输出为:

{"acquired":true,"acquireTime":0.0007710456848144531}
{"acquired":false,"acquireTime":0.00048804283142089844}

请注意,第二个锁是如何未获取的。您应该使用此标志来拒绝用户的请求,并显示错误,而不是创建重复的实体。
如果两个请求间隔足够大,可以依次获得锁,则可以检查实体是否存在(因为它有时间完全提交到数据库)并返回错误。
尽管有这些令人鼓舞的结果,但该文件提到了以下注意事项:
与其他实现不同,锁组件区分锁示例,即使它们是为同一资源创建的。这意味着对于给定的作用域和资源,一个锁示例可以被多次获取。如果一个锁必须由多个服务使用,则它们应该共享由LockFactory::createLock方法返回的同一个锁示例。
我知道两个不同工厂获得的两个锁不应该互相阻塞。除非注解过时或措辞错误,否则在某些情况下似乎可能有不工作的锁。但使用上面的测试代码则不然。

流式响应

当锁定超出范围时,就会解除锁定。
作为一种特殊情况,当返回StreamedResponse时,当控制器返回响应时,锁将超出范围。
要在生成响应时保持锁,必须将其传递给StreamedResponse执行的函数:

public function export(LockFactory $factory): Response
{
    // create a lock with a TTL of 60s
    $lock = $factory->createLock("test", 60);
    if (!$lock->acquire(false)) {
        return new Response("Too many downloads", Response::HTTP_TOO_MANY_REQUESTS);
    }

    $response = new StreamedResponse(function () use ($lock) {
        // now $lock is still alive when this function is executed
        $lockTime = time();
        while (have_some_data_to_output()) {
            if (time() - $lockTime > 50) {
                // refresh the lock well before it expires to be on safe side
                $lock->refresh();
                $lockTime = time();
            }
            output_data();
        }
        $lock->release();
    };

    $response->headers->set('Content-Type', 'text/csv');

    // lock would be released here if it wasn't passed to the StreamedResponse
    return $response;
}

上面的代码每50秒刷新一次锁,以减少与存储引擎(如redis)的通信时间。
如果php进程突然死亡,锁最多保持锁定60秒。

相关问题