数据丢失常见原因(一):网络异常与超时处理

说实话,每次接手一个接入数据丢失的排查,十次有八次都跟网络有关。你想想看,SDK 集成这件事本质上就是让两端通过接口对话,只要其中一条消息在路上丢了,数据就拜拜了。今天我们就来掰扯一下,网络异常和超时到底怎么把你的数据吃掉的。

网络异常:你到底丢在哪一段?

SDK 客户端到服务端,中间隔着成千上万的路由器、防火墙、DNS 服务器。任何一个环节出了岔子,请求就到不了对面。我遇到过最诡异的一个案例——用户连着公司 Wi-Fi 一切正常,回到家就丢数据。查到最后发现,他家路由器的 MTU 设置不对,大包全碎了,小包却还能过。这就是典型的网络异常场景。

常见的网络异常,可以归纳成这几类:

异常类型 表现 数据丢失风险
DNS 解析失败 域名查不到 IP,请求根本发不出去 高风险,请求从源头就被扼杀了
TCP 连接超时 三次握手一直没回来,连接建不起来 高风险,数据压根送不出去
中间网络丢包 路由器或防火墙丢弃了数据包 中风险,数据可能卡在半路
连接重置 RST 包,服务端主动断开了链接 高到中风险,已发送的数据可能已丢失
SSL/TLS 握手失败 证书校验不过或加密协商失败 高风险,连接还没开始就被掐断了

每一种异常,都会导致 SDK 发送的数据包还没抵达服务端就被丢弃了。关键问题是:SDK 有没有意识到来这条数据已经死了?如果没有,SDK 就会认为数据已发送,但服务端实际上什么也没收到。

曾经有个血泪教训:某电商 SDK 在用户下单时采用 Fire-and-Forget 模式(发完就不管了)。结果在国内某运营商网络下,因为中间运营商劫持了 HTTP 请求,返回了 200 假响应。SDK 以为成功了,但服务端根本没收到下单数据。那一个月,该产品的下单成功率比用户真实退款率低了 8 个百分点。所以,只看状态码不校验业务响应体的做法,太容易掉坑里了。

超时机制:你的救命稻草还是隐形杀手?

先问个问题:为什么要设置超时?不设行不行?我以前也这么想过,后来发现不设超时的后果更可怕——线程池被撑爆,整个 APP 卡死。但设置超时的方式不对,又会把本来也许能活着的数据提前判了死刑。

主要涉及三类超时:

  • 连接超时(Connect Timeout):建立 TCP 连接的过程中,超过这个时间没连上就算失败。我个人习惯设置 5~10 秒,太短了容易误伤缓慢但正常的网络。
  • 读取超时(Read Timeout / Socket Timeout):连接建立后,等待服务端返回数据的时间。这个要根据接口的预期处理时间灵活调整。比如一个需要实时计算的报表接口,我一般设 30 秒;一个简单的查询接口,3 秒就够了。
  • 写入超时(Write Timeout):把请求数据发送到服务端的时间。大多数情况下这个值很小,但如果数据体很大或网络差,也会拖死流程。
一个小技巧:在 OKHttp 或 Retrofit 里,你可以分别设置这三类超时。而且可以考虑区分首次连接超时和重试连接超时。首次网络状况未知,可以宽裕一点;重试时既然已经知道网络有问题,就要更激进。我一般在重试策略里把超时缩短到原值的 60%~70%。

超时与重试的博弈

超时了怎么办?直接丢掉?这就太蠢了。标准做法是:先判定这个超时是否值得重试。

你知道,超时不一定意味着数据丢了。比如服务端已经收到请求,只是处理慢了,或者返回结果在路上堵车了。这时候如果 SDK 直接放弃,那请求就白做了。所以重试策略必须明智。

什么时候可以重试?

  • 连接超时:可以重试。连接都没建起来,服务端没收到任何东西,重试不会有副作用。
  • 读取超时:看场景。如果是幂等的读取操作(查询),可以重试。如果是非幂等的写入操作(下单、转账),需要非常谨慎。我曾经踩过这个坑——在线视频 SDK 上报播放状态时,因为网络波动导致读取超时,SDK 自动重试了 5 次。结果服务端其实已收到第一次请求并执行成功,只是返回结果堵路上了。最终给后台插入了 5 条重复的状态变更记录。
  • 写入超时:基本不重试。如果数据发出去了,服务端可能已经收到了,重试会带来重复数据风险。

重试策略一览

策略名称 做法 适用场景
固定间隔 每次重试间隔 2 秒或 5 秒 对时间不敏感的数据上报,适合日志、埋点
指数退避 第一次 1 秒,第二次 2 秒,第三次 4 秒…… 高并发下的临时性故障,防止雪崩
带抖动的指数退避 在指数退避基础上加随机抖动 大规模集群下防止同时重试打垮服务端
即时重试 + 延迟重试 第一次立即重试,后续按策略间隔 对实时性要求高的核心业务,如支付、认证
核心原则:
- 幂等的操作可以大胆重试
- 非幂等的写入操作,一定要加全局去重 ID 或业务序列号
- 重试次数不是越多越好,通常 3 次够了,再多就是给网络添乱

为什么网络异常最容易导致数据丢失?

你看看这张流程图就明白了:

网络异常导致数据丢失的完整链路 客户端准备数据 写入待发送队列 TCP 连接建立 SYN → SYN+ACK → ACK 发送请求数据 HTTP POST / API 调用 网络异常发生 丢包 / 超时 / 连接重置 正常请求处理 服务端正常接收 数据永久丢失 用户感受:操作无效 超时重试(需幂等) 重试成功 或 重复数据 正常响应返回 数据安全落库 右侧路径可以恢复数据,左侧路径数据永久丢失,中间路径对数据完整性有要求

看到没?整个流程中,从客户端准备数据到服务端落库,中间任何一个环节网络断了,数据就处于“薛定谔状态”——既不能确认成功,也不能确认失败。这就是我前面说的,为什么数据丢失排查最恶心,因为它不报错,它只是不通知你。

实战中的超时配置示例

来。直接看代码。假设我们用 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% 的网络丢数据场景你都能覆盖住。