暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

ES与Redis实现千万级数据的范围查询性能比较,远程 http调用耗时也能降低到0ms

Java艺术 2021-09-09
5014
关注“Java艺术”一起来充电吧!

如何使用redis实现IP库的范围查询可以看上篇《基于Redis实现范围查询的IP库缓存设计方案》。很抱歉的说,这段时间需要处理的问题很多,dubbo源码分析需要断更。


本篇内容:

  • ES与Redis实现IP库的范围查询谁性能更强

  • 远程 http调用耗时也能降低到0ms


ES与Redis实现IP库的范围查询谁性能更强


由于ip库的每条记录存储的是一段ip范围内的国家、城市和运营商信息,要想查询一个ip的信息,就需要用到像SQL那样的支持大于等于小于等于的查询。


在上一篇文章中,我介绍了如何使用Redis实现范围查询,也是一个很好的Sorted Set使用场景。在此,我再总结一下,使用Redis实现ip库缓存支持范围查询的设计方案。详细设计可以看下上篇文章。


1、使用hash存储记录

2、使用Sorted Set实现范围查询,查询时间复杂度O(log(n))

3、由于数据量较大,为避免单个Sorted Set过大,上升n,导致查询时间O(log(n))上升,在代码层加逻辑分区,将一个大的Sorted Set分为3750个小的Sorted Set。


在模拟200个线程并发执行的场景下,平均耗时20ms,因为是本地测的,可能受电脑配置差的影响,而且redis也是单节点,这个数值只能做参考。


但redis的执行命令单线程特性,一个查询耗时长会影响到其它业务的接口的执行耗时,并发越高越严重。可以看dubbo服务雪崩那篇文章,有介绍关于redis的执行命令过程。


如果是几十万的数据是没有什么问题的,可以轻松应对,但,对于一个有一千两百多万条记录的ip库,且无法简单的用key-value方式存储。最重要的是用于高并发场景,对单次查询的耗时要求非常高。


因此,我想出了用ES替代Redis的第二个方案。使用ES的实现方案就非常简单,直接创建索引将ip库数据写入ES即可,然后就是使用ES的搜索功能实现快速范围查询。关于ES,本文不做介绍。


使用ES的方案,本地测试平均耗时也是二十多毫秒,但毕竟只是本地做的简单测试,并没有说服力,所以需要经过实际的线上考验,才能对比得出结论。


最后是实现两个方式共存,并将ip库的数据查询功能做为单独的服务,通过restful对外提供查询接口。目的是ip的缓存实现策略更换不影响正常的业务,也是让其它项目或微服务能够共用这个ip库。由于只是提供一个简单的查询接口,所以我使用netty+webflux实现。


为每个实现方案部署一个节点,通过负载均衡,将一半请求打到选用redis实现方案的节点,一半请求打到选用es实现方案的节点,最后统计两者在实际线上高并发场景下性能比较,选择平均耗时最短的方案。下面是测试结果比较。


[图为Redis实现方案的查询耗时日记]


[图为Es实现方案的查询耗时日记]


在实际的线上测试结果中,使用redis方案的查询平均耗时是2ms,而es高达10ms,结果很明显,还是使用redis的性能更高。



远程 http调用耗时也能降低到0ms


我提倡的是将IP库作为一个工具服务,提供api给其它服务调用。这样可以封装为一个工具类,在公共组件下。但是,面对高并发的服务,远程调用的耗时又将会是高并发服务的痛点。面对追求高并发低时延的需求,又不得不重新考虑。


如果必须要坚持将IP库查询作为一个独立的服务,那么当前可选的方案只有以下两种:

1、将IP库使用dubbo提供远程调用,注册到注册中心。

2、netty+webflux提供 restful接口,不需要注册中心,仅提供url。


对于方案一,IP库我并不想将它跟业务捆绑在一起,原本只是一个提供ip位置信息查询的服务,如果使用dubbo将会和业务偶尔,因为调用者必须要依赖一个只有一个接口的jar包。而像管理后台和定时任务这些服务如果用到ip库,又不得不再提供restful接口,这就需要dubbo跟restful共存。


dubbo的rpc远程调用相比http性能更高,如果使用方案二,就可能会增加原本高并发服务的单次处理请求的耗时,导致QPS下降。那么,是否能在高并发服务上消除http远程查询 ip信息的耗时呢。


我结合项目中业务需求,想到的方案就是异步http请求。在一次请求中,如果并不是接收到请求就立即需要知道来源ip的位置信息的时候,就可以使用异步的方式,如图。

从图中可以看出,在处理业务逻辑1和2的时候,还不需要知道request的ip所在国家和城市等位置信息。对于这种场景我们就可以考虑使用异步方案,在接收到请求的时候,发送一个异步请求获取位置,当处理到业务逻辑3时,再获取异步请求的响应结果,如果此时异步请求还未完成,再进行阻塞等待,改进后的流程如下。

在项目中我使用的是OkHttp框架实现http请求,而OkHttp就天然支持异步调用。其实同步也是在异步的基础上实现的,同步无非就是发送请求后就立即阻塞当前线程,直到接收到响应后再继续当前线程。不管是dubbo、okhttp,只要是远程调用的,都是异步调用,这句话百分百没有毛病。


来看下我的实现方案。描述ip国家信息的类IP2LocationBean


1、在业务层提供一个包装类AsyncWraper。包装类实现的功能跟代理差不多。

    public class AsyncWraper<T{
    public interface RefProxy<T> {
    T get();
        }
        
    private RefProxy<T> ref;


    public AsyncWraper(RefProxy<T> ref) {
    this.ref = ref;
    }


    public T getRef() {
    // 调用引用对象的get方法获取结果,
    // 对于异步调用,如果请求未得到响应,此时才会阻塞等待结果
    return ref.get();
        }
    }


    2、在接收请求时,异步调用获取ip位置信息

      OkHttpUtils.Future<IP2LocationUtils.IP2LocationBean> future 
      = IP2LocationUtils.asyncLocationBy(platform, request.getIp());
      bean.setLocationBeanAsyncWraper(new AsyncWraper<>(() -> {
      try {
      return future.get();
           } catch (Exception e) {
      return null;
      }
      }));


      使用包装类的好处是,我们无需关心,业务什么时候是第一次需要获取位置信息,我们在接收请求的时候就将请求的参数解析为一个Bean,所以在其它地方需要获取到请求参数的,都必须是调用该bean的getXxx方法,那么就可以将描述ip位置信息的IP2LocationBean类型的字段使用包装类包装,通过setLocationBeanAsyncWraper方法赋值给bean。


      3、在需要获取ip位置信息的地方调用

        bean.getLocationBeanAsyncWraper.getRef().getCountryCode();

        当调用getRef时,实际上是调用future的get方法获取结果。这就是提供包装类的目的。


        4、实现异步请求

           /**
          * 异步获取结果,在业务之前发送请求,
          * 在业务需要获取请求结果时再调用get
          */
          public static class Future<T> {


          private Class<T> tClass;
          private volatile T response;
          private volatile Exception exception;


          public Future(Class<T> tClass) {
          this.tClass = tClass;
          }


          void setResponse(OkHttpUtils.Response response) {
          try {
          if (response.code == 200) {
          try {
          this.response = JSON.parseObject(response.getBody(), tClass);
          } catch (Exception e) {
          this.exception = e;
          }
          } else {
          exception = new Exception("response code:" + response.code + ", msg:" + response.getBody());
          }
          } finally {
          this.notifyAll();
          }
          }


          void setException(Exception exception) {
          try {
          this.exception = exception;
          } finally {
          this.notifyAll();
          }
          }


          public T get() throws Exception {
          while (response == null || exception == null) {
          this.wait();
          }
          if (exception != null) {
          throw exception;
          }
          return this.response;
          }


          }


          /**
          * 异步get
          *
          * @param url
          * @return Future:在需要获取结果时,调用getResponse获取返回结果,
          * 如果request过程中出错则会在该方法抛出异常
          */
          public static <T> Future<T> asyncSendRequest(String url, Class<T> tClass) {
          Request.Builder builder = new Request.Builder()
          .url(url)
          .get();
          Future<T> future = new Future<>(tClass);
          getHttpClient().newCall(builder.build()).enqueue(new Callback() {
          @Override
          public void onFailure(Call call, IOException e) {
          future.setException(e);
          }


          @Override
          public void onResponse(Call call, okhttp3.Response okHttpResponse) throws IOException {
          OkHttpUtils.Response response = new OkHttpUtils.Response();
          response.setCode(okHttpResponse.code());
          if (okHttpResponse.isSuccessful()) {
          response.setBody(okHttpResponse.body().string());
          }
          future.setResponse(response);
          }
          });
          return future;
          }



          在将请求放入OkHttp的异步队列之后,就返回一个Future。通过向OkHttp注册回调函数可以拿到请求的响应结果,然后将再结果写入到Future中,外部通过调用Future的get方法获取请求的响应结果。


          如果请求未接收到响应结果,异步回调接口就不会被调用,那么Future的get就会堵塞调用者线程,直接接收到请求结果,由回调方法调用FuturesetResponse方法将阻塞的业务线程唤醒。


          假设ip信息查询接口的平均耗时是2ms,而业务服务的处理一次请求耗时是10ms,由于业务的耗时远远大于ip查询的耗时,故可以达到将远程http调用的ip信息查询耗时降为0ms。


          Future使用volatile实现可见性,而wait只会阻塞调用者线程,由于一次请求是在一个线程内完成的,每个请求都是持有独立的一个Future,所以,完美避开加锁操作。



          公众号ID:javaskill
          扫码关注最新动态


          文章转载自Java艺术,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

          评论