Java擴展Nginx之七:共用記憶體

来源:https://www.cnblogs.com/bolingcavalry/archive/2023/07/17/17556048.html
-Advertisement-
Play Games

### 歡迎訪問我的GitHub > 這裡分類和彙總了欣宸的全部原創(含配套源碼):[https://github.com/zq2599/blog_demos](https://github.com/zq2599/blog_demos) ### 本篇概覽 - 作為《Java擴展Nginx》系列的第七 ...


歡迎訪問我的GitHub

這裡分類和彙總了欣宸的全部原創(含配套源碼):https://github.com/zq2599/blog_demos

本篇概覽

  • 作為《Java擴展Nginx》系列的第七篇,咱們來瞭解一個實用工具共用記憶體,正式開始之前先來看一個問題
  • 在一臺電腦上,nginx開啟了多個worker,如下圖,如果此時我們用了nginx-clojure,就相當於有了四個jvm進程,彼此相互獨立,對於同一個url的多次請求,可能被那四個jvm中的任何一個處理:
    在這裡插入圖片描述
  • 現在有個需求:統計某個url被訪問的總次數,該怎麼做呢?在java記憶體中用全局變數肯定不行,因為有四個jvm進程都在響應請求,你存到哪個上面都不行
  • 聰明的您應該想到了redis,確實,用redis可以解決此類問題,但如果不涉及多個伺服器,而只是單機的nginx,還可以考慮nginx-clojure提供的另一個簡單方案:共用記憶體,如下圖,一臺電腦上,不同進程操作同一塊記憶體區域,訪問總數放入這個記憶體區域即可:
    在這裡插入圖片描述
  • 相比redis,共用記憶體的好處也是顯而易見的:
  1. redis是額外部署的服務,共用記憶體不需要額外部署服務
  2. redis請求走網路,共用記憶體不用走網路
  • 所以,單機版nginx如果遇到多個worker的數據同步問題,可以考慮共用記憶體方案,這也是咱們今天實戰的主要內容:在使用nginx-clojure進行java開發時,用共用記憶體在多個worker之間同步數據

  • 本文由以下內容組成:

  1. 先在java記憶體中保存計數,放在多worker環境中運行,驗證計數不准的問題確實存在
  2. 用nginx-clojure提供的Shared Map解決問題

用堆記憶體保存計數

  • 寫一個content handler,代碼如下,用UUID來表明worker身份,用requestCount記錄請求總數,每處理一次請求就加一:
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的worker_processes配置,改為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
4950
4968 Jps
4943
  • 先用Safari瀏覽器訪問/heapbasedcounter,第一次收到的響應如下圖,總數是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配置項shared_map,指定了共用記憶體的名稱是uri_access_counters
# 增加一個共用記憶體的初始化分配,類型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
        };
    }
}
  • 上述代碼已經添加了詳細註釋,相信您一眼就看懂了,我這裡挑幾個重點說明一下:
  1. 寫上述代碼時要牢一件事:這段代碼可能運行在高併發場景,既同一時刻,不同進程不同線程都在執行這段代碼
  2. NginxSharedHashMap類是ConcurrentMap的子類,所以是線程安全的,我們更多考慮應該註意跨進程讀寫時的同步問題,例如接下來要提到的第三和第四點,都是多個進程同時執行此段代碼時要考慮的同步問題
  3. putIntIfAbsent和redis的setnx類似,可以當做跨進程的分散式鎖來使用,只有指定的key不存在的時候才會設置成功,此時返回0,如果返回值不等於0,表示共用記憶體中已經存在此key了
  4. atomicAddInt確保了原子性,多進程併發的時候,用此方法累加可以確保計算準確(如果我們自己寫代碼,先讀取,再累加,再寫入,就會遇到併發的覆蓋問題)
  5. 關於那個atomicAddInt方法,咱們回憶一下java的AtomicInteger類,其incrementAndGet方法在多線程同時調用的場景,也能計算準確,那是因為裡面用了CAS來確保的,那麼nginx-clojure這裡呢?我很好奇的去探尋了一下該方法的實現,這是一段C代碼,最後沒看到CAS有關的迴圈,只看到一段最簡單的累加,如下圖:
    在這裡插入圖片描述
  6. 很明顯,上圖的代碼,在多進程同時執行時,是會出現數據覆蓋的問題的,如此只有兩種可能性了,第一種:即便是多個worker存在,執行底層共用記憶體操作的進程也只有一個
  7. 第二種:欣宸的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瀏覽器訪問/sharedmapbasedcounter,第一次收到的響應如下圖,總數是1:
    在這裡插入圖片描述
  • 刷新頁面,UUID發生變化,證明這次請求到了另一個worker,總數也變成2,這意味著共用記憶體生效了,不同進程使用同一個變數來計算數據:
    在這裡插入圖片描述
  • 改用Chrome瀏覽器,訪問同樣的地址,如下圖,UUID再次變化,證明請求是第三個worker的jvm處理的,但是訪問次數始終正確:
    在這裡插入圖片描述
  • 實戰完成,前面的代碼中只用了兩個API操作共用記憶體,學到的知識點有限,接下來做一些適當的延伸學習

一點延伸

  • 剛纔曾提到NginxSharedHashMap是ConcurrentMap的子類,那些常用的put和get方法,在ConcurrentMap中是在操作當前進程的堆記憶體,如果NginxSharedHashMap直接使用父類的這些方法,豈不是與共用記憶體無關了?
  • 帶著這個疑問,去看NginxSharedHashMap的源碼,如下圖,真相大白:get、put這些常用方法,都被重寫了,紅框中的nget和nputNumber都是native方法,都是在操作共用記憶體:
    在這裡插入圖片描述
  • 至此,nginx-clojure的共用記憶體學習完成,高併發場景下跨進程同步數據又多了個輕量級方案,至於用它還是用redis,相信聰明的您心中已有定論

源碼下載

名稱 鏈接 備註
項目主頁 https://github.com/zq2599/blog_demos 該項目在GitHub上的主頁
git倉庫地址(https) https://github.com/zq2599/blog_demos.git 該項目源碼的倉庫地址,https協議
git倉庫地址(ssh) [email protected]:zq2599/blog_demos.git 該項目源碼的倉庫地址,ssh協議

歡迎關註博客園:程式員欣宸

學習路上,你不孤單,欣宸原創一路相伴...


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • # shell腳本-MySQL資料庫備份 ## 準備: 確保mysql服務啟動 **可以通過mysqldump命令來備份資料庫** 1.mysqldump 命令語法: 使用 mysqldump 命令備份一個資料庫的語法格式如下: ``` mysqldump -u username -p dbpass ...
  • # 一. 索引概述 ## 1. 介紹 **索引是幫助MySQL高效獲取數據的數據結構(有序)。在數據之外,資料庫系統還維護著滿足特定查找演算法的數據結構,這些數據結構以某種方式引用(指向)數據,這樣就可以在這些數據結構上實現高級查找演算法,這種數據結構就是索引。** ![](https://tcs-de ...
  • 原文地址:https://blog.csdn.net/zhanglei5415/article/details/131434931 ## 一、問題 當對含有中文的url字元串,進行NSURL對象包裝時,是不能被識別的。 不會得到期望的NSURL對象,而是返回一個nil 值 ; ```objectiv ...
  • 博客推行版本更新,成果積累制度,已經寫過的博客還會再次更新,不斷地琢磨,高質量高數量都是要追求的,工匠精神是學習必不可少的精神。因此,大家有何建議歡迎在評論區踴躍發言,你們的支持是我最大的動力,你們敢投,我就敢肝 ...
  • 今天接到粉絲私信,詢問是否可以通過Canvas繪製一些圖形,然後根據粉絲提供的模板圖,通過Canvas進行模擬繪製,通過分析發現,圖形雖然相對簡單,但是如果不藉助相應的軟體,純代碼繪製還是稍微費些時間。今天將繪製圖形源碼分享出來,僅供學習分享之用,如有不足之處,還請指正。 ...
  • 博客推行版本更新,成果積累制度,已經寫過的博客還會再次更新,不斷地琢磨,高質量高數量都是要追求的,工匠精神是學習必不可少的精神。因此,大家有何建議歡迎在評論區踴躍發言,你們的支持是我最大的動力,你們敢投,我就敢肝 ...
  • ![](https://img2023.cnblogs.com/blog/3076680/202307/3076680-20230713141300146-1450511408.png) # 1. 水平擴展 ## 1.1. 有助於提高系統的整體容量和韌性 ## 1.2. 現階段構建的幾乎所有系統,都 ...
  • 電腦編程發展至今,一共只有三個編程範式: - 結構化編程 - 面向對象編程 - 函數式編程 ### 編程範式和軟體架構的關係 - 結構化編程是各個模塊的演算法實現基礎 - 多態(面向對象編程)是跨越架構邊界的手段 - 函數式編程是規範和限制數據存放位置與訪問許可權的手段 **軟體架構的三大關註重點** ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...