0

我正在尝试在 MongoDB 中模拟某种“事务”。我正在锁定关键代码部分(使用 memcache),从 MongoDB 读取文档,稍作更改,将其写回并解锁部分。然而,如果并发运行,很少有这样的操作不能按预期工作。确认后(w = 1)写入另一个线程仍然可以读取以前版本的文档。在重负载(10 多个并发请求)下,这种情况大约会发生 1/1000 次,但会破坏整个想法。

我尝试使用 fsync 选项,但它使写入速度太慢而无法使用(这些操作应该经常运行)。日志选项 (j = 1) 不能解决问题。在从某个线程确认写入后,有没有办法让其他线程读取一致版本的文档?

这是示例代码(我将其更改为简单的计数器,无论如何都可以重现该问题):

$mchost = "127.0.0.1";
$mcport = 11211;
$lockkey = "testlockkey";

$mongoconnstr = "mongodb://localhost";
$mongodb = "testdb";
$mongotbl = "test";
$mongoopts = array("w" => 1);
$varname = "counter";

function LIBLockSection($id, $timeout)
{
    for ($i = 0; $i < $timeout; $i++) {
        if (memcache_add($GLOBALS["mc"], $id, 1, FALSE, 60)) return 1;
        usleep(50000);
    }
    return 0;
}

function LIBUnlockSection($id)
{
    return memcache_delete($GLOBALS["mc"], $id);
}

function MNGGetTable($connstr, $dbname, $tname)
{
    $m = new Mongo($connstr);
    $db = $m->selectDB($dbname);
    return $db->selectCollection($tname);
}

$GLOBALS["mc"] = memcache_connect($mchost, $mcport) or die('memcached is down!');
$tbl = MNGGetTable($mongoconnstr, $mongodb, $mongotbl);

$lockres = LIBLockSection($lockkey, 50);
if (!$lockres) {
    exit;
}

$x = $tbl->findOne();
if (!isset($x[$varname])) $x[$varname] = 0;
$x[$varname]++;
$opts = $mongoopts;
$opts["upsert"] = TRUE;
$tbl->update(array(), array($varname => $x[$varname]), $opts);

LIBUnlockSection($lockkey);
trigger_error($x[$varname]);

该脚本将其输出写入标准 PHP 错误日志。要运行它,我使用 ab(用于服务器根目录中的 test.php):

ab -c 10 -n 10000 http://127.0.0.1/test.php

预期的输出是从 1 到 10000 的连续数字。但如果没有 fsync 选项,有时我们会得到双精度:

[Tue Oct 01 00:53:53 2013] [error] [client 127.0.0.1] PHP Notice:  344 in /var/www/test.php on line 49
[Tue Oct 01 00:53:53 2013] [error] [client 127.0.0.1] PHP Notice:  344 in /var/www/test.php on line 49

现在我只看到一种可能的解决方案:在文档中使用“版本”变量并在数据库上使用 findAndModify 操作,以确保只有在操作之间没有更改版本时才更新文档。但是代码看起来很难看,所以我想知道是否有其他方法可以解决这个问题。

4

1 回答 1

1

I see a few problems with your code that explains:

But without fsync option sometimes we get doubles:

You are for one, using the deprecated Mongo class:

$m = new Mongo($connstr);

The default write concern of this is socket acknowledge.

Normally this would be countered by:

$mongoopts = array("w" => 1);

But w is not compatible with Mongo, it is instead compatible with MongoClient. The option for Mongo is safe.

That means you are never actually getting an acknowledgement of a successful OP from MongoDB, instead just getting an acknowledgement from the socket.

Try fixing your code and seeing if the problem comes back.

However this link might help: http://docs.mongodb.org/manual/tutorial/create-an-auto-incrementing-field/ to get atomic, race condition free incrementing keys from MongoDB.

于 2013-10-01T07:14:11.947 回答