001.OpenShift介紹

一 OpenShift特性

1.1 OpenShift概述


Red Hat OpenShijft Container Platform (OpenShift)是一個容器應用程序平台,它為開發人員和IT組織提供了一個雲應用程序平台,用於在安全的、可伸縮的資源上部署新應用程序,而配置和管理開銷最小。

OpenShift構建於Red Hat Enterprise Linux、Docker和Kubernetes之上,為當今的企業級應用程序提供了一個安全且可伸縮的多租戶操作系統,同時還提供了集成的應用程序運行時和庫。

OpenShift帶來了健壯、靈活和可伸縮的特性。容器平台到客戶數據中心,使組織能夠實現滿足安全性、隱私性、遵從性和治理需求的平台。不願意管理自己的OpenShift集群的客戶可以使用Red Hat提供的公共雲平台OpenShift Online。

1.2 OpenShift特性


OpenShift容器平台和OpenShift Online都是基於OpenShift Origin開源軟件項目的,該項目本身使用了許多其他開源項目,如Docker和Kubernetes。

應用程序作為容器運行,容器是單個操作系統內的隔離分區。容器提供了許多與虛擬機相同的好處,比如安全性、存儲和網絡隔離,同時需要的硬件資源要少得多,啟動和終止也更快。OpenShift使用容器有助於提高平台本身及其承載的應用程序的效率、靈活性和可移植性。

OpenShift的主要特性如下:

  • 自助服務平台:OpenShift允許開發人員使用Source-to-Image(S21)從模板或自己的源代碼管理存儲庫創建應用程序。系統管理員可以為用戶和項目定義資源配額和限制,以控制系統資源的使用。
  • 多語言支持:OpenShift支持Java、Node.js、PHP、Perl以及直接來自Red Hat的Ruby。同時也包括來自合作夥伴和更大的Docker社區的許多其他代碼。MySQL、PostgreSQL和MongoDB數據庫。Red Hat還支持在OpenShift上本地運行的中間件產品,如Apache httpd、Apache Tomcat、JBoss EAP、ActiveMQ和Fuse。
  • 自動化:OpenShift提供應用程序生命周期管理功能,當上游源或容器映像發生更改時,可以自動重新構建和重新部署容器。根據調度和策略擴展或故障轉移應用程序。
  • 用戶界面:OpenShift提供用於部署和監視應用程序的web UI,以及用於遠程管理應用程序和資源的CLi。它支持Eclipse IDE和JBoss Developer Studio插件,以便開發人員可以繼續使用熟悉的工具,並支持REST APl與第三方或內部工具集成。
  • 協作:OpenShift允許在組織內或與更大的社區共享項目。
  • 可伸縮性和高可用性:OpenShift提供了容器多租戶和一個分佈式應用程序平台,其中包括彈性,以處理隨需增加的流量。它提供了高可用性,以便應用程序能夠在物理機器宕機等事件中存活下來。OpenShift提供了對容器健康狀況的自動發現和自動重新部署。
  • 容器可移植性:在OpenShift中,應用程序和服務使用標準容器映像進行打包,組合應用程序使用Kubernetes進行管理。這些映像可以部署到基於這些基礎技術的其他平台上。
  • 開源:沒有廠商鎖定。
  • 安全性:OpenShift使用SELinux提供多層安全性、基於角色的訪問控制以及與外部身份驗證系統(如LDAP和OAuth)集成的能力。
  • 動態存儲管理:OpenShift使用Kubernetes持久卷和持久卷聲明的方式為容器數據提供靜態和動態存儲管理
  • 基於雲(或不基於雲):可以在裸機服務器、活來自多個供應商的hypervisor和大多數IaaS雲提供商上部署OpenShift容器平台。
  • 企業級:Red Hat支持OpenShift、選定的容器映像和應用程序運行時。可信的第三方容器映像、運行時和應用程序由Red Hat認證。可以在OpenShift提供的高可用性的強化安全環境中運行內部或第三方應用程序。
  • 日誌聚合和metrics:可以在中心節點收集、聚合和分析部署在OpenShift上的應用程序的日誌信息。OpenShift能夠實時收集關於應用程序的度量和運行時信息,並幫助不斷優化性能。
  • 其他特性:OpenShift支持微服務體繫結構,OpenShift的本地特性足以支持DevOps流程,很容易與標準和定製的持續集成/持續部署工具集成。

二 OpenShift架構

2.1 OpenShift架構概述


OpenShift容器平台是一組構建在Red Hat Enterprise Linux、Docker和Kubernetes之上的模塊化組件和服務。OpenShift增加了遠程管理、多租戶、增強的安全性、應用程序生命周期管理和面向開發人員的自服務接口。

OpenShift的架構:



  • RHEL:基本操作系統是Red Hat Enterprise Linux;
  • Docker:提供基本的容器管理API和容器image文件格式;
  • Kubernetes:管理運行容器的主機集群(物理或虛擬主機)。它處理描述由多個資源組成的多容器應用程序的資源,以及它們如何互連;
  • Etcd:一個分佈式鍵值存儲,Kubernetes使用它來存儲OpenShift集群中容器和其他資源的配置和狀態信息。


OpenShift在Docker + Kubernetes基礎設施之上添加了提供容器應用程序平台所需的更富豐的功能:

OpenShift-Kubernetes extensions:其它資源類型存儲在Etcd中,由Kubernetes管理。這些額外的資源類型形成OpenShift內部狀態和配置,以及由標準 Kubernetes管理的應用程序資源;

Containerized services:完成許多基礎設施功能,如網絡和授權。其中一些一直運行,另一些則按需啟動。OpenShift使用Docker和Kubernetes來實現大多數內部功能。即大多數OpenShift內部服務作為由Kubernetes管理的容器;

Runtimes and xPaaS:供開發人員使用的 base image,每個image都預配置了特定的runtime或db。xPaaS提供了一組用於JBoss中間件產品(如JBoss EAP和ActiveMQ)的 base image;

DevOps tools and user experience:OpenShift提供了Web UI和CLI管理工具,從而實現配置和監視應用程序、OpenShift服務和資源。Web和CLI工具都是由相同的REST api構建的,可供IDE和CI平台等外部工具使用。OpenShift 還可以訪問外部SCM存儲庫和容器registry,並將它們的構件引入OpenShift Cloud。

OpenShift不會向開發人員和系統管理員屏蔽Docker和Kubernetes的核心基礎設施。相反,它將它們用於內部服務,並允許將Docker和Kubernetes資源導入OpenShift集群,同時原始Docker和資源可以從OpenShift集群導出,並導入到其他基於docker的基礎設施中。

OpenShift添加到Docker + Kubernetes的主要價值是自動化開發工作流,因此應用程序的構建和部署在OpenShift集群中按照標準流程進行。開發者不需要知道底層Docker的細節。OpenShift接受應用程序,打包它,並將其作為容器啟動。

2.2 Master和nodes


OpenShift集群是一組節點服務器,它們運行容器,並由一組主服務器集中管理。服務器可以同時充當master和node,但是為了增加穩定性,這些角色通常是分開的。

OpenShift工作原理和交互視圖:




master節點運行OpenShift核心服務,如身份驗證,並未管理員提供API入口。

nodes節點運行包含應用程序的容器,容器又被分組成pod。

OpenShift master運行Kubernetes master服務和Etcd守護進程;

node運行Kubernetes kubelet和kube-proxy守護進程。

雖然在描述中通常沒有聲明,但實際上master本身也是node。

scheduler和management/replication是Kubernetes主服務,而Data Store是Etcd守護進程。

Kubernetes的調度單元是pod,它是一組共享虛擬網絡設備、內部IP地址、TCP/UDP端口和持久存儲的容器。pod可以是任何東西,從完整的企業應用程序(包括作為不同容器的每一層)到單個容器中的單個微服務。例如,一個pod,一個容器在Apache下運行PHP,另一個容器運行MySQL。

Kubernetes管理replicas來縮放pods。副本是一組共享相同定義的pod。

三 管理OpenShift

3.1 OpenShift項目及應用


除了Kubernetes的資源(如pods和services)之外,OpenShift還管理projects和users。一個projects對Kubernetes資源進行分組,以便用戶可以使用訪問權限。還可以為projects分配配額,從而限制了已定義的pod、volumes、services和其他資源。

OpenShift中沒有application的概念,OpenShift client提供了一個new-app命令。此命令在projects中創建資源,但它們都不是應用程序資源。這個命令是為標準開發人員工作流配置帶有公共資源的proiect的快捷方式。

OpenShift使用lables(標籤)對集群中的資源進行分類。默認情況下,OpenShift使用app標籤將相關資源分組到應用程序中。

3.2 使用Source-to-image構建映像


OpenShift允許開發人員使用標準源代碼管理倉庫(SCM)和集成開發環境(ide)來發布應用。

OpenShift中的source -to-lmage (S2I)流程從SCM倉庫中提取代碼,自動判斷所需的runtime,基於runtime啟動一個pod,在pod中編譯應用。

當編譯成功時,將在runtime image中添加層並形成新的image,推送進入OpenShift internal registry倉庫,接着基於這個image將創建新的pod,運行應用程序。

S2I可被視為已經內置到OpenShift中的完整的CI/CD管道。

CI/CD有不同的形式,根據具體場景表現不同。例如,可以使用外部CI工具(如Jenkins)啟動構建並運行測試,然後將新構建的映像標記為成功或失敗,將其推送到QA或生產。

3.2 管理OpenShift資源

OpenShift資源定義,如image、container、pod、service、builder、template等,都存儲在Etcd中,可以由OpenShift CLI, web控制台或REST API進行管理。

OpenShift的資源科通過JSON或YAML文件查看,並且在類似Git或版本控制的SCM中共享。OpenShift甚至可以直接從外部SCM檢索這些資源定義。

大多數OpenShift操作不需要實時響應,OpenShift命令和APIs通常創建或修改存儲在Etcd中的資源描述。Etcd然後通知OpenShift控制器,OpenShift控制器會就更改警告這些資源。

這些控制器採取行動,以便使得資源的最終態反應達到更改效果。例如,如果創建了一個新的pod資源,Kubernetes將在node上調度並啟動該pod,使用pod資源確定要使用哪個映像、要公開哪個端口,等等。或者一個模板被更改,從而指定應該有更多的pod來處理負載,OpenShift會安排額外的pod(副本)來滿足更新后的模板定義。

注意:雖然Docker和Kubernetes是OpenShift的底層,但是必須主要使用OpenShift CLi和OpenShift APls來管理應用程序和基礎設施。OpenShift增加了額外的安全和自動化功能,當直接使用Docker或Kubernetes命令和APls時,這些功能必須手動配置,或者根本不可用。因此強烈建議不要使用docker或Kubernetes的命令直接管理應用。

四 OpenShift網絡

4.1 OpenShift網絡概述


Docker網絡相對簡單,Docker創建一個虛擬內核橋接器(docker0網卡),並將每個容器網絡接口連接到它。

Docker本身沒有提供允許一個主機上的pod連接到另一個主機上的pod的方法。Docker也沒有提供嚮應用程序分配公共固定IP地址的方法,以便外部用戶可以訪問它。

但Kubernetes提供service和route資源來管理pods之間的網絡,以及從外部到pods的路由流量。service在不同pods之間提供負載均衡用於接收網絡請求,同時為service的所有客戶機(通常是其他pods)提供一個內部IP地址。

container和pods不需要知道其他pods在哪裡,它們只連接到service。route為service提供一個固定的惟一DNS名稱,使其對OpenShift集群之外的客戶端可見。

Kubernetes service和route資源需要外部(功能)插件支持。service需要軟件定義的網絡(SDN),它將在不同主機上的pod之間提供通信,route需要轉發或重定向來自外部客戶端的包到服務內部IP。

OpenShift提供了一個基於Open vSwitch的SDN,路由由分佈式HAProxy farm提供。

五 OpenShift持久性存儲

5.1 永久存儲


pod可以在一個節點上停止,並隨時在另一個節點上重新啟動。同時pod的默認存儲是臨時存儲,通過對於類似數據庫需要永久保存數據的應用不適合。

Kubernetes為管理容器的外部持久存儲提供了一個框架。Kubernetes提供了PersistentVolume資源,它可以在本地或網絡中定義存儲。pod資源可以使用PersistentVolumeClaim資源來訪問對應的持久存儲卷。

Kubernetes還指定了一個PersistentVolume資源是否可以在pod之間共享,或者每個pod是否需要具有獨佔訪問權的自己PersistentVolume。當pod移動到另一個節點時,它將保持與相同的PersistentVolumeClaim和PersistentVolumne資源的關聯。這意味着pod的持久存儲數據跟隨它,而不管它將在哪個節點上運行。

OpenShift向Kubernetes提供了多種VolumeProvider,如NFS、iSCSI、FC、Gluster或OpenStack Cinder。

OpenShift還通過StorageClass資源為應用程序提供動態存儲。使用動態存儲,可以選擇不同類型的後端存儲。後面存儲根據應用程序的需要劃分為不同的“tiers”。例如,可以定義一個名為“fast”的存儲類和另一個名為“slow”的存儲類,前者使用更高速的後端存儲,後者提供普通的存儲。當請求存儲時,最終用戶可以指定一個Persistentvolumeclaim,並使用一個註釋指定他們所需的StorageClass。

六 OpenShift高可用

6.1 OpenShift高可用概述


OpenShift平台集群的高可用性(HA)有兩個不同的方面:

OpenShift基礎設施本身的HA(即主機);

以及在OpenShift集群中運行的應用程序的HA。

默認情況下,OpenShift為master提供了完全支持的本機HA機制。

對於應用程序或“pods”,如果pod因任何原因丟失,Kubernetes將調度另一個副本,將其連接到服務層和持久存儲。如果整個節點丟失,Kubernetes會為它所有的pod安排替換節點,最終所有的應用程序都會重新可用。pod中的應用程序負責它們自己的狀態,因此它們需要自己維護應用程序狀態(如HTTP會話複製或數據庫複製)。

七 Image Streams

7.1 Image Streams


要在OpenShift中創建一個新的應用程序,除了應用程序源代碼之外,還需要一個base image(S2I builder image)。如果源代碼或image任何一個更新,就會生成一個新的image,並且基於此新image創建新的pod,同時替換舊的pod。

即當應用程序代碼發生更改時,容器映像需要更新,但如果構建器映像發生更改,則部署的pod也需要更新。

Image Streams包括由tag標識的大量的image。應用程序是針對Image Streams構建的。Image Streams可用於在創建新image時自動執行操作。構建和部署可以監視Image Streams,以便在添加新image時接收通知,並分別執行構建或部署。

OpenShift默認情況下提供了幾個Image Streams,包括許多流行的runtime和frameworks。

Image Streams tag是指向Image Streams中的image的別名。通常縮寫為istag。它包含一個image歷史記錄,表示為tag曾經指向的所有images的堆棧。

每當使用特定的istag標記一個新的或現有的image時,它都會被放在歷史堆棧的第一個位置(標記為latest)。之前tag再次指向舊的image。同時允許簡單的回滾,使標籤再次指向舊的image。 本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

網頁設計公司推薦不同的風格,搶佔消費者視覺第一線

※Google地圖已可更新顯示潭子電動車充電站設置地點!!

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

※別再煩惱如何寫文案,掌握八大原則!

網頁設計最專業,超強功能平台可客製化

聚甘新

人臉識別和手勢識別應用(face++)開發

基礎認識

本項目使用的是face++平台,人臉識別+手勢識別雙確認显示。

python編程,代碼簡介,方便擴展。

 

該項目適用於Windows系統和Linux系統,但必須安裝相應的模塊,其中包括

 

l  Python3  python 庫,邏輯編寫

l  Pillow   窗口開發實現

l  opencv-python python的opencv接口

l  Opencv庫   用於人臉檢測

 

本次測試是在win 10電腦上

 

視頻演示:

https://www.bilibili.com/video/BV1Wk4y1z7H7

 

安裝python3

這個網上到處都是資料,找一找就知道啦

官網:

https://www.python.org/

 安裝pillow

該庫用於python做界面開發,詳細參考:https://www.cnblogs.com/dongxiaodong/p/9971974.html

這個庫一般電腦都自帶有了,可以先不安裝,直接運行代碼。

如果出現以下錯誤,則必須手動安裝

ModuleNotFoundError: No module named ‘PIL’

安裝命令:

pip install pillow

安裝opencv-python

Opencv可以實現人臉檢測、人臉對比識別等功能,但在次只是用它來實現了人臉檢測並做人臉框圖,並沒有更多功能的實現,想要獲取更多功能的學習參考,請訪問:https://www.cnblogs.com/dongxiaodong/p/10134904.html

pip install opencv-python

如果出現紅色字體,表示安裝出錯了,必須從新運行安裝命令

 

 Face++

Face++在項目中用於人臉識別和手勢識別

系統流程主要為如下:

 

測試

(一)  獲取人臉標識

工程目錄:

 

l  運行項目,攝像頭將開啟,實時展示所拍攝的畫面

l  按下空格鍵即可獲取人臉標識,輸出人臉標識和存儲到data文件目錄下

l  此時按下ESC鍵則退出程序

l  同一個人的人臉標識很有可能是不一樣的,因為它更多的是基於本次照片計算

 

(二)  創建人臉庫&人臉標識添加到人臉庫

 

 

 

l  創建人臉標識庫,標識名自定義,但同一用戶內不可有相同的人臉標識庫

l  在函數填寫自己賬戶下唯一的人臉庫標識名

 

 

l  將人臉標識添加到人臉庫中

l  在函數中填寫人臉庫標識和我們第一步獲取的人臉標識,將人臉標識添加到人臉庫中

l  人臉庫可以添加多個不同的人臉標識

 

(三)  人臉庫搜索結果比對

 

l  修改為我們剛剛所創建的人臉庫,進行接下來的人臉識別查找

l  運行工程后將開啟攝像頭進行照片實時捕獲識別,並在屏幕中显示識別結果

l  識別包括人臉識別和手勢識別

l  只有在人臉識別正確的情況下才會開啟手勢識別

 

人臉識別失敗

人臉識別成功,無手勢

人臉識別成功,手勢為合攏

人臉識別成功,手勢為打開

 

 

視頻演示:

https://www.bilibili.com/video/BV1Wk4y1z7H7

 

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※自行創業缺乏曝光? 網頁設計幫您第一時間規劃公司的形象門面

網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

※想知道最厲害的網頁設計公司“嚨底家”!

※別再煩惱如何寫文案,掌握八大原則!

※產品缺大量曝光嗎?你需要的是一流包裝設計!

聚甘新

Linux nohup命令詳解,終端關閉程序依然可以在執行!

大家好,我是良許。

在工作中,我們很經常跑一個很重要的程序,有時候這個程序需要跑好幾個小時,甚至需要幾天,這個時候如果我們退出終端,或者網絡不好連接中斷,那麼程序就會被中止。而這個情況肯定不是我們想看到的,我們希望即使終端關閉,程序依然可以在跑。

這時我們就可以使用 nohup 這個命令。

nohup 命令是英語詞組 no hangup 的縮寫,意思是不掛斷,也就是指程序不退出。這個命令會使程序忽略 HUP 信號,保證程序能夠正常進行。HUP 信號有些人可能比較陌生,它是在終端被中止的時候向它所關聯的進程所發出的信號,進程收到這個信號后就會中止運行。所以如果你不希望進程被這個信號幹掉的話,就可以忽略這個信號。而 nohup 命令做的就是這個事情。

本文我們將詳細介紹 nohup 命令的具體用法。

nohup命令基本語法

nohup 命令的基本語法如下:

$ nohup command arguments

或者:

$ nohup options

如果你想要得到更多關於 nohup 的用法介紹,可以查看它的幫助頁面:

$ nohup --help

如果你需要查看它的版本號,可以使用 --version 選項。

$ nohup --version

使用nohup命令啟動一個程序

如果你需要運行一個程序,即使對應的 Shell 被退出后依然保持運行,可以這樣使用 nohup 運行這個程序:

$ nohup command

當這個程序進行起來之後,這個程序對應的 log 輸出及其錯誤日誌都將被記錄在 nohup.out 文件里,這個文件一般位於家目錄或者當前目錄。

重定向程序的輸出

如果我不想把程序的輸出保存在家目錄或者當前目錄,我想保存在我指定的路徑,並且自定義文件名,要怎麼操作?這時我們就可以使用重定向操作 >

比如,我現在有個腳本 myScript.sh 我想把它的輸出保存在家目錄下的 output 目錄下,文件名為 myOutput.txt ,可以這樣運行:

$ nohup ./myScript.sh > ~/output/myOutput.txt

使用nohup命令後台啟動一個程序

如果想讓程序在後台運行,可以加上 & 符號。但這樣運行之後,程序就無影無蹤了。想要讓程序重新回到終端,可以使用 fg 命令。

這個命令的輸出 log 將保存在 nohup.out 文件里,你可以使用 cat 或其它命令查看。第二行里 8699 這個数字代表這個命令對應的進程號,也就是 pid 。我們可以使用 ps 命令來找到這個進程。

使用nohup同時運行多個程序

如果你需要同時跑多個程序,沒必要一個個運行,直接使用 && 符號即可。比如,你想同時跑 mkdir ,ping,ls 三個命令,可以這樣運行:

$ nohup bash -c 'mkdir files &&
ping -c 1 baidu.com && ls'> output.txt

終止跑在後台的進程

上面有提到,nohup 命令結合 & 符號可以使進程在後台運行,即使關閉了終端依然不受影響。這時,如果想要終止這個進程,要怎麼操作呢?

最簡單的當屬 kill 命令,相信大家用過很多次了。

$ kill -9 PID

那要如何找到進程對應的 pid 呢?我們可以使用 ps 命令。

$ ps aux | grep myScript.sh

或者你使用 pgrep 命令也行。

接下來,再使用 kill 命令就可以終止該進程了。

$ kill -9 14942

公眾號:良許Linux

有收穫?希望老鐵們來個三連擊,給更多的人看到這篇文章

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

※別再煩惱如何寫文案,掌握八大原則!

※教你寫出一流的銷售文案?

※超省錢租車方案

FB行銷專家,教你從零開始的技巧

聚甘新

SpringBoot + Mybatis + Redis 整合入門項目

這篇文章我決定一改以往的風格,以幽默風趣的故事博文來介紹如何整合 SpringBoot、Mybatis、Redis。

很久很久以前,森林里有一隻可愛的小青蛙,他邁着沉重的步伐走向了找工作的道路,結果發現許多的招聘要求都要會 Redis。

小青蛙就想啥是 Redis 呢,為什麼要用 Redis 呢?難道是因為 Mysql 的幣格不夠高嗎,小青蛙點開了收藏已久的網站:十萬個為什麼

發現原來隨着使用網站的用戶越來越多,表中的數據也越來越多,查詢速度越來越慢。

MySql 的性能遇到了瓶頸,所以許多網站都用 Redis 作緩存。

 

然而能作緩存的不僅只有 Redis,還有 Memcache,那為什麼要用 Redis 呢?

1、性能方面:它們都是將數據存放在內存中,所以性能基本相似。

2、數據類型方面:Redis 支持五種數據數據類型:字符串、散列、列表、集合、有序集合,而 Memcache 僅僅支持簡單的 key-value。

3、數據持久化方面:Redis 可以通過 RDB快照、AOF日誌 等方式進行數據持久化,但是 Memcache 不可以。

4、數據備份方面:Redis  支持 master-slave 主從模式的數據備份。

 

在了解到許多 Redis 的好處后,小青蛙已經迫不及待的想了解它了。

為了更好的使用 Redis,了解 Redis 的五種數據類型適應場景是很有必要的。

1、String 類型:一個 key 對應一個 value,而 value 不僅僅是 String,也可以是数字、甚至是一個序列化對象。

2、Hash 類型:一個 key 對應 多個 field,一個 field 對應 yige value,實際上該類型最適合存儲序列化對象。

           key 相當於數據庫表名字,field  相當於主鍵,value 也就是序列化對象。

3、List 類型:簡單的字符串列表,按照插入順序排序,該結構類似於數據結構中的雙向鏈表,可以從頭部插也可以從尾部插。

4、Set 類型:它是字符串集合,只不過它是無序的且不存在重複的字符串,但是它可以實現 交集、並集、差集。

5、ZSet 類型:它也是字符串集合,它和 Set 的區別是該集合的元素存在 score 屬性,按照 score 屬性的高低排序。可以應用在排行榜上。

值得注意的是,不要習慣性的認為 Redis 字符串只能存字符串,實際上,它可以存儲任何序列化后的對象,當然也可以讀出來。

 

小青蛙知道了 Redis 的五種數據類型應用場景后,迫不及待的想要實踐它了。

為了知道如何讓它作為緩存,以及如何操作數據,小青蛙打開了珍藏已久的視頻網站來學習:青蛙愛學習

在該視頻網站上,小青蛙沉迷其中無法自拔,額,呸呸。緩過神來,發現了一個很好的視頻。小青蛙幽默的說:快進我的收藏夾吃灰去吧。

小青蛙向來都不是一個收藏從未停止,學習從未開始的青蛙。它模仿着視頻建了一個 SpringBoot 項目。

此時,小青蛙想為什麼要勾選 Lombok 呢,因為它可以簡化類,並且提供了 log 等功能。

之後小青蛙為了連上 Mysql 和 Redis 就開始配置 application.properties 文件:

# 數據源配置
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/demo?serverTimezone=UTC&characterEncoding=utf-8
spring.datasource.username=root
spring.datasource.password=lemon@mango

# Redis 配置
spring.redis.database=0
spring.redis.host=127.0.0.1
spring.redis.port=6379
spring.redis.password=
spring.redis.lettuce.pool.max-active=8
spring.redis.lettuce.pool.max-wait=-1
spring.redis.lettuce.pool.max-idle=8
spring.redis.lettuce.pool.min-idle=0
spring.redis.lettuce.shutdown-timeout=100

配置好該文件后,需要 redisTemplate 模板 Bean,因為自動配置的 redisTemplate Bean 沒有提供序列化操作:(因為是入門版的,所以這樣最好理解)

package com.demo.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;

@Configuration
public class RedisConfig {
    
    @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
        RedisTemplate<String, Object> template = new RedisTemplate<String, Object>();
                template.setConnectionFactory(factory);
                GenericJackson2JsonRedisSerializer genericJsonRedisSerializer = new GenericJackson2JsonRedisSerializer();
                template.setKeySerializer(new StringRedisSerializer());
                template.setValueSerializer(genericJsonRedisSerializer);
                template.setHashKeySerializer(genericJsonRedisSerializer);
                template.setHashValueSerializer(genericJsonRedisSerializer);
                template.afterPropertiesSet();
                return template;
    }
}

至此就整合完成了,小青蛙心想這就完事了?!!!,不信?那就來演示一下:(先建一個簡單的類測試一下)

package com.test.serviceImpl;

import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.beans.factory.annotation.Autowired;
import lombok.extern.slf4j.Slf4j;

@Service
@Slf4j
public class JustForTest {
    
    @Autowired
    private RedisTemplate redisTemplate;
    
    public void test(String username) {
        
        if(redisTemplate.hasKey(username)) {
            log.info((String)redisTemplate.opsForValue().get(username));
            log.info("get value from redis");
        }else {
            String password = "password";
            log.info(password);
            log.info("get value from mysql");
            log.info("set value to redis");
            redisTemplate.opsForValue().set(username, password);
        }
        
    }

}
package com.test;

import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.beans.factory.annotation.Autowired;
import com.test.serviceImpl.JustForTest;

@SpringBootTest
class TestApplicationTests {
    
    @Autowired
    private JustForTest justFortest;

    @Test
    void contextLoads() {
        justFortest.test("username");
    }
}

哦嚯,報錯了,原來是 pom.xml 中少了 commons.pool 依賴,咱給它加上:

        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-pool2</artifactId>
        </dependency>

再來一次:

再再來一次:

可以看到確實存入 Redis 了,小青蛙便去 Redis 數據庫中看看有沒有:

事實證明確實整合完畢了,小青蛙仍然表示不解,說好的是SpringBoot、Mybatis、Redis的整合呢,怎麼只看到 Redis 的?

小青蛙剛這麼想,然後視頻里就說了,心急吃不了熱豆腐,需要慢慢來。緊接着,小青蛙就看到了完整的項目結構:

為了讓青蛙們只關注有關整合的部分,視頻里僅僅只給出 serviceImpl 中的代碼:

package com.demo.serviceImpl;

import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.beans.factory.annotation.Autowired;
import com.demo.pojo.SimpleUser;
import com.demo.dao.SimpleUserDao;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import lombok.extern.slf4j.Slf4j;

@Service
@Slf4j
@SuppressWarnings({"rawtypes","unchecked"})
public class SimpleUserServiceImpl implements UserDetailsService {

    @Autowired
    private RedisTemplate redisTemplate;
    @Autowired
    private SimpleUserDao userDao;
    @Override 
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        // TODO Auto-generated method stub
        if(redisTemplate.opsForHash().hasKey("user",username)) {
            SimpleUser user = (SimpleUser)redisTemplate.opsForHash().get("user",username);
            return new User(user.getUsername(),user.getPassword(),user.getAuthorities());
        }else {
            SimpleUser user = userDao.findUserByUsername(username);
            if(user != null) {
                redisTemplate.opsForHash().put("user", "username", user);
                return new User(user.getUsername(),user.getPassword(),user.getAuthorities());
            }else {
                throw new UsernameNotFoundException("Username or Password is not correct");
            }
        }
    }
    
    public int addSimpleUser(SimpleUser user) {
        user.setPassword(new BCryptPasswordEncoder().encode(user.getPassword()));
        return userDao.addSimpleUser(user);
    }
}

和上面測試的簡單小例子相似,僅僅是把 String 換成了對象。小青蛙對 Mysql 的表結構和Redis中存入的數據比較感心趣:

由於對 String 類型帶有 ” 符號,所以需要對其進行轉義。

小青蛙是一個有分享精神的蛙,每次它覺得有價值的東西,它都會分享給它的朋友們,項目地址為:GitHub

小青娃逐漸的弄懂了怎麼去進行整合,但是它還是不太明白為什麼這樣就能整合,也就是 know how but don’t konw why!

小青蛙知道:萬事開頭難,但它不知道的是,後面也很難…從此,小青蛙踏上了探尋源碼的道路!呱呱呱……

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※別再煩惱如何寫文案,掌握八大原則!

網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

※超省錢租車方案

※教你寫出一流的銷售文案?

網頁設計最專業,超強功能平台可客製化

聚甘新

基於NACOS和JAVA反射機制動態更新JAVA靜態常量非@Value註解

1.前言

項目中都會使用常量類文件, 這些值如果需要變動需要重新提交代碼,或者基於@Value註解實現動態刷新, 如果常量太多也是很麻煩; 那麼 能不能有更加簡便的實現方式呢?

本文講述的方式是, 一個JAVA類對應NACOS中的一個配置文件,優先使用nacos中的配置,不配置則使用程序中的默認值;

2.正文

nacos的配置如下圖所示,為了滿足大多數情況,配置了 namespace命名空間和group;

 

 

 新建個測試工程 cloud-sm.

bootstrap.yml 中添加nacos相關配置;

為了支持多配置文件需要注意ext-config節點,group對應nacos的添加的配置文件的group; data-id 對應nacos上配置的data-id

配置如下:

server:
  port: 9010
  servlet:
    context-path: /sm
spring:
  application:
    name: cloud-sm
  cloud:
    nacos:
      discovery:
        server-addr: 192.168.100.101:8848 #Nacos服務註冊中心地址
        namespace: 1
      config:
        server-addr: 192.168.100.101:8848 #Nacos作為配置中心地址
        namespace: 1
        ext-config:
          - group: TEST_GROUP
            data-id: cloud-sm.yaml
            refresh: true
          - group: TEST_GROUP
            data-id: cloud-sm-constant.properties
            refresh: true

接下來是本文重點:

1)新建註解ConfigModule,用於在配置類上;一個value屬性;

2)新建個監聽類,用於獲取最新配置,並更新常量值

實現流程:

1)項目初始化時獲取所有nacos的配置

2)遍歷這些配置文件,從nacos上獲取配置

3)遍歷nacos配置文件,獲取MODULE_NAME的值

4)尋找配置文件對應的常量類,從spring容器中尋找 常量類 有註解ConfigModule 且值是 MODULE_NAME對應的

5)使用JAVA反射更改常量類的值

6)增加監聽,用於動態刷新

 

import org.springframework.stereotype.Component;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Component
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE})
public @interface ConfigModule {
    /**
     *  對應配置文件裏面key為( MODULE_NAME ) 的值
     * @return
     */
    String value();
}
import com.alibaba.cloud.nacos.NacosConfigProperties;
import com.alibaba.druid.support.json.JSONUtils;
import com.alibaba.nacos.api.NacosFactory;
import com.alibaba.nacos.api.PropertyKeyConst;
import com.alibaba.nacos.api.config.ConfigService;
import com.alibaba.nacos.api.config.listener.Listener;
import com.alibaba.nacos.api.exception.NacosException;
import com.alibaba.nacos.client.utils.LogUtils;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.ApplicationContext;
import org.springframework.stereotype.Component;

import javax.annotation.PostConstruct;
import java.io.IOException;
import java.io.StringReader;
import java.lang.reflect.Field;
import java.util.Enumeration;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.concurrent.Executor;

/**
 * nacos 自定義監聽
 *
 * @author zch
 */
@Component
public class NacosConfigListener {
    private Logger LOGGER = LogUtils.logger(NacosConfigListener.class);
    @Autowired
    private NacosConfigProperties configs;
    @Value("${spring.cloud.nacos.config.server-addr:}")
    private String serverAddr;
    @Value("${spring.cloud.nacos.config.namespace:}")
    private String namespace;
    @Autowired
    private ApplicationContext applicationContext;
    /**
     * 目前只考慮properties 文件
     */
    private String fileType = "properties";
    /**
     * 需要在配置文件中增加一條 MODULE_NAME 的配置,用於找到對應的 常量類
     */
    private String MODULE_NAME = "MODULE_NAME";

    /**
     * NACOS監聽方法
     *
     * @throws NacosException
     */
    public void listener() throws NacosException {
        if (StringUtils.isBlank(serverAddr)) {
            LOGGER.info("未找到 spring.cloud.nacos.config.server-addr");
            return;
        }
        Properties properties = new Properties();
        properties.put(PropertyKeyConst.SERVER_ADDR, serverAddr.split(":")[0]);
        if (StringUtils.isNotBlank(namespace)) {
            properties.put(PropertyKeyConst.NAMESPACE, namespace);
        }

        ConfigService configService = NacosFactory.createConfigService(properties);
        // 處理每個配置文件
        for (NacosConfigProperties.Config config : configs.getExtConfig()) {
            String dataId = config.getDataId();
            String group = config.getGroup();
            //目前只考慮properties 文件
            if (!dataId.endsWith(fileType)) continue;

            changeValue(configService.getConfig(dataId, group, 5000));

            configService.addListener(dataId, group, new Listener() {
                @Override
                public void receiveConfigInfo(String configInfo) {
                    changeValue(configInfo);
                }

                @Override
                public Executor getExecutor() {
                    return null;
                }
            });
        }
    }

    /**
     * 改變 常量類的 值
     *
     * @param configInfo
     */
    private void changeValue(String configInfo) {
        if(StringUtils.isBlank(configInfo)) return;
        Properties proper = new Properties();
        try {
            proper.load(new StringReader(configInfo)); //把字符串轉為reader
        } catch (IOException e) {
            e.printStackTrace();
        }
        String moduleName = "";
        Enumeration enumeration = proper.propertyNames();
        //尋找MODULE_NAME的值
        while (enumeration.hasMoreElements()) {
            String strKey = (String) enumeration.nextElement();
            if (MODULE_NAME.equals(strKey)) {
                moduleName = proper.getProperty(strKey);
                break;
            }
        }
        if (StringUtils.isBlank(moduleName)) return;
        Class curClazz = null;
        // 尋找配置文件對應的常量類
        // 從spring容器中 尋找類的註解有ConfigModule 且值是 MODULE_NAME對應的
        for (String beanName : applicationContext.getBeanDefinitionNames()) {
            Class clazz = applicationContext.getBean(beanName).getClass();
            ConfigModule configModule = (ConfigModule) clazz.getAnnotation(ConfigModule.class);
            if (configModule != null && moduleName.equals(configModule.value())) {
                curClazz = clazz;
                break;
            }
        }
        if (curClazz == null) return;
        // 使用JAVA反射機制 更改常量
        enumeration = proper.propertyNames();
        while (enumeration.hasMoreElements()) {
            String key = (String) enumeration.nextElement();
            String value = proper.getProperty(key);
            if (MODULE_NAME.equals(key)) continue;
            try {
                Field field = curClazz.getDeclaredField(key);
                //忽略屬性的訪問權限
                field.setAccessible(true);
                Class<?> curFieldType = field.getType();
                //其他類型自行拓展
                if (curFieldType.equals(String.class)) {
                    field.set(null, value);
                } else if (curFieldType.equals(List.class)) { // 集合List元素
                    field.set(null, JSONUtils.parse(value));
                } else if (curFieldType.equals(Map.class)) { //Map
                    field.set(null, JSONUtils.parse(value));
                }
            } catch (NoSuchFieldException | IllegalAccessException e) {
                LOGGER.info("設置屬性失敗:{} {} = {} ", curClazz.toString(), key, value);
            }
        }
    }

    @PostConstruct
    public void init() throws NacosException {
        listener();
    }
}

 3.測試

1)新建常量類Constant,增加註解@ConfigModule(“sm”),盡量測試全面, 添加常量類型有 String, List,Map

@ConfigModule("sm")
public class Constant {

    public static volatile String TEST = new String("test");

    public static volatile List<String> TEST_LIST = new ArrayList<>();
    static {
        TEST_LIST.add("默認值");
    }
    public static volatile Map<String,Object> TEST_MAP = new HashMap<>();
    static {
        TEST_MAP.put("KEY","初始化默認值");
    }
    public static volatile List<Integer> TEST_LIST_INT = new ArrayList<>();
    static {
        TEST_LIST_INT.add(1);
    }
}

2)新建個Controller用於測試這些值

@RestController
public class TestController {

    @GetMapping("/t1")
    public Map<String, Object> test1() {
        Map<String, Object> result = new HashMap<>();

        result.put("string" , Constant.TEST);
        result.put("list" , Constant.TEST_LIST);
        result.put("map" , Constant.TEST_MAP);
        result.put("list_int" , Constant.TEST_LIST_INT);
        result.put("code" , 1);
        return result;
    }
}

3)當前nacos的配置文件cloud-sm-constant.properties為空

 4)訪問測試路徑localhost:9010/sm/t1,返回為默認值

{
    "code": 1,
    "string": "test",
    "list_int": [
        1
    ],
    "list": [
        "默認值"
    ],
    "map": {
        "KEY": "初始化默認值"
    }
}

5)然後更改nacos的配置文件cloud-sm-constant.properties;

 6)再次訪問測試路徑localhost:9010/sm/t1,返回為nacos中的值

{
    "code": 1,
    "string": "12351",
    "list_int": [
        1,
        23,
        4
    ],
    "list": [
        "123",
        "sss"
    ],
    "map": {
        "A": 12,
        "B": 432
    }
}

4.結語

這種實現方式優點如下:

1)動態刷新配置,不需要重啟即可改變程序中的靜態常量值

2)使用簡單,只需在常量類上添加一個註解

3)避免在程序中大量使用@Value,@RefreshScope註解

 不足:

此代碼是個人業餘時間的想法,未經過生產驗證,實現的數據類型暫時只寫幾個,其餘的需要自行拓展

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※教你寫出一流的銷售文案?

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

※回頭車貨運收費標準

※別再煩惱如何寫文案,掌握八大原則!

※超省錢租車方案

※產品缺大量曝光嗎?你需要的是一流包裝設計!

聚甘新

學習ASP.NET Core(11)-解決跨域問題與程序部署

上一篇我們介紹了系統日誌與測試相關的內容並添加了相關的功能;本章我們將介紹跨域與程序部署相關的內容

一、跨域

1、跨域的概念

1、什麼是跨域?

一個請求的URL由協議,域名,端口號組成,以百度的https://www.baidu.com為例,協議為https,域名由子域名www和主域名baidu組成,端口號若為80會自動隱藏(也可以配置為其它端口,通過代理服務器將80端口請求轉發給實際的端口號)。而當請求的URL的協議,域名,端口號任意一個於當前頁面的URL不同即為跨域

2、什麼是同源策略?

瀏覽器存在一個同源策略,即為了防範跨站腳本的攻擊,出現跨域請求時瀏覽器會限制自身不能執行其它網站的腳本(如JavaScript)。所以說當我們把項目部署到Web服務器后,通過瀏覽器進行請求時就會出現同源策略問題;而像PostMan軟件因其是客戶端形式的,所以不存在此類問題

3、跨域會導致什麼問題?

同源策略會限制以下行為:

  • Cookie、LocalStorage和IndexDb的讀取
  • DOM和JS對象的獲取
  • Ajax請求的發送

2、常用的解決方法

這裏我們將簡單介紹針對跨域問題常用的幾種解決辦法,並就其中的Cors方法進行配置,若對其它方式感興趣,可參照老張的哲學的文章,⅖ 種方法實現完美跨域

2.1、JsonP

1、原理

上面有提到瀏覽器基於其同源策略會限制部分行為,但對於Script標籤是沒有限制的,而JsonP就是基於這一點,它會在頁面種動態的插入Script標籤,其Src屬性對應的就是api接口的地址,前端會以Get方式將處理函數以回調的形式傳遞給後端,後端響應後會再以回調的方式傳遞給前端,最終頁面得以显示

2、優缺點

JsonP出現時間較早,所以對舊版本瀏覽器支持性較好;但自身只支持Get請求,無法確認請求是否成功

2.2、 CORS

1、原理

CORS的全稱是Corss Origin Resource Sharing,即跨域資源共享,它允許將當前域下的資源被其它域的腳本請求訪問。其實現原理就是在響應的head中添加Access-Control-Allow-Origin,只要有該字段就支持跨域請求

2、優缺點

Cors支持所有Http方法,不用考慮接口規則,使用簡單;但是對一些舊版本的瀏覽器支持性欠佳

3、使用

其使用非常簡單,以我們的項目為例,在BlogSystem.Core項目的Startup類的ConfigureServices方法中進行如下配置

同時需要開啟使用中間件,如下:

2.3、Nginx

1、原理

跨域問題是指在一個地址中發起另一個地址的請求,而Nginx可以利用其反向代理的功能,接受請求后直接請求該地址,類似打開了一個新的頁面,所以可以避開跨域的問題

2、優缺點

配置簡單,可以降低開發成本,方便配置負載均衡;靈活性差,每個環境都需要進行不同的配置

二、程序部署

1、部署模式

在.NET Core中,有兩種部署模式,分別為FDD(Framework-dependent)框架依賴發布模式和SCD(Self-contained)自包含獨立發布模式

  • FDD:此類部署需要服務器安裝.NET Core SDK環境,部署的包容量會比較小,但可能因SDK版本存在兼容性問題;
  • SCD:此類部署自包含.NET Core SDK的環境,不同.NET Core版本可以共存,其部署包容量會較大,且需要對服務器進行相關配置

2、常用部署方式

以下內容均參考老張的哲學的文章最全的部署方案 & 最豐富的錯誤分析,有興趣的朋友可以參考原文

2.1、Windows平台

  • 直接運行:發布目標選擇windows時會在文件夾中生成一個exe文件,我們可以直接執行exe文件或使用CLI命令調用dll運行;這種方式雖然方便,卻存在一些弊端,比如說部署多個的情況下會存在很多控制台窗口,如誤操作會導致窗口關閉等;
  • 部署服務:除了上述直接運行的方式外,我們還可以將程序發布為服務,發布后我們可以像控制系統服務一樣控製程序的啟動和關閉

需要注意的是上述兩類方法都需要藉助IIS或者是代理服務器進行服務的轉發,否則只能在本地進行訪問;

2.2、Linux平台

Linux平台常用的部署方式即為程序+代理服務器,但是當我們配置完成后運行程序時,該運行命令會一直佔用操作窗口,所以我們需要使用“守護進程”來解決這個問題,簡單來說就是將程序放到後台運行,不影響我們進行其他操作

綜上,部署模式、部署方式及部署平台有多種組合方式,接下來我們挑選下述3種方法進行演示:

方案 依賴運行時/宿主機 依賴代理服務器 其它配置
Windows程序(SCD)+Nginx
Windows服務(FDD)+IIS 設置為服務
Linux程序(FDD)+Nginx 守護進程

3、程序發布

1、這裏我們右擊BlogSystem.Core項目,選擇發布,選擇文件夾后,點擊高級

2、為了演示後面的發布實例,這裏我們分別選擇3種組合模式,①獨立+win-x64;②框架依賴+win-x64;③框架依賴+linux-x64

3、將發布實例拷貝到單獨的文件夾種,這裏我們使用SCD-Window驗證下程序能否直接運行,運行BlogSystem.Core.exe,報錯:

原來還是老問題,BLL沒有添加到發布文件中,我們到項目的bin文件夾下將BLL和DAL的dll文件分別拷貝至3個文件夾,再次運行,出現404錯誤,經過確認發現,首頁對應的是Swagger文檔頁面,而在配置中間件時我們有添加開發環境才配置swagger的邏輯,所以這裏我們可以根據個人需求決定是否添加。

這裏我為了方便確認發布是否成功,所以將其從判斷邏輯中取出了。重新生成發布文件,拷貝BLL和DAL的dll文件,再次運行,還是報錯。原來時Swagger的XML文件缺失,從bin文件夾下拷貝添加至發布文件,運行后成功显示頁面

4、有的朋友會說了,每次都要拷貝這兩個dll和這兩個xml文件,太麻煩了。其實也是有對應的解決辦法的,我們可以使用dotnet的CLI命令進行發布,選擇引用的發布文件夾為bin文件夾,拷貝至發布文件夾即可,有興趣的朋友可以自行研究

三、服務器發布

這裏我用的是阿里雲服務器,Window系統版本是Window Server2012 R2,Linux系統版本是CentOS 8.0;在操作前記得確認拷貝的發布文件能否在本地正常運行

1、Windows程序(SCD)+Nginx

1、解壓后雙擊exe文件網站可以正常運行,如下:

2、這個時候我們發現了一個問題,服務器上沒有數據庫,所以無法確認功能是否正常,這裏我們先下載安裝一個Microsoft SQL Server 2012 Express數據庫(建項目時沒有考慮到發布后測試的問題,實際上像SQLite數據庫是非常符合這類場景的)

安裝完成后我們新建一個BlogSystem的數據庫,通過Sql文件的形式將數據庫結構和數據導入至服務器數據庫,這時候又發現一個問題,由於我們連接數據庫的邏輯放置在model層的BlogSystemContext文件夾下,所以需要將連接中的DataSource更改為Express數據庫,重新發布后覆蓋舊的發布文件(系統設計有缺陷,可以將EF上下文文件放在應用程序層或單獨一層),再次運行,成功執行查詢,如下:

3、這個時候本地已經可以進行正常的訪問了,但是外部網絡是無法訪問調用接口的,這裏我們藉助Nginx進行服務的轉發。下載Nginx后解壓對conf文件夾下的nginx.conf文件進行如下配置:

4、在nginx.exe文件所在目錄的文件路徑輸入cmd,鍵入nginx啟動服務訪問8081端口,成功显示頁面(確保core程序正常運行)如下:

5、這個時候我們使用其它電腦訪問接口,發現還是無法訪問,經過查詢是阿里雲服務器進行了相關的限制,在阿里雲控制台配置安全組規則后即可正常訪問,如下:

6、配置完成后運行,成功訪問該網站且功能正常。這類方法不需要藉助Core的運行時環境,可以說十分便捷

2、Windows服務(FDD)+IIS

1、首先我們將FDD發布文件壓縮后拷貝至Window Server主機,因FDD的部署方法需要藉助.NET Core運行時環境,所以這裏我們首先到官網https://dotnet.microsoft.com/download/dotnet-core/current/runtime下載安裝.NET Core運行時,這裏我們選擇的是右邊這個,安裝完需要重新啟動

2、上一個方法中桌面显示控制台窗口顯然不是一個較佳的方案,所以這裏我們將其註冊為服務。官方提供了ASP.NET Core服務託管的方法,但使用較為複雜,這裏我們藉助一個名為nssm的工具來達到同樣的目的。我們下載nssm后,在其exe路徑運行cmd命令,執行nssm install,在彈出的窗口中進行如下配置:

3、我們在系統服務中開啟BlogSytem.Core_Server,在控制面版中選擇安裝IIS服務,併發布對應的項目,安裝完成后,添加部署為8082端口,將應用程序池修改為無託管,如下:

4、運行網站,成功显示頁面,但是進行功能試用時發現報錯;經過確認是由於IIS應用程序池的用戶驗證模式和sqlserver的驗證模式不同,解決辦法有三種①修改應用程序池高級設置中的進程模型中的標識②將連接數據庫字符串中的Integrated Security=True去除,並添加數據庫連接對應的賬號密碼③在數據庫的“安全性”>“登錄名”裏面,添加對應IIS程序池的名稱,並在這個用戶的“服務器角色”和“用戶映射”中給他對應的權限

後續嘗試方案一失敗,嘗試方案二成功,方案三由於要安裝SSMS所以沒有嘗試,有遇到相同問題的朋友可以自己試下

3、Linux程序(FDD)+Nginx

1、首先我們使用MobaXterm工具登錄至Linux主機(選擇此工具是由於其)同時支持文件傳送和命令行操作),這裏使用的Linux版本是CentOS 8.0;藉助MobaXterm工具在home文件夾下創建WebSite文件夾,並在其內部創建BlogSystem文件夾,將我們準備好的FDD部署方式的發布文件上傳至此文件夾后,使用命令sudo dnf install dotnet-sdk-3.1安裝.net core sdk,如下圖

2、輸入cd /home/WebSite/BlogSystem切換至項目文件夾后,使用dotnet BlogSystem.Core.dll運行程序,成功執行,但是由於我們沒有數據庫,且未配置代理服務器,所以無法驗證服務是否正常運行;所以這裏我們先參照微軟doc快速入門:在 Red Hat 上安裝 SQL Server 並創建數據庫安裝Sql Server數據庫(阿里雲默認安裝了python3作為解釋器所以無需重複安裝),安裝完成后我們開放在阿里雲實例中開放1433端口,使用可視化工具導入表結構和數據

3、完成上述操作后我們需要配置守護進程,將程序放在後台運行。首先我們在/etc/systemd/system下新建守護進程文件,文件名以.service結尾,這裏我們新建名為BlogSystem.service文件,使用MobaXterm自帶的編輯器打開文件後進行如下配置,注意後面的中文備註需要去除否則會報錯

[Unit]
Description=BlogSystem    #服務描述,隨便填就好

[Service]
WorkingDirectory=/home/WebSite/BlogSystem/   #工作目錄,填你應用的絕對路徑
ExecStart=/usr/bin/dotnet /home/WebSite/BlogSystem/BlogSystem.Core.dll    #啟動:前半截是你dotnet的位置(一般都在這個位置),後半部分是你程序入口的dll,中間用空格隔開
Restart=always  
RestartSec=25 #如果服務出現問題會在25秒后重啟,數值可自己設置
SyslogIdentifier=BlogSystem  #設置日誌標識,此行可以沒有
User=root   #配置服務用戶,越高越好
Environment=ASPNETCORE_ENVIRONMENT=Production
[Install]
WantedBy=multi-user.target

我們使用cd /etc/systemd/system/切換至BlogSystem.service對應的目錄,使用systemctl enable BlogSystem.service設置為開機運行后,再使用systemctl start BlogSystem.service啟動服務,另外可以使用systemctl status BlogSystem確認服務狀態

4、接下來我們安裝代理Nginx代理默認的5000端口,使用sudo yum install nginx安裝nginx后,我們到\etc\nginx文件夾下打開nginx.conf文件進行如下配置:

配置完成我們進入\etc\nginx文件夾下,使用systemctl enable nginx將nginx設置為開機啟動,並使用systemctl start nginx啟用服務,同樣可以使用systemctl status nginx確認其狀態。確認無誤后在阿里雲中開放8081端口,外網可正常訪問,但功能試用時報錯,原來是數據庫連接錯誤,重新設置后即可正常訪問

本章完~

本人知識點有限,若文中有錯誤的地方請及時指正,方便大家更好的學習和交流。

本文部分內容參考了網絡上的視頻內容和文章,僅為學習和交流,地址如下:

老張的哲學,系列一、ASP.NET Core 學習視頻教程

solenovex,ASP.NET Core 3.x 入門視頻

聲明

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※超省錢租車方案

※別再煩惱如何寫文案,掌握八大原則!

※回頭車貨運收費標準

※教你寫出一流的銷售文案?

FB行銷專家,教你從零開始的技巧

聚甘新

服務設計思考:平台化

平台是一套完整的服務。也是一套內部自洽的系統。核心在於分離,業務與通用服務隔離,業務與通用功能隔離。

目標:

  • 對需求方: 快速響應。可以敏捷地進行需求迭代。

  • 對第三方業務方: 以產品的方式提供服務。所見即所得。所有功能對業務方透明。

  • 對測試方: 簡易明了的測試方式。利於自動化測試,灰度測試。

  • 對運維方: 持續集成,自動化編排,自動化部署。

  • 數據方: 提供多維度,詳盡的服務數據。可以給數據方提供簡便的數據分析。

  • 內部開發: 敏捷開發。迅速集成。

實現:

  • 如何實現需求的快速響應?
    明確的方向,清晰的邊界。確認通用語言,核心領域。敏捷開發,快速迭代。AB 測試。

  • 如何為第三方提供產品式的服務?

    所見即所得。詳盡的文檔。第三方調試平台,第三方管理平台。

  • mock 服務,自動化測試,swagger 文檔。

  • Devops,CI,DI 等持續集成,服務監控。

  • 業務數據與分析數據異構存儲。提供易於分析的數據服務。

  • 組內服務負責制度,人類最佳的合作人數是 2-3 人。所以兩人維護一個項目,一人主導,一人輔助,兩人交叉合作是一個很好的團隊合作模式。如圖形成一個網狀模式(紅色線代表主導,黑色線輔助)。這樣每一個項目都將有兩個熟悉的人。

原則

  1. 單一職責。
  2. 業務關注業務,功能關注功能。
  3. 確認邊界,確認核心領域。
  4. 所見即所得。

實施

如何推進業務開發快速響應?

  1. 抽離變化與不變。形成基礎服務

    如下面一套用戶體系,將服務抽離,將變與不變隔離。

    用戶 api: 主要提供用戶相關的接口,變化大,更偏向於業務;

    用戶中心: 主要管理用戶核心領域,變動不大,需穩定高可用的服務;

    鑒權授權中心: 變動不大,主要管理用戶憑證核心領域;

  1. 抽離通用功能。

    那些非業務的通用功能應隔離於業務之外:組件化工具化服務化

    來源監控接口限流日誌分析應用監控服務依賴配置管理系統部署等(業務人員不必關心這些功能相關的事情,只需要關注於具體的業務領域)。關注點分離。

    如上面所涉及的,從Spring Cloud的各大組件可以看出,最終的方案都將走上相近的道路。

  1. 領域上下文劃分。劃分微服務項目。業務隔離,數據去中心化。服務組件化。

    Spring cloud 技術棧:

    • 服務治理: 註冊中心,服務調用,衍生的容錯(熔斷器)
    • api 網關: 來源監控,接口限流(Spring Cloud gateway、zuul)
    • **配置中心: ** 配置管理(Apollo)
    • 自動化部署: Jenkins、docker、k8s
    • 日誌與監控: prometheus、influxdb、skywalking、elk
    • 數據可視化: druid、kylin、superset
  1. 細節管控

    接口版本管理, gitflow 管理,項目迭代 release 版本管理,標準化,敏捷開發。

歡迎關注我的公眾號。

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

網頁設計公司推薦不同的風格,搶佔消費者視覺第一線

※Google地圖已可更新顯示潭子電動車充電站設置地點!!

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

※別再煩惱如何寫文案,掌握八大原則!

網頁設計最專業,超強功能平台可客製化

聚甘新

Spring Data 教程 – Redis

1. Redis簡介

Redis(Remote Dictionary Server ),即遠程字典服務,是一個開源的使用ANSI C語言編寫、支持網絡、可基於內存亦可持久化的日誌型、Key-Value 數據庫,並提供多種語言的API。Redis 是一個高性能的key-value數據庫。 redis的出現,在部分場合可以對關係數據庫起到很好的補充作用。它提供了Java,C/C++,C#,PHP,JavaScript,Perl,Object-C,Python,Ruby,Erlang等客戶端,使用很方便。

Redis支持主從同步。數據可以從主服務器向任意數量的從服務器上同步,從服務器可以是關聯其他從服務器的主服務器。這使得Redis可執行單層樹複製。存盤可以有意無意的對數據進行寫操作。由於完全實現了發布/訂閱機制,使得從數據庫在任何地方同步樹時,可訂閱一個頻道並接收主服務器完整的消息發布記錄。同步對讀取操作的可擴展性和數據冗餘很有幫助。

redis的key都是字符串String類型,它的value是多樣化的,如下圖:

redis數據類型 ENCODING返回的編碼 底層對應的數據結構
string int long類型的整數
string embstr embstr編碼的簡單動態字符串
string raw 簡單動態字符串
list ziplist 壓縮列表
list linkedlist 雙向鏈表
hash ziplist 壓縮列表
hash ht 字典
set intset 整數集合
set ht 字典
zset ziplist 壓縮列表
zset skiplist 跳錶

2. Redis的五種數據類型

2.1 字符串對象(String)

字符串對象的模型:

redis底層提供了三種不同的數據結構實現字符串對象,根據不同的數據自動選擇合適的數據結構。這裏的字符串對象並不是指的純粹的字符串,数字也是可以的。

  • int:當數據是long類型的整数字符串時,底層使用long類型的整數實現。這個值會直接存儲在字符串對象的ptr屬性中,同時OBJECT ENCODING為int。

  • raw:當數據為長度大於44字節的字符串時,底層使用簡單動態字符串實現,說到這裏就不得不提下redis的簡單隨機字符串(Simple Dynamic String,SDS),SDS有三個屬性,free,len和buf。free存的是還剩多少空間,len存的是目前字符串長度,不包含結尾的空字符。buf是一個list,存放真實字符串數據,包含free和空字符。針對SDS本文不做詳細介紹,歡迎點擊SDS了解。

  • embstr:當數據為長度小於44字節的字符串時,底層使用embstr編碼的簡單動態字符串實現。相比於raw,embstr內存分配只需要一次就可完成,分配的是一塊連續的內存空間。

2.2 列表對象(List)

列表對象的模型:

redis中的列表對象經常被用作消息隊列使用,底層採用ziplist和linkedlist實現。大家使用的時候當作鏈表使用就可以了。

  • ziplist

    列表對象使用ziplist編碼需要滿足兩個要求,一是所有字符串長度都小於設定值值64字節(可以在配置文件中修改list-max-ziplist-value字段改變)。二是所存元素數量小於設定值512個(可以在配置文件中修改list-max-ziplist-entries字段改變)。ziplist類似與python中的list,佔用一段連續的內存地址,由此減小指針內存佔用。

    zlbytes:占內存總數

    zltail:到尾部的偏移量

    zllen:內部節點數

    node:節點

    zlend:尾部標識

    previous_entry_length:前一節點的長度

    encoding:數據類型

    content:真實數據

    遍歷的時候會根據zlbytes和zltail直接找到尾部節點nodeN,然後根據每個節點的previous_entry_length反向遍歷。增加和刪除節點會導致其他節點連鎖更新,因為每個節點都存儲了前一節點的長度。

  • linkedlist

    linkedlist有三個屬性,head,tail和len。head指向鏈表的頭部,tail指向鏈表的尾部,len為鏈表的長度。

2.3 哈希類型對象(Hash)

哈希類型對象的模型:

redis的value類型hash類型,其實就是map類型,就是在值的位置放一個map類型的數據。大家想詳細了解一下,可以參考一下這篇文章:https://www.jianshu.com/p/658365f0abfc 。

2.4 集合對象(Set)

集合對象類型的模型:

Set類型的value保證每個值都不重複。

redis中的集合對象底層有兩種實現方式,分別有整數集合和hashtable。當所有元素都是整數且元素數小於512(可在配置文件中set-max-intset-entries字段配置)時採用整數集合實現,其餘情況都採用hashtable實現。hashtable請移駕上文鏈接查閱,接下來介紹整數集合intset。intset有三個屬性,encoding:記錄数字的類型,有int16,int32和int64等,length:記錄集合的長度,content:存儲具體數據。具體結構如下圖:

2.5 有序集合對象

有序集合對象(zset)和集合對象(set)沒有很大區別,僅僅是多了一個分數(score)用來排序。

redis中的有序集合底層採用ziplist和skiplist跳錶實現,當所有字符串長度都小於設定值值64字節(可以在配置文件中修改list-max-ziplist-value字段改變),並且所存元素數量小於設定值512個(可以在配置文件中修改list-max-ziplist-entries字段改變)使用ziplist實現,其他情況均使用skiplist實現,跳躍表的實現原理這裏偷個懶,給大家推薦一篇寫的非常好的博客,點擊查看跳躍表原理。

3. Redis的安裝

可以去官網或者中文網下載Redis。redis的windows版本現在已經不更新了,所以我們安裝redis的6.0.3版本,這個版本支持的東西很多,在此次教程中,我們只對redis的五種數據類型做解釋和學習。

官網:https://redis.io/

中文網:https://www.redis.net.cn/

本教程安裝的redis版本為6.0.3版本,redis使用C語言編寫的,CentOS7的gcc自帶版本為4.8.5,而redis6.0+需要的gcc版本為5.3及以上,所以需要升級gcc版本。

下載Linux版本的tar.gz包,解壓以後進入解壓產生的包:

cd redis-6.0.3

發現沒有bin目錄,這裏需要通過make進行安裝。

# 先檢查gcc的環境 
gcc -v 
# 查看gcc版本 
yum -y install centos-release-scl 
# 升級到9.1版本 
yum -y install devtoolset-9-gcc devtoolset-9-gcc- c++ devtoolset-9-binutils 

scl enable devtoolset-9 bash 
#以上為臨時啟用,如果要長期使用gcc 9.1的話: 
echo "source /opt/rh/devtoolset-9/enable" >>/etc/profile 
# 進入redis解壓文件 
make 
# 6.0的坑,gcc版本 9.0 以上
# 等待完畢

執行完make操作之後,就可以在redis目錄看到src目錄了。進入src目錄后就可以看到redis-serverredis-cli

這裏建議將Redis的配置文件複製,保留一份原生的配置文件。

redis的配置大家可以在網上搜一下常用的配置,在這裏給大家推薦一個常用的配置,比較詳細:

https://blog.csdn.net/ymrfzr/article/details/51362125

到這裏redis就可以啟動並且正常訪問了。

注意:一定要將redis的IP地址綁定註釋掉,允許所有的IP地址訪問,不然我們從Windows訪問就訪問不了。

註釋掉下面的這一行:

同時關閉Redis的服務保護模式,將protected-mode設置為no。如下:

4. Spring Boot 整合 Redis

  • 4.1 搭建工程,引入依賴

    搭建工程的操作我這裏就不在寫出來了。直接上pom.xml

    <!--springboot父工程-->
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.2.2.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    
    <dependencies>
        <!--springboot-web組件-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
            <version>2.2.2.RELEASE</version>
        </dependency>
        <!--redis整合springboot組件-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
            <version>2.3.0.RELEASE</version>
        </dependency>
        <!--lombok組件-->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.10</version>
        </dependency>
    </dependencies>
    
  • 4.2 redis的配置

    項目的配置文件,application.yml

    butterflytri:
      host: 127.0.0.1
    server:
      port: 8080 # 應用端口
      servlet:
        context-path: /butterflytri # 應用映射
    spring:
      application:
        name: redis # 應用名稱
      redis:
        host: ${butterflytri.host} # redis地址
        port: 6379 # redis端口,默認是6379
        timeout: 10000 # 連接超時時間(ms)
        database: 0 # redis默認情況下有16個分片,這裏配置具體使用的分片,默認是0
        jedis: # 使用連接redis的工具-jedis
          pool:
            max-active: 8 # 連接池最大連接數(使用負值表示沒有限制) 默認 8
            max-wait: -1 # 連接池最大阻塞等待時間(使用負值表示沒有限制) 默認 -1
            max-idle: 8 # 連接池中的最大空閑連接 默認 8
            min-idle: 0 # 連接池中的最小空閑連接 默認 0
    

    另外還有額外的配置類RedisConfig.java

    package com.butterflytri.config;
    
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.data.redis.connection.RedisConnectionFactory;
    import org.springframework.data.redis.core.RedisTemplate;
    import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
    import org.springframework.data.redis.serializer.RedisSerializer;
    import org.springframework.data.redis.serializer.StringRedisSerializer;
    
    /**
     * @author: WJF
     * @date: 2020/5/24
     * @description: RedisConfig
     */
    
    @Configuration
    public class RedisConfig {
    
        /**
         * redis鍵值對的值的序列化方式:通用方式
         * @return RedisSerializer
         */
        private RedisSerializer redisValueSerializer() {
            return new GenericJackson2JsonRedisSerializer();
        }
    
        /**
         * redis鍵值對的健的序列化方式:所有的健都是字符串
         * @return RedisSerializer
         */
        private RedisSerializer redisKeySerializer() {
            return new StringRedisSerializer();
        }
    
        @Bean("redisTemplate")
        public RedisTemplate redisTemplate(RedisConnectionFactory redisConnectionFactory) {
            RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
            redisTemplate.setConnectionFactory(redisConnectionFactory);
            redisTemplate.setKeySerializer(redisKeySerializer());
            redisTemplate.setValueSerializer(redisValueSerializer());
            return redisTemplate;
        }
    
    }
    
  • 4.3 redisTemplate的使用

    value類型的值的CRUD:

    ValueServiceImpl.java

    package com.butterflytri.service.impl;
    
    import com.butterflytri.service.ValueService;
    import org.springframework.data.redis.core.RedisTemplate;
    import org.springframework.stereotype.Service;
    
    import javax.annotation.Resource;
    
    /**
     * @author: WJF
     * @date: 2020/5/27
     * @description: ValueServiceImpl
     */
    @Service
    public class ValueServiceImpl implements ValueService {
    
        @Resource
        private RedisTemplate<String, Object> redisTemplate;
    
        @Override
        public void addValue(String key, Object value) {
            redisTemplate.opsForValue().set(key,value);
        }
    
        @Override
        public Object get(String key) {
            return redisTemplate.opsForValue().get(key);
        }
    
        @Override
        public Object update(String key, Object newValue) {
            return redisTemplate.opsForValue().getAndSet(key,newValue);
        }
    
        @Override
        public void delete(String key) {
            redisTemplate.delete(key);
        }
    }	
    

    List類型的值的CRUD:

    這裏我加了枚舉類型用來控制增加的位置,因為List類型對應的是鏈表。

    ListServiceImpl.java

    package com.butterflytri.service.impl;
    
    import com.butterflytri.enums.OpsType;
    import com.butterflytri.service.ListService;
    import org.springframework.data.redis.core.RedisTemplate;
    import org.springframework.stereotype.Service;
    
    import javax.annotation.Resource;
    import java.util.List;
    
    /**
     * @author: WJF
     * @date: 2020/5/28
     * @description: ListServiceImpl
     */
    @Service
    public class ListServiceImpl implements ListService {
    
        @Resource
        private RedisTemplate<String, Object> redisTemplate;
    
        @Override
        public void addList(String key, List<Object> list, OpsType type) {
            switch (type) {
                case RIGHT:
                    redisTemplate.opsForList().rightPushAll(key, list);
                    break;
                case LEFT:
                    redisTemplate.opsForList().leftPushAll(key, list);
                    break;
                default:
                    throw new RuntimeException("type不能為null");
            }
        }
    
        @Override
        public void add(String redisKey, Object value, OpsType type) {
            switch (type) {
                case RIGHT:
                    redisTemplate.opsForList().rightPush(redisKey, value);
                    break;
                case LEFT:
                    redisTemplate.opsForList().leftPush(redisKey, value);
                    break;
                default:
                    throw new RuntimeException("type不能為null");
            }
        }
    
        @Override
        public List<Object> get(String key) {
            return redisTemplate.opsForList().range(key, 0, -1);
        }
    
        @Override
        public Object update(String key, Object value, Integer index) {
            Object obj = redisTemplate.opsForList().index(key, index);
            redisTemplate.opsForList().set(key,index,value);
            return obj;
        }
    
        @Override
        public void delete(String key) {
            redisTemplate.delete(key);
        }
    
        @Override
        public void deleteValue(String redisKey, OpsType type) {
            switch (type) {
                case RIGHT:
                    redisTemplate.opsForList().rightPop(redisKey);
                    break;
                case LEFT:
                    redisTemplate.opsForList().leftPop(redisKey);
                    break;
                default:
                    throw new RuntimeException("type不能為null");
            }
        }
    }
    

    Hash類型的值的CRUD:

    hash類型是我們使用最常用的類型。

    HashServiceImpl.java:

    package com.butterflytri.service.impl;
    
    import com.butterflytri.service.HashService;
    import org.springframework.data.redis.core.RedisTemplate;
    import org.springframework.stereotype.Service;
    
    import javax.annotation.Resource;
    import java.util.Map;
    
    /**
     * @author: WJF
     * @date: 2020/5/28
     * @description: HashServiceImpl
     */
    @Service
    public class HashServiceImpl implements HashService {
    
        @Resource
        private RedisTemplate<String, Object> redisTemplate;
    
        @Override
        public void addHashAll(String key, Map<String, Object> value) {
            redisTemplate.opsForHash().putAll(key, value);
        }
    
        @Override
        public void addHash(String redisKey, String key, Object value) {
            redisTemplate.opsForHash().put(redisKey, key, value);
        }
    
        @Override
        public Object get(String redisKey, String key) {
            return redisTemplate.opsForHash().get(redisKey, key);
        }
    
        @Override
        public Object update(String redisKey, String key, Object value) {
            Object obj = this.get(redisKey, key);
            this.delete(redisKey,key);
            redisTemplate.opsForHash().put(redisKey, key, value);
            return obj;
        }
    
        @Override
        public void delete(String redisKey, String key) {
            redisTemplate.opsForHash().delete(redisKey, key);
        }
    
        @Override
        public void deleteAll(String redisKey) {
            redisTemplate.delete(redisKey);
        }
    }
    

    Set的值的CRUD:

    package com.butterflytri.service.impl;
    
    import com.butterflytri.service.SetService;
    import org.springframework.data.redis.core.RedisTemplate;
    import org.springframework.stereotype.Service;
    
    import javax.annotation.Resource;
    import java.util.Set;
    
    /**
     * @author: WJF
     * @date: 2020/5/28
     * @description: SetServiceImpl
     */
    @Service
    public class SetServiceImpl implements SetService {
    
        @Resource
        private RedisTemplate<String, Object> redisTemplate;
    
    
        @Override
        public void addAll(String key, Set<Object> set) {
            redisTemplate.opsForSet().add(key,set);
        }
    
        @Override
        public void add(String key, Object value) {
            redisTemplate.opsForSet().add(key,value);
        }
    
        @Override
        public Set<Object> findAll(String key) {
            return redisTemplate.opsForSet().members(key);
        }
    
        @Override
        public void deleteValue(String key, Object value) {
            redisTemplate.opsForSet().remove(key,value);
        }
    
        @Override
        public void delete(String key) {
            redisTemplate.delete(key);
        }
    }
    

    ZSet類型的值的CRUD:

    package com.butterflytri.service.impl;
    
    import com.butterflytri.service.SortedSetService;
    import org.springframework.data.redis.core.RedisTemplate;
    import org.springframework.stereotype.Service;
    
    import javax.annotation.Resource;
    import java.util.LinkedHashSet;
    
    /**
     * @author: WJF
     * @date: 2020/5/28
     * @description: SortedSetServiceImpl
     */
    @Service
    public class SortedSetServiceImpl implements SortedSetService {
    
        @Resource
        private RedisTemplate<String, Object> redisTemplate;
    
        @Override
        public void add(String key, String value, Double score) {
            redisTemplate.opsForZSet().add(key, value, score);
        }
    
        @Override
        public LinkedHashSet<Object> findAll(String key) {
            return (LinkedHashSet<Object>) redisTemplate.opsForZSet().range(key,0,-1);
        }
    
        @Override
        public Long count(String key, Double scoreFrom, Double scoreTo) {
            return redisTemplate.opsForZSet().count(key,scoreFrom,scoreTo);
        }
    
        @Override
        public LinkedHashSet<Object> findByScore(String key, Double scoreFrom, Double scoreTo) {
            return (LinkedHashSet<Object>) redisTemplate.opsForZSet().rangeByScore(key,scoreFrom,scoreTo);
        }
    
        @Override
        public Long rank(String key, Object value) {
            return redisTemplate.opsForZSet().rank(key,value);
        }
    
        @Override
        public void remove(String key, String value) {
            redisTemplate.opsForZSet().remove(key,value);
        }
    
        @Override
        public void delete(String key) {
            redisTemplate.delete(key);
        }
    
    }
    

    redis的Java客戶端有很多,在這裏我們使用的是jedis,還有一個很好的Java語言的客戶端叫lettuce,大家可以去了解一下,Spring從不重複造輪子,只會簡化輪子的使用,redisTemplate就是一個超級簡單的使用實現。到這裏redis整合Spring Boot 就結束了。

5. 項目地址

本項目傳送門:

  • GitHub —> spring-data-redis
  • Gitee —> spring-data-redis

此教程會一直更新下去,覺得博主寫的可以的話,關注一下,也可以更方便下次來學習。

  • 作者:Butterfly-Tri
  • 出處:Butterfly-Tri個人博客
  • 版權所有,歡迎保留原文鏈接進行轉載

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

網頁設計公司推薦不同的風格,搶佔消費者視覺第一線

※Google地圖已可更新顯示潭子電動車充電站設置地點!!

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

※別再煩惱如何寫文案,掌握八大原則!

網頁設計最專業,超強功能平台可客製化

聚甘新

如何只用5分鐘完成數據 列表、創建頁面

前言

我們當然希望能夠更快的完成我們的工作,這樣我們才能有更多的時間做其他的事情,比如說測試、學習、放鬆。

背景

軟件一般也就這麼幾個方面的工作要做,增、刪、改、查。如果歸結到頁面上來說,那麼無非也就這麼幾個頁面Form頁面(增)、列表頁面(查、刪)、編輯頁面(改)。很大程度上,你的項目就是由不同的實體的這麼幾個頁面組裝起來的。既然他們都是這麼幾個頁面,那麼,我們是不是可以考慮針對這幾個頁面進行抽象呢?然後使用數據描述這幾個頁面的行為。

效果

經典倒敘,先上效果圖

列表頁面

創建頁面

目前就簡單實現了列表頁面和創建頁面。編輯頁面,跟創建頁面太像了。暫時還沒有實現相關內容,不過,這個不是很重要了。

實現過程

需求分析

其實,每個頁面都是存在固定的路數的。

比如說:

列表頁面裡邊主要存在這麼幾個參數:列表名、列表頭上的按鈕、列表的表頭、列表內容、列表每一行中的操作、分頁控件。

表單頁面列表主要存在這麼幾個參數:表單名、表單內容項。

主要的參數出現的位置都是固定的。但是什麼地方出現什麼內容則是可以變化的,一般情況下,我們都是通過代碼,一遍一遍的重寫這些頁面,然後來達到不同的應用之間的變化的目的。其實我們是可以通過數據來描述他們的。比如說向下面這樣。

列表頁面的定義

Form表單頁面的定義

原始數據的定義

然後將這些定義好的屬性通過後端渲染到頁面上。

就可以達到,前邊展示的這種效果了。

數據存儲

因為數據類型是自定義的,所以數據存儲的字段也是可以自己隨便預設的。然後系統就可以直接支持這一數據類型。在這個Demo裡邊,我是簡單粗暴的使用了文件存儲Json文件的方式來進行保存的數據。

其實應該鏈接數據庫的。不過我在Demo項目裡邊留下了相關的接口,只要再實現一個數據庫版本的實例就可以無縫對接了。

其實

當然了這隻是他的最初級的形態,因為現在寫的配置文件都是通過手寫來實現,將來可以做一個編輯器。並且可以實時看到調整過的效果。

其實這個做法,是來源於PaaS項目中的一個很小很小的功能塊。真正的PaaS項目這一整套東西都是在線上直接編輯看效果的。

最後

系列

這個項目將來會融入到我寫的PaaS Demo中作為前端展示部分。 系列的目錄在 https://juejin.im/post/5eca2a186fb9a047e96b2884 這個部分會一點點完善。

開源

雖然東西不大,但是還是希望能給你一點點啟發。 項目地址 https://gitee.com/anxin1225/Dov.GenericWeb

簡單的體驗

部署到雲端了,可以簡單體驗一下。

http://gw.ash50p.com/Generic/Meeting.Record/List

轉載莫忘原文地址:https://juejin.im/post/5eeb85b8e51d45740850f755

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※自行創業缺乏曝光? 網頁設計幫您第一時間規劃公司的形象門面

網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

※想知道最厲害的網頁設計公司“嚨底家”!

※別再煩惱如何寫文案,掌握八大原則!

※產品缺大量曝光嗎?你需要的是一流包裝設計!

聚甘新

80386學習(五) 80386分頁機制與虛擬內存

一. 頁式內存管理介紹

  80386能夠將內存分為不同屬性的段,並通過段描述符、段表以及段選擇子等機制,通過段基址和段內偏移量計算出線性地址進行訪問,這一內存管理方式被稱為段式內存管理

  這裏要介紹的是另一種內存管理的方式:80386在開啟了分頁機制后,便能夠將物理內存劃分為一個個大小相同且連續的物理內存頁,訪問時通過物理內存頁號和頁內偏移計算出最終需要訪問的線性地址進行訪問,由於內存管理單元由段變成了頁,因此這一內存管理方式被稱為頁式內存管理

  80386的分頁機制只能在保護模式下開啟。

為什麼需要頁式內存管理?

  在介紹80386分頁機制前,需要先理解為什麼CPU在管理內存時,要在段式內存管理的基礎上再引入一種有很大差異的頁式內存管理方式?頁式內存管理與純段式內存管理相比到底具有哪些優點?

  一個很重要的原因是為了解決多任務環境下,段式內存管理中多任務的創建與終止時會產生較多內存碎片,使得內存空間使用率不高的問題

  內存碎片分為外碎片和內碎片兩種。

外碎片

  對於指令和數據的訪問通常都是連續的,所以需要為一個任務分配連續的內存空間。在段式內存管理中,通常為任務分配一個完整的內存段,或是按照任務內段功能的不同,分配包括代碼段、數據段和堆棧段在內的多個完整連續段空間。支持多道任務的系統分配的內存空間,會在某些任務退出並釋放內存時,產生外部內存碎片。

  舉個例子,假設當前存在10MB的內存空間,存在A/B/C/D四個任務,併為每個任務分配一整塊的內存空間,其所佔用的內存空間分別為3MB/2MB/4MB/1MB,如下圖所示(一個格子代表1MB內存)。

  當任務B和任務D執行完成后,所佔用的內存空間被釋放,10MB的內存空間中出現了3MB大小的空閑內存。如果此時出現了一個任務E,需要為其分配3MB的內存空間,此時內存雖然存在3MB的內存空間,卻由於空閑內存的不連續,碎片化,導致無法直接分配給任務E使用。而這裏任務B、任務D結束后釋放的空餘內存空間就被視為外碎片。

  這裏的例子任務數量少且內存空間也很小。而在實際的32位甚至64位的系統中,物理內存空間少則4GB,多則幾十甚至上百GB,由於任務內存的反覆分配和釋放,導致出現的外碎片的數量及浪費的內存空間會很多,很大程度上降低了內存空間的利用率。

  雖然理論上能夠通過操作系統小心翼翼的挪動內存,使得外碎片能夠拼接為連續的大塊,得以被有效利用(內存緊縮)。但是操作系統挪動、複製內存本身很佔用CPU資源,且存在對指令進行地址重定位、暫時暫停對所挪動內存區域的訪問等附加問題,造成的效率降低程度幾乎是不可忍受的,因此這一解決方案並沒有被廣泛使用。

  

內碎片

  外碎片指的是不同任務內存之間的碎片,而內碎片指的是一個任務內產生的內存碎片。

  通常操作系統為了管理多任務環境下的物理內存,會將內存分隔為固定大小的分區,使用系統表記錄對應分區內存的使用情況(如是否已分配等)。分區的大小必須適當,如果分區過小,則相同物理內存大小下,系統表項過多使得所佔用的空間過大;可如果分區過大,則會產生過大的內碎片,造成不必要的內存空間浪費。

  以上述介紹外碎片的數據為例,系統中的內存分區固定大小為1MB,其中為任務C分配了4個內存分區,共4MB大小。可實際上任務C實際只需要3.5MB的空間即可滿足需求,但由於分區是內存管理的最小單元,只能為任務分配整數個的內存分區。3個分區3MB並不滿足任務C的3.5MB的內存需求,因此只能分配4個分區給任務C。而這裏任務C額外多佔用的0.5MB內存就是內碎片。 

  內碎片就是已經被分配出去,卻不能被有效利用的內存空間。

80386是如何解決內存碎片問題的?

外碎片的解決

  外碎片問題產生的主要原因是程序所需要分配的內存空間是連續的。為此,80386提供了分頁機制,使得最終分配給任務的物理內存空間可以不連續。如果任務所使用的內存不必連續,前面外碎片例子中提到的任務E就能夠在1MB+2MB的離散物理內存上正常運行,外碎片問題自然就得到了解決。

內碎片的解決

  內碎片從本質上來說是很難完全避免的(內存管理最小單元不能過小),主要的問題在於前面提到的內存分區管理單元大小的較優值不好確定。開啟了分頁管理的80386,允許將物理內存分割最小為4KB固定大小的管理單元,這個固定大小的內存管理單元被稱為頁,並由專門的被稱為頁表的數據結構來追蹤內存頁的使用情況。

  對於頁表項過多的問題,80386的設計者提供了多級頁表機制,減少了頁表所佔用的空間。

  對於內碎片過大的問題,由於80386所運行的任務所佔用的內存段一般遠大於一個內存頁的大小,因此頁機制下所產生的內部碎片是十分有限的,可以達到一個令人滿意的內存使用率。

二. 虛擬內存簡單介紹 

  為了解決應用程序高速增長的內存需求與物理內存增加緩慢的矛盾,計算機科學家們提供了虛擬內存的概念。使用了虛擬內存的系統,可以使得系統內運行的程序所佔用的內存空間總量,遠大於實際物理內存的容量。

  能夠實現虛擬內存的關鍵在於程序在特定時刻所需要訪問的內存地址是符合局部性原理的。通過操作系統和硬件的緊密配合,能夠將任務暫時不需要訪問的內存交換到外部硬盤中,而將物理內存留給真正需要訪問的那部分內存(工作集內存)。

  虛擬內存和分頁機制是一對好搭檔,分頁機制提供了管理內存的基本單位:頁,80386的頁式虛擬內存實現在工作集內存調度時也依賴分頁機制提供的頁來進行。隨着程序的執行,程序的工作集內存在動態變化,當CPU檢測到當前所訪問的內存頁不在物理內存中時,便會通知操作系統(內存缺頁異常),操作系統的缺頁異常處理程序會將硬盤交換區中的對應內存頁數據寫回物理內存。如果物理內存頁已經滿了的情況下,則還需要根據某種算法將另一個物理內存頁替換,來容納這一換入的內存頁。

三. 80386分頁機制原理

  在介紹分頁機制原理之前,需要先理解關於80386保護模式下32位內存尋址時幾種地址的概念。

物理地址(Physical Address):

  物理地址就是32位的地址總線所對應的真實的硬件存儲空間。對於物理內存的訪問,無論中間會經過多少次轉換,最終必須轉換為最終的物理地址進行訪問。

邏輯地址(Logical Address):

  在80386保護模式的程序指令中,對內存的訪問是由段選擇子和段內偏移決定的。段選擇子+段內偏移 –> 邏輯地址。

線性地址(Linear Address):

  CPU在內存尋址時,從指令中獲得段選擇子和段內偏移,即邏輯地址。由段選擇子在段表(GDT或LDT)中找到對應的段描述符,獲取段基址。段基址+段內偏移決定線性地址。

  如果沒有開啟分頁,CPU就使用生成的線性地址直接作為最終的物理地址進行訪問;如果開啟了分頁,則還需要通過頁表等機制,將線性地址進一步處理才能生成物理地址進行訪問。

頁式虛擬內存實現原理

  程序要求訪問一個段時,其線性地址必須是連續的。在純粹的段式內存管理中,線性地址等於物理地址的情況下,就會出現外碎片的問題。而在段式內存管理的基礎上,80386如果還開啟了頁機制,就能通過抽象出一層線性地址到物理地址的映射,使得最終分配給程序的物理內存段不必連續。

  80386中的內存頁大小為4KB,在32位的內存尋址空間中(4GB),存在着0x10000 = 1048576個頁。每個頁對應的起始地址低12位都為0,第一個物理內存頁的物理地址為0x00000000,第二個物理內存頁的物理地址為0x00001000,依此類推,最後一個物理頁的物理地址是0xFFFFF000。

頁表

  在80386的分頁機制的實現中,是通過頁表來實現線性地址到物理地址映射轉換的。每個任務都有一個自己的頁表記錄著任務的線性地址到物理地址的映射關係。

  開啟了頁機制后的線性地址也被稱為虛擬地址,這是因為線性地址已經不再直接對應真實的物理地址,而是一個不承載真實數據的虛擬內存地址。開啟了分頁機制后,一個任務的虛擬地址空間依然是連續的,但所佔用的物理地址空間卻可以不連續

  頁表保存着被稱為頁表項的數據結構集合,每一個頁表項都記載着一個虛擬內存頁到物理內存頁的映射關係。開啟了頁機制之後,CPU在內存尋址時,在通過段表計算出了線性地址(虛擬地址)后,便可以在連續排布的虛擬地址空間中找到對應的頁表項,通過頁表項獲取虛擬內存頁所對應的物理內存頁地址,進行物理內存的訪問。虛擬地址到物理地址映射的細節會在後面進行展開。

  由於是將不斷變化的虛擬內存頁裝載進相對不變的物理內存頁中,就像畫廊中展示的畫會不斷的更替,但畫框基本不變一樣。為了更好的區分這兩者,頁通常特指虛擬內存頁,而物理內存頁則被稱為頁框。

頁表項介紹

  頁表項是32位的,其結構如下圖所示。

  

P位:

  P(Present),存在位。標識當前虛擬內存頁是否存在於物理內存頁中。當P位為1時,表示當前虛擬內存頁存在於物理內存中,可以直接進行訪問。當P位為0時,表示對應的物理內存頁不存在,需要新分配物理內存頁或是從磁盤中將其調度回物理內存。

  分頁模式下的內存尋址,如果CPU發現對應的頁表項P位為0,會引發缺頁異常中斷,操作系統在缺頁異常處理程序中進行對應的處理,以實現虛擬內存。

RW位:

  RW(Read/Write)位,讀寫位。標識當前頁是否能夠寫入。當RW為1時,代表當前頁可讀可寫;當RW為0時,代表當前頁是只讀的。

US位:

  US(User/Supervisor)位,用戶/管理位。當US為1時,標識當前頁是用戶級別的,允許所有當前特權級的任務進行訪問。當US為0時,表示當前頁是屬於管理員級別的,只允許當前特權級為0、1、2的任務進行訪問,而當前特權級為3的用戶態任務無法進行訪問。

PWT位/PCD位:

  PWT(Page-level Write Through)位,頁級通寫位。PWT為1時,表示當前物理頁的高速緩存採用通寫法;PWT為0時,表示當前物理頁的高速緩存採用回寫法。

  PCD(Page-level Cache Disable)位,頁級高速緩存禁止位。PCD為1時,表示訪問當前物理頁禁用高速緩存;PCD為0時,表示訪問當前物理頁時允許使用高速緩存。

  PWT與PCD位的使用,涉及到了80386高速緩存的工作原理與內存一致性問題,限於篇幅不在這裏展開。

A位:

  A(Access)位,訪問位。A位為1時,代表當前頁曾經被訪問過;A位為0時,代表當前頁沒有被訪問過。

  A位的設置由CPU固件在對應內存頁訪問時自動設置為1,且可以由操作系統在適當的時候通過程序指令重置為0,用以計算內存頁的訪問頻率。通過訪問頻率,操作系統能夠以此作為虛擬內存調度算法中評估的依據,在物理內存緊張的情況下,可以選擇將最少使用的內存頁換出,以減少不必要的虛擬內存頁調度時的磁盤I/O,提高虛擬內存的效率。

D位:

  D(Dirty)位,臟位。當D位為1時,表示當前頁被寫入修改過;D位為0時,代表當前頁沒有被寫入修改過。

  臟位由CPU在對應內存頁被寫入時自動設置為1。操作系統在進行內存頁調度時,如果發現需要被換出的內存頁D位為1時,則需要將對應物理內存頁數據寫回虛擬頁對應的磁盤交換區,保證磁盤/內存數據的一致性;當發現需要被換出的物理內存頁的D位為0時,表示當前頁自從換入物理內存以來沒有被修改過,和磁盤交換區中的數據一致,便直接將其覆蓋,而不進行磁盤的寫回,減少不必要的I/O以提高效率。

PAT位:

  PAT(Page Attribute Table),頁屬性表支持位。PAT位的存在使得CPU能夠支持更複雜的,不同頁大小的分頁管理。當PAT=0時,每一頁的大小為4KB;當PAT=1時,每一頁的大小是4MB,或是其它大小(分CPU的情況而定)。

G位:

  G(Global),全局位。表示當前頁是否是全局的,而不是屬於某一特定任務的。G=1時,表示當前頁是全局的;G=0時,表示當前頁是屬於特定任務的。

  為了加速頁表項的訪問,80386提供了TLB快表,作為頁表訪問的高速緩存。當任務切換時,TLB內所有G=0的非全局頁將會被清除,G=1的全局頁將會被保留。將操作系統內核中關鍵的,頻繁訪問的頁設置為全局頁,使得其能夠一直保存在TLB快表中,加速對其的訪問速度,提高效率。

AVL位:

  AVL(Avaliable),可用位。和段描述符中的AVL位功能類似,CPU並不使用它,而是提供給操作系統軟件自定義使用。

頁物理基地址字段:     

  頁物理基地址字段用於標識對應的物理頁,共20位。

  由於32位的80386的頁最小是4KB,而4GB的物理內存被分解為了最多0x10000個4KB的物理頁。20位的頁物理基地址字段作為物理頁的索引標號與每一個具體的物理頁一一對應。通過頁物理基地址字段,便能找到唯一對應的物理內存頁。

多級頁表

  在32位的CPU中,操作系統可以給每個程序分配至多4GB的虛擬內存空間,如果一個內存頁佔4KB,那麼對應的每個程序的頁表中最多需要存放着0x10000個頁表項來進行映射。即使每個頁表項只佔小小的32位共4個字節(4Byte),這依然是一個不小的內存開銷(0x10000個頁表項的大小為4MB)。

  一個應用程序雖然可以被分配4GB的虛擬內存空間,但實際上可能只使用其中的一小部分,例如40MB的大小。通常程序的堆棧段和數據段都分別位於虛擬內存空間的高低兩端,並隨着程序的執行慢慢的向中間擴展,由於頁表項對應與虛擬地址空間的連續性,這就要求任務在執行時必須完整的定義整張頁表。

  可以看到,一級的平面頁表結構存在着明顯的頁表空間浪費的問題。雖然可以要求應用程序不要一下子就以4GB的內存規格進行編程,而是一開始用較小的內存,並在需要更大內存時梯度的申請更大的內存空間,並重新構造數據段和堆棧段以減少每個任務的無用頁表項空間的浪費。但這將頁表空間優化的繁重任務強加給了應用程序,並不是一個好的解決辦法。

  為此,計算機科學家們提出了多級頁表的方案來解決頁表項過多的問題。多級頁表顧名思義,頁表的結構不再是一個一級的平面結構(一級頁表),而是像一顆樹一樣,由頁目錄項節點頁表項節點組成。目錄節點中保存着下一級節點的物理頁地址等信息,恭弘=叶 恭弘子節點中則包含着真正的頁表項信息。查詢頁表項時,從一級頁目錄節點(根目錄)出發,按照一定的規則可以找到對應的下一級子目錄節點,直到查詢出對應的恭弘=叶 恭弘子節點為止。

  

80386頁目錄項介紹

  80386採用的是二級頁表的設計,二級頁表由頁目錄表和頁表共同組成。頁目錄表中存放的是頁目錄項,頁目錄項的大小和頁表項一致,為4字節。

  通過80386指令得到的32位線性地址,其中高20位作為頁表項索引,低12位作為頁內偏移地址(4KB大小的物理頁)。如果採用的是一級頁表結構,20位的頁表項索引能直接找到4MB頁表中的對應頁表項。

  而對於80386二級頁表的設計來說,由於一個物理頁大小為4KB,最多可以容納1024(2^10)個頁表項或者頁目錄項,所以將頁表項索引的高10位作為根目錄頁中頁目錄項的索引值,通過頁目錄項中的頁表項物理頁號可以找到對應的頁表物理頁;再根據頁表項索引的后10位找到頁表中對應的頁表項。

  

80386頁目錄項結構圖

   80386的二級頁表的頁目錄項佔32位,其低12位的含義與頁表項一致。主要區別在於其高20位存放的是下一級頁表的物理頁索引,而不是虛擬地址映射的物理內存頁地址。

  

頁表基址寄存器

  前面提到過,和LDT一樣,每個任務都擁有着自己獨立的頁表。為此80386CPU提供了一個專門的寄存器用於追蹤定位任務自己的頁表,這個寄存器的名稱叫做頁表基址寄存器(Page Directory Base Register,PDBR),也就是控制寄存器CR3。

  由於80386分頁機制使用的是二級頁表,因此PDBR指向的是二級頁表結構中的頁目錄,通過頁目錄表便能夠間接的訪問整個二級頁表。為了效率其中存放的直接就是頁目錄表的32位物理地址,一般由操作系統負責在任務切換時將新任務對應的頁目錄表預先加載進物理內存。

  由於PDBR是和當前任務有關的,在任務切換時會被新任務TSS中的PDBR字段值所替換,指向新任務的頁目錄表,而舊任務的PDBR的值則在保護現場時被存入對應的TSS中。

多級頁表是如何解決頁表項浪費問題的?

  以80386的二級頁表設計為例,最大4GB的虛擬內存空間下,無論如何一級頁目錄表是必須存在的。當不需要為應用程序分配過多的內存時,頁目錄表中的頁目錄項所指向的對應頁表可以不存在,即頁目錄項的P位為0,實際不使用的虛擬內存空間將沒有對應的二級頁表節點,相比一級頁表的設計其浪費的內存會少很多。

  假設需要為一個虛擬地址首尾各需要分配20MB,共佔用40MB內存的任務構建對應的頁表。

  1. 如果使用一級頁表,4GB的虛擬內存空間下需要提供0x10000個頁表項,共4MB,頁表的體積達到了任務自身所需40MB內存的10%,但其中絕大多數的頁表項都是沒用的(P位為0),不會對應實際的物理內存,空間效率很低。

  2. 如果使用二級頁表,除了佔一個物理頁4KB大小的頁目錄表是必須存在的外,其頁目錄表中只有首尾兩項的P位為1,分別指向一個實際存在的頁表(二級節點),頁目錄表中間其它的頁目錄項P位都為0,不需要為這些不會使用到的虛擬地址分配頁表。對於這個40MB的程序來說,其頁表只佔了3個物理頁面,共12KB,空間效率相比一級頁表高很多。

TLB快表

  前面提到了多級頁表所帶來的好處:通過頁表分層,可以減少順序排列的無效頁表項數量,節約內存空間;頁表的層級越多,空間效率也越高。

  計算機領域中,通常並沒有免費的午餐,一個問題的解決,往往會帶來新的問題:多級頁表本質上是一個樹狀結構,每一個節點頁都是離散的,因此每一層級訪問都需要進行一次內存尋址操作,頁表的層級越多,訪問的次數也就越多,虛擬頁地址映射過程也越慢。在32位的80386中,2級頁表下問題還不算特別嚴重;但64位CPU的出現帶來了更大的尋址空間,也需要更多的頁表項,頁表的層級也漸漸的從2級變成了3級、4級甚至更多。頁機制開啟之後,所有的內存尋址都需要經過CPU的頁部件進行轉化才能獲得最終的物理地址,因此這一過程必須要快,不能因為頁表的離散層次訪問就嚴重影響虛擬地址空間到物理地址空間的轉換速度。

  要加快原本相對耗時的查詢操作,一個常用的辦法便是引入緩存。為了加速通用內存的訪問,80386利用局部性原理提供了高速緩存;為了加速多級頁表的頁表項訪問,80386提供了TLB。

  TLB(Translation Lookaside Buffer)直譯為地址轉換後援緩衝器,根據其作用也被稱為頁表緩存或是快表(快速頁表)。TLB中存放着一張表,其中的每一項用於緩存當前任務虛擬頁號和對應頁表項中的關鍵信息,被稱為TLB項。

  TLB的工作原理和高速緩存類似:當CPU訪問某一虛擬頁時,通過虛擬頁號先在TLB中尋找,如果發現對應的TLB項存在,則直接以TLB項中的數據進行物理地址的轉換,這被稱為TLB命中;當發現對應的TLB項不存在時(TLB未命中),則進行內存的訪問,在獲取內存中頁表項數據的同時,也將對應頁表項緩存入TLB中。如果TLB已滿則需要通過某種置換算法選出一個已存在的TLB項將其替換。

  TLB的查詢速度比內存快,但容量相對內存小很多,因此只能緩存數量有限的頁表項。但由於內存訪問的局部性,只要通過合理的設計提高TLB的命中率(通常可以達到90%以上),就能達到很好的效果。 

四. 80386分頁機制下的內存尋址流程

  下面總結一下開啟了分頁機制的80386是如何進行內存尋址的。

  1. CPU首先從內存訪問指令中獲取段選擇子和段內偏移地址

  2. 根據段選擇子從段表(GDT或LDT)中查詢出對應的段描述符

  3. 根據段描述符中的段基址和指令中的段內偏移地址生成32位的線性地址(頁機制下的虛擬地址)

  4. 32位的線性地址根據80386二級頁表的設計,拆分成三個部分:高10位作為頁目錄項索引,中間次高10位作為頁表項索引,低12位作為頁內偏移地址。

  5. 通過高10位的頁目錄項索引從一級頁目錄表中獲取二級頁表的物理頁地址(通過物理頁框號可得),再根據中間10位的頁表項索引找到對應的物理頁框。根據物理頁框號與頁內偏移地址共同生成最終的物理地址,進行物理內存的訪問。

五. 總結

  想要通過學習操作系統來更好的理解計算機程序底層的工作原理,基礎的硬件知識是必須要了解的。紙上得來終覺淺,絕知此事要躬行,在理解了基礎原理后,還需要通過實踐來加深對原理知識的理解,而閱讀相關操作系統的實現源碼就是一個很好的將實踐與原理緊密結合的學習方式。

  希望通過對硬件和操作系統的學習能幫助我打開計算機程序底層運行的神秘黑盒子一窺究竟,在思考問題時能夠換一個角度從底層的視角出發,去更好的理解和掌握上層的應用技術,以避免迷失在快速發展的技術浪潮中。

   

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

※別再煩惱如何寫文案,掌握八大原則!

※教你寫出一流的銷售文案?

※超省錢租車方案

FB行銷專家,教你從零開始的技巧

聚甘新