uploadBlock 返回的数据重复
测试了下,如果app存活期间产生的日志没有全部upload,下次启动app后会返回之前已经上报过的日志; 这边也调用了UploadSucess方法;
下面截图里,点击按钮push了200条日志,(日志内容是数字0、1、3...),第一次上报到29,保存上报记录到本地,然后关闭app再启动又从0开始上报
测试了下,如果app存活期间产生的日志没有全部upload,下次启动app后会返回之前已经上报过的日志; 这边也调用了UploadSucess方法;
下面截图里,点击按钮push了200条日志,(日志内容是数字0、1、3...),第一次上报到29,保存上报记录到本地,然后关闭app再启动又从0开始上报
大佬能帮忙看看吗?需要demo的话我能提供iOS的项目
需要服务端处理去重逻辑, 去重判断业务可以加唯一id来区分。 因为网络传输是不稳定的,端上是在服务端确认收到数据后才删除掉本地数据。 但是如果在这个传输过程中出现网络问题,或者端上杀进程。 为了保证数据不丢失, 端上都不会删除数据,端上会在服务端明确收到数据后,才删除本地数据,否则,下一次启动会上报上一次没有确认收到的数据。 所以解决这个重复的问题方案就需要 业务的服务端进行数据去重处理。
需要服务端处理去重逻辑, 去重判断业务可以加唯一id来区分。 因为网络传输是不稳定的,端上是在服务端确认收到数据后才删除掉本地数据。 但是如果在这个传输过程中出现网络问题,或者端上杀进程。 为了保证数据不丢失, 端上都不会删除数据,端上会在服务端明确收到数据后,才删除本地数据,否则,下一次启动会上报上一次没有确认收到的数据。 所以解决这个重复的问题方案就需要 业务的服务端进行数据去重处理。
服务端去重逻辑确实是有的; 这个库也集成到项目里好久了,但是线上发现有用户上报的日志还是半年之前的,占比能达到10%~20%,这个比例不正常;同时logid重复的情况很多,生成ID的api,iOS这边使用的[[NSUUID UUID] UUIDString]; 几乎不会重复的。 我这边用demo测试了下,怀疑有调用uploadSuccess时没有成功删除的情况;
以下是测试逻辑: 1、push200条日志; 2、上传日志时,记录下来上传记录(key和日志内容); 3、上传一部分后将上传记录写入本地,并杀死app; 4、然后再启动app可以看到又从0开始上传
如果等这200条日志全部上传完成再杀死app后重启则不会有重复上报现象。
大佬能帮忙分析下吗?
下面是部分逻辑代码
MyViewController
//
// DataReporterManager.m
// reporterPath
//
// Created by luojilab on 2018/11/10.
// Copyright © 2018年 luojilab. All rights reserved.
//
#import "DataReporterManager.h"
#import <DataReporter/DataReporter.h>
static NSString *kReporterIndentify = @"kReporterIndentify";
static NSUInteger kMaxFileSize = 100 * 1024;
//static instance
static void *reporterInstanse;
@interface DataReporterManager()
@property (nonatomic,copy) NSString *reporterPath;
@end
@implementation DataReporterManager
#pragma mark - life cycle
- (void)dealloc{
[DataReporterManager stopMonitorReport];
}
+ (instancetype)sharedInstance {
static id singleton = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
singleton = [[super allocWithZone:nil] initInstance];
});
return singleton;
}
- (instancetype)initInstance {
self = [super init];
if (self) {
}
return self;
}
#pragma mark - private Methods
/**
初始化注册登录通知
*/
- (void)addNotification {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
});
}
/**
上报数据到服务器
*/
- (void)uploadData:(int64_t)key dataArrays:(NSArray *)dataArrays {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[self uploadSuccessed:key]; // 默认成功
// 返回数据记录
!self.uploadDataCallback ?: self.uploadDataCallback(key, dataArrays);
});
}
- (void)uploadSuccessed:(int64_t)key {
//重要,通知DataReporter,report success,每次回调必须执行成功或者失败
dispatch_async(dispatch_get_main_queue(), ^{
[DataReporter UploadSucess:reporterInstanse key:key];
DebugLog(@"UploadSucess -> should not upload again = %lld",key);
});
}
- (void)uploadFailed:(int64_t)key {
//重要,通知DataReporter,report Failed,每次回调必须执行成功或者失败
//失败后,间隔一段时间会重新发送,上层不必多余处理
dispatch_async(dispatch_get_main_queue(), ^{
[DataReporter UploadFailed:reporterInstanse key:key];
DebugLog(@"UploadFailed -> should upload again = %lld",key);
});
}
/**
初始化实例
*/
- (void)initReporterInstance{
[DataReporterManager stopMonitorReport];
reporterInstanse = [DataReporter MakeReporter:kReporterIndentify cachePath:self.reporterPath encryptKey:@"" uploadBlock:^(int64_t key, NSArray *dataArrays) {
if (dataArrays== nil || [dataArrays count] == 0 || reporterInstanse == nil){
return;
}
dispatch_async(dispatch_get_main_queue(), ^{
[self uploadData:key dataArrays:dataArrays];
});
}];
//set report max count 设置每次上报最大的数据量 10表示,一次最多10条报一次
[DataReporter SetReportCount:reporterInstanse count:10];
//set report ExpiredTime 0表示永久有效 所有数据上报,10*24*60*60 表示10天内有效,10天外数据不上报
[DataReporter SetExpiredTime:reporterInstanse expiredTime:0];
//set report reporterInstanse 上报间隔 单位i毫秒 10 表示每隔10毫秒上报一次,0表示有数据立即上报
[DataReporter SetReportingInterval:reporterInstanse reportingInterval:1000*10];
//set report retryInterval 重试间隔 单位i秒 5 表示每第一次上报错误后延迟5秒重试上报,再次错误,再加5秒,也就是10秒后再重试,最长1个小时后重试,如果为0,表示出错后立即重试,容易导致服务器压力过大,默认值为5
[DataReporter SetRetryInterval:reporterInstanse retryInterval:5];
//set save file size 设置缓存文件大小, 大小一定要比单条push进来的数据大
[DataReporter SetFileMaxSize:reporterInstanse fileMaxSize:kMaxFileSize];
}
/**
开始上报任务
*/
- (void)startReporter{
if (reporterInstanse) {
[DataReporter Start:reporterInstanse];
}
}
#pragma mark - public Methods
/**
start Report
*/
+ (void)startMonitorReport{
[[self sharedInstance] initReporterInstance];
[[self sharedInstance] startReporter];
}
/**
save ReportData
@param data reportData
*/
+ (void)pushData:(NSData *)data{
if ([data length] == 0){
return;
}
if (!reporterInstanse) {
return;
}
[DataReporter Push:reporterInstanse byteArray:data];
}
/**
Stop - 结束上报
*/
+ (void)stopMonitorReport{
if (reporterInstanse == nil) {
return;
}
[DataReporter ReleaseReporter:reporterInstanse];
reporterInstanse = NULL;
}
@end
MyViewController
#import "MyViewController.h"
#import "DataReporterManager.h"
static NSString *log_key_UserDefaults = @"logData";
@interface MyViewController ()
@property (nonatomic, strong) UIButton *saveButton;
@property (nonatomic, strong) UIButton *pushButton;
@property (nonatomic, strong) UITextView *textView;
@property (nonatomic, strong) NSString *logInfo;
@end
@implementation MyViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.view.backgroundColor = [UIColor whiteColor];
self.pushButton = [UIButton buttonWithType:UIButtonTypeSystem];
[self.pushButton setTitle:@"pushData" forState:UIControlStateNormal];
[self.pushButton setFrame:CGRectMake(30, 100, 80, 30)];
[self.pushButton setBackgroundColor:[UIColor lightGrayColor]];
[self.pushButton addTarget:self action:@selector(pushData) forControlEvents:UIControlEventTouchUpInside];
[self.view addSubview:self.pushButton];
self.saveButton = [UIButton buttonWithType:UIButtonTypeSystem];
[self.saveButton setTitle:@"saveData" forState:UIControlStateNormal];
[self.saveButton setFrame:CGRectMake(150, 100, 80, 30)];
[self.saveButton setBackgroundColor:[UIColor lightGrayColor]];
[self.saveButton addTarget:self action:@selector(saveData) forControlEvents:UIControlEventTouchUpInside];
[self.view addSubview:self.saveButton];
self.textView = [[UITextView alloc] init];
self.textView.font = [UIFont systemFontOfSize:15];
self.textView.scrollEnabled = YES;
self.textView.editable = NO;
self.textView.layoutManager.allowsNonContiguousLayout = NO;
self.textView.translatesAutoresizingMaskIntoConstraints = NO;
[self.view addSubview:self.textView];
[self.view addConstraints:@[leftConstraint, rightConstraint, topConstraint, bottomConstraint]];
[[DataReporterManager sharedInstance] setUploadDataCallback:^(int64_t key, NSArray *array) {
[self showLogWithKey:key data:array];
}];
self.logInfo = [[NSUserDefaults standardUserDefaults] objectForKey:log_key_UserDefaults];
self.textView.text = self.logInfo;
}
- (void)pushData {
//big data save and reporter
for (NSUInteger i = 0; i < 200; i++) {
NSString *str = [NSString stringWithFormat:@"%ld",(long)i];
NSData *data = [NSData dataWithBytes:[str UTF8String] length:[str length]];
[DataReporterManager pushData:data];
}
}
- (void)showLogWithKey:(int64_t)key data:(NSArray *)array {
dispatch_async(dispatch_get_main_queue(), ^(void) {
NSString *temp = @"";
for (NSData *data in array) {
NSString *log = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
NSString *str = [NSString stringWithFormat:@"key:%lld,log:%@\n", key, log];
NSLog(@"%@", str);
temp = [NSString stringWithFormat:@"%@%@", temp, str];
}
self.logInfo = [NSString stringWithFormat:@"%@%@", self.logInfo?:@"", temp];
self.textView.text = self.logInfo;
});
}
- (void)saveData {
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
[defaults setObject:self.logInfo forKey:log_key_UserDefaults];
}
@end
可以看到日志内容是重复的,但是两次返回的int64_t key不同,这个key的生成逻辑是什么样的呀?
可以看到日志内容是重复的,但是两次返回的int64_t key不同,这个key的生成逻辑是什么样的呀? int64_t key 是根据时间算的,重启后第二次肯定不一样。 SetFileMaxSize 中 文件的大小设置适当的大小,根据单条数据的大小,设置一个合理的大小,这样当文件达到一定条数就会落盘。一旦上传成功就会清理单片落盘文件。就不会出现上面这种数据很多重复上报情况。 DataReporter是一个准实时的上报设计, 适用于实时上报比较频繁场景。 如果数据量巨大。 实时场景不强的,像Log的上报可以用DataTransHub,这种上报更合适。
可以看到日志内容是重复的,但是两次返回的int64_t key不同,这个key的生成逻辑是什么样的呀? int64_t key 是根据时间算的,重启后第二次肯定不一样。 SetFileMaxSize 中 文件的大小设置适当的大小,根据单条数据的大小,设置一个合理的大小,这样当文件达到一定条数就会落盘。一旦上传成功就会清理单片落盘文件。就不会出现上面这种数据很多重复上报情况。 DataReporter是一个准实时的上报设计, 适用于实时上报比较频繁场景。 如果数据量巨大。 实时场景不强的,像Log的上报可以用DataTransHub,这种上报更合适。
感谢回复,我看了下源码,key的逻辑明白了;
另外看读数据和删除数据的方法, 删除数据时(DataProvider::ClearItem),会从内存或文件删除,这里会判断item.fromPath是否存在,ClearFile是删除整个文件,然后我看了FileInputStream::ReadData方法,只有最后一条数据会被赋值fromPath。
所以,假如某个文件有 100 条记录,每次上传 10 条,当上传到20条时进程被关闭,重启后会重新从第 1 条开始上报吗?
void DataProvider::ClearItem(CacheItem &item) {
if (!item.fromPath.empty()) {
ClearFile(item.fromPath);
}
if (item.fromMem != NULL) {
ClearMem();
}
}
if (m_Offset >= fileSize) {
if (DataProvider::IsExpired(dateInItem, expiredTime)) {
if (!ret->empty()) {
ret->back()->fromPath = m_Path;
}
i++;
continue;
} else {
cacheItem->fromPath = m_Path;
}
}
可以看到日志内容是重复的,但是两次返回的int64_t key不同,这个key的生成逻辑是什么样的呀? int64_t key 是根据时间算的,重启后第二次肯定不一样。 SetFileMaxSize 中 文件的大小设置适当的大小,根据单条数据的大小,设置一个合理的大小,这样当文件达到一定条数就会落盘。一旦上传成功就会清理单片落盘文件。就不会出现上面这种数据很多重复上报情况。 DataReporter是一个准实时的上报设计, 适用于实时上报比较频繁场景。 如果数据量巨大。 实时场景不强的,像Log的上报可以用DataTransHub,这种上报更合适。
感谢回复,我看了下源码,key的逻辑明白了;
另外看读数据和删除数据的方法, 删除数据时(DataProvider::ClearItem),会从内存或文件删除,这里会判断item.fromPath是否存在,ClearFile是删除整个文件,然后我看了FileInputStream::ReadData方法,只有最后一条数据会被赋值fromPath。
所以,假如某个文件有 100 条记录,每次上传 10 条,当上传到20条时进程被关闭,重启后会重新从第 1 条开始上报吗?
void DataProvider::ClearItem(CacheItem &item) { if (!item.fromPath.empty()) { ClearFile(item.fromPath); } if (item.fromMem != NULL) { ClearMem(); } }if (m_Offset >= fileSize) { if (DataProvider::IsExpired(dateInItem, expiredTime)) { if (!ret->empty()) { ret->back()->fromPath = m_Path; } i++; continue; } else { cacheItem->fromPath = m_Path; } }
对的,理解的完全正确。 所以可以通过预判业务单条数据大小 合理设置maxFileSize,可以尽早落盘,来达到符合自己业务场景的情况。 DataReporter设计初衷是尽可能在生命周期活着的时候尽快报完数据。 杀进程恢复数据是兜底逻辑。
对的,理解的完全正确。 所以可以通过预判业务单条数据大小 合理设置maxFileSize,可以尽早落盘,来达到符合自己业务场景的情况。 DataReporter设计初衷是尽可能在生命周期活着的时候尽快报完数据。 杀进程恢复数据是兜底逻辑。
感谢!明白了, 假设一条数据大小是2k,一次上报10条,设置maxFileSize为20-50k是不是算是比较合理呢?
对的,理解的完全正确。 所以可以通过预判业务单条数据大小 合理设置maxFileSize,可以尽早落盘,来达到符合自己业务场景的情况。 DataReporter设计初衷是尽可能在生命周期活着的时候尽快报完数据。 杀进程恢复数据是兜底逻辑。
感谢!明白了, 假设一条数据大小是2k,一次上报10条,设置maxFileSize为20-50k是不是算是比较合理呢?
对的, 差不多是这个意思
大佬能帮忙看看吗?需要demo的话我能提供iOS的项目