核心数据多线程导入(重复对象)

13
我有一个NSOperationQueue,用它从Web API获取对象并将其导入到Core Data中。每个操作都有自己的私有子ManagedObjectContext,属于应用程序的主ManagedObjectContext。每个操作都会检查要导入的对象是否已存在,如果存在则更新该对象;如果不存在,则创建新对象。这些对私有子上下文所做的更改然后传播到主管理对象上下文。
这种设置对我非常有效,但是存在重复问题。当我在两个不同的并发操作中导入相同的对象时,会出现具有完全相同数据的重复对象。(它们都检查对象是否存在,并且对它们来说似乎不存在)。我在大约同时处理“新”API调用和“获取”API调用时会收到2个相同的对象,由于我的设置是同时异步的,很难确保我永远不会有重复的对象尝试导入。
因此,我的问题是解决这个特定问题的最佳方法是什么?我考虑过将导入限制为最大并发操作为1 (我不喜欢这样做,因为会降低性能)。同样,我考虑在每个导入操作之后需要保存,并尝试处理上下文合并。此外,我考虑在之后对数据进行整理以偶尔清除重复项。最后,我考虑只处理所有获取请求中的重复项。但是,我认为这些解决方案都不是很好,也许有一种简单的解决方案我已经忽略了。

很棒的问题。我也遇到了同样的问题,本来想自己问这个问题。 - djskinner
3个回答

5
因此问题是:
  • 上下文是一个刮板 —— 除非您保存,否则在其中进行的更改不会推送到持久存储;
  • 您希望一个上下文能够了解另一个尚未被推送的上下文中所做的更改。
对我来说,似乎在上下文之间进行合并并不起作用——上下文不是线程安全的。因此,要发生合并,其他上下文线程/队列中不能有任何其他运行内容。因此,您永远无法消除新对象在另一个上下文正在进行其插入过程的途中插入的风险。
其他观察:
  • SQLite 在实际意义上不是线程安全的;
  • 因此,无论如何发出请求,所有访问持久存储的都将被序列化。
鉴于问题和 SQLite 的限制,在我的应用程序中,我们采用了一个框架,其中 Web 调用自然并发,如 NSURLConnection,随后对结果进行解析(JSON 解析加上一些钓探),然后查找或创建步骤被通道化为串行队列。
由于 SQLite 访问本来就需要序列化,因此几乎没有浪费处理时间,并且它们是序列化内容的绝大部分。

是的,这是有用的信息。我已经通过设置我的Core Data堆栈解决了你所说的一些SQL问题。我有一个类型为NSPrivateQueueConcurrencyType的私有上下文,这个上下文的唯一工作就是写入持久存储。从这个上下文中,我有一个类型为NSMainQueueConcurrencyType的子上下文,我将其用作我的应用程序的主要上下文。这种设置的美妙之处在于我可以控制何时写入我的持久存储。如果有人感兴趣,我的设置遵循这个设置:http://www.cocoanetics.com/2012/07/multi-context-coredata/ - hatunike
在这个思路上,它并不是功能的真正用途,但我发现当应用程序变为非活动状态时,使用 UIApplication -beginBackgroundTaskWithExpirationHandler: 并在其中执行昂贵的阻塞 Core Data 工作是符合苹果公司要求的。只需确保在应用程序再次变为活动状态时可以中断它。那就是我们进行删除操作的地方。如果您能够推迟写入磁盘,那么这可能是一个非常好的机会。 - Tommy
这是非常好的建议!我没有考虑过删除方面。谢谢。 - hatunike

3
开始时,请创建您的操作之间的依赖关系。确保一个操作在其依赖项完成之前不能完成。
请查看http://developer.apple.com/library/mac/documentation/Cocoa/Reference/NSOperation_class/Reference/Reference.html#//apple_ref/occ/instm/NSOperation/addDependency
每个操作完成后都应调用保存(save)方法。接下来,我建议尝试此处建议的查找或创建(Find-Or-Create)方法: https://developer.apple.com/library/ios/documentation/Cocoa/Conceptual/CoreData/Articles/cdImporting.html 它将解决您的重复问题,并且可能会导致您执行较少的获取操作(这些操作很昂贵、速度慢,因此会快速耗尽电池)。
您还可以创建全局子上下文来处理所有导入,然后在最后合并整个巨大的事物,但这真的取决于数据集的大小和您的内存考虑。

所以我相信我正在遵循“查找或创建”方法中建议的模式。我的重复问题仅会发生在同时执行两个导入时。我应该更清楚,但是我的设置中的单个操作可以处理特定对象的整个数组。例如,一个操作将处理从“获取”请求返回的数组,另一个操作将处理从“新建”请求返回的单个对象。 - hatunike
请参见上面的编辑,但我认为在您的NSOperation子类上使用addDependency:是至关重要的,如果您想一次处理多个项目。当然,您可以将并发操作降至1个,但您真正面临的是依赖性问题。 - Jeremy Massel
所以你的建议是让导入相同实体类型的操作成为依赖操作?这仍然允许对象类型不同时进行并发操作,但需要一个顺序,可能会有问题?我想我喜欢这个建议。我会进一步探索这个问题。感谢您的建议! - hatunike
使用依赖操作解决方案的一个缺点是,如果您导入的对象更加复杂(具有其他对象类型的关系),则还必须遍历这些关系以确定操作依赖性。这可能会变得非常棘手。 - hatunike
关于您提出的全局子上下文建议,我认为这可能存在问题,因为托管对象上下文不是线程安全的。而且您将从多个线程访问同一个上下文。请记住,每个操作都在不同的线程上执行。(当然,如果我错了,那么这个解决方案可能会解决我的确切问题。) - hatunike
这不是最优雅的解决方案,但是你可以在其他地方存储另一个managedObjectContext(比如在你的App Delegate中)。你甚至可以将其放在一个访问器方法中,该方法执行NSAssert以确保您永远不会跨越线程边界。是的,找出依赖关系非常困难,这让我想知道是否有一种方法来优化您的API。完美的REST兼容性很好,除非客户端变得非常复杂才能使用它;) 但是你绝对不缺少选择。 - Jeremy Massel

2

我一直在为同样的问题苦苦挣扎。到目前为止,对这个问题的讨论给了我一些想法,现在我将分享。

请注意,由于在我的情况下,我只在测试过程中很少看到这个重复问题,而且没有明显的方法可以轻松地重现它,因此本质上是未经测试的。

我有相同的CoreData堆栈设置 - 在私有队列上拥有主MOC,该主MOC具有子MOC并用作应用程序的主要上下文。最后,使用后台队列将批量导入操作(查找或创建)传递给第三个MOC。完成操作后,保存将向PSC传播。

我已将所有Core Data堆栈从AppDelegate移动到一个单独的类(AppModel),该类提供了访问域的聚合根对象(Player)以及在模型上执行后台操作的帮助函数(performBlock:onSuccess:onError:)。

对我来说,幸运的是,所有主要的CoreData操作都通过这个方法进行漏斗,因此如果我能确保这些操作按顺序运行,则可以解决重复问题。

- (void) performBlock: (void(^)(Player *player, NSManagedObjectContext *managedObjectContext)) operation onSuccess: (void(^)()) successCallback onError:(void(^)(id error)) errorCallback
{
    //Add this operation to the NSOperationQueue to ensure that 
    //duplicate records are not created in a multi-threaded environment
    [self.operationQueue addOperationWithBlock:^{

        NSManagedObjectContext *managedObjectContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
        [managedObjectContext setUndoManager:nil];
        [managedObjectContext setParentContext:self.mainManagedObjectContext];

        [managedObjectContext performBlockAndWait:^{

            //Retrive a copy of the Player object attached to the new context
            id player = [managedObjectContext objectWithID:[self.player objectID]];
            //Execute the block operation
            operation(player, managedObjectContext);

            NSError *error = nil;
            if (![managedObjectContext save:&error])
            {
                //Call the error handler
                dispatch_async(dispatch_get_main_queue(), ^{
                    NSLog(@"%@", error);
                    if(errorCallback) return errorCallback(error);
                });
                return;
            }

            //Save the parent MOC (mainManagedObjectContext) - WILL BLOCK MAIN THREAD BREIFLY
            [managedObjectContext.parentContext performBlockAndWait:^{
                NSError *error = nil;
                if (![managedObjectContext.parentContext save:&error])
                {
                    //Call the error handler
                    dispatch_async(dispatch_get_main_queue(), ^{
                        NSLog(@"%@", error);
                        if(errorCallback) return errorCallback(error);
                    });
                    return;
                }
            }];

            //Attempt to clear any retain cycles created during operation
            [managedObjectContext reset];

            //Call the success handler
            dispatch_async(dispatch_get_main_queue(), ^{
                if (successCallback) return successCallback();
            });
        }];
    }];
}

我希望我添加的内容能解决我的问题,那就是将整个操作包装在addOperationWithBlock中。我的操作队列只是简单地配置如下:
single.operationQueue = [[NSOperationQueue alloc] init];
[single.operationQueue setMaxConcurrentOperationCount:1];

在我的API类中,我可能会执行以下操作导入我的操作:
- (void) importUpdates: (id) methodResult onSuccess: (void (^)()) successCallback onError: (void (^)(id error)) errorCallback
{
    [_model performBlock:^(Player *player, NSManagedObjectContext *managedObjectContext) {
        //Perform bulk import for data in methodResult using the provided managedObjectContext
    } onSuccess:^{
        //Call the success handler
        dispatch_async(dispatch_get_main_queue(), ^{
            if (successCallback) return successCallback();
        });
    } onError:errorCallback];
}

现在使用NSOperationQueue,就不可能同时进行多个批处理操作了。

是的,这是我遇到的特定问题的确认解决方案。对我来说,不允许队列有超过1个并发操作太大程度上影响了性能。但对于很多人来说,这可能是一个很好的解决方案。 - hatunike

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接