欢送拜访我的GitHub

这里分类和汇总了欣宸的全副原创(含配套源码):https://github.com/zq2599/blog_demos

本篇概览

  • 作为《Java扩大Nginx》系列的第七篇,咱们来理解一个实用工具<font color="blue">共享内存</font>,正式开始之前先来看一个问题
  • 在一台电脑上,nginx开启了多个worker,如下图,如果此时咱们用了nginx-clojure,就相当于有了四个jvm过程,彼此互相独立,对于同一个url的屡次申请,可能被那四个jvm中的任何一个解决:
  • 当初有个需要:统计某个url被拜访的总次数,该怎么做呢?在java内存中用全局变量必定不行,因为有四个jvm过程都在响应申请,你存到哪个下面都不行
  • 聪慧的您应该想到了redis,的确,用redis能够解决此类问题,但如果不波及多个服务器,而只是单机的nginx,还能够思考nginx-clojure提供的另一个简略计划:共享内存,如下图,一台电脑上,不同过程操作同一块内存区域,拜访总数放入这个内存区域即可:
  • 相比redis,共享内存的益处也是不言而喻的:
  • redis是额定部署的服务,共享内存不须要额定部署服务
  • redis申请走网络,共享内存不必走网络
  • 所以,单机版nginx如果遇到多个worker的数据同步问题,能够思考共享内存计划,这也是咱们明天实战的次要内容:在应用nginx-clojure进行java开发时,用共享内存在多个worker之间同步数据
  • 本文由以下内容组成:
  • 先在java内存中保留计数,放在多worker环境中运行,验证计数不准的问题的确存在
  • 用nginx-clojure提供的Shared Map解决问题

    用堆内存保留计数

  • 写一个content handler,代码如下,用UUID来表明worker身份,用<font color="blue">requestCount</font>记录申请总数,每解决一次申请就加一:

    package com.bolingcavalry.sharedmap;import nginx.clojure.java.ArrayMap;import nginx.clojure.java.NginxJavaRingHandler;import java.io.IOException;import java.util.Map;import java.util.UUID;import static nginx.clojure.MiniConstants.CONTENT_TYPE;import static nginx.clojure.MiniConstants.NGX_HTTP_OK;public class HeapSaveCounter implements NginxJavaRingHandler {  /**   * 通过UUID来表明以后jvm过程的身份   */  private String tag = UUID.randomUUID().toString();  private int requestCount = 1;  @Override  public Object[] invoke(Map<String, Object> map) throws IOException {      String body = "From "                  + tag                  + ", total request count [ "                  + requestCount++                  + "]";      return new Object[] {              NGX_HTTP_OK, //http status 200              ArrayMap.create(CONTENT_TYPE, "text/plain"), //headers map              body      };  }}
  • 批改nginx.conf的<font color="blue">worker_processes</font>配置,改为auto,则依据电脑CPU核数主动设置worker数量:

    worker_processes  auto;
  • nginx减少一个location配置,服务类是方才写的HeapSaveCounter:

    location /heapbasedcounter {  content_handler_type 'java';  content_handler_name 'com.bolingcavalry.sharedmap.HeapSaveCounter';}
  • 编译构建部署,再启动nginx,先看jvm过程有几个,如下可见,除了jps本身之外有8个jvm过程,等于电脑的CPU核数,和设置的worker_processes是合乎的:

    (base) willdeMBP:~ will$ jps494449454946494749484949
  • Jps
    4943

  • 先用Safari浏览器拜访<font color="blue">/heapbasedcounter</font>,第一次收到的响应如下图,总数是1:
  • 刷新页面,UUID不变,总数变成2,这意味着两次申请到了同一个worker的JVM上:
  • 改用Chrome浏览器,拜访同样的地址,如下图,这次UUID变了,证实申请是另一个worker的jvm解决的,总数变成了1:
  • 至此,问题失去证实:多个worker的时候,用jvm的类的成员变量保留的计数只是各worker的状况,不是整个nginx的总数
  • 接下来看如何用共享内存解决此类问题

    对于共享内存

  • nginx-clojure提供的共享内存有两种:Tiny Map和Hash Map,它们都是key&value类型的存储,键和值均能够是这四种类型:int,long,String, byte array
  • Tiny Map和Hash Map的区别,用下表来比照展现,可见次要是量化的限度以及应用内存的多少:
个性Tiny MapHash Map
键数量2^31=2.14Billions64位零碎:2^63
32位零碎:2^31
应用内存下限64位零碎:4G
32位零碎:2G
受限于操作系统
单个键的大小16M受限于操作系统
单个值的大小64位零碎:4G
32位零碎:2G
受限于操作系统
entry对象本身所用内存24 byte64位零碎:40 byte
32位零碎:28 byte
  • 您能够基于上述区别来选自应用Tiny Map和Hash Map,就本文的实战而言,应用Tiny Map就够用了
  • 接下来进入实战

应用共享内存

  • 应用共享内存一共分为两步,如下图,先配置再应用:
  • 当初nginx.conf中减少一个http配置项<font color="blue">shared_map</font>,指定了共享内存的名称是<font color="red">uri_access_counters</font>:

    # 减少一个共享内存的初始化调配,类型tiny,空间1M,键数量8Kshared_map uri_access_counters  tinymap?space=1m&entries=8096;
  • 而后写一个新的content handler,该handler在收到申请时,会在共享内存中更新申请次数,总的代码如下,有几处要重点留神的中央,稍后会提到:

    package com.bolingcavalry.sharedmap;import nginx.clojure.java.ArrayMap;import nginx.clojure.java.NginxJavaRingHandler;import nginx.clojure.util.NginxSharedHashMap;import java.io.IOException;import java.util.Map;import java.util.UUID;import static nginx.clojure.MiniConstants.CONTENT_TYPE;import static nginx.clojure.MiniConstants.NGX_HTTP_OK;public class SharedMapSaveCounter implements NginxJavaRingHandler {  /**   * 通过UUID来表明以后jvm过程的身份   */  private String tag = UUID.randomUUID().toString();  private NginxSharedHashMap smap = NginxSharedHashMap.build("uri_access_counters");  @Override  public Object[] invoke(Map<String, Object> map) throws IOException {      String uri = (String)map.get("uri");      // 尝试在共享内存中新建key,并将其值初始化为1,      // 如果初始化胜利,返回值就是0,      // 如果返回值不是0,示意共享内存中该key曾经存在      int rlt = smap.putIntIfAbsent(uri, 1);      // 如果rlt不等于0,示意这个key在调用putIntIfAbsent之前曾经在共享内存中存在了,      // 此时要做的就是加一,      // 如果relt等于0,就把rlt改成1,示意拜访总数曾经等于1了      if (0==rlt) {          rlt++;      } else {          // 原子性加一,这样并发的时候也会程序执行          rlt = smap.atomicAddInt(uri, 1);          rlt++;      }      // 返回的body内容,要体现出JVM的身份,以及share map中的计数      String body = "From "              + tag              + ", total request count [ "              + rlt              + "]";      return new Object[] {              NGX_HTTP_OK, //http status 200              ArrayMap.create(CONTENT_TYPE, "text/plain"), //headers map              body      };  }}
  • 上述代码曾经增加了具体正文,置信您一眼就看懂了,我这里挑几个重点阐明一下:
  • 写上述代码时要牢一件事:这段代码可能运行在高并发场景,既同一时刻,不同过程不同线程都在执行这段代码
  • NginxSharedHashMap类是ConcurrentMap的子类,所以是线程平安的,咱们更多思考应该留神跨过程读写时的同步问题,例如接下来要提到的第三和第四点,都是多个过程同时执行此段代码时要思考的同步问题
  • <font color="blue">putIntIfAbsent</font>和redis的setnx相似,能够当做跨过程的分布式锁来应用,只有指定的key不存在的时候才会设置胜利,此时返回0,如果返回值不等于0,示意共享内存中曾经存在此key了
  • <font color="blue">atomicAddInt</font>确保了原子性,多过程并发的时候,用此办法累加能够确保计算精确(如果咱们本人写代码,先读取,再累加,再写入,就会遇到并发的笼罩问题)
  • 对于那个atomicAddInt办法,咱们回顾一下java的AtomicInteger类,其incrementAndGet办法在多线程同时调用的场景,也能计算精确,那是因为外面用了CAS来确保的,那么nginx-clojure这里呢?我很好奇的去探寻了一下该办法的实现,这是一段C代码,最初没看到CAS无关的循环,只看到一段最简略的累加,如下图:
  • 很显著,上图的代码,在多过程同时执行时,是会呈现数据笼罩的问题的,如此只有两种可能性了,第一种:即使是多个worker存在,执行底层共享内存操作的过程也只有一个
  • 第二种:欣宸的C语言程度不行,基本没看懂JVM调用C的逻辑,自我感觉这种可能性很大:如果C语言程度能够,欣宸就用C去做nginx扩大了,没必要来钻研nginx-clojure呀!(如果您看懂了此段代码的调用逻辑,还望您指导欣宸一二,谢谢啦)
  • 编码实现,在nginx.conf上配置一个location,用SharedMapSaveCounter作为content handler:

    location /sharedmapbasedcounter {  content_handler_type 'java';   content_handler_name 'com.bolingcavalry.sharedmap.SharedMapSaveCounter';}
  • 编译构建部署,重启nginx
  • 先用Safari浏览器拜访<font color="blue">/sharedmapbasedcounter</font>,第一次收到的响应如下图,总数是1:
  • 刷新页面,UUID发生变化,证实这次申请到了另一个worker,总数也变成2,这意味着共享内存失效了,不同过程应用同一个变量来计算数据:
  • 改用Chrome浏览器,拜访同样的地址,如下图,UUID再次变动,证实申请是第三个worker的jvm解决的,然而拜访次数始终正确:
  • 实战实现,后面的代码中只用了两个API操作共享内存,学到的知识点无限,接下来做一些适当的延长学习

    一点延长

  • 方才曾提到NginxSharedHashMap是ConcurrentMap的子类,那些罕用的put和get办法,在ConcurrentMap中是在操作以后过程的堆内存,如果NginxSharedHashMap间接应用父类的这些办法,岂不是与共享内存无关了?
  • 带着这个疑难,去看NginxSharedHashMap的源码,如下图,水落石出:get、put这些罕用办法,都被重写了,红框中的nget和nputNumber都是native办法,都是在操作共享内存:
  • 至此,nginx-clojure的共享内存学习实现,高并发场景下跨进程同步数据又多了个轻量级计划,至于用它还是用redis,置信聪慧的您心中已有定论

    源码下载

  • 《Java扩大Nginx》的残缺源码可在GitHub下载到,地址和链接信息如下表所示(https://github.com/zq2599/blog_demos):
名称链接备注
我的项目主页https://github.com/zq2599/blog_demos该我的项目在GitHub上的主页
git仓库地址(https)https://github.com/zq2599/blog_demos.git该我的项目源码的仓库地址,https协定
git仓库地址(ssh)git@github.com:zq2599/blog_demos.git该我的项目源码的仓库地址,ssh协定
  • 这个git我的项目中有多个文件夹,本篇的源码在<font color="blue">nginx-clojure-tutorials</font>文件夹下的<font color="red">shared-map-demo</font>子工程中,如下图红框所示:
  • 本篇波及到nginx.conf的批改,残缺的参考在此:https://raw.githubusercontent.com/zq2599/blog_demos/master/ng...

    欢送关注思否:程序员欣宸

学习路上,你不孤独,欣宸原创一路相伴...