計算機專業學生常用網站

這篇文章我很早之前就想寫了,但奈何一直沒有合適的時間,也不知道應該怎樣去表達。現在自己是一個大三的學生,學習了兩年多的計算機,我不知道自己算不算合格,和那些初中就搞OI,大三實習就業的大佬們相比,我肯定是不行的,但我覺得我這兩年的學習經歷應該更符合一般的計算機專業學生,可能也更有參考性吧。這裏我主要來分享一下我常用的一些網站,這些網站主要受眾還是學生,關注我的人也大都是同輩的學生,大家可以在評論區討論或補充,這篇博客也會一直更新下去。

1、Google

這個網站就不必說了吧,我一直有一種觀點,搜索資料對計算機專業的學生來說是一種特別重要的能力。現在的世界有那麼多的知識,誰也不可能都學會,咱們的大腦只是一個cache級別的存儲器,想要獲得知識、解決問題,學會使用搜索引擎必不可少!至於google被牆,使用百度也是可以的,但注意對信息要辨偽存真,多去比較。

2、GitHub

這是開發者最為重要的網站了吧,代碼託管網站。大概大一下學期才注意到這個網站,各種資源應有盡有,想要什麼輪子,上去搜就好了。最令我震驚的是,裏面還有這種網課學習資料,清華北大浙大等名校計算機專業所有課程的資料都齊全,互聯網時代真的太便利了。

 

3、Stack Overflow

這個網站也挺有用的,之前我查找一個C++問題,搜索的結果都答不到點上,在上面看到一個美國人的回答才如夢初醒。後來學習過程中遇到什麼 問題,上去搜一下,大概率能搜到答案。缺點也很明顯,這是一個英語網站,大多數回答都是英文,所以要有點計算機英語基礎。至於同樣英文網站的GitHub,我只能感慨,中國程序員太厲害了,中文項目一點也不少,可以說GitHub的繁榮離不開中國人。

 

4、bilibili

或許最初這隻是一個二次元愛好者的聚集地,但現在B站應該可以算是中國的油管吧。我在高中的時候入坑B站,申請會員需要答題就讓我知道這個網站應該有着素質不錯的成員,果然這裡有無數的沙雕網友逗你開心,逛B站也成為我高中生活最快樂的消遣方式。上了大學才發現B站也可以是一個學習網站,甚至中央都表揚過B站。跟着B站上的老師學了高數、離散數學、線性代數、数字邏輯、概率論、計算機組成原理等等,我一般是在課堂上聽一遍,課後快到考試周了跟着B站視頻複習一遍,很穩!我愛死這個小破站了!(不知道大家有沒有注意到我博客背景上的22娘33娘呢?)

5、中國大學MOOC

 這個算是對B站的一個補充的,其實B站中很多學校視頻都是轉自這裏的,很多網課其實是侵權的,隨時可能被B站刪掉。雖然有些可能需要收費,但也還能接受,至少質量有保證。

6、知乎

為什麼要提知乎這個看似很無關的網站?我主要覺得這是一個可以用來增長見識的社區,雖然現在它一點點的向微博貼吧靠近。

知乎給自己的定位是一個網絡問答社區,用戶在上面分享自己專業的知識和見解。據統計與計算機相關的用戶佔總用戶很大的比重,可能大家上班都在划水?

也有很多大佬直接在知乎上寫博客,平時逛逛知乎把握一下行業動向,聽聽大佬吹吹牛逼,很有意思。

7、博客園

這個我立足的地方嘛!我在大一下學期才正式開始寫博客,最初搞ACM,師哥讓我們寫題解,最開始真是不愛寫,隨便應付一下,後來發現寫博客的樂趣,一發不可收拾。每天逛一下,大家說話有好聽,我超級喜歡這裏!

8、CSDN

這應該是一個邁不過去的話題吧,我也不想修什麼優越感,但是現在CSDN真的令人不悅呀!以前聽前輩講起過它的輝煌,現在也不能說是沒落了吧,依舊有龐大的用戶,龐大的流量,也算是一個成功的商業網址了。CSDN最讓我反感的一點是找一個問題,所有答案都是一樣複製粘貼過來的,很難找到原作者,問轉載者詳情問題詳情也無法解答。我不反對轉載別人的博客,畢竟要有開源精神,但轉載能不能貼一個原文鏈接?還有積分下載,講一個笑話,我一個師哥發了一份JDK1.8,5C幣下載,一年賺了300C幣,這是在收智商稅?

9、MSDN 我告訴你

這個網站可就厲害了,整合了許多微軟原版系統鏡像,精簡版本,純凈系統。

10、W3school

就算不去學前端,我們多多少少也要了解一些相關知識,這也算是一個前端只是比較全的網站了。

11、鳩摩搜索

能夠搜索出不少專業書,配合pandownload使用吧,這個給出pandownload的使用說明

12、菜鳥教程

 

 有朋友提到了菜鳥教程,這裏也來介紹一下吧,個人覺得菜鳥教程提供了較為全面,系統詳細的教程,廣告少,界面簡約,聽說網站最早是一個人寫的,很牛。這個網站挺適合新手做輔助,個人覺得就是有些教程獲取不到較為詳細的說明。

13、各大OJ

北京大學

杭州电子科技大學

浙江大學

codeforces

PAT

藍橋杯

 

 

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

【其他文章推薦】

※為什麼 USB CONNECTOR 是電子產業重要的元件?

網頁設計一頭霧水??該從何著手呢? 找到專業技術的網頁設計公司,幫您輕鬆架站!

※想要讓你的商品成為最夯、最多人討論的話題?網頁設計公司讓你強力曝光

※想知道最厲害的台北網頁設計公司推薦台中網頁設計公司推薦專業設計師”嚨底家”!!

新北清潔公司,居家、辦公、裝潢細清專業服務

Java I/O體系從原理到應用,這一篇全說清楚了

本文介紹操作系統I/O工作原理,Java I/O設計,基本使用,開源項目中實現高性能I/O常見方法和實現,徹底搞懂高性能I/O之道

基礎概念

在介紹I/O原理之前,先重溫幾個基礎概念:

  • (1) 操作系統與內核

操作系統:管理計算機硬件與軟件資源的系統軟件
內核:操作系統的核心軟件,負責管理系統的進程、內存、設備驅動程序、文件和網絡系統等等,為應用程序提供對計算機硬件的安全訪問服務

  • 2 內核空間和用戶空間

為了避免用戶進程直接操作內核,保證內核安全,操作系統將內存尋址空間劃分為兩部分:
內核空間(Kernel-space),供內核程序使用
用戶空間(User-space),供用戶進程使用
為了安全,內核空間和用戶空間是隔離的,即使用戶的程序崩潰了,內核也不受影響

  • 3 數據流

計算機中的數據是基於隨着時間變換高低電壓信號傳輸的,這些數據信號連續不斷,有着固定的傳輸方向,類似水管中水的流動,因此抽象數據流(I/O流)的概念:指一組有順序的、有起點和終點的字節集合

抽象出數據流的作用:實現程序邏輯與底層硬件解耦,通過引入數據流作為程序與硬件設備之間的抽象層,面向通用的數據流輸入輸出接口編程,而不是具體硬件特性,程序和底層硬件可以獨立靈活替換和擴展

I/O 工作原理

1 磁盤I/O

典型I/O讀寫磁盤工作原理如下:

tips: DMA:全稱叫直接內存存取(Direct Memory Access),是一種允許外圍設備(硬件子系統)直接訪問系統主內存的機制。基於 DMA 訪問方式,系統主內存與硬件設備的數據傳輸可以省去CPU 的全程調度

值得注意的是:

  • 讀寫操作基於系統調用實現
  • 讀寫操作經過用戶緩衝區,內核緩衝區,應用進程並不能直接操作磁盤
  • 應用進程讀操作時需阻塞直到讀取到數據

2 網絡I/O

這裏先以最經典的阻塞式I/O模型介紹:

tips:recvfrom,經socket接收數據的函數

值得注意的是:

  • 網絡I/O讀寫操作經過用戶緩衝區,Sokcet緩衝區
  • 服務端線程在從調用recvfrom開始到它返回有數據報準備好這段時間是阻塞的,recvfrom返回成功后,線程開始處理數據報

Java I/O設計

1 I/O分類

Java中對數據流進行具體化和實現,關於Java數據流一般關注以下幾個點:

  • (1) 流的方向
    從外部到程序,稱為輸入流;從程序到外部,稱為輸出流

  • (2) 流的數據單位
    程序以字節作為最小讀寫數據單元,稱為字節流,以字符作為最小讀寫數據單元,稱為字符流

  • (3) 流的功能角色

從/向一個特定的IO設備(如磁盤,網絡)或者存儲對象(如內存數組)讀/寫數據的流,稱為節點流
對一個已有流進行連接和封裝,通過封裝后的流來實現數據的讀/寫功能,稱為處理流(或稱為過濾流);

2 I/O操作接口

java.io包下有一堆I/O操作類,初學時看了容易搞不懂,其實仔細觀察其中還是有規律:
這些I/O操作類都是在繼承4個基本抽象流的基礎上,要麼是節點流,要麼是處理流

2.1 四個基本抽象流

java.io包中包含了流式I/O所需要的所有類,java.io包中有四個基本抽象流,分別處理字節流和字符流:

  • InputStream
  • OutputStream
  • Reader
  • Writer

2.2 節點流

節點流I/O類名由節點流類型 + 抽象流類型組成,常見節點類型有:

  • File文件
  • Piped 進程內線程通信管道
  • ByteArray / CharArray (字節數組 / 字符數組)
  • StringBuffer / String (字符串緩衝區 / 字符串)

節點流的創建通常是在構造函數傳入數據源,例如:

FileReader reader = new FileReader(new File("file.txt"));
FileWriter writer = new FileWriter(new File("file.txt"));

2.3 處理流

處理流I/O類名由對已有流封裝的功能 + 抽象流類型組成,常見功能有:

  • 緩衝:對節點流讀寫的數據提供了緩衝的功能,數據可以基於緩衝批量讀寫,提高效率。常見有BufferedInputStream、BufferedOutputStream
  • 字節流轉換為字符流:由InputStreamReader、OutputStreamWriter實現
  • 字節流與基本類型數據相互轉換:這裏基本數據類型數據如int、long、short,由DataInputStream、DataOutputStream實現
  • 字節流與對象實例相互轉換:用於實現對象序列化,由ObjectInputStream、ObjectOutputStream實現

處理流的應用了適配器/裝飾模式,轉換/擴展已有流,處理流的創建通常是在構造函數傳入已有的節點流或處理流:

FileOutputStream fileOutputStream = new FileOutputStream("file.txt");
// 擴展提供緩衝寫
BufferedOutputStream bufferedOutputStream = new BufferedOutputStream(fileOutputStream);
 // 擴展提供提供基本數據類型寫
DataOutputStream out = new DataOutputStream(bufferedOutputStream);

3 Java NIO

3.1 標準I/O存在問題

Java NIO(New I/O)是一個可以替代標準Java I/O API的IO API(從Java 1.4開始),Java NIO提供了與標準I/O不同的I/O工作方式,目的是為了解決標準 I/O存在的以下問題:

  • (1) 數據多次拷貝

標準I/O處理,完成一次完整的數據讀寫,至少需要從底層硬件讀到內核空間,再讀到用戶文件,又從用戶空間寫入內核空間,再寫入底層硬件

此外,底層通過write、read等函數進行I/O系統調用時,需要傳入數據所在緩衝區起始地址和長度
由於JVM GC的存在,導致對象在堆中的位置往往會發生移動,移動後傳入系統函數的地址參數就不是真正的緩衝區地址了

可能導致讀寫出錯,為了解決上面的問題,使用標準I/O進行系統調用時,還會額外導致一次數據拷貝:把數據從JVM的堆內拷貝到堆外的連續空間內存(堆外內存)

所以總共經歷6次數據拷貝,執行效率較低

  • (2) 操作阻塞

傳統的網絡I/O處理中,由於請求建立連接(connect),讀取網絡I/O數據(read),發送數據(send)等操作是線程阻塞的

// 等待連接
Socket socket = serverSocket.accept();

// 連接已建立,讀取請求消息
StringBuilder req = new StringBuilder();
byte[] recvByteBuf = new byte[1024];
int len;
while ((len = socket.getInputStream().read(recvByteBuf)) != -1) {
    req.append(new String(recvByteBuf, 0, len, StandardCharsets.UTF_8));
}

// 寫入返回消息
socket.getOutputStream().write(("server response msg".getBytes()));
socket.shutdownOutput();

以上面服務端程序為例,當請求連接已建立,讀取請求消息,服務端調用read方法時,客戶端數據可能還沒就緒(例如客戶端數據還在寫入中或者傳輸中),線程需要在read方法阻塞等待直到數據就緒

為了實現服務端併發響應,每個連接需要獨立的線程單獨處理,當併發請求量大時為了維護連接,內存、線程切換開銷過大

3.2 Buffer

Java NIO核心三大核心組件是Buffer(緩衝區)、Channel(通道)、Selector

Buffer提供了常用於I/O操作的字節緩衝區,常見的緩存區有ByteBuffer, CharBuffer, DoubleBuffer, FloatBuffer, IntBuffer, LongBuffer, ShortBuffer,分別對應基本數據類型: byte, char, double, float, int, long, short,下面介紹主要以最常用的ByteBuffer為例,Buffer底層支持Java堆內(HeapByteBuffer)或堆外內存(DirectByteBuffer)

堆外內存是指與堆內存相對應的,把內存對象分配在JVM堆以外的內存,這些內存直接受操作系統管理(而不是虛擬機,相比堆內內存,I/O操作中使用堆外內存的優勢在於:

  • 不用被JVM GC線回收,減少GC線程資源佔有
  • 在I/O系統調用時,直接操作堆外內存,可以節省一次堆外內存和堆內內存的複製

ByteBuffer底層堆外內存的分配和釋放基於malloc和free函數,對外allocateDirect方法可以申請分配堆外內存,並返回繼承ByteBuffer類的DirectByteBuffer對象:

public static ByteBuffer allocateDirect(int capacity) {
    return new DirectByteBuffer(capacity);
}

堆外內存的回收基於DirectByteBuffer的成員變量Cleaner類,提供clean方法可以用於主動回收,Netty中大部分堆外內存通過記錄定位Cleaner的存在,主動調用clean方法來回收;
另外,當DirectByteBuffer對象被GC時,關聯的堆外內存也會被回收

tips: JVM參數不建議設置-XX:+DisableExplicitGC,因為部分依賴Java NIO的框架(例如Netty)在內存異常耗盡時,會主動調用System.gc(),觸發Full GC,回收DirectByteBuffer對象,作為回收堆外內存的最後保障機制,設置該參數之後會導致在該情況下堆外內存得不到清理

堆外內存基於基礎ByteBuffer類的DirectByteBuffer類成員變量:Cleaner對象,這個Cleaner對象會在合適的時候執行unsafe.freeMemory(address),從而回收這塊堆外內存

Buffer可以見到理解為一組基本數據類型,存儲地址連續的的數組,支持讀寫操作,對應讀模式和寫模式,通過幾個變量來保存這個數據的當前位置狀態:capacity、 position、 limit:

  • capacity 緩衝區數組的總長度
  • position 下一個要操作的數據元素的位置
  • limit 緩衝區數組中不可操作的下一個元素的位置:limit <= capacity

3.3 Channel

Channel(通道)的概念可以類比I/O流對象,NIO中I/O操作主要基於Channel:
從Channel進行數據讀取 :創建一個緩衝區,然後請求Channel讀取數據
從Channel進行數據寫入 :創建一個緩衝區,填充數據,請求Channel寫入數據

Channel和流非常相似,主要有以下幾點區別:

  • Channel可以讀和寫,而標準I/O流是單向的
  • Channel可以異步讀寫,標準I/O流需要線程阻塞等待直到讀寫操作完成
  • Channel總是基於緩衝區Buffer讀寫

Java NIO中最重要的幾個Channel的實現:

  • FileChannel: 用於文件的數據讀寫,基於FileChannel提供的方法能減少讀寫文件數據拷貝次數,後面會介紹
  • DatagramChannel: 用於UDP的數據讀寫
  • SocketChannel: 用於TCP的數據讀寫,代表客戶端連接
  • ServerSocketChannel: 監聽TCP連接請求,每個請求會創建會一個SocketChannel,一般用於服務端

基於標準I/O中,我們第一步可能要像下面這樣獲取輸入流,按字節把磁盤上的數據讀取到程序中,再進行下一步操作,而在NIO編程中,需要先獲取Channel,再進行讀寫

FileInputStream fileInputStream = new FileInputStream("test.txt");
FileChannel channel = fileInputStream.channel();

tips: FileChannel僅能運行在阻塞模式下,文件異步處理的 I/O 是在JDK 1.7 才被加入的 java.nio.channels.AsynchronousFileChannel

// server socket channel:
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.bind(new InetSocketAddress(InetAddress.getLocalHost(), 9091));

while (true) {
    SocketChannel socketChannel = serverSocketChannel.accept();
    ByteBuffer buffer = ByteBuffer.allocateDirect(1024);
    int readBytes = socketChannel.read(buffer);
    if (readBytes > 0) {
        // 從寫數據到buffer翻轉為從buffer讀數據
        buffer.flip();
        byte[] bytes = new byte[buffer.remaining()];
        buffer.get(bytes);
        String body = new String(bytes, StandardCharsets.UTF_8);
        System.out.println("server 收到:" + body);
    }
}

3.4 Selector

Selector(選擇器) ,它是Java NIO核心組件中的一個,用於檢查一個或多個NIO Channel(通道)的狀態是否處於可讀、可寫。實現單線程管理多個Channel,也就是可以管理多個網絡連接

Selector核心在於基於操作系統提供的I/O復用功能,單個線程可以同時監視多個連接描述符,一旦某個連接就緒(一般是讀就緒或者寫就緒),能夠通知程序進行相應的讀寫操作,常見有select、poll、epoll等不同實現

Java NIO Selector基本工作原理如下:

  • (1) 初始化Selector對象,服務端ServerSocketChannel對象
  • (2) 向Selector註冊ServerSocketChannel的socket-accept事件
  • (3) 線程阻塞於selector.select(),當有客戶端請求服務端,線程退出阻塞
  • (4) 基於selector獲取所有就緒事件,此時先獲取到socket-accept事件,向Selector註冊客戶端SocketChannel的數據就緒可讀事件事件
  • (5) 線程再次阻塞於selector.select(),當有客戶端連接數據就緒,可讀
  • (6) 基於ByteBuffer讀取客戶端請求數據,然後寫入響應數據,關閉channel

示例如下,完整可運行代碼已經上傳github(https://github.com/caison/caison-blog-demo):

Selector selector = Selector.open();
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.bind(new InetSocketAddress(9091));
// 配置通道為非阻塞模式
serverSocketChannel.configureBlocking(false);
// 註冊服務端的socket-accept事件
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);

while (true) {
    // selector.select()會一直阻塞,直到有channel相關操作就緒
    selector.select();
    // SelectionKey關聯的channel都有就緒事件
    Iterator<SelectionKey> keyIterator = selector.selectedKeys().iterator();

    while (keyIterator.hasNext()) {
        SelectionKey key = keyIterator.next();
        // 服務端socket-accept
        if (key.isAcceptable()) {
            // 獲取客戶端連接的channel
            SocketChannel clientSocketChannel = serverSocketChannel.accept();
            // 設置為非阻塞模式
            clientSocketChannel.configureBlocking(false);
            // 註冊監聽該客戶端channel可讀事件,併為channel關聯新分配的buffer
            clientSocketChannel.register(selector, SelectionKey.OP_READ, ByteBuffer.allocateDirect(1024));
        }

        // channel可讀
        if (key.isReadable()) {
            SocketChannel socketChannel = (SocketChannel) key.channel();
            ByteBuffer buf = (ByteBuffer) key.attachment();

            int bytesRead;
            StringBuilder reqMsg = new StringBuilder();
            while ((bytesRead = socketChannel.read(buf)) > 0) {
                // 從buf寫模式切換為讀模式
                buf.flip();
                int bufRemain = buf.remaining();
                byte[] bytes = new byte[bufRemain];
                buf.get(bytes, 0, bytesRead);
                // 這裏當數據包大於byteBuffer長度,有可能有粘包/拆包問題
                reqMsg.append(new String(bytes, StandardCharsets.UTF_8));
                buf.clear();
            }
            System.out.println("服務端收到報文:" + reqMsg.toString());
            if (bytesRead == -1) {
                byte[] bytes = "[這是服務回的報文的報文]".getBytes(StandardCharsets.UTF_8);

                int length;
                for (int offset = 0; offset < bytes.length; offset += length) {
                    length = Math.min(buf.capacity(), bytes.length - offset);
                    buf.clear();
                    buf.put(bytes, offset, length);
                    buf.flip();
                    socketChannel.write(buf);
                }
                socketChannel.close();
            }
        }
        // Selector不會自己從已selectedKeys中移除SelectionKey實例
        // 必須在處理完通道時自己移除 下次該channel變成就緒時,Selector會再次將其放入selectedKeys中
        keyIterator.remove();
    }
}

tips: Java NIO基於Selector實現高性能網絡I/O這塊使用起來比較繁瑣,使用不友好,一般業界使用基於Java NIO進行封裝優化,擴展豐富功能的Netty框架來優雅實現

高性能I/O優化

下面結合業界熱門開源項目介紹高性能I/O的優化

1 零拷貝

零拷貝(zero copy)技術,用於在數據讀寫中減少甚至完全避免不必要的CPU拷貝,減少內存帶寬的佔用,提高執行效率,零拷貝有幾種不同的實現原理,下面介紹常見開源項目中零拷貝實現

1.1 Kafka零拷貝

Kafka基於Linux 2.1內核提供,並在2.4 內核改進的的sendfile函數 + 硬件提供的DMA Gather Copy實現零拷貝,將文件通過socket傳送

函數通過一次系統調用完成了文件的傳送,減少了原來read/write方式的模式切換。同時減少了數據的copy, sendfile的詳細過程如下:

基本流程如下:

  • (1) 用戶進程發起sendfile系統調用
  • (2) 內核基於DMA Copy將文件數據從磁盤拷貝到內核緩衝區
  • (3) 內核將內核緩衝區中的文件描述信息(文件描述符,數據長度)拷貝到Socket緩衝區
  • (4) 內核基於Socket緩衝區中的文件描述信息和DMA硬件提供的Gather Copy功能將內核緩衝區數據複製到網卡
  • (5) 用戶進程sendfile系統調用完成並返回

相比傳統的I/O方式,sendfile + DMA Gather Copy方式實現的零拷貝,數據拷貝次數從4次降為2次,系統調用從2次降為1次,用戶進程上下文切換次數從4次變成2次DMA Copy,大大提高處理效率

Kafka底層基於java.nio包下的FileChannel的transferTo:

public abstract long transferTo(long position, long count, WritableByteChannel target)

transferTo將FileChannel關聯的文件發送到指定channel,當Comsumer消費數據,Kafka Server基於FileChannel將文件中的消息數據發送到SocketChannel

1.2 RocketMQ零拷貝

RocketMQ基於mmap + write的方式實現零拷貝:
mmap() 可以將內核中緩衝區的地址與用戶空間的緩衝區進行映射,實現數據共享,省去了將數據從內核緩衝區拷貝到用戶緩衝區

tmp_buf = mmap(file, len); 
write(socket, tmp_buf, len);

mmap + write 實現零拷貝的基本流程如下:

  • (1) 用戶進程向內核發起系統mmap調用
  • (2) 將用戶進程的內核空間的讀緩衝區與用戶空間的緩存區進行內存地址映射
  • (3) 內核基於DMA Copy將文件數據從磁盤複製到內核緩衝區
  • (4) 用戶進程mmap系統調用完成並返回
  • (5) 用戶進程向內核發起write系統調用
  • (6) 內核基於CPU Copy將數據從內核緩衝區拷貝到Socket緩衝區
  • (7) 內核基於DMA Copy將數據從Socket緩衝區拷貝到網卡
  • (8) 用戶進程write系統調用完成並返回

RocketMQ中消息基於mmap實現存儲和加載的邏輯寫在org.apache.rocketmq.store.MappedFile中,內部實現基於nio提供的java.nio.MappedByteBuffer,基於FileChannel的map方法得到mmap的緩衝區:

// 初始化
this.fileChannel = new RandomAccessFile(this.file, "rw").getChannel();
this.mappedByteBuffer = this.fileChannel.map(MapMode.READ_WRITE, 0, fileSize);

查詢CommitLog的消息時,基於mappedByteBuffer偏移量pos,數據大小size查詢:

public SelectMappedBufferResult selectMappedBuffer(int pos, int size) {
    int readPosition = getReadPosition();
    // ...各種安全校驗
    
    // 返回mappedByteBuffer視圖
    ByteBuffer byteBuffer = this.mappedByteBuffer.slice();
    byteBuffer.position(pos);
    ByteBuffer byteBufferNew = byteBuffer.slice();
    byteBufferNew.limit(size);
    return new SelectMappedBufferResult(this.fileFromOffset + pos, byteBufferNew, size, this);
}

tips: transientStorePoolEnable機制
Java NIO mmap的部分內存並不是常駐內存,可以被置換到交換內存(虛擬內存),RocketMQ為了提高消息發送的性能,引入了內存鎖定機制,即將最近需要操作的CommitLog文件映射到內存,並提供內存鎖定功能,確保這些文件始終存在內存中,該機制的控制參數就是transientStorePoolEnable

因此,MappedFile數據保存CommitLog刷盤有2種方式:

  • 1 開啟transientStorePoolEnable:寫入內存字節緩衝區(writeBuffer) -> 從內存字節緩衝區(writeBuffer)提交(commit)到文件通道(fileChannel) -> 文件通道(fileChannel) -> flush到磁盤
  • 2 未開啟transientStorePoolEnable:寫入映射文件字節緩衝區(mappedByteBuffer) -> 映射文件字節緩衝區(mappedByteBuffer) -> flush到磁盤

RocketMQ 基於 mmap+write 實現零拷貝,適用於業務級消息這種小塊文件的數據持久化和傳輸
Kafka 基於 sendfile 這種零拷貝方式,適用於系統日誌消息這種高吞吐量的大塊文件的數據持久化和傳輸

tips: Kafka 的索引文件使用的是 mmap+write 方式,數據文件發送網絡使用的是 sendfile 方式

1.3 Netty零拷貝

Netty 的零拷貝分為兩種:

  • 1 基於操作系統實現的零拷貝,底層基於FileChannel的transferTo方法
  • 2 基於Java 層操作優化,對數組緩存對象(ByteBuf )進行封裝優化,通過對ByteBuf數據建立數據視圖,支持ByteBuf 對象合併,切分,當底層僅保留一份數據存儲,減少不必要拷貝

2 多路復用

Netty中對Java NIO功能封裝優化之後,實現I/O多路復用代碼優雅了很多:

// 創建mainReactor
NioEventLoopGroup boosGroup = new NioEventLoopGroup();
// 創建工作線程組
NioEventLoopGroup workerGroup = new NioEventLoopGroup();

final ServerBootstrap serverBootstrap = new ServerBootstrap();
serverBootstrap 
     // 組裝NioEventLoopGroup 
    .group(boosGroup, workerGroup)
     // 設置channel類型為NIO類型
    .channel(NioServerSocketChannel.class)
    // 設置連接配置參數
    .option(ChannelOption.SO_BACKLOG, 1024)
    .childOption(ChannelOption.SO_KEEPALIVE, true)
    .childOption(ChannelOption.TCP_NODELAY, true)
    // 配置入站、出站事件handler
    .childHandler(new ChannelInitializer<NioSocketChannel>() {
        @Override
        protected void initChannel(NioSocketChannel ch) {
            // 配置入站、出站事件channel
            ch.pipeline().addLast(...);
            ch.pipeline().addLast(...);
        }
    });

// 綁定端口
int port = 8080;
serverBootstrap.bind(port).addListener(future -> {
    if (future.isSuccess()) {
        System.out.println(new Date() + ": 端口[" + port + "]綁定成功!");
    } else {
        System.err.println("端口[" + port + "]綁定失敗!");
    }
});

3 頁緩存(PageCache)

頁緩存(PageCache)是操作系統對文件的緩存,用來減少對磁盤的 I/O 操作,以頁為單位的,內容就是磁盤上的物理塊,頁緩存能幫助程序對文件進行順序讀寫的速度幾乎接近於內存的讀寫速度,主要原因就是由於OS使用PageCache機制對讀寫訪問操作進行了性能優化:

頁緩存讀取策略:當進程發起一個讀操作 (比如,進程發起一個 read() 系統調用),它首先會檢查需要的數據是否在頁緩存中:

  • 如果在,則放棄訪問磁盤,而直接從頁緩存中讀取
  • 如果不在,則內核調度塊 I/O 操作從磁盤去讀取數據,並讀入緊隨其後的少數幾個頁面(不少於一個頁面,通常是三個頁面),然後將數據放入頁緩存中

頁緩存寫策略:當進程發起write系統調用寫數據到文件中,先寫到頁緩存,然後方法返回。此時數據還沒有真正的保存到文件中去,Linux 僅僅將頁緩存中的這一頁數據標記為“臟”,並且被加入到臟頁鏈表中

然後,由flusher 回寫線程周期性將臟頁鏈表中的頁寫到磁盤,讓磁盤中的數據和內存中保持一致,最後清理“臟”標識。在以下三種情況下,臟頁會被寫回磁盤:

  • 空閑內存低於一個特定閾值
  • 臟頁在內存中駐留超過一個特定的閾值時
  • 當用戶進程調用 sync() 和 fsync() 系統調用時

RocketMQ中,ConsumeQueue邏輯消費隊列存儲的數據較少,並且是順序讀取,在page cache機制的預讀取作用下,Consume Queue文件的讀性能幾乎接近讀內存,即使在有消息堆積情況下也不會影響性能,提供了2種消息刷盤策略:

  • 同步刷盤:在消息真正持久化至磁盤后RocketMQ的Broker端才會真正返回給Producer端一個成功的ACK響應
  • 異步刷盤,能充分利用操作系統的PageCache的優勢,只要消息寫入PageCache即可將成功的ACK返回給Producer端。消息刷盤採用後台異步線程提交的方式進行,降低了讀寫延遲,提高了MQ的性能和吞吐量

Kafka實現消息高性能讀寫也利用了頁緩存,這裏不再展開

參考

《深入理解Linux內核 —— Daniel P.Bovet》

更多精彩,歡迎關注公眾號 分佈式系統架構

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

【其他文章推薦】

USB CONNECTOR掌控什麼技術要點? 帶您認識其相關發展及效能

※評比前十大台北網頁設計台北網站設計公司知名案例作品心得分享

※智慧手機時代的來臨,RWD網頁設計已成為網頁設計推薦首選

※評比南投搬家公司費用收費行情懶人包大公開

※幫你省時又省力,新北清潔一流服務好口碑

Redis 的底層數據結構(壓縮列表)

上一篇我們介紹了 redis 中的整數集合這種數據結構的實現,也談到了,引入這種數據結構的一個很大的原因就是,在某些僅有少量整數元素的集合場景,通過整數集合既可以達到字典的效率,也能使用遠少於字典的內存達到同樣的效果。

我們本篇介紹的壓縮列表,相信你從他的名字里應該也能看出來,又是一個為了節約內存而設計的數據結構,它的數據結構相對於整數集合來說會複雜了很多,但是整數集合只能允許存儲少量的整型數據,而我們的壓縮列表可以允許存儲少量的整型數據或字符串。

這是他們之間的一個區別,下面我們來看看這種數據結構。

一、基本的結構定義

  • ZIPLIST_BYTES:四個字節,記錄了整個壓縮列表總共佔用了多少字節數
  • ZIPLIST_TAIL_OFFSET:四個字節,記錄了整個壓縮列表第一個節點到最後一個節點跨越了多少個字節,通故這個字段可以迅速定位到列表最後一個節點位置
  • ZIPLIST_LENGTH:兩個字節,記錄了整個壓縮列表中總共包含幾個 zlentry 節點
  • zlentry:非固定字節,記錄的是單個節點,這是一個複合結構,我們等下再說
  • 0xFF:一個字節,十進制的值為 255,標誌壓縮列表的結尾

其中,zlentry 在 redis 中確實有着這樣的結構體定義,但實際上這個結構定義了一堆類似於 length 這樣的字段,記錄前一個節點和自身節點佔用的字節數等等信息,用處不多,而我們更傾向於使用這樣的邏輯結構來描述 zlentry 節點。

這種結構在 redis 中是沒有具體結構體定義的,請知悉,網上的很多博客文章都直接描述 zlentry 節點是這樣的一種結構,其實是不準確的。

簡單解釋一下這三個字段的含義:

  • previous_entry_length:每個節點會使用一個或者五個字節來描述前一個節點佔用的總字節數,如果前一個節點佔用的總字節數小於 254,那麼就用一個字節存儲,反之如果前一個節點佔用的總字節數超過了 254,那麼一個字節就不夠存儲了,這裡會用五個字節存儲並將第一個字節的值存儲為固定值 254 用於區分。
  • encoding:壓縮列表可以存儲 16位、32位、64位的整數以及字符串,encoding 就是用來區分後面的 content 字段中存儲於的到底是哪種內容,分別佔多少字節,這個我們等下細說。
  • content:沒什麼特別的,存儲的就是具體的二進制內容,整數或者字符串。

下面我們細說一個 encoding 具體是怎麼存儲的。

主要分為兩種,一種是字符串的存儲格式:

編碼 編碼長度 content類型
00xxxxxx 一個字節 長度小於 63 的字符串
01xxxxxx xxxxxxxx 兩個字節 長度小於 16383 的字符串
10xxxxxx xxxxxxxx xxxxxxxx xxxxxxxx xxxxxxxx 五個字節 長度小於 4294967295 的字符串

content 的具體長度,由編碼除去高兩位剩餘的二進制位表示。

編碼 編碼長度 content類型
11000000 一個字節 int16_t 類型的整數
11010000 一個字節 int32_t 類型的整數
11100000 一個字節 int64_t 類型的整數
11110000 一個字節 24 位有符號整數
11111110 一個字節 8 位有符號整數

注意,整型數據的編碼是固定 11 開頭的八位二進制,而字符串類型的編碼都是非固定的,因為它還需要通過後面的二進制位得到字符串的長度,稍有區別。

這就是壓縮列表的基本的結構定義情況,下面我們通過節點的增刪改查方法源碼實現來看看 redis 中具體的實現情況。

二、redis 的具體源碼實現

1、ziplistNew

我們先來看看壓縮列表初始化的方法實現:

unsigned char *ziplistNew(void) {
    //bytes=2*4+2
    //分配壓縮列表結構所需要的字節數
    //ZIPLIST_BYTES + ZIPLIST_TAIL_OFFSET + ZIPLIST_LENGTH
    unsigned int bytes = ZIPLIST_HEADER_SIZE+1;
    unsigned char *zl = zmalloc(bytes);
    //初始化 ZIPLIST_BYTES 字段
    ZIPLIST_BYTES(zl) = intrev32ifbe(bytes);
    //初始化 ZIPLIST_TAIL_OFFSET
    ZIPLIST_TAIL_OFFSET(zl) = intrev32ifbe(ZIPLIST_HEADER_SIZE);
    //初始化 ZIPLIST_LENGTH 字段
    ZIPLIST_LENGTH(zl) = 0;
    //為壓縮列表最後一個字節賦值 255
    zl[bytes-1] = ZIP_END;
    return zl;
}

2、ziplistPush

接着我們看新增節點的源碼實現:

unsigned char *ziplistPush(unsigned char *zl, unsigned char *s
        ,unsigned int slen, int where) {
    unsigned char *p;
    //找到待插入的位置,頭部或者尾部
    p = (where == ZIPLIST_HEAD) ? ZIPLIST_ENTRY_HEAD(zl) : ZIPLIST_ENTRY_END(zl);
    return __ziplistInsert(zl,p,s,slen);
}

解釋一下 ziplistPush 的幾個入參的含義。

zl 指向一個壓縮列表的首地址,s 指向一個字符串首地址),slen 指向字符串的長度(如果節點存儲的值是整型,存儲的就是整型值),where 指明新節點的插入方式,頭插亦或尾插。

ziplistPush 方法的核心是 __ziplistInsert:

unsigned char *__ziplistInsert(unsigned char *zl, unsigned char *p, unsigned char *s, unsigned int slen) {
    size_t curlen = intrev32ifbe(ZIPLIST_BYTES(zl)), reqlen;
    unsigned int prevlensize, prevlen = 0;
    size_t offset;
    int nextdiff = 0;
    unsigned char encoding = 0;
    long long value = 123456789; 
    zlentry tail;
    //prevlensize 存儲前一個節點長度,本節點使用了幾個字節 1 or 5
    //prelen  存儲前一個節點實際佔用了幾個字節
    if (p[0] != ZIP_END) {
        ZIP_DECODE_PREVLEN(p, prevlensize, prevlen);
    } else {
        unsigned char *ptail = ZIPLIST_ENTRY_TAIL(zl);
        if (ptail[0] != ZIP_END) {
            prevlen = zipRawEntryLength(ptail);
        }
    }
    if (zipTryEncoding(s,slen,&value,&encoding)) {
        //s 指針指向一個整數,嘗試進行一個轉換並得到存儲這個整數佔用了幾個字節
        reqlen = zipIntSize(encoding);
    } else {
        //s 指針指向一個字符串(字符數組),slen 就是他佔用的字節數
        reqlen = slen;
    }
    //當前節點存儲數據佔用 reqlen 個字節,加上存儲前一個節點長度佔用的字節數
    reqlen += zipStorePrevEntryLength(NULL,prevlen);
    //encoding 字段存儲實際佔用字節數
    reqlen += zipStoreEntryEncoding(NULL,encoding,slen);
    //至此,reqlen 保存了存儲當前節點數據佔用字節數和 encoding 編碼佔用的字節數總和
    int forcelarge = 0;
    //當前節點佔用的總字節減去存儲前一個節點字段佔用的字節
    //記錄的是這一個節點的插入會引起下一個節點佔用字節的變化量
    nextdiff = (p[0] != ZIP_END) ? zipPrevLenByteDiff(p,reqlen) : 0;
    if (nextdiff == -4 && reqlen < 4) {
        nextdiff = 0;
        forcelarge = 1;
    }
    //擴容有可能導致 zl 的起始位置偏移,故記錄 p 與 zl 首地址的相對偏差數,事後還原 p 指針指向
    offset = p-zl;
    zl = ziplistResize(zl,curlen+reqlen+nextdiff);
    p = zl+offset;
    if (p[0] != ZIP_END) {
        memmove(p+reqlen,p-nextdiff,curlen-offset-1+nextdiff);
        //把當前節點佔用的字節數存儲到下一個節點的頭部字段
        if (forcelarge)
            zipStorePrevEntryLengthLarge(p+reqlen,reqlen);
        else
            zipStorePrevEntryLength(p+reqlen,reqlen);

        //更新 tail_offset 字段,讓他保存從頭節點到尾節點之間的距離
        ZIPLIST_TAIL_OFFSET(zl) =
            intrev32ifbe(intrev32ifbe(ZIPLIST_TAIL_OFFSET(zl))+reqlen);
        zipEntry(p+reqlen, &tail);
        if (p[reqlen+tail.headersize+tail.len] != ZIP_END) {
            ZIPLIST_TAIL_OFFSET(zl) =
                intrev32ifbe(intrev32ifbe(ZIPLIST_TAIL_OFFSET(zl))+nextdiff);
        }
    } else {
        ZIPLIST_TAIL_OFFSET(zl) = intrev32ifbe(p-zl);
    }
    //是否觸發連鎖更新
    if (nextdiff != 0) {
        offset = p-zl;
        zl = __ziplistCascadeUpdate(zl,p+reqlen);
        p = zl+offset;
    }
    //將節點寫入指定位置
    p += zipStorePrevEntryLength(p,prevlen);
    p += zipStoreEntryEncoding(p,encoding,slen);
    if (ZIP_IS_STR(encoding)) {
        memcpy(p,s,slen);
    } else {
        zipSaveInteger(p,value,encoding);
    }
    ZIPLIST_INCR_LENGTH(zl,1);
    return zl;
}

具體細節我不再贅述,總結一下整個插入節點的步驟。

  1. 計算並得到前一個節點的總長度,並判斷得到當前待插入節點保存前一個節點長度的 previous_entry_length 佔用字節數
  2. 根據傳入的 s 和 slen,計算並保存 encoding 字段內容
  3. 構建節點並將數據寫入節點添加到壓縮列表中

ps:重點要去理解壓縮列表節點的數據結構定義,previous_entry_length、encoding、content 字段,這樣才能比較容易理解節點新增操作的實現。

三、連鎖更新

談到 redis 的壓縮列表,就必然會談到他的連鎖更新,我們先引一張圖:

假設原本 entry1 節點佔用字節數為 211(小於 254),那麼 entry2 的 previous_entry_length 會使用一個字節存儲 211,現在我們新插入一個節點 NEWEntry,這個節點比較大,佔用了 512 個字節。

那麼,我們知道,NEWEntry 節點插入后,entry2 的 previous_entry_length 存儲不了 512,那麼 redis 就會重分配內存,增加 entry2 的內存分配,並分配給 previous_entry_length 五個字節存儲 NEWEntry 節點長度。

看似沒什麼問題,但是如果極端情況下,entry2 擴容四個字節后,導致自身佔用字節數超過 254,就會又觸發后一個節點的內存佔用空間擴大,非常極端情況下,會導致所有的節點都擴容,這就是連鎖更新,一次更新導致大量甚至全部節點都更新內存的分配。

如果連鎖更新發生的概率很高的話,壓縮列表無疑就會是一個低效的數據結構,但實際上連鎖更新發生的條件是非常苛刻的,其一是需要大量節點長度小於 254 連續串聯連接,其二是我們更新的節點位置恰好也導致后一個節點內存擴充更新。

基於這兩點,且少量的連鎖更新對性能是影響不大的,所以這裏的連鎖更新對壓縮列表的性能是沒有多大的影響的,可以忽略,但需要知曉。

同樣的,如果覺得我寫的對你有點幫助的話,順手點一波關注吧,也歡迎加作者微信深入探討,我們逐漸開始走近 redis 比較實用性的相關內容了,盡請關注。

關注公眾不迷路,一個愛分享的程序員。


公眾號回復「1024」加作者微信一起探討學習!


每篇文章用到的所有案例代碼素材都會上傳我個人 github





歡迎來踩!

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

【其他文章推薦】

台北網頁設計公司這麼多,該如何挑選?? 網頁設計報價省錢懶人包"嚨底家"

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

※想知道購買電動車哪裡補助最多?台中電動車補助資訊懶人包彙整

南投搬家費用,距離,噸數怎麼算?達人教你簡易估價知識!

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

Hibernate一對多、多對一的關係表達

一、關係表達:

1、一對多、多對一表的關係:

學生表:

 

 

  班級表:

 

 

 在學生表中,學生的學號是主鍵。在班級表中,班級號是主鍵,因此,學生表的外鍵是classno。因此,班級對應學生是一對多,學生對應班級是多對一。因為,一個班級可以有多個學生,但是一個學生只能在一個班級。

2、對象的一對多、多對一關係:

(1)在Class類中,定義Set集合,表達一對多的關係:

 

 

 

package pers.zhb.domain;
import java.util.HashSet;
import java.util.Set;
public class Class {
    private String classno;
    private String department;
    private String monitor;
    private String classname;
    private Set<Student> classes=new HashSet<Student>();//使用set集合表達一對多關係
    public Class(){
    }
    public Set<Student> getClasses() {
        return classes;
    }
    public void setClasses(Set<Student> classes) {
        this.classes = classes;
    }
    public String getMonitor() {
        return monitor;
    }

    public void setMonitor(String monitor) {
        this.monitor = monitor;
    }

    public String getDepartment() {
        return department;
    }

    public void setDepartment(String department) {
        this.department = department;
    }



    public String getClassname() {
        return classname;
    }

    public void setClassname(String classname) {
        this.classname = classname;
    }
    public String getClassno() {
        return classno;
    }

    public void setClassno(String classno) {
        this.classno = classno;
    }
    @Override
    public String toString() {
        return "Class{" +
                "classno='" + classno + '\'' +
                ", department='" + department + '\'' +
                ", monitor='" + monitor + '\'' +
                ", classname='" + classname + '\'' +
                ", classes=" + classes +
                '}';
    }
}
package pers.zhb.domain;
public class Student {
    private Integer studentno;
    private String sname;
    private String sex;
    private String birthday;
    private String classno;
    private Float point;
    private String phone;
    private String email;
    private Clas aClas;
    public Student(){//無參的構造方法
    }
    public Clas getaClas() {
        return aClas;
    }

    public void setaClas(Clas aClas) {
        this.aClas = aClas;
    }
    @Override
    public String toString() {
        return "Student{" +
                "studentno='" + studentno + '\'' +
                ", sname='" + sname + '\'' +
                ", sex='" + sex + '\'' +
                ", birthday='" + birthday + '\'' +
                ", classno='" + classno + '\'' +
                ", point=" + point +
                ", phone='" + phone + '\'' +
                ", email='" + email + '\'' +
                '}';
    }

    public int getStudentno() {
        return studentno;
    }

    public void setStudentno(int studentno) {
        this.studentno = studentno;
    }

    public String getSname() {
        return sname;
    }

    public void setSname(String sname) {
        this.sname = sname;
    }

    public String getSex() {
        return sex;
    }

    public void setSex(String sex) {
        this.sex = sex;
    }

    public String getBirthday() {
        return birthday;
    }

    public void setBirthday(String birthday) {
        this.birthday = birthday;
    }

    public String getClassno() {
        return classno;
    }

    public void setClassno(String classno) {
        this.classno = classno;
    }

    public float getPoint() {
        return point;
    }

    public void setPoint(float point) {
        this.point = point;
    }

    public String getPhone() {
        return phone;
    }

    public void setPhone(String phone) {
        this.phone = phone;
    }

    public String getEmail() {
        return email;
    }

    public void setEmail(String email) {
        this.email = email;
    }
}

  

  

(2)定義學生和班級的關係:

 

 

 

package pers.zhb.domain;
import java.util.HashSet;
import java.util.Set;
public class Clas {
    private String classno;
    private String department;
    private String monitor;
    private String classname;
    private Set<Student> students=new HashSet<Student>();//使用set集合表達一對多關係
    public Clas(){
    }
    public Set<Student> getStudents() {
        return students;
    }
    public void setClasses(Set<Student> students) {
        this.students = students;
    }
    public String getMonitor() {
        return monitor;
    }

    public void setMonitor(String monitor) {
        this.monitor = monitor;
    }

    public String getDepartment() {
        return department;
    }

    public void setDepartment(String department) {
        this.department = department;
    }



    public String getClassname() {
        return classname;
    }

    public void setClassname(String classname) {
        this.classname = classname;
    }
    public String getClassno() {
        return classno;
    }

    public void setClassno(String classno) {
        this.classno = classno;
    }
    @Override
    public String toString() {
        return "Class{" +
                "classno='" + classno + '\'' +
                ", department='" + department + '\'' +
                ", monitor='" + monitor + '\'' +
                ", classname='" + classname + '\'' +
                ",students=" + students +
                '}';
    }
}

  

3、配置映射文件:

  Class.hbm.xml:

(1)實現一對多的關係映射,即:一個班級對應多個學生:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE hibernate-mapping PUBLIC
        "-//Hibernate/Hibernate Mapping DTD 3.0//EN"
        "http://www.hibernate.org/dtd/hibernate-mapping-3.0.dtd">
<hibernate-mapping package="pers.zhb.domain">
    <class name="Clas" table="class">
        <id name="classno" column="classno">
            <generator class="native"></generator>
        </id><!--主鍵-->
        <property name="department" column="department"></property>
        <property name="monitor" column="monitor"></property>
        <property name="classname" column="classname"></property>
        <set name="students" table="student"><!--一對多關係配置-->
        <key column="classno" update="false"></key><!--指定了集合表的外鍵-->
            <one-to-many class="Student"></one-to-many>
        </set>
    </class>
</hibernate-mapping>

 

<set name="students">

指定映射的存儲學生的集合的名字。

<key column="classesno"></key>

映射的class表的外鍵。

<one-to-many class="Student"></one-to-many>

指定學生的類型。

(2)實現多對一的關係映射,即:多個學生對應一個班級。

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE hibernate-mapping PUBLIC
        "-//Hibernate/Hibernate Mapping DTD 3.0//EN"
        "http://www.hibernate.org/dtd/hibernate-mapping-3.0.dtd">
<hibernate-mapping package="pers.zhb.domain">
    <class name="Student" table="student">
        <id name="studentno" column="studentno" >
            <generator class="native"></generator>
        </id>
        <property name="birthday" column="birthday"></property>
        <property name="classno" column="classno" insert="false" update="false"></property>
        <property name="email" column="email"></property>
        <property name="phone" column="phone"></property>
        <property name="sex" column="sex"></property>
        <property name="sname" column="sname"></property>
        <property name="point" column="point"></property>
        <many-to-one name="aClas" column="classno" class="Clas"></many-to-one>
    </class>
</hibernate-mapping>

 

name屬性:映射的班級。

column屬性:映射的班級對象對應的外鍵。

class屬性:指定班級的類型。

4、主配置文件:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE hibernate-configuration PUBLIC
        "-//Hibernate/Hibernate Configuration DTD 3.0//EN"
        "http://www.hibernate.org/dtd/hibernate-configuration-3.0.dtd">
<hibernate-configuration>
    <!--配置數據庫信息-必須的-->
    <session-factory>
        <property name="hibernate.connection.driver_class">com.mysql.jdbc.Driver</property>
        <property name="hibernate.connection.url">jdbc:mysql://localhost:3306/stu_mangement</property>
        <property name="hibernate.connection.username">root</property>
        <property name="hibernate.connection.password">root</property>
        <!--配置hibernate信息-可選的-->
        <property name="hibernate.show_sql">true</property><!--輸出底層sql語句-->
        <property name="hibernate.format_sql">true</property><!--格式化輸出sql語句-->
        <property name="hibernate.hbm2ddl.auto">update</property><!--hibernate幫助創建表,如果已經有表更新表,如果沒有則創建新表-->
        <property name="hibernate.dialect">org.hibernate.dialect.MySQL5Dialect</property>
        <property name="hibernate.connection.isolation">4</property>
        <!--指定session與當前線程綁定-->
        <property name="hibernate.current_session_context_class">thread</property>
        <!--配置數據庫的方言,讓hibernate識別框架自己的特有語句-->
        <!--把映射文件放到核心配置文件-->
        <mapping resource="pers/zhb/domain/Student.hbm.xml"/><!--都在src目錄下-->
        <mapping resource="pers/zhb/domain/Class.hbm.xml"/><!--都在src目錄下-->
    </session-factory>
</hibernate-configuration>

 二、具體運用:

1、增加:

(1)創建一個新班級併為新班級添加兩名學生:

public class Test {
    public static void testSel() {
            Session session = HibernateUtils.openSession();//獲得session
            Transaction transaction = session.beginTransaction();//開啟事務
            Clas clas=new Clas();
            clas.setClassname("計科171");
            clas.setClassno(4600);
            clas.setDepartment("一號樓");
            clas.setMonitor("zhai");

            Student student=new Student();
            student.setSname("");
            student.setStudentno(2017151411);
            student.setPoint(123f);
            student.setSex("");
            student.setBirthday("2019-11-11");
            student.setPhone("18739496522");
            student.setClassno("221221");
            student.setEmail("34288334@qq.com");

            Student student1=new Student();
            student1.setSname("翟hb");
            student1.setStudentno(2017151419);
            student1.setPoint(666f);
            student1.setSex("");
            student1.setBirthday("2019-11-11");
            student1.setPhone("18739496522");
            student1.setClassno("221221");
            student1.setEmail("34288334@qq.com");

            clas.getStudents().add(student);//一對多,一個班級下有多個學生
            clas.getStudents().add(student1);//獲取Set集合對象並向其中添加元素

            student.setaClas(clas);//多對一,學生屬於哪一個班級
            student1.setaClas(clas);

            session.save(clas);
            session.save(student);
            session.save(student1);

            transaction.commit();//提交事務
            session.close();//關閉資源
        }

 

 (2)為一個已經存在的班級添加學生:

 public static void testAdd(){
            Session session = HibernateUtils.openSession();//獲得session
            Transaction transaction = session.beginTransaction();//開啟事務
            Clas clas=session.get(Clas.class,80501);//獲得一個已經存在的班級
            Student student=new Student();//創建一個學生對象
            student.setSname("翟zz");
            student.setStudentno(20190000);
            student.setPoint(133f);
            student.setSex("男");
            student.setBirthday("2019-11-16");
            student.setPhone("18739496522");
            student.setEmail("34288334@qq.com");

            Student student1=new Student();//再創建一個學生對象
            student1.setSname("翟zz");
            student1.setStudentno(20190000);
            student1.setPoint(133f);
            student1.setSex("男");
            student1.setBirthday("2019-11-16");
            student1.setPhone("18739496522");
            student1.setEmail("34288334@qq.com");

            clas.getStudents().add(student);//學生添加到班級
            student.setaClas(clas);//班級與學生對應
            clas.getStudents().add(student1);
            student1.setaClas(clas);

            session.save(student);
            session.save(student1);


            transaction.commit();//提交事務
            session.close();//關閉資源

        }

  

 

 2、刪除:

刪除80501班的一名學生信息:

 public static void testDel() {
           Session session = HibernateUtils.openSession();//獲得session
           Transaction transaction = session.beginTransaction();//開啟事務
           Clas clas=session.get(Clas.class,80501);//獲得要刪除的學生屬於那一個班級
           Student student=session.get(Student.class,937221532);//獲得要刪除的學生
           clas.getStudents().remove(student);
           student.setaClas(null);
           transaction.commit();//提交事務
           session.close();//關閉資源
       }

 

 

 

 

 

 

 

 

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

【其他文章推薦】

※想知道網站建置網站改版該如何進行嗎?將由專業工程師為您規劃客製化網頁設計後台網頁設計

※不管是台北網頁設計公司台中網頁設計公司,全省皆有專員為您服務

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

※帶您來看台北網站建置台北網頁設計,各種案例分享

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

基於cookie的用戶登錄狀態管理

cookie是什麼

先來花5分鐘看完這篇文章:

看完上文,相信大家對cookie已經有了一個整體的概念,我再強調一下,cookie是一個客戶端概念,它是存儲在瀏覽器本地的一小段文本(通常由服務器來生成這段文本)。

cookie的作用

如上文所說,cookie有許多作用,如會話狀態管理,個性化設置,瀏覽器行為跟蹤,客戶端數據的存儲等等。本篇文章就來講講基於cookie的用戶登錄狀態管理。

插一句哈,一般提到cookie,還會有一個叫session的傢伙和它一起出現,下篇文章我會講到它,以及兩者的區別。

cookie的產生過程

如上圖所示,客戶端攜帶賬號和密碼向服務器發起請求,服務器在校驗通過後,通過HTTP Respose Header中的Set-Cookie頭部,將一小段文本寫入客戶端瀏覽器,在以後的每個客戶端HTTP Request Header的Cookie頭部中會自動攜帶這段文本。

基於cookie的用戶登錄狀態管理

下面我基於golang和gin框架(中間件使用比較舒服)來簡單的實現一個基於cookie的用戶登錄狀態管理demo

package main

import (
    "net/http"
    "time"

    "github.com/gin-gonic/gin"
)

func main() {
    r := gin.Default()

    r.GET("/login", Login)
    
    // 需要登陸保護的
    auth := r.Group("")
    auth.Use(AuthRequired())
    {
        auth.GET("/me", UserInfo)
        auth.GET("/logout", Logout)
    }

    r.Run("localhost:9000")
}

// 登陸
func Login(c *gin.Context) {
    // 為了演示方便,我直接通過url明文傳遞賬號密碼,實際生產中應該用HTTP POST在body中傳遞
    userID := c.Query("user_id")
    password := c.Query("password")

    // 用戶身份校驗(查詢數據庫)
    if userID == "007" && password == "007" {
        // 生成cookie
        expiration := time.Now()
        expiration = expiration.AddDate(0, 0, 1)
        // 實際生產中我們可以加密userID
        cookie := http.Cookie{Name: "userID", Value: userID, Expires: expiration}
        http.SetCookie(c.Writer, &cookie)

        c.JSON(http.StatusOK, gin.H{"msg": "Hello " + userID})
        return
    }
    c.JSON(http.StatusBadRequest, gin.H{"msg": "賬號或密碼錯誤"})
}

// 檢測是否登陸的中間件
func AuthRequired() gin.HandlerFunc {
    return func(c *gin.Context) {
        cookie, _ := c.Request.Cookie("userID")
        if cookie == nil {
            c.JSON(http.StatusUnauthorized, gin.H{"msg": "請先登陸"})
            c.Abort()
        }
        // 實際生產中應校驗cookie是否合法
        c.Next()
    }
}

// 查看用戶個人信息
func UserInfo(c *gin.Context) {
    c.JSON(http.StatusOK, gin.H{"msg": "007的個人頁面"})
}

// 退出登陸
func Logout(c *gin.Context) {
    // 設置cookie過期
    expiration := time.Now()
    expiration = expiration.AddDate(0, 0, -1)
    cookie := http.Cookie{Name: "userID", Value: "", Expires: expiration}
    http.SetCookie(c.Writer, &cookie)

    c.JSON(http.StatusOK, gin.H{"msg": "退出成功"})
}

我們來看具體的演示流程和效果:

如下圖所示,當我們退出后再去嘗試訪問個人頁面時,會出現401沒有權限的錯誤。

上述例子的缺點

先來說說上面的demo存在的問題吧,我們的退出登錄函數本質是設置了一個過期了的cookie來覆蓋以前發送給用戶的正常cookie。

但是,這兒存在着一個重大的安全問題。如果用戶將之前未過期的正常cookie記錄下來(即本例子中的userID=007),即使調用了我們的logout接口,只要用戶自己手動輸入之前未過期的正常cookie,也是可以通過服務器的驗證。

而且,最重要的是,我們無法讓其失效,因為cookie的過期刪除機制是由瀏覽器來控制的,但是當用戶記錄了cookie中的哪段文本后,在cookie到期后,瀏覽器只能刪除存在於瀏覽器中的cookie,對用戶自己記錄下來的cookie確無能為力,也就是說這段cookie永遠有效。(後面我們會講一種叫json web token的技術,可以做到讓我們簽發的憑證自帶過期機制,而不依賴瀏覽器)

當然,有同學會說,我們可以在服務器存儲一份有效的cookie列表,在用戶退出登錄后,從有效列表中刪除對應的cookie,這種在服務端維護用戶狀態的機制本質是session的思想,我們後面會講基於session的用戶登錄狀態管理。

再來說說cookie別的缺點:

當然這個我們通過設置cookie的屬性為HttpOnly,來禁止JavaScript讀取cookie值,可以起到一定的防護作用。

當然,cookie也是有優點的,我們把用戶的登錄狀態保存在客戶端,這樣就不需要每一次去訪問數據庫來檢測用戶是否登錄,減少了系統的IO開銷。

最後

本文希望通過一個不是很完美的demo來講述基於cookie的用戶登錄狀態管理,下期我們來講講session。

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

【其他文章推薦】

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

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

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

南投搬家前需注意的眉眉角角,別等搬了再說!

新北清潔公司,居家、辦公、裝潢細清專業服務

Net Core Identity 身份驗證:註冊、登錄和註銷 (簡單示例)

一、前言

  一般我們自己的系統都會用自己設置的一套身份驗證授權的代碼,這次用net core的identity來完成簡單的註冊、登錄和註銷。

二、數據庫

  首先就是創建上下文,我這裏簡單的建了Users和UserClaim表,要是沒有UserClaim等下的登錄操作是會報錯的,應該是有身份認證方面的關係。

    public class DataBaseContext : DbContext
    {
        public DataBaseContext(DbContextOptions<DataBaseContext> options)
        : base(options)
        { }
        public DbSet<User> Users { get; set; }
        public DbSet<IdentityUserClaim<string>> UserClaim { get; set; }
    }
    public class User : IdentityUser
    {
        public string companyId { get; set; }
        public string PassWord { get; set; }
    }

  這裏User繼承了IdentityUser,IdentityUser中就用很多的基礎字段,像是UserName等所以我們可以再User類中擴展我們的字段。

  add-migration Init和update-database Init再控制台執行,生成表。

三、Startup註冊服務

  在ConfigureServices中註冊如下

  1、數據庫上下文連接

  //添加數據庫連接
  services.AddDbContext<DataBaseContext>(options => options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));

  2、添加標識服務,包括默認的UI、令牌提供和身份驗的cookie,並且添加identity信息存儲的實體框架實現,用於關聯數據庫創建用戶獲取用戶信息等。AddDefaultIdentity一個相當於AddIdentity、AddDefaultUI和AddDefaultTokenProviders三個。如果User不繼承IdentityUser使用AddEntityFrameworkStores會報錯。

  services.AddDefaultIdentity<User>().AddEntityFrameworkStores<DataBaseContext>();

  3、添加Identity的選項,可以設定密碼的強度、長度、使用字符、密碼輸入錯誤次數等等。

    services.Configure<IdentityOptions>(options =>
    {
        // 密碼設置
        options.Password.RequireDigit = false;
        options.Password.RequireLowercase = false;
        options.Password.RequireNonAlphanumeric = false;
        options.Password.RequireUppercase = false;
        options.Password.RequiredLength = 1;
        options.Password.RequiredUniqueChars = 1;

        // 鎖定設置
        options.Lockout.DefaultLockoutTimeSpan = TimeSpan.FromMinutes(5);
        options.Lockout.MaxFailedAccessAttempts = 5;
        options.Lockout.AllowedForNewUsers = true;

        // 用戶設置
        options.User.AllowedUserNameCharacters =
        "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-._@+";
        options.User.RequireUniqueEmail = false;
    });

  4、配置應用程序的cookie

    services.ConfigureApplicationCookie(options =>
    {
        // Cookie設置
        options.Cookie.HttpOnly = true;
        options.ExpireTimeSpan = TimeSpan.FromMinutes(5);

        options.LoginPath = "/Login/Index";
        options.AccessDeniedPath = "/Home/Index";
        options.SlidingExpiration = true;
    });

  在Config中註冊身份認證

  app.UseAuthentication();

四、簡單的登錄、註冊和註銷

  既然是簡單的例子,那我是真的怎麼簡單怎麼來,代碼也就沒幾條。

  先創建一個Home控制器並加上[Authorize]特性,沒得到驗證的就統統無法訪問。按照上面的Startup.cs已經將UserManager依賴注入了,它是用來管理用戶的比如註冊啥的。

  [Authorize]
    public class HomeController : Controller
    { 
        private UserManager<User> userManager;public HomeController(UserManager<User> _userManager)
        {
            userManager = _userManager;
        }
        public async Task<IActionResult> Index()
        {
            var res = await userManager.GetUserAsync(HttpContext.User);
            return View();
        }
    }

  然後創建一個Login控制器,我們可以在裏面寫登錄、註冊和註銷的方法。Login控制器除了UserManager外還要注入SignManager,它是用來用戶登錄、註銷等操作的。

    public class LoginController : Controller
    {
        //用於提供持久性存儲的用戶信息
        private UserManager<User> userManager;
        private SignInManager<User> signManager;
        public LoginController(UserManager<User> _userManager,SignInManager<User> _signManager)
        {
            userManager = _userManager;
            signManager = _signManager;
        }
    }

  1、註冊

   註冊直接用CreateAsync方法,會自動在數據庫創建用戶。SignInAsync方法用於剛註冊的馬上用戶登錄。

        public async Task<IActionResult> Register()
        {
            var user = new User() { UserName = "xu2", PhoneNumber = "123", companyId = "1" };
            var result = await userManager.CreateAsync(user, "123");
            await signManager.SignInAsync(user, true);
            if (result.Succeeded)
                return Redirect("/Home/Index");
            return Redirect("/Login/Index");
        }

  2、登錄

  登錄不能用SignInAsync了,要用PasswordSignInAsync密碼登錄

        public async Task<IActionResult> Index()
        {
            var s = await signManager.PasswordSignInAsync("xu", "123", true, false);
            return View();
        }

  3、註銷

  這邊直接return view()是無法註銷的,因為cookie會被重新加載,需要return Redirect(“/Home/Index”)重定向

        public async Task<IActionResult> LogOut()
        {
            await signManager.SignOutAsync();
            return View();
        }

  4、獲取當前登錄用戶

    var res = await userManager.GetUserAsync(HttpContext.User);

 

 

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

【其他文章推薦】

※如何讓商品強力曝光呢? 網頁設計公司幫您建置最吸引人的網站,提高曝光率!!

網頁設計一頭霧水??該從何著手呢? 找到專業技術的網頁設計公司,幫您輕鬆架站!

※想知道最厲害的台北網頁設計公司推薦台中網頁設計公司推薦專業設計師”嚨底家”!!

※幫你省時又省力,新北清潔一流服務好口碑

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

程序員需要了解的硬核知識之操作系統入門

對於程序員來說,最莫大的榮耀莫過於自己的軟件被大多數人使用了吧。

歷史文章請戳

本篇文章作為操作系統的入門文章,可能入門都算不上吧,畢竟操作系統太龐大和複雜了。本篇文章主要帶你了解一下我們常用的操作系統環境。

操作系統環境

程序中包含着運行環境這一內容,可以說 運行環境 = 操作系統 + 硬件 ,操作系統又可以被稱為軟件,它是由一系列的指令組成的。我們不介紹操作系統,我們主要來介紹一下硬件的識別。

我們肯定都玩兒過遊戲,你玩兒遊戲前需要干什麼?是不是需要先看一下自己的筆記本或者電腦是不是能肝的起遊戲?下面是一個遊戲的配置(懷念一下 wow)

圖中的主要配置如下

  • 操作系統版本:說的就是應用程序運行在何種系統環境,現在市面上主要有三種操作系統環境,Windows 、Linux 和 Unix ,一般我們玩兒的大型遊戲幾乎都是在 Windows 上運行,可以說 Windows 是遊戲的天堂。Windows 操作系統也會有區分,分為32位操作系統和64位操作系統,互不兼容。
  • 處理器:處理器指的就是 CPU,你的電腦的計算能力,通俗來講就是每秒鐘能處理的指令數,如果你的電腦覺得卡帶不起來的話,很可能就是 CPU 的計算能力不足導致的。想要加深理解,請閱讀博主的另一篇文章:

  • 顯卡:顯卡承擔圖形的輸出任務,因此又被稱為圖形處理器(Graphic Processing Unit,GPU),顯卡也非常重要,比如我之前玩兒的劍靈開五檔(其實就是圖像變得更清晰)會卡,其實就是顯卡显示不出來的原因。
  • 內存:內存即主存,就是你的應用程序在運行時能夠動態分析指令的這部分存儲空間,它的大小也能決定你電腦的運行速度,想要加深理解,請閱讀博主的另一篇文章

  • 存儲空間:存儲空間指的就是應用程序安裝所佔用的磁盤空間,由圖中可知,此遊戲的最低存儲空間必須要大於 5GB,其實我們都會遺留很大一部分用來安裝遊戲。

從程序的運行環境這一角度來考量的話,CPU 的種類是特別重要的參數,為了使程序能夠正常運行,必須滿足 CPU 所需的最低配置。

CPU 只能解釋其自身固有的語言。不同的 CPU 能解釋的機器語言的種類也是不同的。機器語言的程序稱為 本地代碼(native code),程序員用 C 等高級語言編寫的程序,僅僅是文本文件。文本文件(排除文字編碼的問題)在任何環境下都能显示和編輯。我們稱之為源代碼。通過對源代碼進行編譯,就可以得到本地代碼。下圖反映了這個過程。

Windows 操作系統克服了CPU以外的硬件差異

計算機的硬件並不僅僅是由 CPU 組成的,還包括用於存儲程序指令的數據和內存,以及通過 I/O 連接的鍵盤、显示器、硬盤、打印機等外圍設備。

在 WIndows 軟件中,鍵盤輸入、显示器輸出等並不是直接向硬件發送指令。而是通過向 Windows 發送指令實現的。因此,程序員就不用注意內存和 I/O 地址的不同構成了。Windows 操作的是硬件而不是軟件,軟件通過操作 Windows 系統可以達到控制硬件的目的。

不同操作系統的 API 差異性

接下來我們看一下操作系統的種類。同樣機型的計算機,可安裝的操作系統類型也會有多種選擇。例如:AT 兼容機除了可以安裝 Windows 之外,還可以採用 Unix 系列的 Linux 以及 FreeBSD (也是一種Unix操作系統)等多個操作系統。當然,應用軟件則必須根據不同的操作系統類型來專門開發。CPU 的類型不同,所對應機器的語言也不同,同樣的道理,操作系統的類型不同,應用程序向操作系統傳遞指令的途徑也不同

應用程序向系統傳遞指令的途徑稱為 API(Application Programming Interface)。Windows 以及 Linux 操作系統的 API,提供了任何應用程序都可以利用的函數組合。因為不同操作系統的 API 是有差異的。所以,如何要將同樣的應用程序移植到另外的操作系統,就必須要覆蓋應用所用到的 API 部分。

鍵盤輸入、鼠標輸入、显示器輸出、文件輸入和輸出等同外圍設備進行交互的功能,都是通過 API 提供的。

這也就是為什麼 Windows 應用程序不能直接移植到 Linux 操作系統上的原因,API 差異太大了。

在同類型的操作系統下,不論硬件如何,API 幾乎相同。但是,由於不同種類 CPU 的機器語言不同,因此本地代碼也不盡相同。

FreeBSD Port 幫你輕鬆使用源代碼

不知道你有沒有這個想法:“既然 CPU 不同會導致本地代碼不同,那為何不將源代碼直接發送給程序呢?”這確實是一種解決辦法,Unix 系列的 FreeBSD 操作系統就使用了這種方式。

Unix 系列操作系統 FreeBSD 中,存在一種名為 Ports 的機制。該機制能夠結合當前運行環境的硬件環境來編譯應用的源代碼,進而得到可以運行的本地代碼。如果目標應用的源代碼在硬件上找不到,Ports 就會自動使用 FTP 連接到相應站點下載代碼。

全球有很多站點都提供適用於 FreeBSD 的應用源代碼。通過使用 Ports 可以利用的程序源代碼,大約有 16000 種。根據不同的領域進行分類,可以隨時使用。

FreeBSD 上應用的源代碼,大部分是用 C 語言來標註的,C 編譯器可以結合 FreeBSD 的運行環境來生成合適的本地代碼。

FTP( File Transfer Protocol) 是連接到互聯網上的計算機之間的傳送文件的協議。

可以使用虛擬機獲取其他環境

即使不通過應用程序的移植,在同一個操作系統上仍然可以使用其他的操作系統,那就是使用 虛擬機軟件。虛擬機(Virtual Machine)指通過軟件的具有完整硬件系統功能的、運行在一個完全隔離環境中的完整計算機系統。在實體計算機中能夠完成的工作在虛擬機中都能夠實現。

提供相同運行環境的 Java 虛擬機

總算是提到大 Java 了, Java 大法好,除了虛擬機的方法之外,還有一種方法能夠提供不依賴於特定硬件和操作系統的程序運行環境,那就是 Java。

大家說的 Java 其實有兩層意思,一種是作為編程語言的 Java;一種是作為程序運行環境的 Java。Java 與其他語言相同,都是通過源代碼編譯后運行的。不過,編譯後生成的不是特定 CPU 使用的本地代碼,而是名為字節代碼 的程序。直接代碼的運行環境就稱為 Java 虛擬機(Java Virtual Machine)。Java 虛擬機是一邊把 Java 字節代碼逐一轉換為本地代碼一邊在運行着。

程序運行時,將編譯后的字節代碼轉換為本地代碼,這樣的操作看上去有些迂迴,但由此可以實現相同的字節碼可以在不同的操作系統環境下運行。

想象一下,你開發完成的應用部署到 Linux 環境下,是不是什麼都不用管?

Windows 有專門的 Windows 虛擬機,Macintosh 有 Macintosh 專門的虛擬機。從操作系統來看,Java虛擬機就是一個應用,從運行環境上來看,Java 虛擬機就是運行環境。

BIOS 和引導

最後對一些比較基礎的部分做一些補充說明。程序的運行環境,存在着名為 BIOS(Basic Input/Output System)的系統。BIOS 存儲在 ROM 中,是預先內置在計算機主機內部的程序。BIOS 除了鍵盤、磁盤和顯卡等基本控制外,還有引導程序的功能。引導程序是存儲在啟動驅動器啟示區域的小程序。操作系統的啟動驅動器一般硬盤。不過有時也可能是 CD-ROM 或軟盤。

電腦開機后,BIOS 會確認硬件是否正常運行,沒有異常的話會直接啟動引導程序。引導程序的功能是把在硬盤等記錄的 OS 加載到內存中運行。雖然啟動應用是 OS 的功能,但 OS 不能啟動自己,是通過引導程序來啟動的。

文章參考:

《程序是怎樣跑起來的》第七章

關注公眾號後台回復 191106 即可獲得《程序是怎樣跑起來的》电子書

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

【其他文章推薦】

※帶您來了解什麼是 USB CONNECTOR  ?

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

※如何讓商品強力曝光呢? 網頁設計公司幫您建置最吸引人的網站,提高曝光率!!

※綠能、環保無空污,成為電動車最新代名詞,目前市場使用率逐漸普及化

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

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

MapReduce任務提交源碼分析

  為了測試MapReduce提交的詳細流程。需要在提交這一步打上斷點:

   F7進入方法:

   進入submit方法:

   注意這個connect方法,它在連接誰呢?我們知道,Driver是作為客戶端存在的,那麼客戶端連接的應該就是Yarn集群,但是在這個簡單的WordCount案例中,並沒有將任務提交到Yarn集群,而是在本機中執行的。所以先假設這裏連接的自然就是本機。

進入這個connect方法,然後在裏面的Cluster(集群的意思)方法上打上斷點:

 

 很明顯,這是一個構造器,他把集群抽象成了一個對象。進入此方法:

 初始化了一個客戶端協議,進入這個create方法,看看他是如何初始化客戶端協議的:

   注意這個mapreduce.framework.name,他就是mapred-site.xml文件中的mapreduce.framework.name項的值,由於我並沒有提交到集群上,而是在本機,所以他會加載本機的mapred-site.xml文件,但是我本機下的該文件並沒有像集群上那樣配置了mapred-site.xml文件,只有一份mapred-site.xml.template文件,更沒有mapreduce.framework.name這一項,所以上面代碼中的值就默認為local了。

  本機上的配置:

 集群上的配置:

create方法最終反回了一個LocalJobRunner對象,如果上面的值是yarn,返回的則是YarnJobRunner。繼續往下:

   會發現這個LocalJobRunner就是客戶端。然後一直往下,直到完成connect方法,會發現整個過程中在connect方法中創建了一個Cluster對象,然後在Cluster對象裏面獲取到了客戶端。

  緊接着,由於connect方法初始化了cluster對象,所以接下來創建了submitter對象,用於提交任務。進入submitJobInternal方法打上斷點

 

這個checkSpecs方法就是用來檢查路徑的,當輸入路徑不存在或輸出路徑已存在時會報錯。進入此方法后再進入內部方法就一目瞭然了:

 

繼續往下執行,完成checkSpecs方法后完初始化一個路徑:

打開此路徑,此時還是空的文件夾:

繼續往下,隨後做了一些獲取IP和往配置文件中設置IP和hostname的操作:

 繼續往下,隨後又在上面的路徑下隨機創建了一個目錄:

繼續往下,見到copyAndConfigureFiles時,進入此方法,然後在進入內部方法uploadFiles():

在uploadFiles方法中有,執行后的效果:

 

 

 

 

 這個文件夾暫時不知道幹嘛的,只知道copyAndConfigureFiles方法創建了這個路徑。此方法執行完后,執行到writeSplits進入此方法:

 這個方法是重點:切片是怎麼切的呢?

 maps是int型,這表示這啟動的maptask的數量也該和切片的數量保持一致。而具體切成多少片呢?

上面的方法多態調用到子類的方法writeNewSplits,然後調用getSplits方法:

getSplits方法中有這樣一段:

   也就是說:當剩餘長度比切片尺寸大於1.1時,就會產生新的切片,比如說文件65m,splitSize為32m,第一片切到32m,剩餘33m,由於33/32<1.1,所以就不再切片,65m被切成兩片,0~32、32~65,而不是0~32 、 32~64 、 64~65三片。

   當執行完writeSplits方法后,會在上面創建的目錄下生成幾個文件:

 上面生成的文件中保存了“切片的規劃信息”。繼續往下,當執行到writeConf方法后,會繼續在上面的目錄下生成與此Job相關的配置文件:

 

   繼續執行,當執行完submitJob方法后,job本身(即WordCount程序本身會被打成jar包被提交)就被提交了:

  但是由於我是直接在本地執行的,直接從main方法進來執行的,沒必要再打成jar包再從main方法進入,所以並不用提交,但是如果是是在yarn 上執行的時候,會把程序打成jar包放在上面的目錄下。

   當任務執行完畢后,上面的目錄會被清空:

   至此,整個任務的提交執行完成,試想一下,如果這個任務在Yarn上執行是什麼樣子呢?基本的邏輯不會變:

  首先,還是執行connect方法,初始化到Cluster對象,然後創建JobRunner,不過在Yarn上執行的JobRunner就不是LocalJobRunner了,而是YarnJobRunner。執行完connect方法後會在HDFS文件系統創建一個路徑,其作用與上面創建的路徑相同,用於保存切片方案信息和配置文件信息,同時會將任務本身的jar包放入其中,最後任務執行完,這些內容又將被銷毀。

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

【其他文章推薦】

※為什麼 USB CONNECTOR 是電子產業重要的元件?

網頁設計一頭霧水??該從何著手呢? 找到專業技術的網頁設計公司,幫您輕鬆架站!

※想要讓你的商品成為最夯、最多人討論的話題?網頁設計公司讓你強力曝光

※想知道最厲害的台北網頁設計公司推薦台中網頁設計公司推薦專業設計師”嚨底家”!!

新北清潔公司,居家、辦公、裝潢細清專業服務

python機器學習——隨機梯度下降

上一篇我們實現了使用梯度下降法的自適應線性神經元,這個方法會使用所有的訓練樣本來對權重向量進行更新,也可以稱之為批量梯度下降(batch gradient descent)。假設現在我們數據集中擁有大量的樣本,比如百萬條樣本,那麼如果我們現在使用批量梯度下降來訓練模型,每更新一次權重向量,我們都要使用百萬條樣本,訓練時間很長,效率很低,我們能不能找到一種方法,既能使用梯度下降法,但是又不要每次更新權重都要使用到所有的樣本,於是隨機梯度下降法(stochastic gradient descent)便被提出來了。

隨機梯度下降法可以只用一個訓練樣本來對權重向量進行更新:
\[ \eta(y^i-\phi(z^i))x^i \]
這種方法比批量梯度下降法收斂的更快,因為它可以更加頻繁的更新權重向量,並且使用當個樣本來更新權重,相比於使用全部的樣本來更新更具有隨機性,有助於算法避免陷入到局部最小值,使用這個方法的要注意在選取樣本進行更新時一定要隨機選取,每次迭代前都要打亂所有的樣本順序,保證訓練的隨機性,並且在訓練時的學習率也不是固定不變的,可以隨着迭代次數的增加,學習率逐漸減小,這種方法可以有助於算法收斂。

現在我們有了使用全部樣本的批量梯度下降法,也有了使用單個樣本的隨機梯度下降法,那麼一種折中的方法,稱為最小批學習(mini-batch learning),它每次使用一部分訓練樣本來更新權重向量。

接下來我們實現使用隨機梯度下降法的Adaline

from numpy.random import seed
class AdalineSGD(object):
    """ADAptive LInear NEuron classifier.

    Parameters
    ----------
    eta:float
        Learning rate(between 0.0 and 1.0
    n_iter:int
        Passes over the training dataset.

    Attributes
    ----------
    w_: 1d-array
        weights after fitting.
    errors_: list
        Number of miscalssifications in every epoch.
    shuffle:bool(default: True)
        Shuffle training data every epoch
        if True to prevent cycles.
    random_state: int(default: None)
        Set random state for shuffling
        and initalizing the weights.

    """

    def __init__(self, eta=0.01, n_iter=10, shuffle=True, random_state=None):
        self.eta = eta
        self.n_iter = n_iter
        self.w_initialized = False
        self.shuffle = shuffle
        if random_state:
            seed(random_state)

    def fit(self, X, y):
        """Fit training data.

        :param X:{array-like}, shape=[n_samples, n_features]
        :param y: array-like, shape=[n_samples]
        :return:
        self:object

        """

        self._initialize_weights(X.shape[1])
        self.cost_ = []

        for i in range(self.n_iter):
            if self.shuffle:
                X, y = self._shuffle(X, y)
            cost = []
            for xi, target in zip(X, y):
                cost.append(self._update_weights(xi, target))
            avg_cost = sum(cost)/len(y)
            self.cost_.append(avg_cost)
        return self
    
    def partial_fit(self, X, y):
        """Fit training data without reinitializing the weights."""
        if not self.w_initialized:
            self._initialize_weights(X.shape[1])
        if y.ravel().shape[0] > 1:
            for xi, target in zip(X, y):
                self._update_weights(xi, target)
        else:
            self._update_weights(X, y)
        return self
    
    def _shuffle(self, X, y):
        """Shuffle training data"""
        r = np.random.permutation(len(y))
        return X[r], y[r]
    
    def _initialize_weights(self, m):
        """Initialize weights to zeros"""
        self.w_ = np.zeros(1 + m)
        self.w_initialized = True
    
    def _update_weights(self, xi, target):
        """Apply Adaline learning rule to update the weights"""
        output = self.net_input(xi)
        error = (target - output)
        self.w_[1:] += self.eta * xi.dot(error)
        self.w_[0] += self.eta * error
        cost = 0.5 * error ** 2
        return cost
    
    def net_input(self, X):
        """Calculate net input"""
        return np.dot(X, self.w_[1:]) + self.w_[0]
    
    def activation(self, X):
        """Computer linear activation"""
        return self.net_input(X)
    
    def predict(self, X):
        """Return class label after unit step"""
        return np.where(self.activation(X) >= 0.0, 1, -1)

其中_shuffle方法中,調用numpy.random中的permutation函數得到0-100的一個隨機序列,然後這個序列作為特徵矩陣和類別向量的下標,就可以起到打亂樣本順序的功能。

現在開始訓練

ada = AdalineSGD(n_iter=15, eta=0.01, random_state=1)
ada.fit(X_std, y)

畫出分界圖和訓練曲線圖

plot_decision_region(X_std, y, classifier=ada)
plt.title('Adaline - Stochastic Gradient Desent')
plt.xlabel('sepal length [standardized]')
plt.ylabel('petal length [standardized]')
plt.legend(loc = 'upper left')
plt.show()
plt.plot(range(1, len(ada.cost_) + 1), ada.cost_, marker='o')
plt.xlabel('Epochs')
plt.ylabel('Average Cost')
plt.show()

從上圖可以看出,平均損失下降很快,在大概第15次迭代后,分界線和使用批量梯度下降的Adaline分界線很類似。

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

【其他文章推薦】

USB CONNECTOR掌控什麼技術要點? 帶您認識其相關發展及效能

※評比前十大台北網頁設計台北網站設計公司知名案例作品心得分享

※智慧手機時代的來臨,RWD網頁設計已成為網頁設計推薦首選

※評比南投搬家公司費用收費行情懶人包大公開

※幫你省時又省力,新北清潔一流服務好口碑

自己實現 aop 和 spring aop

說到,我們可以在 BeanPostProcessor 中對 bean 的初始化前化做手腳,當時也說了,我完全可以生成一個代理類丟回去。

代理類肯定要為用戶做一些事情,不可能像學設計模式的時候創建個代理類,然後簡單的在前面打印一句話,後面打印一句話,這叫啥事啊,難怪當時聽不懂。最好是這個方法的前後過程可以自戶自己定義。

小明說,這還不好辦,cglib 已經有現成的了,jdk 也可以實現動態代理,看 mybatis 其實也是這麼乾的,不然你想它一個接口怎麼就能找到 xml 的實現呢,可以參照下 mybatis 的代碼。

所以首先學習下 cglib 和 jdk 的動態代理,我們來模擬下 mybatis 是如何通過接口來實現方法調用的

cglib

目標接口:

public interface UserOperator {
    User queryUserByName(String name);
}

代理處理類:

import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;

public class ProxyHandle implements MethodInterceptor{
    // 實現 MethodInterceptor 的代理攔截接口
    public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
        System.out.println("獲取到 sqlId:"+method);
        System.out.println("獲取到執行參數列表:"+args[0]);
        System.out.println("解析 spel 表達式,並獲取到完整的 sql 語句");
        System.out.println("執行 sql ");
        System.out.println("結果集處理,並返回綁定對象");
        return new User("sanri",1);
    }
}

真正調用處:

Enhancer enhancer = new Enhancer();

enhancer.setSuperclass(UserOperator.class);
enhancer.setCallback(new ProxyHandle());

//可以把這個類添加進 ioc 容器,這就是真正的代理類
UserOperator userOperator = (UserOperator) enhancer.create();

User sanri = userOperator.queryByName("sanri");
System.out.println(sanri);

jdk

import java.lang.reflect.InvocationHandler;
public class ProxyHandler implements InvocationHandler {
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println("獲取到 sqlId:"+method);
        System.out.println("獲取到執行參數列表:"+args[0]);
        System.out.println("解析 spel 表達式,並獲取到完整的 sql 語句");
        System.out.println("執行 sql ");
        System.out.println("結果集處理,並返回綁定對象");
        return new User("sanri",1);
    }
}

真正調用處:

UserOperator proxyInstance = (UserOperator)Proxy.newProxyInstance(ClassLoader.getSystemClassLoader(), new Class[]{UserOperator.class}, new ProxyHandler());
User sanri = proxyInstance.queryByName("sanri");
System.out.println(sanri);

注:jdk 只能支持代理接口,但 cglib 是接口和實體類都可以代理; jdk 是使用實現接口方式,可以多實現,但 cglib 是繼承方式,也支持接口方式。

代理模式和裝飾模式的區別:

從這也可以看到代理模式和裝飾模式的區別 ,代理模式的方法簽名一般是不動的,但裝飾模式是為了方法的增強,一般會使用別的更好的方法來代替原方法。

如何織入

回到正文,這時我們已經可以創建一個代理類了,如何把用戶行為給弄進來呢,哎,又只能 回調 了,我們把現場信息給用戶,用戶實現我的接口,然後我找到接口的所有實現類進行順序調用,但這時候小明想到了幾個問題

  • 用戶不一定每個方法都要做代理邏輯,可能只是部分方法需要,我們應該能夠識別出是哪些方法需要做代理邏輯 (Pointcut)
  • 方法加代理邏輯的位置,方法執行前(Before),方法執行后(After),方法返回數據后(AfterReturning),方法出異常后(AfterThrowing),自定義執行(Around)

根據單一職責原則,得寫五個接口,每個接口要包含 getPointCut() 方法和 handler() 方法,或者繞過單一職責原則,在一個接口中定義 6 個方法,用戶不想實現留空即可。總得來說,用戶只需要提交一份規則給我就行,這個規則你不管是用 json,xml ,或者 註解的方式,只要我能夠識別在 這個 pointcut 下,需要有哪些自定義行為,在另一個 pointcut 下又有哪些自定義行為即可。

現拿到用戶行為了和切點了,還需要創建目標類的代理類,並把行為給綁定上去,在什麼時候創建代理類呢,肯定在把 bean 交給容器的時候悄悄的換掉啊, 說到 bean 有一個生命周期是用於做所有 bean 攔截的,並且可以在初始化前和初始化後進行攔截,沒錯,就是 BeanPostProcessor 我們可以在初始化後生成代理類。

這裏需要注意,並不是所有類都需要創建代理。我們可以這樣檢測,讓 pointcut 提供一個方法用於匹配當前方法是否需要代理,當然這也是 pointcut 的職責,如果當前類有一個方法需要代理,那麼當前類是需要代理的,否則認為不需要代理,這麼做需要遍歷所有類的所有方法,如果運氣差的話,看上去很耗費性能 ,但 spring 也是這麼乾的。。。。。。優化的方案可以這麼玩,如果方法需要代理,在類上做一個標識,如果類上存在這個標識,則可以直接創建代理類。

現在我們把用戶行為綁定到代理類,根據上面 jdk 動態代理和 cglib 動態代理的學習,我們發現,它們都有一個共同的傢伙,那就是方法攔截,用於攔截目標類的當前正在執行的方法,並增強其功能,我們可以在創建代理類的時候找到所有的用戶行為並按照順序和類型依次綁定,可以用責任鏈模式。

看一下 spring 是怎麼玩的

spring 也是在 BeanPostProcessor 接口的 postProcessAfterInitialization 生命周期進行攔截,具體的類為 AspectJAwareAdvisorAutoProxyCreator

spring 配置切面有兩種方式,使用註解和使用配置,當然,現在流行註解的方式,更方便,但不管是配置還是註解,最後都會被解析成 Advisor(InstantiationModelAwarePointcutAdvisorImpl),spring 查找了所有實現 Advisor 的類,源代碼在 BeanFactoryAdvisorRetrievalHelper.findAdvisorBeans

advisorNames = BeanFactoryUtils.beanNamesForTypeIncludingAncestors(this.beanFactory, Advisor.class, true, false);

緊接着,spring 會使使用 Advisor 中的 pointcut 來看當前類是否需要創建代理類,跟進方法可以看到 canApply 方法中是遍歷了所有方法一個個匹配來看是否需要創建代理類的,如果有一個需要,則直接返回 true 。當然 spring 更嚴謹一些,它考慮到了可能有接口的方法需要有代理,我上面說在類加標識是不正確的。

然後通過 createProxy 創建了代理類,裏面有區分 cglib 還是 aop ,下面單拿 cglib 來說

CglibAopProxy.getProxy 中對類進行增強,主要看 Enhancer 類是如何設置的就好了,有一個 callback 參數 ,我們一般是第 0 個 callback 也即 DynamicAdvisedInterceptor 它是一個 cglib 的 MethodInterceptor

它重寫的是 MethodInterceptor 的 intercept 方法,下面看這個方法,this.advised 是前面傳過來的用戶行為,getInterceptorsAndDynamicInterceptionAdviceAdvisor 適配成了 org.aopalliance.intercept.MethodInterceptor 分別對應切面的五種行為

AbstractAspectJAdvice
  |- AspectJAfterReturningAdvice
  |- AspectJAfterAdvice implements org.aopalliance.intercept.MethodInterceptor
  |- AspectJAroundAdvice implements org.aopalliance.intercept.MethodInterceptor
  |- AspectJAfterThrowingAdvice implements org.aopalliance.intercept.MethodInterceptor
  |- AspectJMethodBeforeAdvice

最後它封裝一個執行器,根據順序調用攔截器鏈,也即用戶行為列表,封裝執行的時候是強轉 org.aopalliance.intercept.MethodInterceptor 來執行的,但 AspectJAfterReturningAdviceAspectJMethodBeforeAdvice 沒有實現 org.aopalliance.intercept.MethodInterceptor 怎麼辦,所以 spring 在獲取用戶行為鏈的時候增加了一個適配器,專門用於把這兩種轉換成 MethodInterceptor

其它說明

  • cglib 的 callback 只能寫一個,filter 用於選擇是第幾個 callback ,不要認為也是鏈式的

  • spring aop 中有比較多的設計模式,學設計模式的可以看下這塊的源碼 ,至少責任鏈,適配器,動態代理都可以在這看到
  • 切面類中如果有兩個一樣的行為,比如有兩個 @Before,排序規則為看方法名的 ascii 碼值,只測試過,並沒經過源碼,有興趣的可以自己去看一下。

來個示例更容易理解

我們除了使用 @Aspect 註解把切面規則告訴 spring 外,也可以學本身 aop 的實現,我們自己定義一個 Advisor ,因為 spring 就是掃描這個的,然後實現 pointcut 和 invoke 方法,一樣可以實現 aop 。

聯繫上文: 我們來看看 spring 的 redis-cache 是如何做切面的

文章說到,主要工作的類是 CacheInterceptor 它是一個 org.aopalliance.intercept.MethodInterceptor

Advisor 是 BeanFactoryCacheOperationSourceAdvisor 也就是說創建代理類會掃描到這個類,最後執行會把其轉成 MethodInterceptor,因為它是一個 PointcutAdvisor ,查看 DefaultAdvisorChainFactory.getInterceptorsAndDynamicInterceptionAdvice 方法,第一個就是把 PointcutAdvisor 轉成 MethodInterceptor 繼續進入獲取攔截器的方法,可以知道就是獲取的 advice 屬性 CacheInterceptor

一點小推廣

創作不易,希望可以支持下我的開源軟件,及我的小工具,歡迎來 gitee 點星,fork ,提 bug 。

Excel 通用導入導出,支持 Excel 公式
博客地址:
gitee:

使用模板代碼 ,從數據庫生成代碼 ,及一些項目中經常可以用到的小工具
博客地址:
gitee:

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

【其他文章推薦】

台北網頁設計公司這麼多,該如何挑選?? 網頁設計報價省錢懶人包"嚨底家"

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

※想知道購買電動車哪裡補助最多?台中電動車補助資訊懶人包彙整

南投搬家費用,距離,噸數怎麼算?達人教你簡易估價知識!

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