我正在开发一个包含以下内容的系统:
关键的一点是,不应该只有一名工人同时连续操作。
当然这里有很多策略......我可以锁定数据库级别的东西,我可以使用互斥锁等。
但无论我如何实现这一点,我都需要能够测试系统以确保我做得对。
测试这个的正确方法是什么?
我一直在做的就是运行数百个线程并不断检查意外重叠。问题是,它是命中还是错过。这是概率。即使我运行500个线程一小时,仍然可能有一个线程与另一个线程重叠,很少。
另外,我如何正确检查重叠? “重叠检查器”本身具有有限的分辨率,可能缺少正在发生的实际重叠......
我知道并发是一个复杂的话题,但肯定必须有一些最佳实践或推荐的方法来测试这样一个系统,除了运行它很长一段时间并且交叉手指...
答案 0 :(得分:0)
测试此方法的正确方法是什么?
这里没有简单的答案。要考虑的一件事是创建一个数据库代理,您可以在工作线程和数据库层之间注入,以捕获重叠。您可以在某些并发映射中记录客户端操作的行ID,并在数据库RPC调用完成时将其删除。
我将使用Java作为伪代码。
try {
// add it to some atomic concurrent hash-map
if (workingRowIdMap.putIfAbsent(rowId, null) != null) {
// scream and shout and log a concurrency failure
}
// do the _real_ database stuff here
} finally {
workingRowIdMap.remove(rowId);
}
如果您正在处理SQL以提取相关数据的行ID,则可能很难。如果你正在处理的SQL很复杂,它甚至可能是不可能的。如果没有关于架构的更多细节,很难知道。
此外,您希望您的代理尽可能轻量级,否则如果添加额外的锁或太多的内存障碍,您可能会隐藏问题。
另一个想法是使用数据库工具查看二进制更改日志以查看指令是否有任何重叠。我知道MySQL有一些工具可以让你调查他们的binlogs。可能需要做很多工作来定制工具,以确定您的数据更新是否重叠。
另外,我如何正确检查重叠? “重叠检查器”本身具有有限的分辨率,可能缺少正在发生的实际重叠......
同意。测试错误并不是一门精确的科学 - 特别是对于高度并发的软件。这个想法是给你最好的尝试。我总是尝试模拟实时服务流量,这通常是复制软件在生产中看到的条件的最佳方式。重播生产日志可能有助于您发现问题。
除了测试之外,您还可能需要围绕软件的高并发部分进行一些组代码审查会话。正确隔离该代码,以便锁定简单,具有良好的try / finally块等,这也是一个很好的投资。
答案 1 :(得分:0)
我发现做这种事情的最好方法是启动大量线程,然后通过随机挂起和恢复线程来引入随机抖动。
这为您提供了许多有趣的线程调度。它是一种蒙特卡罗算法,逐渐覆盖越来越多的搜索空间。
不是你可以随机调度线程,你需要声明没有错误存在。我的最佳选择是:添加新列WorkerCount int not null
。将其初始化为零。当一个工人在它上面运行时,增加它(不要将它设置为一个 - 增加它)。在那里放一个检查约束来检查WorkerCount IN (0, 1)
。工人完成后,减少计数。
这将在第一次重叠时中断。
答案 2 :(得分:0)
也许我完全不了解你的情况,但我正在使用以下内容来使用C#中的多个线程将数千条记录保存到数据库
为了在没有死锁的情况下将多个记录同时保存到数据库,您可以使用SQLBulkCopy(ADO.Net)或批量插入(SQL Server)实用程序。
他们维持共同货币并且永远不会导致死锁情况。
对于日志记录,我编写了一个单例类并将其对象传递给每个线程,回到代码中我使用ConcurrentList和Lock(对象)来存储所有日志,并且在每5秒后我将所有这些记录保存到数据库和清理列表,再次使用SQLBulkCopy命令。
如果您需要更多信息,请告诉我......
答案 3 :(得分:0)
正如您在问题中所说,重点应放在可测试性上,而不是性能上。
我建议生产者/消费者模型。您可以在数据库中创建尽可能多的线程(新行),并让数据库服务器处理并发。这是系统的第一部分,许多线程将行抽入表中。
为了每行只处理一次,我建议一个线程负责加载新行并将它们抽取到队列中。然后,您可以拥有任意数量的线程来处理队列。完成处理后,他们可以更新数据库中的行或写入输出队列,其他线程将批量收集和处理更新请求。
想象一下,您的表中有一个PROCESSING_STATUS列,并且新行始终具有PROCESSING_STATUS = 0.因此,线程可以自由地向此表添加新行。 另一个线程将连续查询(以预定义的间隔/事件或简单轮询)此表,选择PROCESSING_STATUS = 0的所有行。然后,每行将添加到队列中。加载后,您可以将PROCESSING_STATUS更新为1.您必须在再次查询之前完成此操作,这对于避免两次加载同一行非常重要。
真正的工作线程将消耗此队列,我假设您正在使用并发队列或类似结构,能够处理许多消费者。 Queue算法应该保证只有一个线程可以使用相同的元素。这种Queue很容易在Python,C#或Java的标准库中找到。然后,真正的胎面会处理这一行并将它们写回输出队列。
负责写回行的线程将更新工作线程生成的数据和PROCESSING_STATUS列,例如将其设置为2。此更新应使用行的所有已知键和值来完成,以确保它自读取后未更改。写入线程还应该检查更新查询中受影响行的值,以检查自处理以来该行是否未被删除或更改。
关于可测试性,您可以检查是否存在未处理的行,检查其PROCESSING_STATUS列。如果PROCESSING_STATUS = 0 - 未加载此行,如果它等于1,则加载但未处理/写回。 2表示已处理。您仍然需要检查每行的处理是否正确完成,但这是标准测试。
您可以检查多个线程是否尝试访问同一行,或者自第一次读取行以来是否更改了行检查更新语句中受影响的行。如果更新不影响任何行,则表示已处理或更改了行。
因此,此方案中可测试性的关键是使用队列进行线程同步并检查对数据库的更新。您还可以使用队列和处理线程上的计数器来检查已加载行数是否=已处理行数=已写入行数。
如果您希望许多线程从数据库加载数据,您也可以扩展PROCESSING_STATUS列。想象一下,将使用PROCESSING_STATUS = 0添加未处理的(新)行。然后,一组读取线程,每个读取线程的唯一编号为正且不等于0,将更新与有限的select语句组合在一起。 类似的东西:
update TABLE_X set PROCESSING_STATUS = MY_UNIQUE_THREAD_ID
where key in (select key from TABLE_X where PROCESSING_STATUS = 0 LIMIT 5)
and PROCESSING_STATUS = 0
如果受感染的行不为零,则此线程将加载一些行。下一步是加载PROCESSING_STATUS = MY_UNIQUE_THREAD_ID所在的所有行。然后,可以再次使用相同的算法。处理行时,我们使用MY_UNIQUE_THREAD_ID的负值更新其PROCESSING_STATUS。这样您就可以使用数据库来处理并发性,但这并不意味着您将获得最佳性能。至少,原始问题将得到解决:只处理每一行一次。
在不对数据库服务器造成压力的情况下仅加载一次行的替代方法是对密钥使用模运算(如果它是串行密钥)。使用select语句中的键(k%n_readers)上的模数。加载:
SELECT * from TABLE_X WHERE (key % N) == MY_UNIQUE_THREAD_ID
答案 4 :(得分:0)
真正测试重叠;
要测试性能,您应该创建一个以类似生产的方式生成数据的测试集。并使用类似的硬件......
至于锁定行,如果所有都在一个应用程序中,我想我会创建一个ConcurrentDictionary,其中包含正在处理的行的ID或沿着这些行的ID。或者使用一些具有ConcurrentQueues的系统,其中行通过排队/去队列进行处理。
答案 5 :(得分:0)
我建议你提取一些工作并用row_id链接它们,而不是你必须测试它。
在.NET中我会做类似这样的事情:
private var rowWorkers = new Dictionary<int,Task>();
public void ScheduleWorkOnRow(int id)
{
// starting empty worker to be able to continue on it
if(rowWorkers[id] == null) rowWorkers.Add(id, Task.Run(() => { });
// scheduling continuation
rowWorkers[id].ContinueWith(WorkOnRow, id);
}
private void WorkOnRow(Task task, object id)
{
//your code
}
这个片段远非理想,但我认为你可以明白这一点。
答案 6 :(得分:0)
在我说过之前,和其他人一样,唯一正确的方法是对你有用的。没有对错,只有好的和更好的。话虽如此:
您的目标是:
关键的一点是,不应该有一个工人同时连续操作。
因此,您有数千个线程,只有一个线程可以执行更改,无论它们打开了多少。线程是动态创建的,很难跟踪它们,只是浪费资源和时间。
但是数据库是唯一的,因此不是控制线程,而是我们可以瓶颈数据库,每个特定行只允许一个线程。为此,我们需要为每列插入一个新的检查器。让我们将列命名为 ThreadCheck ,它可以是您想要的任何内容,对于此示例,我们可以将其设置为文本。
该专栏背后的想法是,每个想要在数据库中的某一行上操作的线程都作为Thread [Thread ID]保存到ThreadCheck中。当线程在行上完成操作时,ThreadCheck将保存为null或特定值。它可以是你想要的任何东西,因为这个例子让它命名为“空”。所以线程完成工作,值变为空。
接下来要实施的是队列。队列的逻辑基于FIFO(第一个输入,第一个输出),它可以模拟银行等待列表。在银行,我们有多个人尝试为多个客户提供服务。对于您的应用程序,该银行只有一个人试图为许多客户端(线程)提供服务。在构建队列时,每个新线程都有一个等待票,另一个简单的计数器模拟线程在等待列表中的位置。
当一个线程想要对一行进行操作,并且该行为空并且该线程有一个等待票值为counter + 1的值时,它可以对它进行操作。当计数器为1时,等待线程有一个计数器2,下一个线程计数器3直到X。
现在要向前推进这条线,你需要某种优秀的人物,比如保安或经理告诉人们“下一个!!!”。在线程等待列表中,这个高级函数可以是一个周期性循环,只有在当前线程完成工作时才会向前移动该行。这节省了时间和资源,使得基于循环事件而不是持续检查。
您可以使用动态计数器++和counter--增强总队列,并在每次线程完成时为等待票证提供新值。或者同时应用1234567890个线程的队列限制。在队列移动之前,任何新线程都会被拒绝。
创建一个线程并给出一个等待票。当线程完成时,服务计数器将线路向上移动一个位置。直到最后一个线程。通过线程检查和安全功能推送队列,存在一个瓶颈,而不是一大堆线程都希望同时运行。
如果我试图详细说明的逻辑有问题,或者你不理解某些部分,请评论我。
另外,因为你试图自己解决这个问题,所以我没有包含任何有意义的代码,既可以帮助你设计自己的代码,又可以省去尝试从头开始编写代码的麻烦。
答案 7 :(得分:0)
为了触发Heisenbug而不是激发一堆线程,你可能会看一下像Chess这样的工具。我必须承认我从来没有给它一个旋转,但它看起来仍然适合你的问题,因为它意味着积极探索发生的所有交错。
根据研究人员的说法,CHESS已被集成到微软内部许多代码库的测试框架中,并且每天都被测试人员使用。
请注意,该项目最初是作为win32解决方案开发的,但已移植到.NET。该页面的代码项目链接已失效,但快速搜索显示代码为still available
答案 8 :(得分:0)
我正在分裂你的问题。你好像在这里混淆了两件事。 1)如何避免多个线程在同一行上工作? 2)如何测试你的应用程序以确保没有发生这种情况?
我没有看到你提到的一件事是如何处理正在写入数据库的线程的排序。当您处理用户提供的数据时,应用更改的顺序非常重要。为了以防万一,只是把它扔出去。
如何避免:
您没有说明您正在使用哪个数据库。一些高端商业产品具有行锁定功能以及许多其他可能对您非常感兴趣的功能。您应该与您的DBA交谈,看看他们是否无法帮助您。我完全同意限制这个的适当位置是在数据库中。这是唯一可以确保100%捕获它们的地方。
那就是说,当你绝对肯定要做对时,你应该选择多部分解决方案。然后如果有一件事失败了,其他人就可以为它做好准备了。所以添加一些这里提到的其他措施。
如何测试:
创建具有已知行重叠数据的数据集,然后尝试使用Grimace的Chess工具。但是,您需要知道的数据会导致问题,以便查看代码如何处理它们并查看它是否正确处理它们。不要只是随意丢弃随机数据,希望能有所作为。例如,如果您启动了500个线程,而这些线程都试图访问同一行,可能是因为您指示了相同的主键,会发生什么?