我正在尝试在 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 操作,以确保只有在操作之间没有更改版本时才更新文档。但是代码看起来很难看,所以我想知道是否有其他方法可以解决这个问题。