利用RequestScope优化性能

41 阅读5分钟

背景

代码要实现的功能是:

  • 1:feign调用A获取相机的播放串

  • 2:feign调用B根据播放串获取截图

  • 3: 相机是集合,该集合外层还有一堆逻辑和嵌套循环

现在发现的问题是整个流程下来总耗时太长,初步排查发现: 获取截图接口太慢,3s才返回一条数据;大量的http调用。

假设

假设即前提,无法改变的因素:

  • 1:截图接口不做优化(如果可以优化直接解决问题)

  • 2:获取播放串和获取截图无法合并为一个接口(如果可以合并将减少大量的http请求)

  • 3:不要多线程调用截图接口(3s+的响应时间,其实多线程调用该接口也没有意义)

  • 4:代码不增加异步处理机制(例如增加多线程和MQ解耦)

  • 5:代码不增加批量处理机制(例如将多层嵌套的循环扁平化,减少数据库访问)

  • 6:无法从需求角度减少数据量,无法从需求角度改变用户操作流程

解决方案

在毙掉了“假设”中的解决方案后,从缓存的思路解决这个问题,核心改动为:

  • 以相机为维度缓存相机的截图

  • 以相机为维度缓存相机的播放串

简单来说,将获取播放串和获取截图封装到一个组件中,当需要截图的时候,先检查缓存中是否有数据,若没有需要获取播放串执行截图并在得到截图后缓存起来,在获取播放串的时候先检查缓存中是否有数据,若没有才调用获取播放串接口并在得到播放串后缓存起来。

缓存作用域设定:

  • 线程维度太细,并且如果采用ThreadLocal存储还有内存泄露的风险

  • 全局维度太粗,而且也不符合需求,虽然从短周期来说允许截图相同,例如3m内截图一样是业务可以接受的,但是3h的话就无法接受了。

  • Request请求维度合适,虽然全局维度通过一些自定义设置和逻辑处理也能达到要求,但是难免开发复杂,性价比不高。在SpringBoot中使用@RequestScope注解可以很好的处理请求级别的缓存。

  • 由于视频汇聚平台对播放串有有效期控制,所以对于“播放串”的缓存需要在Request请求中进一步限定是否超过有效期,好在这个有效期是固定值,可以直接写死。

使用RequestScope缓存数据

首先增加一个 ScreenShotRequestCache 类,并添加 @RequestScope 注解,在其中定义两个缓存 map,一个用来存放播放串,另外一个用来存放截图。


 
@Slf4j

@Component

@RequestScope

public class ScreenShotRequestCache {

    /**

     * 存储 cameraIndexCode -> CameraUrlCache

     */

    private final Map<String, CameraUrlCache> urlCache = new HashMap<>();

    /**

     * 存储 cameraIndexCode -> 截图结果

     */

    private final Map<String, String> screenshotCache = new HashMap<>();

    /**

     * 获取播放url

     * @param cameraIndexCode

     * @return

     */

    public CameraUrlCache getUrl(String cameraIndexCode) {

        return urlCache.get(cameraIndexCode);

    }

    public void putUrl(String cameraIndexCode, CameraUrlCache cache) {

        urlCache.put(cameraIndexCode, cache);

    }

    public boolean containsUrl(String cameraIndexCode) {

        return urlCache.containsKey(cameraIndexCode);

    }

    /**

     * 获取截图

     * @param cameraIndexCode

     * @return

     */

    public String getScreenshot(String cameraIndexCode) {

        return screenshotCache.get(cameraIndexCode);

    }

    public void putScreenshot(String cameraIndexCode, String fileName) {

        screenshotCache.put(cameraIndexCode, fileName);

    }

    public boolean containsScreenshot(String cameraIndexCode) {

        return screenshotCache.containsKey(cameraIndexCode);

    }

}

  

然后定义一个 ScreenShotSupport 类,用来处理获取播放串和获取截图,并且在里面增加缓存的功能。

具体来说,在外部代码需要获取截图时候:

  • 先检查screenshotCache(截图缓存)中是否有数据

  • 如果没有截图缓存数据,需要获取播放串并截图

  • 获取播放串时候先检查urlCache缓存是否有数据并且是否满足有效期

  • 如果没有播放串缓存或者缓存已过有效期,重新获取播放串并缓存下来

  • 根据播放串截图并缓存下来


  
@Slf4j

@RequiredArgsConstructor

@Component

public class ScreenShotSupport {

    /**

     * 相机播放url缓存时间 , 4分钟

     */

    private static final long URL_TTL_MS = 4 * 60 * 1000;

    private final ScreenShotRequestCache requestCache;

    /**

     * 截图

     * @param cameraIndexCode

     * @return

     */

    public String screenShot(String cameraIndexCode) {

        if (requestCache.containsScreenshot(cameraIndexCode)) {

            String cached = requestCache.getScreenshot(cameraIndexCode);

            log.debug("命中请求缓存 cameraIndexCode:{} -> {}", cameraIndexCode, cached);

            return cached;

        }

        String url = getValidCameraUrl(cameraIndexCode);

        if (url == null || url.isEmpty()) {

            return "";

        }

        String fileName = doScreenShot(cameraIndexCode, url);

        if (!fileName.isEmpty()) {

            requestCache.putScreenshot(cameraIndexCode, fileName);

        }

        return fileName;

    }

    /**

     * 截图

     * @param cameraIndexCode

     * @param url

     * @return

     */

    private String doScreenShot(String cameraIndexCode, String url) {

        String fileName = "/data/camera/screenshot/" + cameraIndexCode + "-" + LocalDateTimeUtil.getCurrentTime() + ".jpg";

        String requestUrl = xxxConfig.getxxServiceUrl() + "screenshot?name=" + fileName + "&url=" + URLEncoder.encode(url);

        JSONObject jsonObject = OpenAipHttpUtil.httpURLConectionGET(requestUrl);

        if (null != jsonObject ) {

            log.debug("截图成功cameraIndexCode:{} ,img_url:{}", cameraIndexCode, fileName);

            return fileName;

        } else {

            log.debug("截图失败cameraIndexCode:{} ,url:{};requestUrl:{}", cameraIndexCode, url, requestUrl);

            return "";

        }

    }

    /**

     * 获取有效的URL(带缓存+过期检查)

     */

    public String getValidCameraUrl(String cameraIndexCode) {

        if (requestCache.containsUrl(cameraIndexCode)) {

            CameraUrlCache cache = requestCache.getUrl(cameraIndexCode);

            if (System.currentTimeMillis() - cache.getCreateTime() < URL_TTL_MS) {

                log.debug("命中URL缓存 cameraIndexCode:{} -> {}", cameraIndexCode, cache.getUrl());

                return cache.getUrl();

            } else {

                log.debug("URL已过期 cameraIndexCode:{}", cameraIndexCode);

            }

        }

        // 没缓存或已过期,重新获取

        String newUrl = fetchCameraUrl(cameraIndexCode);

        if (!newUrl.isEmpty()) {

            requestCache.putUrl(cameraIndexCode, new CameraUrlCache(newUrl, System.currentTimeMillis()));

        }

        return newUrl;

    }

    /**

     * 调用接口获取新的URL

     */

    private String fetchCameraUrl(String cameraIndexCode) {

        GetVideoStreamingParam param = new GetVideoStreamingParam();

        param.setCameraIndexCode(cameraIndexCode);

        param.setProtocol("rtsp");

        Object streaming = xxxServiceClient.getStreamingUrl(param);

        if (streaming != null) {

            String url = streaming.getData();

            log.debug("获取视频流成功 cameraIndexCode:{} -> {}", cameraIndexCode, url);

            return url;

        } else {

            log.debug("获取视频流失败 cameraIndexCode:{}; param={}; response={}",

                    cameraIndexCode, JSON.toJSONString(param), JSON.toJSONString(streaming));

            return "";

        }

    }

}
 


总结

通过引入 RequestScope 来处理缓存,我们在不改变接口调用方式、不增加异步机制和批量处理的前提下,有效解决了截图接口性能不足的问题,同时减少了重复的 HTTP 请求。

具体收益有:

  • 减少重复调用:对于同一次请求中的相机截图操作,相同的 cameraIndexCode 只会调用一次获取播放串和一次截图接口,其余地方直接走缓存。在外层复杂的嵌套循环和多层逻辑下,可以大幅降低 HTTP 请求数量。

  • 控制缓存粒度:使用 @RequestScope 限定缓存的生命周期与请求一致,避免了全局缓存带来的脏数据风险。对播放串增加了过期检查(例如 4 分钟),保证不会使用失效的播放串。

  • 开发成本低:没有引入多线程、消息队列、批量接口等复杂机制,仅通过 Spring 提供的 @RequestScope 特性实现。保持了原有的调用逻辑,对外接口没有变化。

这种方式既满足了业务对性能的要求,又保持了代码结构的简洁和可维护性。

代码

github.com/Naylor55/Ja…

引用