欢送拜访我的 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$ jps 4944 4945 4946 4947 4948 4949
-
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,键数量 8K shared_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…
欢送关注思否:程序员欣宸
学习路上,你不孤独,欣宸原创一路相伴 …