数据丢失常见原因(一):网络异常与超时处理
说实话,每次接手一个接入数据丢失的排查,十次有八次都跟网络有关。你想想看,SDK 集成这件事本质上就是让两端通过接口对话,只要其中一条消息在路上丢了,数据就拜拜了。今天我们就来掰扯一下,网络异常和超时到底怎么把你的数据吃掉的。
网络异常:你到底丢在哪一段?
SDK 客户端到服务端,中间隔着成千上万的路由器、防火墙、DNS 服务器。任何一个环节出了岔子,请求就到不了对面。我遇到过最诡异的一个案例——用户连着公司 Wi-Fi 一切正常,回到家就丢数据。查到最后发现,他家路由器的 MTU 设置不对,大包全碎了,小包却还能过。这就是典型的网络异常场景。
常见的网络异常,可以归纳成这几类:
| 异常类型 | 表现 | 数据丢失风险 |
|---|---|---|
| DNS 解析失败 | 域名查不到 IP,请求根本发不出去 | 高风险,请求从源头就被扼杀了 |
| TCP 连接超时 | 三次握手一直没回来,连接建不起来 | 高风险,数据压根送不出去 |
| 中间网络丢包 | 路由器或防火墙丢弃了数据包 | 中风险,数据可能卡在半路 |
| 连接重置 | RST 包,服务端主动断开了链接 | 高到中风险,已发送的数据可能已丢失 |
| SSL/TLS 握手失败 | 证书校验不过或加密协商失败 | 高风险,连接还没开始就被掐断了 |
每一种异常,都会导致 SDK 发送的数据包还没抵达服务端就被丢弃了。关键问题是:SDK 有没有意识到来这条数据已经死了?如果没有,SDK 就会认为数据已发送,但服务端实际上什么也没收到。
超时机制:你的救命稻草还是隐形杀手?
先问个问题:为什么要设置超时?不设行不行?我以前也这么想过,后来发现不设超时的后果更可怕——线程池被撑爆,整个 APP 卡死。但设置超时的方式不对,又会把本来也许能活着的数据提前判了死刑。
主要涉及三类超时:
- 连接超时(Connect Timeout):建立 TCP 连接的过程中,超过这个时间没连上就算失败。我个人习惯设置 5~10 秒,太短了容易误伤缓慢但正常的网络。
- 读取超时(Read Timeout / Socket Timeout):连接建立后,等待服务端返回数据的时间。这个要根据接口的预期处理时间灵活调整。比如一个需要实时计算的报表接口,我一般设 30 秒;一个简单的查询接口,3 秒就够了。
- 写入超时(Write Timeout):把请求数据发送到服务端的时间。大多数情况下这个值很小,但如果数据体很大或网络差,也会拖死流程。
超时与重试的博弈
超时了怎么办?直接丢掉?这就太蠢了。标准做法是:先判定这个超时是否值得重试。
你知道,超时不一定意味着数据丢了。比如服务端已经收到请求,只是处理慢了,或者返回结果在路上堵车了。这时候如果 SDK 直接放弃,那请求就白做了。所以重试策略必须明智。
什么时候可以重试?
- 连接超时:可以重试。连接都没建起来,服务端没收到任何东西,重试不会有副作用。
- 读取超时:看场景。如果是幂等的读取操作(查询),可以重试。如果是非幂等的写入操作(下单、转账),需要非常谨慎。我曾经踩过这个坑——在线视频 SDK 上报播放状态时,因为网络波动导致读取超时,SDK 自动重试了 5 次。结果服务端其实已收到第一次请求并执行成功,只是返回结果堵路上了。最终给后台插入了 5 条重复的状态变更记录。
- 写入超时:基本不重试。如果数据发出去了,服务端可能已经收到了,重试会带来重复数据风险。
重试策略一览
| 策略名称 | 做法 | 适用场景 |
|---|---|---|
| 固定间隔 | 每次重试间隔 2 秒或 5 秒 | 对时间不敏感的数据上报,适合日志、埋点 |
| 指数退避 | 第一次 1 秒,第二次 2 秒,第三次 4 秒…… | 高并发下的临时性故障,防止雪崩 |
| 带抖动的指数退避 | 在指数退避基础上加随机抖动 | 大规模集群下防止同时重试打垮服务端 |
| 即时重试 + 延迟重试 | 第一次立即重试,后续按策略间隔 | 对实时性要求高的核心业务,如支付、认证 |
- 幂等的操作可以大胆重试
- 非幂等的写入操作,一定要加全局去重 ID 或业务序列号
- 重试次数不是越多越好,通常 3 次够了,再多就是给网络添乱
为什么网络异常最容易导致数据丢失?
你看看这张流程图就明白了:
看到没?整个流程中,从客户端准备数据到服务端落库,中间任何一个环节网络断了,数据就处于“薛定谔状态”——既不能确认成功,也不能确认失败。这就是我前面说的,为什么数据丢失排查最恶心,因为它不报错,它只是不通知你。
实战中的超时配置示例
来。直接看代码。假设我们用 OkHttp 做底层网络库,怎么配置才能既防丢又能快速发现丢包?
// 核心配置示例
OkHttpClient.Builder builder = new OkHttpClient.Builder()
.connectTimeout(8, TimeUnit.SECONDS) // 连接超时:8秒
.readTimeout(15, TimeUnit.SECONDS) // 读取超时:15秒
.writeTimeout(10, TimeUnit.SECONDS) // 写入超时:10秒
.retryOnConnectionFailure(false); // 关闭 OkHttp 内置的重试,自己控制
// 自定义重试拦截器
Interceptor retryInterceptor = chain -> {
Request request = chain.request();
Response response = null;
int retryCount = 0;
int maxRetries = 3;
while (retryCount < maxRetries) {
try {
response = chain.proceed(request);
// 如果响应码不对,也走重试逻辑
if (response.isSuccessful()) {
return response;
}
// 针对 5xx 服务端错误重试
if (response.code() >= 500) {
retryCount++;
// 指数退避:1s, 2s, 4s
Thread.sleep((long) Math.pow(2, retryCount) * 1000);
continue;
}
return response;
} catch (SocketTimeoutException e) {
// 读取超时,如果请求是幂等的则重试
if (isIdempotent(request)) {
retryCount++;
// 带抖动的指数退避
long backoff = (long) (Math.pow(2, retryCount) * 1000 + Math.random() * 500);
Thread.sleep(backoff);
} else {
throw e; // 非幂等操作,放弃重试
}
} catch (ConnectException e) {
// 连接异常,必重试
retryCount++;
Thread.sleep((long) Math.pow(2, retryCount) * 1000);
}
}
throw new IOException("重试" + maxRetries + "次后仍然失败");
};
retryOnConnectionFailure(false) 是我特别建议的。因为 OkHttp 自己的重试不够智能,它只看连接失败,不看业务层是否允许重试。关掉它,自己写拦截器,你才能把超时重试和业务语义绑在一起。我自己接入过的所有 SDK 项目中,没有一个是直接用框架自带重试的。
小结一下
网络异常和超时是数据丢失的头号杀手。核心应对策略其实就是三点:感知异常(通过超时机制)、智能判重(通过幂等设计)、可控重试(通过指数退避和抖动)。搞定这三样,至少 70% 的网络丢数据场景你都能覆盖住。