Android调用springboot接口上传大字段,偶现接口超时的优化

介绍

最近有个功能,Android通过okhttp上传实体类,实体类包含一个大字段,上传的字符串长度达到300k,偶现接口超时的情况,大概100次有5次,看日志发现数据并没有到达接口,可能在网络传输中就超时了

优化1

​​直接接收对象:

java 复制代码
    @ApiOperation(value = "插入主动测量记录")
    @PostMapping("/insertMeasureRecord")
    public Result insertMeasureRecord(@RequestBody LomeRingMeasureRecords lomeRingMeasureRecords ){

        return iRingAppService.insertMeasureRecord(lomeRingMeasureRecords);
    }

刚开始是这样接收的,后来优化成流式解析

流式解析:

java 复制代码
  @Override
    public Result insertMeasureRecordRequest(HttpServletRequest request) {
        // 1. 使用流式读取避免内存爆炸
        try (InputStream is = request.getInputStream();
             Reader reader = new InputStreamReader(is, StandardCharsets.UTF_8)) {

            // 2. 带超时的JSON解析
            CompletableFuture<LomeRingMeasureRecords> future = CompletableFuture.supplyAsync(() -> {
                return JSON.parseObject(reader, LomeRingMeasureRecords.class);
            });

            LomeRingMeasureRecords records = future.get(15, TimeUnit.SECONDS); // 15秒超时

            // 3. 继续原有处理逻辑
            return Result.ok(lomeRingMeasureRecordsService.insertMeasureRecords(records));

        } catch (TimeoutException e) {
            
            return Result.fail(ResultsCode.REQUEST_FAILED);
        } catch (Exception e) {
            return Result.fail(ResultsCode.REQUEST_FAILED);
        }
    }

优化2

即使修改成这样,还是偶现超时,所以对大字段进行了压缩,然后服务端进行解压

Android部分

java 复制代码
public static Observable<JsonResult> insertMeasureRecord(String mac,String value,String testType,String filePath,String fileName,String label,String waveForm) {
        Log.i("waveForm size",waveForm.length()+"");
        String compressString="";
        try {
             compressString = DataCovertUtils.compressString(waveForm);
        } catch (IOException e) {
           e.printStackTrace();
        }
        Log.i("compressString size",compressString.length()+"");
        Map<String, Object> map = new HashMap<>();
        map.put("mac", mac);
        map.put("value", value);
        map.put("testType", testType);
        map.put("filePath", filePath);
        map.put("fileName", fileName);
        map.put("label", label);
        map.put("compressedWaveForm", compressString);
        return ServerAPIClient.getApi().insertMeasureRecord(map).subscribeOn(Schedulers.io())
                .unsubscribeOn(Schedulers.io())
                .observeOn(AndroidScheduler.mainThread());
    }
java 复制代码
  // GZIP压缩字符串
    public  static String compressString(String str) throws IOException {
        if (str == null || str.isEmpty()) {
            return str;
        }

        ByteArrayOutputStream out = new ByteArrayOutputStream();
        try (GZIPOutputStream gzip = new GZIPOutputStream(out)) {
            gzip.write(str.getBytes(StandardCharsets.UTF_8));
        }
        return Base64.encodeToString(out.toByteArray(), Base64.NO_WRAP);
    }

然后springboot解压

java 复制代码
 @Override
    public Result insertMeasureRecordRequest(HttpServletRequest request) {

        try (InputStream is = request.getInputStream();
             Reader reader = new InputStreamReader(is, StandardCharsets.UTF_8)) {

            LomeRingMeasureRecords records = JSON.parseObject(reader, LomeRingMeasureRecords.class);

                // 解压处理
                if (records.getCompressedWaveForm() != null ) {
                    try {
                        String decompressed = decompressString(records.getCompressedWaveForm());
                        records.setWaveForm(decompressed);
                    } catch (IOException e) {
                        // 解压失败记录日志,保持原数据
                        log.error("解压waveForm失败", e);
                        return Result.fail(ResultsCode.REQUEST_FAILED);
                    }
                }

            return Result.ok(lomeRingMeasureRecordsService.insertMeasureRecords(records));

        } catch (Exception e) {
            log.error("处理测量记录请求失败", e);
            return Result.fail(ResultsCode.REQUEST_FAILED);
        }
    }
    // GZIP解压字符串
    private String decompressString(String compressedStr) throws IOException {
        if (compressedStr == null || compressedStr.isEmpty()) {
            return compressedStr;
        }

        byte[] compressed = Base64.getDecoder().decode(compressedStr);
        ByteArrayInputStream bis = new ByteArrayInputStream(compressed);
        GZIPInputStream gis = new GZIPInputStream(bis);

        // 使用ByteArrayOutputStream直接读取所有字节
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        byte[] buffer = new byte[1024];
        int len;
        while ((len = gis.read(buffer)) > 0) {
            baos.write(buffer, 0, len);
        }

        // 转换为字符串,保留原始换行符
        String result = baos.toString(StandardCharsets.UTF_8.name());

        gis.close();
        bis.close();
        baos.close();

        return result;
    }

这样原字段大概是300k,然后压缩完是55k,之前接口返回是3s,修改完是2.5s,因为涉及到python计算,没办法再提高了

疑问

按理说,字段没超过1M,不应该出现超时的情况,根本没有到达接口,可能还是网络问题

场景化建议

  1. 推荐使用直接接收对象的场景
    字段大小<1MB且结构固定(如普通表单数据)。
    需要快速开发,减少手动编码量。
    服务端内存充足(如JVM堆内存>2GB)。
  2. 推荐使用流式解析的场景
    字段大小>10MB或包含压缩数据(如音频、视频、科学计算数据)。
    需要支持分片传输或断点续传(如大文件上传)。
    服务端内存受限(如云函数环境)

性能关键点分析

  1. 数据解析性能
    方式1(对象绑定):
    依赖Jackson或Gson等库的优化解析,实测1MB JSON数据解析耗时约5-20ms(Spring Boot默认配置)。
    瓶颈:大字段(如10MB+)可能导致堆内存压力和GC停顿。
    方式2(流式解析):
    使用BufferedReader逐行读取或JSONReader流式解析,1MB数据耗时约2-8ms(分块处理减少内存峰值)。
    优势:避免一次性加载大对象到内存,适合处理压缩数据流或分片传输场景。
  2. 内存与GC影响
    方式1:
    若字段包含大文本或二进制数据(如压缩后的波形数据),可能直接导致堆内存溢出(默认堆大小约128MB-1GB)。
    示例:10MB的compressedWaveForm字段会占用约20MB堆内存(含对象头和引用)。
    方式2:
    流式处理可将内存占用控制在常量级别(如分块读取时每次仅加载64KB),适合处理超大数据流(如50MB+的压缩文件)
  3. 压缩数据处理
    方式1的缺陷:
    若字段已压缩(如GZIP),直接反序列化会触发二次解压(框架先解压到临时文件或内存,再解析JSON),导致CPU和内存双重消耗。
    实测:10MB GZIP数据全量解压后约25MB,解析耗时增加50%。
    方式2的优化:
    可在流式解析中直接处理压缩流(如GZIPInputStream),避免中间数据存储,节省50%以上CPU和内存。

性能测试数据参考

相关推荐
子兮曰3 天前
jev-ultrafast 深度解析:7 秒订机票的浏览器 Agent 是如何炼成的
前端·后端·agent
子兮曰3 天前
Jev 爆发一周:7 秒 Agent 背后的 System One 生态与三场争议
前端·后端·ai编程
爱勇宝3 天前
ZCode 开源 24 小时:一份没有历史的账本,回答不了"有没有偷代码"
前端·后端·chatglm (智谱)
千里马学框架3 天前
一起学 Android 14:ShellTransition 屏幕旋转过程深度剖析
android·智能手机·性能优化·framework·性能·屏幕旋转·rotation
美狐美颜SDK开放平台3 天前
开发直播APP时如何接入视频美颜SDK?开发流程与注意事项
android·人工智能·计算机视觉·音视频·直播美颜sdk
胡写代码3 天前
别再前后端各写一套表单校验了
java·后端
AFinalStone3 天前
Android7 SystemUI源码解析(七)Keyguard锁屏模块深度解析
android·systemui
大勇前进3 天前
原生 PHP 还是 Laravel?小项目到底要不要上框架
后端
vipxieliang3 天前
ValidX 在 DDD 领域驱动设计中的实践
java·spring boot
yuzhi_liu3 天前
我用 LangGraph4j 实现 Multi-Agent Supervisor
后端