我有一个名为T1
的线程,用于读取平面文件并解析它。我需要创建一个名为T2
的新线程来解析此文件的某些部分,稍后这个T2
线程需要更新原始实体的状态,原始实体也会被原始实体解析和更新线程T1
。我该如何处理这种情况?
我收到一个包含以下样本记录的平面文件:
AAAA
BBBB
AACC
BBCC
AADD
BBDD
首先,此文件以Received
状态保存在数据库中。现在,所有以BB
或AA
开头的记录都需要在单独的线程中处理。成功解析后,两个线程都会尝试将数据库中此文件对象的状态更新为Parsed
。在某些情况下,我得到staleObjectException
。 编辑:在异常之前任何线程完成的工作都将丢失。我们正在使用乐观锁定。避免此问题的最佳方法是什么?
Possible hibernate exceptions when two threads update the same Object?
以上帖子有助于理解其中的一部分,但它无助于解决我的问题。
答案 0 :(得分:11)
第1部分 - 您的问题
您收到此异常的主要原因是您正在使用Hibernate 乐观锁定。这基本上告诉你线程T1或线程T2已经将状态更新为 PARSED ,现在另一个线程持有旧版本的行,其版本小于数据库中保存的版本并尝试将状态更新为 PARSED 。
这里的问题是" 两个线程是否试图保留相同的数据?"。如果答案是肯定的,那么即使最后一次更新成功,也不应该有任何问题,因为最终他们将行更新为相同的状态。在这种情况下,您不需要乐观锁定,因为您的数据无论如何都会同步。
如果在重置到下一个状态时两个线程T1和T2实际上彼此依赖,则在状态设置为 RECIEVED 之后会出现主要问题。在这种情况下,您需要确保如果T1已首先执行(反之亦然),T2需要刷新更新行的数据,并根据T1已推送的更改重新应用其更改。在这种情况下,解决方案如下。如果遇到staleObjectException,则基本上需要从数据库刷新数据并重新启动操作。
发布链接的第2部分分析 Possible hibernate exceptions when two threads update the same Object? 方法1 ,这或多或少是最后更新Wins 的情况。它或多或少地避免了乐观锁定(版本计数)。如果您没有从T1到T2的依赖或反向以设置状态 PARSED 。这应该是好的。
Aproach 2乐观锁定这就是您现在所拥有的。解决方案是刷新数据并重新启动操作。
Aproach 3行级数据库锁这里的解决方案与方法2的解决方案大致相同,只有悲观锁定的小修正。主要的区别在于,在这种情况下,它可能是一个READ锁,你可能甚至无法从数据库中读取数据,以便在它是PESSIMISTIC READ时刷新它。
Aproach 4应用程序级别同步有许多不同的方法可以进行同步。一个示例是将所有更新实际安排在BlockingQueue或JMS队列中(如果您希望它是持久的)并从单个线程推送所有更新。为了使它可视化,T1和T2将把元素放在队列上,并且将有一个T3线程读取操作并将它们推送到数据库服务器。
如果您使用应用程序级别同步,您应该知道在多服务器部署中不能分发所有结构。
我现在还无法想到其他任何事情:)
答案 1 :(得分:3)
我不确定我理解这个问题,但似乎它会构成一个线程T1的逻辑错误,它只处理,例如,以AA开头的记录将整个文件标记为"解析的&#34 ;?例如,如果您的应用程序在T1更新后崩溃但T2仍在处理BB记录时会发生什么?有些BB记录可能会丢失,对吗?
无论如何,问题的症结在于你有一个竞争条件,两个线程更新同一个对象。陈旧的对象异常只是意味着你的一个线程失去了竞争。一个更好的解决方案完全避免了比赛。
(我假设个别记录处理是幂等的,如果不是这种情况我认为你有更大的问题,因为一些失败模式将导致记录的重新处理。如果记录处理必须发生曾经且只有一次,那么你有一个更难的问题,消息队列可能是一个更好的解决方案。)
我会利用java.util.concurrent的功能将记录分发给线程工作者,让线程与hibernate块交互,直到所有记录都被处理完毕,此时该线程可以将文件标记为&#34 ;解析的"
例如,
// do something like this during initialization, or use a Guava LoadingCache...
Map<RecordType, Executor> executors = new HashMap<>();
// note I'm assuming RecordType looks like an enum
executors.put(RecordType.AA_RECORD, Executors.newSingleThreadExecutor());
然后在处理文件时,按如下方式调度每条记录,构建与排队任务状态对应的期货列表。让我们假设成功处理一条记录返回一个布尔值&#34; true&#34;:
List<Future<Boolean>> tasks = new ArrayList<>();
for (Record record: file.getRecords()) {
Executor executorForRecord = executors.get(record.getRecordType());
tasks.add(executor.submit(new RecordProcessor(record)));
}
现在等待所有任务顺利完成 - 有更优雅的方法可以做到这一点,特别是对于番石榴。请注意,如果您的任务因异常而失败,您还需要处理ExecutionException,我在此处对此进行了修改。
boolean allSuccess = true;
for (Future<Boolean> task: tasks) {
allSuccess = allSuccess && task.get();
if (!allSuccess) break;
}
// if all your tasks completed successfully, update the file record
if (allSuccess) {
file.setStatus("Parsed");
}
答案 2 :(得分:2)
假设每个线程T1,T2将解析文件的不同部分,意味着没有人覆盖其他线程解析。最好的办法是从数据库提交中解析解析过程。
T1,T2将执行解析T3或主线程将在T1,T2完成后执行提交。我认为在这种方法中,只有当两个线程都完成时才更正确地将文件状态更改为Parsed
。
您可以将T3视为CommitService类,等待T1,T2 finsih然后提交到DB
CountDownLatch是一个有用的工具。这是一个Example