Java擴(kuò)展Nginx之共享內(nèi)存
本篇概覽
作為《Java擴(kuò)展Nginx》系列的第七篇,咱們來(lái)了解一個(gè)實(shí)用工具共享內(nèi)存,正式開(kāi)始之前先來(lái)看一個(gè)問(wèn)題 在一臺(tái)電腦上,nginx開(kāi)啟了多個(gè)worker,如下圖,如果此時(shí)我們用了nginx-clojure,就相當(dāng)于有了四個(gè)jvm進(jìn)程,彼此相互獨(dú)立,對(duì)于同一個(gè)url的多次請(qǐng)求,可能被那四個(gè)jvm中的任何一個(gè)處理:
現(xiàn)在有個(gè)需求:統(tǒng)計(jì)某個(gè)url被訪問(wèn)的總次數(shù),該怎么做呢?在java內(nèi)存中用全局變量肯定不行,因?yàn)橛兴膫€(gè)jvm進(jìn)程都在響應(yīng)請(qǐng)求,你存到哪個(gè)上面都不行 聰明的您應(yīng)該想到了redis,確實(shí),用redis可以解決此類(lèi)問(wèn)題,但如果不涉及多個(gè)服務(wù)器,而只是單機(jī)的nginx,還可以考慮nginx-clojure提供的另一個(gè)簡(jiǎn)單方案:共享內(nèi)存,如下圖,一臺(tái)電腦上,不同進(jìn)程操作同一塊內(nèi)存區(qū)域,訪問(wèn)總數(shù)放入這個(gè)內(nèi)存區(qū)域即可:
相比redis,共享內(nèi)存的好處也是顯而易見(jiàn)的: redis是額外部署的服務(wù),共享內(nèi)存不需要額外部署服務(wù) redis請(qǐng)求走網(wǎng)絡(luò),共享內(nèi)存不用走網(wǎng)絡(luò)
所以,單機(jī)版nginx如果遇到多個(gè)worker的數(shù)據(jù)同步問(wèn)題,可以考慮共享內(nèi)存方案,這也是咱們今天實(shí)戰(zhàn)的主要內(nèi)容:在使用nginx-clojure進(jìn)行java開(kāi)發(fā)時(shí),用共享內(nèi)存在多個(gè)worker之間同步數(shù)據(jù)
本文由以下內(nèi)容組成:
先在java內(nèi)存中保存計(jì)數(shù),放在多worker環(huán)境中運(yùn)行,驗(yàn)證計(jì)數(shù)不準(zhǔn)的問(wèn)題確實(shí)存在 用nginx-clojure提供的Shared Map解決問(wèn)題
用堆內(nèi)存保存計(jì)數(shù)
寫(xiě)一個(gè)content handler,代碼如下,用UUID來(lái)表明worker身份,用requestCount記錄請(qǐng)求總數(shù),每處理一次請(qǐng)求就加一:
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 { /** * 通過(guò)UUID來(lái)表明當(dāng)前jvm進(jìn)程的身份 */ 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的worker_processes配置,改為auto,則根據(jù)電腦CPU核數(shù)自動(dòng)設(shè)置worker數(shù)量:
worker_processes auto;
nginx增加一個(gè)location配置,服務(wù)類(lèi)是剛才寫(xiě)的HeapSaveCounter:
location /heapbasedcounter { content_handler_type 'java'; content_handler_name 'com.bolingcavalry.sharedmap.HeapSaveCounter'; }
編譯構(gòu)建部署,再啟動(dòng)nginx,先看jvm進(jìn)程有幾個(gè),如下可見(jiàn),除了jps自身之外有8個(gè)jvm進(jìn)程,等于電腦的CPU核數(shù),和設(shè)置的worker_processes是符合的:
(base) willdeMBP:~ will$ jps 4944 4945 4946 4947 4948 4949 4950 4968 Jps 4943
先用Safari瀏覽器訪問(wèn)/heapbasedcounter,第一次收到的響應(yīng)如下圖,總數(shù)是1:
刷新頁(yè)面,UUID不變,總數(shù)變成2,這意味著兩次請(qǐng)求到了同一個(gè)worker的JVM上:
改用Chrome瀏覽器,訪問(wèn)同樣的地址,如下圖,這次UUID變了,證明請(qǐng)求是另一個(gè)worker的jvm處理的,總數(shù)變成了1:
至此,問(wèn)題得到證明:多個(gè)worker的時(shí)候,用jvm的類(lèi)的成員變量保存的計(jì)數(shù)只是各worker的情況,不是整個(gè)nginx的總數(shù)
接下來(lái)看如何用共享內(nèi)存解決此類(lèi)問(wèn)題
關(guān)于共享內(nèi)存
nginx-clojure提供的共享內(nèi)存有兩種:Tiny Map和Hash Map,它們都是key&value類(lèi)型的存儲(chǔ),鍵和值均可以是這四種類(lèi)型:int,long,String, byte array Tiny Map和Hash Map的區(qū)別,用下表來(lái)對(duì)比展示,可見(jiàn)主要是量化的限制以及使用內(nèi)存的多少:
特性 | Tiny Map | Hash Map |
---|---|---|
鍵數(shù)量 | 2^31=2.14Billions | 64位系統(tǒng):2^63 32位系統(tǒng):2^31 |
使用內(nèi)存上限 | 64位系統(tǒng):4G 32位系統(tǒng):2G | 受限于操作系統(tǒng) |
單個(gè)鍵的大小 | 16M | 受限于操作系統(tǒng) |
單個(gè)值的大小 | 64位系統(tǒng):4G 32位系統(tǒng):2G | 受限于操作系統(tǒng) |
entry對(duì)象自身所用內(nèi)存 | 24 byte | 64位系統(tǒng):40 byte 32位系統(tǒng):28 byte |
您可以基于上述區(qū)別來(lái)選自使用Tiny Map和Hash Map,就本文的實(shí)戰(zhàn)而言,使用Tiny Map就夠用了 接下來(lái)進(jìn)入實(shí)戰(zhàn)
使用共享內(nèi)存
使用共享內(nèi)存一共分為兩步,如下圖,先配置再使用:
現(xiàn)在nginx.conf中增加一個(gè)http配置項(xiàng)shared_map,指定了共享內(nèi)存的名稱(chēng)是uri_access_counters:
# 增加一個(gè)共享內(nèi)存的初始化分配,類(lèi)型tiny,空間1M,鍵數(shù)量8K shared_map uri_access_counters tinymap?space=1m&entries=8096;
然后寫(xiě)一個(gè)新的content handler,該handler在收到請(qǐng)求時(shí),會(huì)在共享內(nèi)存中更新請(qǐng)求次數(shù),總的代碼如下,有幾處要重點(diǎn)注意的地方,稍后會(huì)提到:
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 { /** * 通過(guò)UUID來(lái)表明當(dāng)前jvm進(jìn)程的身份 */ 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"); // 嘗試在共享內(nèi)存中新建key,并將其值初始化為1, // 如果初始化成功,返回值就是0, // 如果返回值不是0,表示共享內(nèi)存中該key已經(jīng)存在 int rlt = smap.putIntIfAbsent(uri, 1); // 如果rlt不等于0,表示這個(gè)key在調(diào)用putIntIfAbsent之前已經(jīng)在共享內(nèi)存中存在了, // 此時(shí)要做的就是加一, // 如果relt等于0,就把rlt改成1,表示訪問(wèn)總數(shù)已經(jīng)等于1了 if (0==rlt) { rlt++; } else { // 原子性加一,這樣并發(fā)的時(shí)候也會(huì)順序執(zhí)行 rlt = smap.atomicAddInt(uri, 1); rlt++; } // 返回的body內(nèi)容,要體現(xiàn)出JVM的身份,以及share map中的計(jì)數(shù) 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 }; } }
上述代碼已經(jīng)添加了詳細(xì)注釋?zhuān)嘈拍谎劬涂炊?,我這里挑幾個(gè)重點(diǎn)說(shuō)明一下:
寫(xiě)上述代碼時(shí)要牢一件事:這段代碼可能運(yùn)行在高并發(fā)場(chǎng)景,既同一時(shí)刻,不同進(jìn)程不同線程都在執(zhí)行這段代碼
NginxSharedHashMap類(lèi)是ConcurrentMap的子類(lèi),所以是線程安全的,我們更多考慮應(yīng)該注意跨進(jìn)程讀寫(xiě)時(shí)的同步問(wèn)題,例如接下來(lái)要提到的第三和第四點(diǎn),都是多個(gè)進(jìn)程同時(shí)執(zhí)行此段代碼時(shí)要考慮的同步問(wèn)題
putIntIfAbsent和redis的setnx類(lèi)似,可以當(dāng)做跨進(jìn)程的分布式鎖來(lái)使用,只有指定的key不存在的時(shí)候才會(huì)設(shè)置成功,此時(shí)返回0,如果返回值不等于0,表示共享內(nèi)存中已經(jīng)存在此key了
atomicAddInt確保了原子性,多進(jìn)程并發(fā)的時(shí)候,用此方法累加可以確保計(jì)算準(zhǔn)確(如果我們自己寫(xiě)代碼,先讀取,再累加,再寫(xiě)入,就會(huì)遇到并發(fā)的覆蓋問(wèn)題)
關(guān)于那個(gè)atomicAddInt方法,咱們回憶一下java的AtomicInteger類(lèi),其incrementAndGet方法在多線程同時(shí)調(diào)用的場(chǎng)景,也能計(jì)算準(zhǔn)確,那是因?yàn)槔锩嬗昧薈AS來(lái)確保的,那么nginx-clojure這里呢?我很好奇的去探尋了一下該方法的實(shí)現(xiàn),這是一段C代碼,最后沒(méi)看到CAS有關(guān)的循環(huán),只看到一段最簡(jiǎn)單的累加,如下圖:
很明顯,上圖的代碼,在多進(jìn)程同時(shí)執(zhí)行時(shí),是會(huì)出現(xiàn)數(shù)據(jù)覆蓋的問(wèn)題的,如此只有兩種可能性了,第一種:即便是多個(gè)worker存在,執(zhí)行底層共享內(nèi)存操作的進(jìn)程也只有一個(gè)
第二種:欣宸的C語(yǔ)言水平不行,根本沒(méi)看懂JVM調(diào)用C的邏輯,自我感覺(jué)這種可能性很大:如果C語(yǔ)言水平可以,欣宸就用C去做nginx擴(kuò)展了,沒(méi)必要來(lái)研究nginx-clojure呀!(如果您看懂了此段代碼的調(diào)用邏輯,還望您指點(diǎn)欣宸一二,謝謝啦)
編碼完成,在nginx.conf上配置一個(gè)location,用SharedMapSaveCounter作為content handler:
location /sharedmapbasedcounter { content_handler_type 'java'; content_handler_name 'com.bolingcavalry.sharedmap.SharedMapSaveCounter'; }
編譯構(gòu)建部署,重啟nginx
先用Safari瀏覽器訪問(wèn)/sharedmapbasedcounter,第一次收到的響應(yīng)如下圖,總數(shù)是1:
刷新頁(yè)面,UUID發(fā)生變化,證明這次請(qǐng)求到了另一個(gè)worker,總數(shù)也變成2,這意味著共享內(nèi)存生效了,不同進(jìn)程使用同一個(gè)變量來(lái)計(jì)算數(shù)據(jù):
改用Chrome瀏覽器,訪問(wèn)同樣的地址,如下圖,UUID再次變化,證明請(qǐng)求是第三個(gè)worker的jvm處理的,但是訪問(wèn)次數(shù)始終正確:
實(shí)戰(zhàn)完成,前面的代碼中只用了兩個(gè)API操作共享內(nèi)存,學(xué)到的知識(shí)點(diǎn)有限,接下來(lái)做一些適當(dāng)?shù)难由鞂W(xué)習(xí)
一點(diǎn)延伸
剛才曾提到NginxSharedHashMap是ConcurrentMap的子類(lèi),那些常用的put和get方法,在ConcurrentMap中是在操作當(dāng)前進(jìn)程的堆內(nèi)存,如果NginxSharedHashMap直接使用父類(lèi)的這些方法,豈不是與共享內(nèi)存無(wú)關(guān)了?
帶著這個(gè)疑問(wèn),去看NginxSharedHashMap的源碼,如下圖,真相大白:get、put這些常用方法,都被重寫(xiě)了,紅框中的nget和nputNumber都是native方法,都是在操作共享內(nèi)存:
至此,nginx-clojure的共享內(nèi)存學(xué)習(xí)完成,高并發(fā)場(chǎng)景下跨進(jìn)程同步數(shù)據(jù)又多了個(gè)輕量級(jí)方案,至于用它還是用redis,相信聰明的您心中已有定論 源碼下載 《Java擴(kuò)展Nginx》的完整源碼可在GitHub下載到,地址和鏈接信息如下表所示(https://github.com/zq2599/blog_demos):
名稱(chēng) | 鏈接 | 備注 |
---|---|---|
項(xiàng)目主頁(yè) | https://github.com/zq2599/blog_demos | 該項(xiàng)目在GitHub上的主頁(yè) |
git倉(cāng)庫(kù)地址(https) | https://github.com/zq2599/blog_demos.git | 該項(xiàng)目源碼的倉(cāng)庫(kù)地址,https協(xié)議 |
git倉(cāng)庫(kù)地址(ssh) | git@github.com:zq2599/blog_demos.git | 該項(xiàng)目源碼的倉(cāng)庫(kù)地址,ssh協(xié)議 |
這個(gè)git項(xiàng)目中有多個(gè)文件夾,本篇的源碼在nginx-clojure-tutorials文件夾下的shared-map-demo子工程中,如下圖紅框所示:
本篇涉及到nginx.conf的修改,完整的參考在此:https://raw.githubusercontent.com/zq2599/blog_demos/master/nginx-clojure-tutorials/files/nginx.conf
到此這篇關(guān)于Java擴(kuò)展Nginx之共享內(nèi)存的文章就介紹到這了,更多相關(guān)Java擴(kuò)展Nginx 共享內(nèi)存內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Java?Collection接口中的常用方法總結(jié)
這篇文章將大概用代碼案例簡(jiǎn)單總結(jié)一下?Collection?接口中的一些方法,我們會(huì)以他的實(shí)現(xiàn)類(lèi)?Arraylist?為例創(chuàng)建對(duì)象。快一起來(lái)看看吧2022-12-12Java數(shù)據(jù)結(jié)構(gòu)之常見(jiàn)排序算法(下)
這篇文章主要介紹了Java數(shù)據(jù)結(jié)構(gòu)之常見(jiàn)排序算法(下),與之相對(duì)有(上),想了解的朋友可以去本網(wǎng)站掃搜,在這兩篇文章里涵蓋關(guān)于八大排序算法的所有內(nèi)容,需要的朋友可以參考下2023-01-01Java實(shí)現(xiàn)級(jí)聯(lián)下拉結(jié)構(gòu)的示例代碼
在開(kāi)發(fā)過(guò)程中,會(huì)遇到很多的實(shí)體需要將查出的數(shù)據(jù)處理為下拉或者級(jí)聯(lián)下拉的結(jié)構(gòu),提供給前端進(jìn)行展示。本文為大家介紹了java封裝下拉和級(jí)聯(lián)下拉的通用工具類(lèi),需要的可以參考一下2022-06-06解決PageHelper的上下文問(wèn)題導(dǎo)致SQL查詢(xún)結(jié)果不正確
主要介紹了PageHelper在使用過(guò)程中出現(xiàn)的分頁(yè)上下文問(wèn)題,并分析了可能的原因和解決方案,主要解決方案包括每次分頁(yè)查詢(xún)后調(diào)用`PageHelper.clearPage()`清理分頁(yè)上下文,確保每次查詢(xún)前正確調(diào)用`startPage`,以及避免在條件判斷未執(zhí)行SQL時(shí)影響后續(xù)查詢(xún)2024-12-12利用5分鐘快速搭建一個(gè)springboot項(xiàng)目的全過(guò)程
Spring Boot的監(jiān)控能夠使開(kāi)發(fā)者更好地掌控應(yīng)用程序的運(yùn)行狀態(tài),下面這篇文章主要給大家介紹了關(guān)于如何利用5分鐘快速搭建一個(gè)springboot項(xiàng)目的相關(guān)資料,文中通過(guò)示例代碼介紹的非常詳細(xì),需要的朋友可以參考下2022-05-05