欢送拜访我的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 Map | Hash Map |
---|---|---|
键数量 | 2^31=2.14Billions | 64位零碎:2^63 32位零碎:2^31 |
应用内存下限 | 64位零碎:4G 32位零碎:2G | 受限于操作系统 |
单个键的大小 | 16M | 受限于操作系统 |
单个值的大小 | 64位零碎:4G 32位零碎:2G | 受限于操作系统 |
entry对象本身所用内存 | 24 byte | 64位零碎: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...
欢送关注思否:程序员欣宸
学习路上,你不孤独,欣宸原创一路相伴...