我创建了以下程序来演示官方C#mongodb驱动程序中的并发更新问题。我的测试版本:1.9.0。如果我遗漏了什么,你能帮忙吗。我相信这是C#驱动程序中的一个错误。
说明:
- 程序在mytest数据库中删除并创建新的页面集合
- 用1000个具有增量ID的页面填充页面集合
- 启动2个更新任务,每个任务一个正向循环,一个反向循环
- 根据记录的数据,总共必须有1000次更新
- 因为在每次更新之前都会进行时间条件检查
- 测试显示大约有1000次更新。不确定性情况
- 我可以理解大于1000的结果,但不能理解低于1000的结果——我可以看到三分之一的执行
感谢您的帮助。谢谢
public class Document
{
public int _id { get; set; }
public long time { get; set; }
}
class Program
{
MongoClient Client;
MongoServer Server;
MongoDatabase DB;
MongoCollection<Document> Pages;
int NextId = 0;
DateTime TimeLimit;
int TotalUpdate = 0;
int totalPage = 1000;
private void Start()
{
Client = new MongoClient("mongodb://localhost:9147");
Server = Client.GetServer();
DB = Server.GetDatabase("mytest");
Pages = DB.GetCollection<Document>("pages");
Pages.Drop();
if (Pages.Count() > 0)
{
NextId = Pages.AsQueryable().Max(x => x._id);
}
else
{
Console.WriteLine(DateTime.Now + " Inserts started...");
for (int i = 0; i < totalPage; ++i)
{
Pages.Save(new Document
{
_id = Interlocked.Increment(ref NextId),
time = DateTime.Now.Ticks
});
}
Console.WriteLine(DateTime.Now + " Inserts finished...");
}
TimeLimit = DateTime.Now;
Thread.Sleep(500);
List<Task> tasks =new List<Task>();
tasks.Add(Task.Factory.StartNew(() => Updates()));
tasks.Add(Task.Factory.StartNew(() => Updates2()));
Task.WaitAll(tasks.ToArray());
Console.WriteLine("Total Update: " + TotalUpdate);
Console.WriteLine("Expected Total Update: " + totalPage);
}
private void Updates()
{
Console.WriteLine(DateTime.Now + " Updates 1 started...");
Parallel.ForEach(Enumerable.Range(1, totalPage), (id) =>
{
try
{
if (id % 100 == 0)
Console.WriteLine(DateTime.Now + " Update1: " + id);
var found = Pages.FindOne(Query.EQ("_id", id)).time;
if (found < TimeLimit.Ticks)
{
var res = Pages.Update(Query.EQ("_id", id), Update.Set("time", DateTime.Now.Ticks), UpdateFlags.Upsert, WriteConcern.Acknowledged);
if (res.DocumentsAffected > 0)
Interlocked.Increment(ref TotalUpdate);
else
Console.WriteLine(res.Response);
}
}
catch(Exception e)
{
Console.WriteLine(e.Message);
}
});
Console.WriteLine(DateTime.Now + " Updates 1 finished...");
}
private void Updates2()
{
Console.WriteLine(DateTime.Now + " Updates 2 started...");
Parallel.ForEach(Enumerable.Range(1, totalPage), (id) =>
{
try
{
if ((id - 1) % 100 == 0)
Console.WriteLine(DateTime.Now + " Update2: " + (totalPage - id + 1));
var found = Pages.FindOne(Query.EQ("_id", totalPage - id + 1)).time;
if (found < TimeLimit.Ticks)
{
var res = Pages.Update(Query.EQ("_id", totalPage - id + 1), Update.Set("time", DateTime.Now.Ticks), UpdateFlags.Multi, WriteConcern.Acknowledged);
if (res.DocumentsAffected > 0)
Interlocked.Increment(ref TotalUpdate);
else
Console.WriteLine(res.Response);
}
}
catch(Exception e)
{
Console.WriteLine(e.Message);
}
});
Console.WriteLine(DateTime.Now + " Updates 2 finished...");
}
static void Main(string[] args)
{
new Program().Start();
}
}
之所以会发生这种情况,是因为DateTime.Now.Ticks
不是很精确,并且通常会返回相同的值。你看了你正在创建的数据吗?许多time
值是相等的(取决于机器的速度)。
碰撞只能发生在两个行走方向相交的中间位置。当你让他们以同样的方式行走时,问题会急剧恶化。
使用性能计数器获取高分辨率时间,或者使用计数器,或者将比较从if (found < TimeLimit.Ticks)
更改为if (found <= TimeLimit.Ticks)
。后者会不时给你带来比预期更大的结果,但永远不会低于预期。
编辑
也许我遗漏了什么,但我不确定你的测试是否能提供可重复的结果。一旦另一个线程完成,检查if found < TimeLimit
将总是失败。然而,在另一个线程中,检查if found < currentTime()
(其中currentTime
是一个高性能计时器)实际上总是结果为true,即使另一个螺纹在一微秒之前刚刚更新了文档。因此,该测试应该产生大约1500个更新,而不是1000个。粗糙度取决于计时器的精度和机器的性能。