Redis是什麼?看這一篇就夠了

本文由葡萄城技術團隊編撰並首發

轉載請註明出處:,葡萄城為開發者提供專業的開發工具、解決方案和服務,賦能開發者。

引言

在Web應用發展的初期,那時關係型數據庫受到了較為廣泛的關注和應用,原因是因為那時候Web站點基本上訪問和併發不高、交互也較少。而在後來,隨着訪問量的提升,使用關係型數據庫的Web站點多多少少都開始在性能上出現了一些瓶頸,而瓶頸的源頭一般是在磁盤的I/O上。而隨着互聯網技術的進一步發展,各種類型的應用層出不窮,這導致在當今雲計算、大數據盛行的時代,對性能有了更多的需求,主要體現在以下四個方面:

  1. 低延遲的讀寫速度:應用快速地反應能極大地提升用戶的滿意度
  2. 支撐海量的數據和流量:對於搜索這樣大型應用而言,需要利用PB級別的數據和能應對百萬級的流量
  3. 大規模集群的管理:系統管理員希望分佈式應用能更簡單的部署和管理
  4. 龐大運營成本的考量:IT部門希望在硬件成本、軟件成本和人力成本能夠有大幅度地降低

為了克服這一問題,NoSQL應運而生,它同時具備了高性能、可擴展性強、高可用等優點,受到廣泛開發人員和倉庫管理人員的青睞。

Redis是什麼

Redis是現在最受歡迎的NoSQL數據庫之一,Redis是一個使用ANSI C編寫的開源、包含多種數據結構、支持網絡、基於內存、可選持久性的鍵值對存儲數據庫,其具備如下特性:

  • 基於內存運行,性能高效
  • 支持分佈式,理論上可以無限擴展
  • key-value存儲系統
  • 開源的使用ANSI C語言編寫、遵守BSD協議、支持網絡、可基於內存亦可持久化的日誌型、Key-Value數據庫,並提供多種語言的API

相比於其他數據庫類型,Redis具備的特點是:

  • C/S通訊模型
  • 單進程單線程模型
  • 豐富的數據類型
  • 操作具有原子性
  • 持久化
  • 高併發讀寫
  • 支持lua腳本

哪些大廠在使用Redis?

  • github
  • twitter
  • 微博
  • Stack Overflow
  • 阿里巴巴
  • 百度
  • 美團
  • 搜狐

Redis的應用場景有哪些?

Redis 的應用場景包括:緩存系統(“熱點”數據:高頻讀、低頻寫)、計數器、消息隊列系統、排行榜、社交網絡和實時系統。

 

Redis的數據類型及主要特性

Redis提供的數據類型主要分為5種自有類型和一種自定義類型,這5種自有類型包括:String類型、哈希類型、列表類型、集合類型和順序集合類型。

String類型:

它是一個二進制安全的字符串,意味着它不僅能夠存儲字符串、還能存儲圖片、視頻等多種類型, 最大長度支持512M。

對每種數據類型,Redis都提供了豐富的操作命令,如:

  • GET/MGET
  • SET/SETEX/MSET/MSETNX
  • INCR/DECR
  • GETSET
  • DEL

哈希類型:

該類型是由field和關聯的value組成的map。其中,field和value都是字符串類型的。

Hash的操作命令如下:

  • HGET/HMGET/HGETALL
  • HSET/HMSET/HSETNX
  • HEXISTS/HLEN
  • HKEYS/HDEL
  • HVALS

列表類型:

該類型是一個插入順序排序的字符串元素集合, 基於雙鏈表實現。

List的操作命令如下:

  • LPUSH/LPUSHX/LPOP/RPUSH/RPUSHX/RPOP/LINSERT/LSET
  • LINDEX/LRANGE
  • LLEN/LTRIM

集合類型:

Set類型是一種無順序集合, 它和List類型最大的區別是:集合中的元素沒有順序, 且元素是唯一的。

Set類型的底層是通過哈希表實現的,其操作命令為:

  • SADD/SPOP/SMOVE/SCARD
  • SINTER/SDIFF/SDIFFSTORE/SUNION

Set類型主要應用於:在某些場景,如社交場景中,通過交集、並集和差集運算,通過Set類型可以非常方便地查找共同好友、共同關注和共同偏好等社交關係。

順序集合類型:

ZSet是一種有序集合類型,每個元素都會關聯一個double類型的分數權值,通過這個權值來為集合中的成員進行從小到大的排序。與Set類型一樣,其底層也是通過哈希表實現的。

ZSet命令:

  • ZADD/ZPOP/ZMOVE/ZCARD/ZCOUNT
  • ZINTER/ZDIFF/ZDIFFSTORE/ZUNION

Redis的數據結構

Redis的數據結構如下圖所示:

關於上表中的部分釋義:

  1. 壓縮列表是列表鍵和哈希鍵的底層實現之一。當一個列表鍵只包含少量列表項,並且每個列表項要麼就是小整數,要麼就是長度比較短的字符串,Redis就會使用壓縮列表來做列表鍵的底層實現
  2. 整數集合是集合鍵的底層實現之一,當一個集合只包含整數值元素,並且這個集合的元素數量不多時,Redis就會使用整數集合作為集合鍵的底層實現

如下是定義一個Struct數據結構的例子:

 

簡單動態字符串SDS (Simple Dynamic String)

基於C語言中傳統字符串的缺陷,Redis自己構建了一種名為簡單動態字符串的抽象類型,簡稱SDS,其結構如下:

SDS幾乎貫穿了Redis的所有數據結構,應用十分廣泛。

SDS的特點

和C字符串相比,SDS的特點如下:

  1. 常數複雜度獲取字符串長度

    Redis中利用SDS字符串的len屬性可以直接獲取到所保存的字符串的長
    度,直接將獲取字符串長度所需的複雜度從C字符串的O(N)降低到了O(1)。

  2. 減少修改字符串時導致的內存重新分配次數

    通過C字符串的特性,我們知道對於一個包含了N個字符的C字符串來說,其底層實現總是N+1個字符長的數組(額外一個空字符結尾)

    那麼如果這個時候需要對字符串進行修改,程序就需要提前對這個C字符串數組進行一次內存重分配(可能是擴展或者釋放) 

    而內存重分配就意味着是一個耗時的操作。

Redis巧妙的使用了SDS避免了C字符串的缺陷。在SDS中,buf數組的長度不一定就是字符串的字符數量加一,buf數組裡面可以包含未使用的字節,而這些未使用的字節由free屬性記錄。

與此同時,SDS採用了空間預分配的策略,避免C字符串每一次修改時都需要進行內存重分配的耗時操作,將內存重分配從原來的每修改N次就分配N次——>降低到了修改N次最多分配N次。

如下是Redis對SDS的簡單定義:

  

Redis特性1:事務

  • 命令序列化,按順序執行
  • 原子性
  • 三階段: 開始事務 – 命令入隊 – 執行事務
  • 命令:MULTI/EXEC/DISCARD

Redis特性2:發布訂閱(Pub/Sub)

  • Pub/sub是一種消息通訊模式
  • Pub發送消息, Sub接受消息
  • Redis客戶端可以訂閱任意數量的頻道
  • “fire and forgot”, 發送即遺忘
  • 命令:Publish/Subscribe/Psubscribe/UnSub

  

Redis特性3:Stream

  • Redis 5.0新增
  • 等待消費
  • 消費組(組內競爭)
  • 消費歷史數據
  • FIFO

 

 

 

以上就是Redis的基本概念,下面我們將介紹在開發過程中可能會踩到的“坑”。

Redis常見問題解析:擊穿

概念:在Redis獲取某一key時, 由於key不存在, 而必須向DB發起一次請求的行為, 稱為“Redis擊穿”。

引發擊穿的原因:

  • 第一次訪問
  • 惡意訪問不存在的key
  • Key過期

合理的規避方案:

  • 服務器啟動時, 提前寫入
  • 規範key的命名, 通過中間件攔截
  • 對某些高頻訪問的Key,設置合理的TTL或永不過期

Redis常見問題解析:雪崩

概念:Redis緩存層由於某種原因宕機后,所有的請求會湧向存儲層,短時間內的高併發請求可能會導致存儲層掛機,稱之為“Redis雪崩”。

合理的規避方案:

  • 使用Redis集群
  • 限流

Redis在產品開發中的應用實踐

為此,我很高興的為大家介紹,葡萄城架構師Jim將在2019-11-27 14:00 為大家帶來一場公開課,其中 Jim除了為大家講解Redis的基礎,同時也會實際演示他所在的項目組使用Redis時碰到的問題以及解決方案,對於剛接觸Redis的同學來說,更具參考意義和學習價值,歡迎大家屆時參加,公開課地址:。

  • 後端採用nodeJS
  • 使用Azure的Redis服務
  • Redis的使用場景

    -  token緩存, 用於令牌驗證

    -  IP白名單

碰到的問題

  • “網絡抖動”或者Redis服務異常導致Redis訪問超時
  • Redis客戶端驅動穩定性問題

    -  連接池 “Broken connection” 問題

    -  JS的Promise引出的Redis重置問題

下面我們來簡單了解一下Redis的進階知識。

進階之Redis協議簡介

Redis客戶端通訊協議:RESP(Redis Serialization Protocol),其特點是:

  • 簡單
  • 解析速度快
  • 可讀性好

Redis集群內部通訊協議:RECP(Redis Cluster Protocol ) ,其特點是:

  • 每一個node兩個tcp 連接
  • 一個負責client-server通訊(P: 6379)
  • 一個負責node之間通訊(P: 10000 + 6379)

 

Redis協議支持的數據類型:

  • 簡單字符(首字節: “+”)

        “+OK\r\n”

  • 錯誤(首字節: “-”)

        “-error msg\r\n”

  • 数字(首字節: “:”)

        “:123\r\n”

  • 批量字符(首字節: “$”)

        “&hello\r\nWhoa re you\r\n”

  • 數組(首字節: “*”)

        “*0\r\n”

        “*-1\r\n”

除了Redis,還有什麼NoSQL型數據庫

市面上類似於Redis,同樣是NoSQL型的數據庫有很多,如下圖所示,除了Redis,還有MemCache、Cassadra和Mongo。下面,我們就分別對這幾個數據庫做一下簡要的介紹:

 

 

Memcache這是一個和Redis非常相似的數據庫,但是它的數據類型沒有Redis豐富。Memcache由LiveJournal的Brad Fitzpatrick開發,作為一套分佈式的高速緩存系統,被許多網站使用以提升網站的訪問速度,對於一些大型的、需要頻繁訪問數據庫的網站訪問速度的提升效果十分顯著。

Apache Cassandra(社區內一般簡稱為C*)這是一套開源分佈式NoSQL數據庫系統。它最初由Facebook開發,用於儲存收件箱等簡單格式數據,集Google BigTable的數據模型與Amazon Dynamo的完全分佈式架構於一身。Facebook於2008將 Cassandra 開源,由於其良好的可擴展性和性能,被 Apple、Comcast、Instagram、Spotify、eBay、Rackspace、Netflix等知名網站所採用,成為了一種流行的分佈式結構化數據存儲方案。

MongoDB:是一個基於分佈式文件存儲、面向文檔的NoSQL數據庫,由C++編寫,旨在為WEB應用提供可擴展的高性能數據存儲解決方案。MongoDB是一個介於關係數據庫和非關係數據庫之間的產品,是非關係數據庫當中功能最豐富,最像關係型數據庫的,它支持的數據結構非常鬆散,是一種類似json的BSON格式。

總結

以上就是Redis入門介紹教程,如果各位還想了解更多,歡迎通過評論和私信的方式告訴我。

 

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理【其他文章推薦】

3c收購,鏡頭 收購有可能以全新價回收嗎?

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

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

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

賣IPHONE,iPhone回收,舊換新!教你怎麼賣才划算?

面試官:你連RESTful都不知道我怎麼敢要你?

目錄

面試官:了解RESTful嗎?

我:聽說過。

面試官:那什麼是RESTful?

我:就是用起來很規範,挺好的

面試官:是RESTful挺好的,還是自我感覺挺好的

我:都挺好的。

面試官:… 把門關上。

我:…. 要幹嘛?先關上再說。

面試官:我說出去把門關上。

我:what ?,奪門而去

@

01 前言

回歸正題,看過很多RESTful相關的文章總結,參齊不齊,結合工作中的使用,非常有必要歸納一下關於RESTful架構方式了,RESTful只是一種架構方式的約束,給出一種約定的標準,完全嚴格遵守RESTful標準並不是很多,也沒有必要。但是在實際運用中,有RESTful標準可以參考,是十分有必要的。

實際上在工作中對api接口規範、命名規則、返回值、授權驗證等進行一定的約束,一般的項目api只要易測試、足夠安全、風格一致可讀性強、沒有歧義調用方便我覺得已經足夠了,接口是給開發人員看的,也不是給普通用戶去調用。

02 RESTful的來源

REST:Representational State Transfer(表象層狀態轉變),如果沒聽說過REST,你一定以為是rest這個單詞,剛開始我也是這樣認為的,後來發現是這三個單詞的縮寫,即使知道了這三個單詞理解起來仍然非常晦澀難懂。如何理解RESTful架構,最好的辦法就是深刻理解消化Representational State Transfer這三個單詞到底意味着什麼。

1.每一個URI代表一種資源;

2.客戶端和服務器之間,傳遞這種資源的某種表現層;

3.客戶端通過四個HTTP動詞(get、post、put、delete),對服務器端資源進行操作,實現”表現層狀態轉化”。

是由美國計算機科學家Roy Fielding(百度百科沒有介紹,真是尷尬了)。Adobe首席科學家、Http協議的首要作者之一、Apache項目聯合創始人。

03 RESTful6大原則

REST之父Roy Fielding在論文中闡述REST架構的6大原則。

1. C-S架構

數據的存儲在Server端,Client端只需使用就行。兩端徹底分離的好處使client端代碼的可移植性變強,Server端的拓展性變強。兩端單獨開發,互不干擾。

2. 無狀態

http請求本身就是無狀態的,基於C-S架構,客戶端的每一次請求帶有充分的信息能夠讓服務端識別。請求所需的一些信息都包含在URL的查詢參數、header、body,服務端能夠根據請求的各種參數,無需保存客戶端的狀態,將響應正確返回給客戶端。無狀態的特徵大大提高的服務端的健壯性和可拓展性。

當然這總無狀態性的約束也是有缺點的,客戶端的每一次請求都必須帶上相同重複的信息確定自己的身份和狀態(這也是必須的),造成傳輸數據的冗餘性,但這種確定對於性能和使用來說,幾乎是忽略不計的。

3.統一的接口

這個才是REST架構的核心,統一的接口對於RESTful服務非常重要。客戶端只需要關注實現接口就可以,接口的可讀性加強,使用人員方便調用。

4.一致的數據格式

服務端返回的數據格式要麼是XML,要麼是Json(獲取數據),或者直接返回狀態碼,有興趣的可以看看博客園的開放平台的操作數據的api,post、put、patch都是返回的一個狀態碼 。

自我描述的信息,每項數據應該是可以自我描述的,方便代碼去處理和解析其中的內容。比如通過HTTP返回的數據裏面有 [MIME type ]信息,我們從MIME type裏面可以知道數據的具體格式,是圖片,視頻還是JSON,客戶端通過body內容、查詢串參數、請求頭和URI(資源名稱)來傳送狀態。服務端通過body內容,響應碼和響應頭傳送狀態給客戶端。這項技術被稱為超媒體(或超文本鏈接)。

除了上述內容外,HATEOS也意味着,必要的時候鏈接也可被包含在返回的body(或頭部)中,以提供URI來檢索對象本身或關聯對象。下文將對此進行更詳細的闡述。

如請求一條微博信息,服務端響應信息應該包含這條微博相關的其他URL,客戶端可以進一步利用這些URL發起請求獲取感興趣的信息,再如分頁可以從第一頁的返回數據中獲取下一頁的URT也是基於這個原理。

4.系統分層

客戶端通常無法表明自己是直接還是間接與端服務器進行連接,分層時同樣要考慮安全策略。

5.可緩存

在萬維網上,客戶端可以緩存頁面的響應內容。因此響應都應隱式或顯式的定義為可緩存的,若不可緩存則要避免客戶端在多次請求後用舊數據或臟數據來響應。管理得當的緩存會部分地或完全地除去客戶端和服務端之間的交互,進一步改善性能和延展性。

6.按需編碼、可定製代碼(可選)

服務端可選擇臨時給客戶端下發一些功能代碼讓客戶端來執行,從而定製和擴展客戶端的某些功能。比如服務端可以返回一些 Javascript 代碼讓客戶端執行,去實現某些特定的功能。
提示:REST架構中的設計準則中,只有按需編碼為可選項。如果某個服務違反了其他任意一項準則,嚴格意思上不能稱之為RESTful風格。

03 RESTful的7個最佳實踐

1. 版本

如github開放平台
https://developer.github.com/v3/

就是將版本放在url,簡潔明了,這個只有用了才知道,一般的項目加版本v1,v2,v3?好吧,這個加版本估計只有大公司大項目才會去使用,說出來不怕尷尬,我真沒用過。有的會將版本號放在header裏面,但是不如url直接了當。

https://example.com/api/v1/

2.參數命名規範

query parameter可以採用駝峰命名法,也可以採用下劃線命名的方式,推薦採用下劃線命名的方式,據說後者比前者的識別度要高,可能是用的人多了吧,因人而異,因團隊規範而異吧。

https://example.com/api/users/today_login 獲取今天登陸的用戶 
https://example.com/api/users/today_login&sort=login_desc 獲取今天登陸的用戶、登陸時間降序排列

3.url命名規範

API 命名應該採用約定俗成的方式,保持簡潔明了。在RESTful架構中,每個url代表一種資源所以url中不能有動詞,只能有名詞,並且名詞中也應該使用複數。實現者應使用相應的Http動詞GET、POST、PUT、PATCH、DELETE、HEAD來操作這些資源即可

不規範的的url,冗餘沒有意義,形式不固定,不同的開發者還需要了解文檔才能調用。

https://example.com/api/getallUsers GET 獲取所有用戶 
https://example.com/api/getuser/1 GET 獲取標識為1用戶信息 
https://example.com/api/user/delete/1 GET/POST 刪除標識為1用戶信息 
https://example.com/api/updateUser/1 POST 更新標識為1用戶信息 
https://example.com/api/User/add POST 添加新的用戶

規範后的RESTful風格的url,形式固定,可讀性強,根據users名詞和http動詞就可以操作這些資源

https://example.com/api/users GET 獲取所有用戶信息 
https://example.com/api/users/1 GET 獲取標識為1用戶信息 
https://example.com/api/users/1 DELETE 刪除標識為1用戶信息 
https://example.com/api/users/1 Patch 更新標識為1用戶部分信息,包含在body中 
https://example.com/api/users POST 添加新的用戶

4. 統一返回數據格式

對於合法的請求應該統一返回數據格式,這裏演示的是json

  • code——包含一個整數類型的HTTP響應狀態碼。
  • status——包含文本:”success”,”fail”或”error”。HTTP狀態響應碼在500-599之間為”fail”,在400-499之間為”error”,其它均為”success”(例如:響應狀態碼為1XX、2XX和3XX)。這個根據實際情況其實是可要可不要的。
  • message——當狀態值為”fail”和”error”時有效,用於显示錯誤信息。參照國際化(il8n)標準,它可以包含信息號或者編碼,可以只包含其中一個,或者同時包含並用分隔符隔開。
  • data——包含響應的body。當狀態值為”fail”或”error”時,data僅包含錯誤原因或異常名稱、或者null也是可以的

返回成功的響應json格式

{
  "code": 200,
  "message": "success",
  "data": {
    "userName": "123456",
    "age": 16,
    "address": "beijing"
  }
}

返回失敗的響應json格式

{
  "code": 401,
  "message": "error  message",
  "data": null
}

下面這個ApiResult的泛型類是在項目中用到的,拓展性強,使用方便。返回值使用統一的 ApiResult 或 ApiResult
錯誤返回 使用 ApiResult.Error 進行返回; 成功返回,要求使用 ApiResult.Ok 進行返回

public class ApiResult: ApiResult
    {
        public new static ApiResult<T> Error(string message)
        {
            return new ApiResult<T>
            {
                Code = 1,
                Message = message,
            };
        }
        [JsonProperty("data")]
        public T Data { get; set; }
    }
    public class ApiResult
    {
        public static ApiResult Error(string message)
        {
            return new ApiResult
            {
                Code = 1,
                Message = message,
            };
        }

        public static ApiResult<T> Ok<T>(T data)
        {
            return new ApiResult<T>()
            {
                Code = 0,
                Message = "",
                Data = data
            };
        }
        /// <summary>
        /// 0 是 正常 1 是有錯誤
        /// </summary>
        [JsonProperty("code")]
        public int Code { get; set; }
        [JsonProperty("msg")]
        public string Message { get; set; }

        [JsonIgnore]
        public bool IsSuccess => Code == 0;
    }

5. http狀態碼

在之前開發的xamarin android博客園客戶端的時候,patch、delete、post操作時body響應裏面沒有任何信息,僅僅只有http status code。HTTP狀態碼本身就有足夠的含義,根據http status code就可以知道刪除、添加、修改等是否成功。(ps:有點linux設計的味道哦,沒有返回消息就是最好的消息,表示已經成功了)服務段向用戶返回這些狀態碼並不是一個強制性的約束。簡單點說你可以指定這些狀態,但是不是強制的。常用HTTP狀態碼對照表
HTTP狀態碼也是有規律的

  • 1**請求未成功
  • 2**請求成功、表示成功處理了請求的狀態代碼。
  • 3**請求被重定向、表示要完成請求,需要進一步操作。 通常,這些狀態代碼用來重定向。
  • 4** 請求錯誤這些狀態代碼錶示請求可能出錯,妨礙了服務器的處理。
  • 5**(服務器錯誤)這些狀態代碼錶示服務器在嘗試處理請求時發生內部錯誤。 這些錯誤可能是服務器本身的錯誤,而不是請求出錯。

    6. 合理使用query parameter

    在請求數據時,客戶端經常會對數據進行過濾和分頁等要求,而這些參數推薦採用HTTP Query Parameter的方式實現

比如設計一個最近登陸的所有用戶
https://example.com/api/users?recently_login_day=3
搜索用戶,並按照註冊時間降序
https://example.com/api/users?recently_login_day=3
搜索用戶,並按照註冊時間升序、活躍度降序
https://example.com/api/users?q=key&sort=create_title_asc,liveness_desc
關於分頁,看看博客園開放平台分頁獲取精華區博文列表
https://api.cnblogs.com/api/blogposts/@picked?pageIndex={pageIndex}&pageSize={pageSize} 
返回示例: 
[ 
{ 
“Id”: 1, 
“Title”: “sample string 2”, 
“Url”: “sample string 3”, 
“Description”: “sample string 4”, 
“Author”: “sample string 5”, 
“BlogApp”: “sample string 6”, 
“Avatar”: “sample string 7”, 
“PostDate”: “2017-06-25T20:13:38.892135+08:00”, 
“ViewCount”: 9, 
“CommentCount”: 10, 
“DiggCount”: 11 
}, 
{ 
“Id”: 1, 
“Title”: “sample string 2”, 
“Url”: “sample string 3”, 
“Description”: “sample string 4”, 
“Author”: “sample string 5”, 
“BlogApp”: “sample string 6”, 
“Avatar”: “sample string 7”, 
“PostDate”: “2017-06-25T20:13:38.892135+08:00”, 
“ViewCount”: 9, 
“CommentCount”: 10, 
“DiggCount”: 11 
} 
]

7. 多表、多參數連接查詢如何設計URL

這是一個比較頭痛的問題,在做單個實體的查詢比較容易和規範操作,但是在實際的API並不是這麼簡單而已,這其中常常會設計到多表連接、多條件篩選、排序等。
比如我想查詢一個獲取在6月份的訂單中大於500元的且用戶地址是北京,用戶年齡在22歲到40歲、購買金額降序排列的訂單列表

https://example.com/api/orders?order_month=6&order_amount_greater=500&address_city=北京&sort=order_amount_desc&age_min=22&age_max=40

從這個URL上看,參數眾多、調用起來還得一個一個仔細對着,而且API本身非常不容易維護,命名看起來不是很容易,不能太長,也不能太隨意。

在.net WebAPI總我們可以使用屬性路由,屬性路由就是講路由附加到特定的控制器或操作方法上裝飾Controll及其使用[Route]屬性定義路由的方法稱為屬性路由。

這種好處就是可以精準地控制URL,而不是基於約定的路由,簡直就是為這種多表查詢量身定製似的的。 從webapi 2開發,現在是RESTful API開發中最推薦的路由類型。
我們可以在Controll中標記Route

[Route(“api/orders/{address}/{month}”)] 

Action中的查詢參數就只有金額、排序、年齡。減少了查詢參數、API的可讀性和可維護行增強了。

https://example.com/api/orders/beijing/6?order_amount_greater=500&sort=order_amount_desc&age_min=22&age_max=40

這種屬性路由比如在博客園開放的API也有這方面的應用,如獲取個人博客隨筆列表

請求方式:GET 
請求地址:https://api.cnblogs.com/api/blogs/{blogApp}/posts?pageIndex={pageIndex} 
(ps:blogApp:博客名)

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理【其他文章推薦】

3c收購,鏡頭 收購有可能以全新價回收嗎?

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

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

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

賣IPHONE,iPhone回收,舊換新!教你怎麼賣才划算?

地理文本處理技術在高德的演進(上)

一、背景

地圖App的功能可以簡單概括為定位,搜索,導航三部分,分別解決在哪裡,去哪裡,和怎麼去的問題。高德地圖的搜索場景下,輸入的是,地理相關的檢索query,用戶位置,App圖面等信息,輸出的是,用戶想要的POI。如何能夠更加精準地找到用戶想要的POI,提高滿意度,是評價搜索效果的最關鍵指標。

一個搜索引擎通常可以拆分成query分析、召回、排序三個部分,query分析主要是嘗試理解query表達的含義,為召回和排序給予指導。

地圖搜索的query分析不僅包括通用搜索下的分詞,成分分析,同義詞,糾錯等通用NLP技術,還包括城市分析,wherewhat分析,路徑規劃分析等特定的意圖理解方式。

常見的一些地圖場景下的query意圖表達如下:

query分析是搜索引擎中策略密集的場景,通常會應用NLP領域的各種技術。地圖場景下的query分析,只需要處理地理相關的文本,多樣性不如網頁搜索,看起來會簡單一些。但是,地理文本通常比較短,並且用戶大部分的需求是唯一少量結果,要求精準度非常高,如何能夠做好地圖場景下的文本分析,並提升搜索結果的質量,是充滿挑戰的。

二、整體技術架構

搜索架構

類似於通用檢索的架構,地圖的檢索架構包括query分析,召回,排序三個主要部分。先驗的,用戶的輸入信息可以理解為多種意圖的表達,同時下發請求嘗試獲取檢索結果。后驗的,拿到每種意圖的檢索結果時,進行綜合判斷,選擇效果最好的那個。

query分析流程

具體的意圖理解可分為基礎query分析和應用query分析兩部分,基礎query分析主要是使用一些通用的NLP技術對query進行理解,包括分析,成分分析,省略,同義詞,糾錯等。應用query分析主要是針對地圖場景里的特定問題,包括分析用戶目標城市,是否是where+what表達,是否是從A到B的路徑規劃需求表達等。

整體技術演進

在地里文本處理上整體的技術演進經歷了規則為主,到逐步引入機器學習,到機器學習全面應用的過程。由於搜索模塊是一個高併發的線上服務,對於深度模型的引入有比較苛刻的條件,但隨着性能問題逐漸被解決,我們從各個子方向逐步引入深度學習的技術,進行新一輪的效果提升。

NLP領域技術在最近幾年取得了日新月異的發展,bert,XLNet等模型相繼霸榜,我們逐步統一化各個query分析子任務,使用統一的向量表示對進行用戶需求進行表達,同時進行seq2seq的多任務學習,在效果進一步提升的基礎上,也能夠保證系統不會過於臃腫。

本文就高德地圖搜索的地理文本處理,介紹相關的技術在過去幾年的演進。我們將選取一些點分上下兩篇進行介紹,上篇主要介紹搜索引擎中一些通用的query分析技術,包括糾錯,改寫和省略。下篇着重介紹地圖場景中特有query分析技術,包括城市分析,wherewhat分析,路徑規劃。

三、通用query分析技術演進

3.1 糾錯

在搜索引擎中,用戶輸入的檢索詞(query)經常會出現拼寫錯誤。如果直接對錯誤的query進行檢索,往往不會得到用戶想要的結果。因此不管是通用搜索引擎還是垂直搜索引擎,都會對用戶的query進行糾錯,最大概率獲得用戶想搜的query。

在目前的地圖搜索中,約有6%-10%的用戶請求會輸入錯誤,所以query糾錯在地圖搜索中是一個很重要的模塊,能夠極大的提升用戶搜索體驗。

在搜索引擎中,低頻和中長尾問題往往比較難解決,也是糾錯模塊面臨的主要問題。另外,地圖搜索和通用搜索,存在一個明顯的差異,地圖搜索query結構化比較突出,query中的片段往往包含一定的位置信息,如何利用好query中的結構化信息,更好地識別用戶意圖,是地圖糾錯獨有的挑戰。

常見錯誤分類

(1) 拼音相同或者相近,例如: 盤橋物流園-潘橋物流園
(2) 字形相近,例如: 河北冒黎-河北昌黎
(3) 多字或者漏字,例如: 泉州州頂街-泉州頂街

糾錯現狀

原始糾錯模塊包括多種召回方式,如:

拼音糾錯:主要解決短query的拼音糾錯問題,拼音完全相同或者模糊音作為糾錯候選。
拼寫糾錯:也叫形近字糾錯,通過遍歷替換形近字,用query熱度過濾,加入候選。
組合糾錯:通過翻譯模型進行糾錯替換,資源主要是通過query對齊挖掘的各種替換資源。

組合糾錯翻譯模型計算公式:

其中p(f)是語言模型,p(f|e)是替換模型。

問題1:召回方式存在缺陷。目前query糾錯模塊主要召回策略包括拼音召回、形近字召回,以及替換資源召回。對於低頻case,解決能力有限。

問題2:排序方式不合理。糾錯按照召回方式分為幾個獨立的模塊,分別完成相應的召回和排序,不合理。

技術改造

改造1:基於空間關係的實體糾錯
原始的糾錯主要是基於用戶session挖掘片段替換資源,所以對於低頻問題解決能力有限。但是長尾問題往往集中在低頻,所以低頻問題是當前的痛點。

地圖搜索與通用搜索引擎有個很大的區別在於,地圖搜索query比較結構化,例如北京市朝陽區阜榮街10號首開廣場。我們可以對query進行結構化切分(也就是地圖中成分分析的工作),得到這樣一種帶有類別的結構化描述,北京市【城市】朝陽區【區縣】阜榮街【道路】10號【門址後綴】首開廣場【通用實體】。

同時,我們擁有權威的地理知識數據,利用權威化的地理實體庫進行前綴樹+後綴樹的索引建庫,提取疑似糾錯的部分在索引庫中進行拉鏈召回,同時利用實體庫中的邏輯隸屬關係對糾錯結果進行過濾。實踐表明,這種方式對低頻的區劃或者實體的錯誤有着明顯的作用。

基於字根的字形相似度計算

上文提到的排序策略裏面通過字形的編輯距離作為排序的重要特徵,這裏我們開發了一個基於字根的字形相似度計算策略,對於編輯距離的計算更為細化和準確。漢字信息有漢字的字根拆分詞表和漢字的筆畫數。

將一個漢字拆分成多個字根,尋找兩個字的公共字根,根據公共字根筆畫數來計算連個字的相似度。

改造2:排序策略重構

原始的策略召回和排序策略耦合,導致不同的召回鏈路,存在顧此失彼的情況。為了能夠充分發揮各種召回方式的優勢,急需要對召回和排序進行解耦並進行全局排序優化。為此我們增加了排序模塊,將流程分為召回和排序兩階段。

模型選擇

對於這個排序問題,這裏我們參考業界的實踐,使用了基於pair-wise的gbrank進行模型訓練。

樣本建設

通過線上輸出結合人工review的方式構造樣本。

特徵建設
(1) 語義特徵。如統計語言模型。
(2) 熱度特徵。pv,點擊等。
(3) 基礎特徵。編輯距離,切詞和成分特徵,累積分佈特徵等。

這裏解決了糾錯模塊兩個痛點問題,一個是在地圖場景下的大部分低頻糾錯問題。另一個是重構了模塊流程,將召回和排序解耦,充分發揮各個召回鏈路的作用,召回方式更新后只需要重訓排序模型即可,使得模塊更加合理,為後面的深度模型升級打下良好的基礎。後面在這個框架下,我們通過深度模型進行seq2seq的糾錯召回,取得了進一步的收益。

3.2 改寫

糾錯作為query變換的一種方式的召回策略存在諸多限制,對於一些非典型的query變換表達,存在策略的空白。比如query=永城市新農合辦,目標POI是永城市新農合服務大廳。用戶的低頻query,往往得不到較好搜索效果,但其實用戶描述的語義與主poi的高頻query是相似的。

這裏我們提出一種query改寫的思路,可以將低頻query改寫成語義相似的高頻query,以更好地滿足用戶需求多樣性的表達。

這是一個從無到有的實現。用戶表達的query是多樣的,使用規則表達顯然是難以窮盡的,直觀的思路是通過向量的方式召回,但是向量召回的方式很可能出現泛化過多,不適應地圖場景的檢索的問題,這些都是要在實踐過程中需要考慮的問題。

方案

整體看,方案包括召回,排序,過濾,三個階段。

召回階段

我們調研了句子向量表示的幾種方法,選擇了算法簡單,效果和性能可以和CNN,RNN媲美的SIF(Smooth Inverse Frequency)。向量召回可以使用開源的Faiss向量搜索引擎,這裏我們使用了阿里內部的性能更好的的向量檢索引擎。

排序階段
樣本構建
原query與高頻query候選集合,計算語義相似度,選取語義相似度的TOPK,人工標註的訓練樣本。

特徵建設

1.基礎文本特徵
2.編輯距離
3.組合特徵

模型選擇

使用xgboost進行分數回歸

過濾階段
通過向量召回的query過度泛化非常嚴重,為了能夠在地圖場景下進行應用,增加了對齊模型。使用了兩種統計對齊模型giza和fastalign,實驗證明二者效果幾乎一致,但fastalign在性能上好於giza,所以選擇fastalign。

通過對齊概率和非對齊概率,對召回的結果進行進一步過濾,得到精度比較高的結果。

query改寫填補了原始query分析模塊中一些低頻表達無法滿足的空白,區別於同義詞或者糾錯的顯式query變換表達,句子的向量表示是相似query的一種隱式的表達,有其相應的優勢。

向量表示和召回也是深度學習模型逐步開始應用的嘗試。同義詞,改寫,糾錯,作為地圖中query變換主要的三種方式,以往在地圖模塊里比較分散,各司其職,也會有互相重疊的部分。在後續的迭代升級中,我們引入了統一的query變換模型進行改造,在取得收益的同時,也擺脫掉了過去很多規則以及模型耦合造成的歷史包袱。

3.2 省略

在地圖搜索場景里,有很多query包含無效詞,如果用全部query嘗試去召回很可能不能召回有效結果。如廈門市搜”湖裡區縣后高新技術園新捷創運營中心11樓1101室 縣后brt站”。這就需要一種檢索意圖,在不明顯轉義下,使用核心term進行召回目標poi候選集合,當搜索結果無果或者召回較差時起到補充召回的作用。

在省略判斷的過程中存在先驗后驗平衡的問題。省略意圖是一個先驗的判斷,但是期望的結果是能夠進行POI有效召回,和POI的召回字段的現狀密切相關。如何能夠在策略設計的過程中保持先驗的一致性,同時能夠在後驗POI中拿到相對好的效果,是做好省略模塊比較困難的地方。

原始的省略模塊主要是基於規則進行的,規則依賴的主要特徵是上游的成分分析特徵。由於基於規則擬合,模型效果存在比較大的優化空間。另外,由於強依賴成分分析,模型的魯棒性並不好。

技術改造

省略模塊的改造主要完成了規則到crf模型的升級,其中也離線應用了深度學習模型輔助樣本生成。

模型選擇

識別出來query哪些部分是核心哪些部分是可以省略的,是一個序列標註問題。在淺層模型的選型中,顯而易見地,我們使用了crf模型。

特徵建設

term特徵。使用了賦權特徵,詞性,先驗詞典特徵等。
成分特徵。仍然使用成分分析的特徵。
統計特徵。統計片段的左右邊界熵,城市分佈熵等,通過分箱進行離散化。

樣本建設

項目一期我們使用了使用線上策略粗標,外包細標的方式,構造了萬級的樣本供crf模型訓練。

但是省略query的多樣性很高,使用萬級的樣本是不夠的,在線上模型無法快速應用深度模型的情況下,我們使用了boostraping的方式,藉助深度模型的泛化能力,離線構造了大量樣本。

使用了這種方式,樣本從萬級很容易擴充到百萬級,我們仍然使用crf模型進行訓練和線上應用。

在省略模塊,我們完成了規則到機器學習的升級,引入了成分以外的其他特徵,提升了模型的魯棒性。同時並且利用離線深度學習的方式進行樣本構造的循環,提升了樣本的多樣性,使得模型能夠更加接近crf的天花板。

在後續深度模型的建模中,我們逐步擺脫了對成分分析特徵的依賴,對query到命中poi核心直接進行建模,構建大量樣本,取得了進一步的收益。

 

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理【其他文章推薦】

3c收購,鏡頭 收購有可能以全新價回收嗎?

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

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

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

賣IPHONE,iPhone回收,舊換新!教你怎麼賣才划算?

如何使用偽類選擇器

偽類選擇器介紹

  • 偽類選擇器就是用來給超級鏈接設置不同的狀態樣式。
  • 超級鏈接分為4種狀態如:正常狀態、訪問過後狀態、鼠標放上狀態、激活狀態。

偽類選擇器說明表

選擇器 描述
:link 向未被訪問的超級鏈接添加樣式,正常狀態。
:visited 向已經被訪問的超級鏈接添加樣式,訪問過後狀態。
:hover 當鼠標懸浮在超級鏈接上方時,向超級鏈接添加樣式,鼠標放上狀態。
:active 鼠標放在超級鏈接上並且點擊的一瞬間,向超級鏈接添加樣式,激活狀態。

偽類選擇器實踐

  • 讓我們進入偽類選擇器實踐,實踐內容將超級鏈接4種狀態進行演示,演示效果如:將向未被訪問的超級鏈接文本顏色設置為紅色、已經被訪問的超級鏈接文本顏色設置為綠色、當鼠標懸浮在超級鏈接上文本顏色設置為紫色、用鼠標點擊超級鏈接的一瞬間文本顏色設置為藍色

  • 代碼塊

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>偽類選擇器</title>
    <style>
        a:link{
            color:red;
        }
        a:visited{
            color: lime;
        }
        a:hover{
            color: purple;
        }
        a:active{
            color: blue;
        }
    </style>
</head>
  
<body>
    <a href="https://www.cnblogs.com/lq000122/">微笑是最初的信仰</a>
</body>
</html>
  • 正常狀態結果圖

  • 鼠標放上狀態結果圖

  • 激活狀態結果圖

  • 訪問過後狀態

總結

  • 超級鏈接的不同狀態他其實是由順序,也就是說偽類選擇器設置其實是順序的,如果按照偽類選擇器的順序,那麼設置的樣式就不會被渲染。
  • 順序:linkvisitedhoveractive

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理【其他文章推薦】

3c收購,鏡頭 收購有可能以全新價回收嗎?

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

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

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

賣IPHONE,iPhone回收,舊換新!教你怎麼賣才划算?

帶你漲姿勢的認識一下 Kafka 消費者

之前我們介紹過了 Kafka 整體架構,Kafka 生產者,Kafka 生產的消息最終流向哪裡呢?當然是需要消費了,要不只產生一系列數據沒有任何作用啊,如果把 Kafka 比作餐廳的話,那麼生產者就是廚師的角色,消費者就是客人,只有廚師的話,那麼炒出來的菜沒有人吃也沒有意義,如果只有客人沒有廚師的話,誰會去這個店吃飯呢?!所以如果你看完前面的文章意猶未盡的話,可以繼續讓你爽一爽。如果你沒看過前面的文章,那就從現在開始讓你爽。

Kafka 消費者概念

應用程序使用 KafkaConsumer 從 Kafka 中訂閱主題並接收來自這些主題的消息,然後再把他們保存起來。應用程序首先需要創建一個 KafkaConsumer 對象,訂閱主題並開始接受消息,驗證消息並保存結果。一段時間后,生產者往主題寫入的速度超過了應用程序驗證數據的速度,這時候該如何處理?如果只使用單個消費者的話,應用程序會跟不上消息生成的速度,就像多個生產者像相同的主題寫入消息一樣,這時候就需要多個消費者共同參与消費主題中的消息,對消息進行分流處理。

Kafka 消費者從屬於消費者群組。一個群組中的消費者訂閱的都是相同的主題,每個消費者接收主題一部分分區的消息。下面是一個 Kafka 分區消費示意圖

上圖中的主題 T1 有四個分區,分別是分區0、分區1、分區2、分區3,我們創建一個消費者群組1,消費者群組中只有一個消費者,它訂閱主題T1,接收到 T1 中的全部消息。由於一個消費者處理四個生產者發送到分區的消息,壓力有些大,需要幫手來幫忙分擔任務,於是就演變為下圖

這樣一來,消費者的消費能力就大大提高了,但是在某些環境下比如用戶產生消息特別多的時候,生產者產生的消息仍舊讓消費者吃不消,那就繼續增加消費者。

如上圖所示,每個分區所產生的消息能夠被每個消費者群組中的消費者消費,如果向消費者群組中增加更多的消費者,那麼多餘的消費者將會閑置,如下圖所示

向群組中增加消費者是橫向伸縮消費能力的主要方式。總而言之,我們可以通過增加消費組的消費者來進行水平擴展提升消費能力。這也是為什麼建議創建主題時使用比較多的分區數,這樣可以在消費負載高的情況下增加消費者來提升性能。另外,消費者的數量不應該比分區數多,因為多出來的消費者是空閑的,沒有任何幫助。

Kafka 一個很重要的特性就是,只需寫入一次消息,可以支持任意多的應用讀取這個消息。換句話說,每個應用都可以讀到全量的消息。為了使得每個應用都能讀到全量消息,應用需要有不同的消費組。對於上面的例子,假如我們新增了一個新的消費組 G2,而這個消費組有兩個消費者,那麼就演變為下圖這樣

在這個場景中,消費組 G1 和消費組 G2 都能收到 T1 主題的全量消息,在邏輯意義上來說它們屬於不同的應用。

總結起來就是如果應用需要讀取全量消息,那麼請為該應用設置一個消費組;如果該應用消費能力不足,那麼可以考慮在這個消費組裡增加消費者

消費者組和分區重平衡

消費者組是什麼

消費者組(Consumer Group)是由一個或多個消費者實例(Consumer Instance)組成的群組,具有可擴展性和可容錯性的一種機制。消費者組內的消費者共享一個消費者組ID,這個ID 也叫做 Group ID,組內的消費者共同對一個主題進行訂閱和消費,同一個組中的消費者只能消費一個分區的消息,多餘的消費者會閑置,派不上用場。

我們在上面提到了兩種消費方式

  • 一個消費者群組消費一個主題中的消息,這種消費模式又稱為點對點的消費方式,點對點的消費方式又被稱為消息隊列
  • 一個主題中的消息被多個消費者群組共同消費,這種消費模式又稱為發布-訂閱模式

消費者重平衡

我們從上面的消費者演變圖中可以知道這麼一個過程:最初是一個消費者訂閱一個主題並消費其全部分區的消息,後來有一個消費者加入群組,隨後又有更多的消費者加入群組,而新加入的消費者實例分攤了最初消費者的部分消息,這種把分區的所有權通過一個消費者轉到其他消費者的行為稱為重平衡,英文名也叫做 Rebalance 。如下圖所示

重平衡非常重要,它為消費者群組帶來了高可用性伸縮性,我們可以放心的添加消費者或移除消費者,不過在正常情況下我們並不希望發生這樣的行為。在重平衡期間,消費者無法讀取消息,造成整個消費者組在重平衡的期間都不可用。另外,當分區被重新分配給另一個消費者時,消息當前的讀取狀態會丟失,它有可能還需要去刷新緩存,在它重新恢復狀態之前會拖慢應用程序。

消費者通過向組織協調者(Kafka Broker)發送心跳來維護自己是消費者組的一員並確認其擁有的分區。對於不同不的消費群體來說,其組織協調者可以是不同的。只要消費者定期發送心跳,就會認為消費者是存活的並處理其分區中的消息。當消費者檢索記錄或者提交它所消費的記錄時就會發送心跳。

如果過了一段時間 Kafka 停止發送心跳了,會話(Session)就會過期,組織協調者就會認為這個 Consumer 已經死亡,就會觸發一次重平衡。如果消費者宕機並且停止發送消息,組織協調者會等待幾秒鐘,確認它死亡了才會觸發重平衡。在這段時間里,死亡的消費者將不處理任何消息。在清理消費者時,消費者將通知協調者它要離開群組,組織協調者會觸發一次重平衡,盡量降低處理停頓。

重平衡是一把雙刃劍,它為消費者群組帶來高可用性和伸縮性的同時,還有有一些明顯的缺點(bug),而這些 bug 到現在社區還無法修改。

重平衡的過程對消費者組有極大的影響。因為每次重平衡過程中都會導致萬物靜止,參考 JVM 中的垃圾回收機制,也就是 Stop The World ,STW,(引用自《深入理解 Java 虛擬機》中 p76 關於 Serial 收集器的描述):

更重要的是它在進行垃圾收集時,必須暫停其他所有的工作線程。直到它收集結束。Stop The World 這個名字聽起來很帥,但這項工作實際上是由虛擬機在後台自動發起並完成的,在用戶不可見的情況下把用戶正常工作的線程全部停掉,這對很多應用來說都是難以接受的。

也就是說,在重平衡期間,消費者組中的消費者實例都會停止消費,等待重平衡的完成。而且重平衡這個過程很慢……

創建消費者

上面的理論說的有點多,下面就通過代碼來講解一下消費者是如何消費的

在讀取消息之前,需要先創建一個 KafkaConsumer 對象。創建 KafkaConsumer 對象與創建 KafkaProducer 對象十分相似 — 把需要傳遞給消費者的屬性放在 properties 對象中,後面我們會着重討論 Kafka 的一些配置,這裏我們先簡單的創建一下,使用3個屬性就足矣,分別是 bootstrap.serverkey.deserializervalue.deserializer

這三個屬性我們已經用過很多次了,如果你還不是很清楚的話,可以參考

還有一個屬性是 group.id 這個屬性不是必須的,它指定了 KafkaConsumer 是屬於哪個消費者群組。創建不屬於任何一個群組的消費者也是可以的

Properties properties = new Properties();
        properties.put("bootstrap.server","192.168.1.9:9092");     properties.put("key.serializer","org.apache.kafka.common.serialization.StringSerializer");   properties.put("value.serializer","org.apache.kafka.common.serialization.StringSerializer");
KafkaConsumer<String,String> consumer = new KafkaConsumer<>(properties);

主題訂閱

創建好消費者之後,下一步就開始訂閱主題了。subscribe() 方法接受一個主題列表作為參數,使用起來比較簡單

consumer.subscribe(Collections.singletonList("customerTopic"));

為了簡單我們只訂閱了一個主題 customerTopic,參數傳入的是一個正則表達式,正則表達式可以匹配多個主題,如果有人創建了新的主題,並且主題的名字與正則表達式相匹配,那麼會立即觸發一次重平衡,消費者就可以讀取新的主題。

要訂閱所有與 test 相關的主題,可以這樣做

consumer.subscribe("test.*");

輪詢

我們知道,Kafka 是支持訂閱/發布模式的,生產者發送數據給 Kafka Broker,那麼消費者是如何知道生產者發送了數據呢?其實生產者產生的數據消費者是不知道的,KafkaConsumer 採用輪詢的方式定期去 Kafka Broker 中進行數據的檢索,如果有數據就用來消費,如果沒有就再繼續輪詢等待,下面是輪詢等待的具體實現

try {
  while (true) {
    ConsumerRecords<String, String> records = consumer.poll(Duration.ofSeconds(100));
    for (ConsumerRecord<String, String> record : records) {
      int updateCount = 1;
      if (map.containsKey(record.value())) {
        updateCount = (int) map.get(record.value() + 1);
      }
      map.put(record.value(), updateCount);
    }
  }
}finally {
  consumer.close();
}
  • 這是一個無限循環。消費者實際上是一個長期運行的應用程序,它通過輪詢的方式向 Kafka 請求數據。
  • 第三行代碼非常重要,Kafka 必須定期循環請求數據,否則就會認為該 Consumer 已經掛了,會觸發重平衡,它的分區會移交給群組中的其它消費者。傳給 poll() 方法的是一個超市時間,用 java.time.Duration 類來表示,如果該參數被設置為 0 ,poll() 方法會立刻返回,否則就會在指定的毫秒數內一直等待 broker 返回數據。
  • poll() 方法會返回一個記錄列表。每條記錄都包含了記錄所屬主題的信息,記錄所在分區的信息、記錄在分區中的偏移量,以及記錄的鍵值對。我們一般會遍歷這個列表,逐條處理每條記錄。
  • 在退出應用程序之前使用 close() 方法關閉消費者。網絡連接和 socket 也會隨之關閉,並立即觸發一次重平衡,而不是等待群組協調器發現它不再發送心跳並認定它已經死亡。

線程安全性

在同一個群組中,我們無法讓一個線程運行多個消費者,也無法讓多個線程安全的共享一個消費者。按照規則,一個消費者使用一個線程,如果一個消費者群組中多個消費者都想要運行的話,那麼必須讓每個消費者在自己的線程中運行,可以使用 Java 中的 ExecutorService 啟動多個消費者進行進行處理。

消費者配置

到目前為止,我們學習了如何使用消費者 API,不過只介紹了幾個最基本的屬性,Kafka 文檔列出了所有與消費者相關的配置說明。大部分參數都有合理的默認值,一般不需要修改它們,下面我們就來介紹一下這些參數。

  • fetch.min.bytes

該屬性指定了消費者從服務器獲取記錄的最小字節數。broker 在收到消費者的數據請求時,如果可用的數據量小於 fetch.min.bytes 指定的大小,那麼它會等到有足夠的可用數據時才把它返回給消費者。這樣可以降低消費者和 broker 的工作負載,因為它們在主題使用頻率不是很高的時候就不用來回處理消息。如果沒有很多可用數據,但消費者的 CPU 使用率很高,那麼就需要把該屬性的值設得比默認值大。如果消費者的數量比較多,把該屬性的值調大可以降低 broker 的工作負載。

  • fetch.max.wait.ms

我們通過上面的 fetch.min.bytes 告訴 Kafka,等到有足夠的數據時才會把它返回給消費者。而 fetch.max.wait.ms 則用於指定 broker 的等待時間,默認是 500 毫秒。如果沒有足夠的數據流入 kafka 的話,消費者獲取的最小數據量要求就得不到滿足,最終導致 500 毫秒的延遲。如果要降低潛在的延遲,就可以把參數值設置的小一些。如果 fetch.max.wait.ms 被設置為 100 毫秒的延遲,而 fetch.min.bytes 的值設置為 1MB,那麼 Kafka 在收到消費者請求后,要麼返回 1MB 的數據,要麼在 100 ms 后返回所有可用的數據。就看哪個條件首先被滿足。

  • max.partition.fetch.bytes

該屬性指定了服務器從每個分區里返回給消費者的最大字節數。它的默認值時 1MB,也就是說,KafkaConsumer.poll() 方法從每個分區里返回的記錄最多不超過 max.partition.fetch.bytes 指定的字節。如果一個主題有20個分區和5個消費者,那麼每個消費者需要至少4 MB的可用內存來接收記錄。在為消費者分配內存時,可以給它們多分配一些,因為如果群組裡有消費者發生崩潰,剩下的消費者需要處理更多的分區。max.partition.fetch.bytes 的值必須比 broker 能夠接收的最大消息的字節數(通過 max.message.size 屬性配置大),否則消費者可能無法讀取這些消息,導致消費者一直掛起重試。 在設置該屬性時,另外一個考量的因素是消費者處理數據的時間。消費者需要頻繁的調用 poll() 方法來避免會話過期和發生分區再平衡,如果單次調用poll() 返回的數據太多,消費者需要更多的時間進行處理,可能無法及時進行下一個輪詢來避免會話過期。如果出現這種情況,可以把 max.partition.fetch.bytes 值改小,或者延長會話過期時間。

  • session.timeout.ms

這個屬性指定了消費者在被認為死亡之前可以與服務器斷開連接的時間,默認是 3s。如果消費者沒有在 session.timeout.ms 指定的時間內發送心跳給群組協調器,就會被認定為死亡,協調器就會觸發重平衡。把它的分區分配給消費者群組中的其它消費者,此屬性與 heartbeat.interval.ms 緊密相關。heartbeat.interval.ms 指定了 poll() 方法向群組協調器發送心跳的頻率,session.timeout.ms 則指定了消費者可以多久不發送心跳。所以,這兩個屬性一般需要同時修改,heartbeat.interval.ms 必須比 session.timeout.ms 小,一般是 session.timeout.ms 的三分之一。如果 session.timeout.ms 是 3s,那麼 heartbeat.interval.ms 應該是 1s。把 session.timeout.ms 值設置的比默認值小,可以更快地檢測和恢復崩憤的節點,不過長時間的輪詢或垃圾收集可能導致非預期的重平衡。把該屬性的值設置得大一些,可以減少意外的重平衡,不過檢測節點崩潰需要更長的時間。

  • auto.offset.reset

該屬性指定了消費者在讀取一個沒有偏移量的分區或者偏移量無效的情況下的該如何處理。它的默認值是 latest,意思指的是,在偏移量無效的情況下,消費者將從最新的記錄開始讀取數據。另一個值是 earliest,意思指的是在偏移量無效的情況下,消費者將從起始位置處開始讀取分區的記錄。

  • enable.auto.commit

我們稍後將介紹幾種不同的提交偏移量的方式。該屬性指定了消費者是否自動提交偏移量,默認值是 true,為了盡量避免出現重複數據和數據丟失,可以把它設置為 false,由自己控制何時提交偏移量。如果把它設置為 true,還可以通過 auto.commit.interval.ms 屬性來控制提交的頻率

  • partition.assignment.strategy

我們知道,分區會分配給群組中的消費者。PartitionAssignor 會根據給定的消費者和主題,決定哪些分區應該被分配給哪個消費者,Kafka 有兩個默認的分配策略RangeRoundRobin

  • client.id

該屬性可以是任意字符串,broker 用他來標識從客戶端發送過來的消息,通常被用在日誌、度量指標和配額中

  • max.poll.records

該屬性用於控制單次調用 call() 方法能夠返回的記錄數量,可以幫你控制在輪詢中需要處理的數據量。

  • receive.buffer.bytes 和 send.buffer.bytes

socket 在讀寫數據時用到的 TCP 緩衝區也可以設置大小。如果它們被設置為 -1,就使用操作系統默認值。如果生產者或消費者與 broker 處於不同的數據中心內,可以適當增大這些值,因為跨數據中心的網絡一般都有比較高的延遲和比較低的帶寬。

提交和偏移量的概念

特殊偏移

我們上面提到,消費者在每次調用poll() 方法進行定時輪詢的時候,會返回由生產者寫入 Kafka 但是還沒有被消費者消費的記錄,因此我們可以追蹤到哪些記錄是被群組裡的哪個消費者讀取的。消費者可以使用 Kafka 來追蹤消息在分區中的位置(偏移量)

消費者會向一個叫做 _consumer_offset 的特殊主題中發送消息,這個主題會保存每次所發送消息中的分區偏移量,這個主題的主要作用就是消費者觸發重平衡後記錄偏移使用的,消費者每次向這個主題發送消息,正常情況下不觸發重平衡,這個主題是不起作用的,當觸發重平衡后,消費者停止工作,每個消費者可能會分到對應的分區,這個主題就是讓消費者能夠繼續處理消息所設置的。

如果提交的偏移量小於客戶端最後一次處理的偏移量,那麼位於兩個偏移量之間的消息就會被重複處理

如果提交的偏移量大於最後一次消費時的偏移量,那麼處於兩個偏移量中間的消息將會丟失

既然_consumer_offset 如此重要,那麼它的提交方式是怎樣的呢?下面我們就來說一下

提交方式

KafkaConsumer API 提供了多種方式來提交偏移量

自動提交

最簡單的方式就是讓消費者自動提交偏移量。如果 enable.auto.commit 被設置為true,那麼每過 5s,消費者會自動把從 poll() 方法輪詢到的最大偏移量提交上去。提交時間間隔由 auto.commit.interval.ms 控制,默認是 5s。與消費者里的其他東西一樣,自動提交也是在輪詢中進行的。消費者在每次輪詢中會檢查是否提交該偏移量了,如果是,那麼就會提交從上一次輪詢中返回的偏移量。

提交當前偏移量

auto.commit.offset 設置為 false,可以讓應用程序決定何時提交偏移量。使用 commitSync() 提交偏移量。這個 API 會提交由 poll() 方法返回的最新偏移量,提交成功后馬上返回,如果提交失敗就拋出異常。

commitSync() 將會提交由 poll() 返回的最新偏移量,如果處理完所有記錄后要確保調用了 commitSync(),否則還是會有丟失消息的風險,如果發生了在均衡,從最近一批消息到發生在均衡之間的所有消息都將被重複處理。

異步提交

異步提交 commitAsync() 與同步提交 commitSync() 最大的區別在於異步提交不會進行重試,同步提交會一致進行重試。

同步和異步組合提交

一般情況下,針對偶爾出現的提交失敗,不進行重試不會有太大的問題,因為如果提交失敗是因為臨時問題導致的,那麼後續的提交總會有成功的。但是如果在關閉消費者或再均衡前的最後一次提交,就要確保提交成功。

因此,在消費者關閉之前一般會組合使用commitAsync和commitSync提交偏移量

提交特定的偏移量

消費者API允許調用 commitSync() 和 commitAsync() 方法時傳入希望提交的 partition 和 offset 的 map,即提交特定的偏移量。

文章參考:

《極客時間-Kafka核心技術與實戰》

《Kafka 權威指南》

關注公眾號獲取更多優質电子書,關注一下你就知道資源是有多好了

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理【其他文章推薦】

收購3c,收購IPHONE,收購蘋果電腦-詳細收購流程一覽表

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

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

※公開收購3c價格,不怕被賤賣!

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

自己動手實現分佈式任務調度框架(續)

  之前寫過一篇:本來是用來閑來分享一下自己的思維方式,時至今日發現居然有些人正在使用了,本着對代碼負責人的態度,對代碼部分已知bug進行了修改,並增加了若干功能,如立即啟動,實時停止等功能,新增加的功能會在這一篇做詳細的說明。

  提到分佈式任務調度,市面上本身已經有一些框架工具可以使用,但是個人覺得功能做的都太豐富,架構都過於複雜,所以才有了我重複造輪子。個人喜歡把複雜的問題簡單化,利用有限的資源實現竟可能多的功能。因為有幾個朋友問部署方式,這裏再次強調下:我的這個服務可以直接打成jar放在自己本地倉庫,然後依賴進去,或者直接copy代碼過去,當成自己項目的一部分就可以了。也就是說跟隨你們自己的項目啟動,所以我這裏也沒有寫界面。下面先談談怎麼基於上次的代碼實現任務立即啟動吧!

  調度和自己服務整合後部署圖抽象成如下:

  

 

 

   用戶在前端點擊立即請求按鈕,通過各種負載均衡軟件或者設備,到達某台機器的某個帶有本調度框架的服務,然後進行具體的執行,也就是說這個立即啟動就是一個最常見最簡單的請求,沒有過多複雜的問題(比如多節點會不會重複執行這些)。最簡單的辦法,當用戶請求過來直接用一個線程或者線程池執行用戶點的那個任務的邏輯代碼就行了,當然我這裏沒有那麼粗暴,現有的調度代碼資源如下:

package com.rdpaas.task.scheduler;

import com.rdpaas.task.common.Invocation;
import com.rdpaas.task.common.Node;
import com.rdpaas.task.common.NotifyCmd;
import com.rdpaas.task.common.Task;
import com.rdpaas.task.common.TaskDetail;
import com.rdpaas.task.common.TaskStatus;
import com.rdpaas.task.config.EasyJobConfig;
import com.rdpaas.task.repository.NodeRepository;
import com.rdpaas.task.repository.TaskRepository;
import com.rdpaas.task.strategy.Strategy;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import javax.annotation.PostConstruct;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.Callable;
import java.util.concurrent.DelayQueue;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

/**
 * 任務調度器
 * @author rongdi
 * @date 2019-03-13 21:15
 */
@Component
public class TaskExecutor {

    private static final Logger logger = LoggerFactory.getLogger(TaskExecutor.class);

    @Autowired
    private TaskRepository taskRepository;

    @Autowired
    private NodeRepository nodeRepository;

    @Autowired
    private EasyJobConfig config;

    /**
     * 創建任務到期延時隊列
      */
    private DelayQueue<DelayItem<Task>> taskQueue = new DelayQueue<>();

    /**
     * 可以明確知道最多只會運行2個線程,直接使用系統自帶工具就可以了
     */
    private ExecutorService bossPool = Executors.newFixedThreadPool(2);

    /**
     * 正在執行的任務的Future
     */
    private Map<Long,Future> doingFutures = new HashMap<>();

    /**
     * 聲明工作線程池
     */
    private ThreadPoolExecutor workerPool;
    
    /**
     * 獲取任務的策略
     */
    private Strategy strategy;


    @PostConstruct
    public void init() {
        /**
         * 根據配置選擇一個節點獲取任務的策略
         */
        strategy = Strategy.choose(config.getNodeStrategy());
        /**
         * 自定義線程池,初始線程數量corePoolSize,線程池等待隊列大小queueSize,當初始線程都有任務,並且等待隊列滿后
         * 線程數量會自動擴充最大線程數maxSize,當新擴充的線程空閑60s后自動回收.自定義線程池是因為Executors那幾個線程工具
         * 各有各的弊端,不適合生產使用
         */
        workerPool = new ThreadPoolExecutor(config.getCorePoolSize(), config.getMaxPoolSize(), 60, TimeUnit.SECONDS, new ArrayBlockingQueue<>(config.getQueueSize()));
        /**
         * 執行待處理任務加載線程
         */
        bossPool.execute(new Loader());
        /**
         * 執行任務調度線程
         */
        bossPool.execute(new Boss());
    
    }

    class Loader implements Runnable {

        @Override
        public void run() {
            for(;;) {
                try { 
                    /**
                     * 先獲取可用的節點列表
                     */
                    List<Node> nodes = nodeRepository.getEnableNodes(config.getHeartBeatSeconds() * 2);
                    if(nodes == null || nodes.isEmpty()) {
                        continue;
                    }
                    /**
                     * 查找還有指定時間(單位秒)才開始的主任務列表
                     */
                    List<Task> tasks = taskRepository.listNotStartedTasks(config.getFetchDuration());
                    if(tasks == null || tasks.isEmpty()) {
                        continue;
                    }
                    for(Task task:tasks) {
                        
                        boolean accept = strategy.accept(nodes, task, config.getNodeId());
                        /**
                         * 不該自己拿就不要搶
                         */
                        if(!accept) {
                            continue;
                        }
                        /**
                         * 先設置成待執行
                         */
                        task.setStatus(TaskStatus.PENDING);
                        task.setNodeId(config.getNodeId());
                        /**
                         * 使用樂觀鎖嘗試更新狀態,如果更新成功,其他節點就不會更新成功。如果其它節點也正在查詢未完成的
                         * 任務列表和當前這段時間有節點已經更新了這個任務,version必然和查出來時候的version不一樣了,這裏更新
                         * 必然會返回0了
                         */
                        int n = taskRepository.updateWithVersion(task);
                        Date nextStartTime = task.getNextStartTime();
                        if(n == 0 || nextStartTime == null) {
                            continue;
                        }
                        /**
                         * 封裝成延時對象放入延時隊列,這裏再查一次是因為上面樂觀鎖已經更新了版本,會導致後面結束任務更新不成功
                         */
                        task = taskRepository.get(task.getId());
                        DelayItem<Task> delayItem = new DelayItem<Task>(nextStartTime.getTime() - new Date().getTime(), task);
                        taskQueue.offer(delayItem);
                        
                    }
                    Thread.sleep(config.getFetchPeriod());
                } catch(Exception e) {
                    logger.error("fetch task list failed,cause by:{}", e);
                }
            }
        }
        
    }
    
    class Boss implements Runnable {
        @Override
        public void run() {
            for (;;) {
                try {
                     /**
                     * 時間到了就可以從延時隊列拿出任務對象,然後交給worker線程池去執行
                     */
                    DelayItem<Task> item = taskQueue.take();
                    if(item != null && item.getItem() != null) {
                        Task task = item.getItem();
                        /**
                         * 真正開始執行了設置成執行中
                         */
                        task.setStatus(TaskStatus.DOING);
                        /**
                         * loader線程中已經使用樂觀鎖控制了,這裏沒必要了
                         */
                        taskRepository.update(task);
                        /**
                         * 提交到線程池
                         */
                        Future future = workerPool.submit(new Worker(task));
                        /**
                         * 暫存在doingFutures
                         */
                        doingFutures.put(task.getId(),future);
                    }
                     
                } catch (Exception e) {
                    logger.error("fetch task failed,cause by:{}", e);
                }
            }
        }

    }

    class Worker implements Callable<String> {

        private Task task;

        public Worker(Task task) {
            this.task = task;
        }

        @Override
        public String call() {
            logger.info("Begin to execute task:{}",task.getId());
            TaskDetail detail = null;
            try {
                //開始任務
                detail = taskRepository.start(task);
                if(detail == null) return null;
                //執行任務
                task.getInvokor().invoke();
                //完成任務
                finish(task,detail);
                logger.info("finished execute task:{}",task.getId());
                /**
                 * 執行完后刪了
                 */
                doingFutures.remove(task.getId());
            } catch (Exception e) {
                logger.error("execute task:{} error,cause by:{}",task.getId(), e);
                try {
                    taskRepository.fail(task,detail,e.getCause().getMessage());
                } catch(Exception e1) {
                    logger.error("fail task:{} error,cause by:{}",task.getId(), e);
                }
            }
            return null;
        }

    }

    /**
     * 完成子任務,如果父任務失敗了,子任務不會執行
     * @param task
     * @param detail
     * @throws Exception
     */
    private void finish(Task task,TaskDetail detail) throws Exception {

        //查看是否有子類任務
        List<Task> childTasks = taskRepository.getChilds(task.getId());
        if(childTasks == null || childTasks.isEmpty()) {
            //當沒有子任務時完成父任務
            taskRepository.finish(task,detail);
            return;
        } else {
            for (Task childTask : childTasks) {
                //開始任務
                TaskDetail childDetail = null;
                try {
                    //將子任務狀態改成執行中
                    childTask.setStatus(TaskStatus.DOING);
                    childTask.setNodeId(config.getNodeId());
                    //開始子任務
                    childDetail = taskRepository.startChild(childTask,detail);
                    //使用樂觀鎖更新下狀態,不然這裏可能和恢複線程產生併發問題
                    int n = taskRepository.updateWithVersion(childTask);
                    if (n > 0) {
                        //再從數據庫取一下,避免上面update修改后version不同步
                        childTask = taskRepository.get(childTask.getId());
                        //執行子任務
                        childTask.getInvokor().invoke();
                        //完成子任務
                        finish(childTask, childDetail);
                    }
                } catch (Exception e) {
                    logger.error("execute child task error,cause by:{}", e);
                    try {
                        taskRepository.fail(childTask, childDetail, e.getCause().getMessage());
                    } catch (Exception e1) {
                        logger.error("fail child task error,cause by:{}", e);
                    }
                }
            }
            /**
             * 當有子任務時完成子任務后再完成父任務
             */
            taskRepository.finish(task,detail);

        }

    }

    /**
     * 添加任務
     * @param name
     * @param cronExp
     * @param invockor
     * @return
     * @throws Exception
     */
    public long addTask(String name, String cronExp, Invocation invockor) throws Exception {
        Task task = new Task(name,cronExp,invockor);
        return taskRepository.insert(task);
    }

    /**
     * 添加子任務
     * @param pid
     * @param name
     * @param cronExp
     * @param invockor
     * @return
     * @throws Exception
     */
    public long addChildTask(Long pid,String name, String cronExp, Invocation invockor) throws Exception {
        Task task = new Task(name,cronExp,invockor);
        task.setPid(pid);
        return taskRepository.insert(task);
    }

   
}

  上面主要就是三組線程,Loader負責加載將要執行的任務放入本地的任務隊列,Boss線程負責取出任務隊列的任務,然後分配Worker線程池的一個線程去執行。由上面的代碼可以看到如果要立即執行,其實只需要把一個延時為0的任務放入任務隊列,等着Boss線程去取然後分配給worker執行就可以實現了,代碼如下:

    /**
     * 立即執行任務,就是設置一下延時為0加入任務隊列就好了,這個可以外部直接調用
     * @param taskId
     * @return
     */
    public boolean startNow(Long taskId) {
        Task task = taskRepository.get(taskId);
        task.setStatus(TaskStatus.DOING);
        taskRepository.update(task);
        DelayItem<Task> delayItem = new DelayItem<Task>(0L, task);
        return taskQueue.offer(delayItem);
    }

  啟動不用再多說,下面介紹一下停止任務,根據面向對象的思維,用戶要想停止一個任務,最終執行停止任務的就是正在執行任務的那個節點。停止任務有兩種情況,第一種任務沒有正在運行如何停止,第二種是任務正在運行如何停止。第一種其實直接改變一下任務對象的狀態為停止就行了,不必多說。下面主要考慮如何停止正在運行的任務,細心的朋友可能已經發現上面代碼和之前那一篇代碼有點區別,之前用的Runnble作為線程實現接口,這個用了Callable,其實在java中停止線程池中正在運行的線程最常用的就是直接調用future的cancel方法了,要想獲取到這個future對象就需要將以前實現Runnbale改成實現Callable,然後提交到線程池由execute改成submit就可以了,然後每次提交到線程池得到的future對象使用taskId一起保存在一個map中,方便根據taskId隨時找到。當然任務執行完后要及時刪除這個map里的任務,以免常駐其中導致內存溢出。停止任務的請求流程如下

  

 

 

  圖還是原來的圖,但是這時候情況不一樣了,因為停止任務的時候假如當前正在執行這個任務的節點處於服務1,負載均衡是不知道要去把你引到服務1的,他可能會引入到服務2,那就悲劇了,所以通用的做法就是停止請求過來不管落到哪個節點上,那個節點就往一個公用的mq上發一個帶有停止任務業務含義的消息,各個節點訂閱這個消息,然後判斷都判斷任務在不在自己這裏執行,如果在就執行停止操作。但是這樣勢必讓我們的調度服務又要依賴一個外部的消息隊列服務,就算很方便的就可以引入一個外部的消息隊列,但是你真的可以駕馭的了嗎,消息丟了咋辦,重複發送了咋辦,消息服務掛了咋辦,網絡斷了咋辦,又引入了一大堆問題,那我是不是又要寫n篇文章來分別解決這些問題。往往現實卻是就是這麼殘酷,你解決了一個問題,引入了更多的問題,這就是為什麼bug永遠改不完的道理了。當然這不是我的風格,我的風格是利用有限的資源做盡可能多的事情(可能是由於我工作的企業都是那種資源貧瘠的,養成了我這種習慣,土豪公司的程序員請繞道,哈哈)。

  簡化一下問題:目前的問題就是如何讓正在執行任務的節點知道,然後停止正在執行的這個任務,其實就是這個停止通知如何實現。這不免讓我想起了12306網站上買票,其實我們作為老百姓多麼希望12306可以在有票的時候發個短信通知一下我們,然後我們上去搶,但是現實卻是,你要麼使用軟件一直刷,要麼是自己隔一段時間上去瞄一下有沒有票。如果把有票了給我們發短信通知定義為異步通知,那麼這種我們要隔一段時間自己去瞄一下的方式就是同步輪訓。這兩種方式都能達到告知的目的,關鍵的區別在於你到底有沒有時間去一直去瞄,不過相比於可以回家,這些時間都是值得的。個人認為軟件的設計其實就是一個權衡是否值得的過程。如果約定了不使用外部消息隊列這種異步通知的方式,那麼我們只能使用同步輪訓的方式了。不過正好我們的任務調度本身已經有一個心跳機制,沒隔一段時間就去更新一下節點狀態,如果我們把用戶的停止請求作為命令信息更新到每個節點的上,然後隨着心跳獲取到這個節點的信息,然後判斷這個命令,做相應的處理是不是就可以完美解決這個問題。值得嗎?很明顯是值得的,我們只是在心跳邏輯上加一個小小的副作用就實現了通知功能了。代碼如下

package com.rdpaas.task.common;

/**
 * @author rongdi
 * @date 2019/11/26
 */
public enum NotifyCmd {

    //沒有通知,默認狀態
    NO_NOTIFY(0),
    //開啟任務(Task)
    START_TASK(1),
    //修改任務(Task)
    EDIT_TASK(2),
    //停止任務(Task)
    STOP_TASK(3);

    int id;

    NotifyCmd(int id) {
        this.id = id;
    }

    public int getId() {
        return id;
    }

    public static NotifyCmd valueOf(int id) {
        switch (id) {
            case 1:
                return START_TASK;
            case 2:
                return EDIT_TASK;
            case 3:
                return STOP_TASK;
            default:
                return NO_NOTIFY;
        }
    }

}
package com.rdpaas.task.handles;

import com.rdpaas.task.common.NotifyCmd;
import com.rdpaas.task.utils.SpringContextUtil;

/**
 * @author: rongdi
 * @date:
 */
public interface NotifyHandler<T> {

    static NotifyHandler chooseHandler(NotifyCmd notifyCmd) {
        return SpringContextUtil.getByTypeAndName(NotifyHandler.class,notifyCmd.toString());
    }

    public void update(T t);

}
package com.rdpaas.task.handles;

import com.rdpaas.task.scheduler.TaskExecutor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

/**
 * @author: rongdi
 * @date:
 */
@Component("STOP_TASK")
public class StopTaskHandler implements NotifyHandler<Long> {

    @Autowired
    private TaskExecutor taskExecutor;

    @Override
    public void update(Long taskId) {
        taskExecutor.stop(taskId);
    }

}
class HeartBeat implements Runnable {
        @Override
        public void run() {
            for(;;) {
                try {
                    /**
                     * 時間到了就可以從延時隊列拿出節點對象,然後更新時間和序號,
                     * 最後再新建一個超時時間為心跳時間的節點對象放入延時隊列,形成循環的心跳
                     */
                    DelayItem<Node> item = heartBeatQueue.take();
                    if(item != null && item.getItem() != null) {
                        Node node = item.getItem();
                        handHeartBeat(node);
                    }
                    heartBeatQueue.offer(new DelayItem<>(config.getHeartBeatSeconds() * 1000,new Node(config.getNodeId())));
                } catch (Exception e) {
                    logger.error("task heart beat error,cause by:{} ",e);
                }
            }
        }
    }

    /**
     * 處理節點心跳
     * @param node
     */
    private void handHeartBeat(Node node) {
        if(node == null) {
            return;
        }
        /**
         * 先看看數據庫是否存在這個節點
         * 如果不存在:先查找下一個序號,然後設置到node對象中,最後插入
         * 如果存在:直接根據nodeId更新當前節點的序號和時間
         */
        Node currNode= nodeRepository.getByNodeId(node.getNodeId());
        if(currNode == null) {
            node.setRownum(nodeRepository.getNextRownum());
            nodeRepository.insert(node);
        } else  {
            nodeRepository.updateHeartBeat(node.getNodeId());
            NotifyCmd cmd = currNode.getNotifyCmd();
            String notifyValue = currNode.getNotifyValue();
            if(cmd != null && cmd != NotifyCmd.NO_NOTIFY) {
                /**
                 * 藉助心跳做一下通知的事情,比如及時停止正在執行的任務
                 * 根據指令名稱查找Handler
                 */
                NotifyHandler handler = NotifyHandler.chooseHandler(currNode.getNotifyCmd());
                if(handler == null || StringUtils.isEmpty(notifyValue)) {
                    return;
                }
                /**
                 * 執行操作
                 */
                handler.update(Long.valueOf(notifyValue));
            }
            
        }


    }

  最終的任務調度代碼如下:

package com.rdpaas.task.scheduler;

import com.rdpaas.task.common.Invocation;
import com.rdpaas.task.common.Node;
import com.rdpaas.task.common.NotifyCmd;
import com.rdpaas.task.common.Task;
import com.rdpaas.task.common.TaskDetail;
import com.rdpaas.task.common.TaskStatus;
import com.rdpaas.task.config.EasyJobConfig;
import com.rdpaas.task.repository.NodeRepository;
import com.rdpaas.task.repository.TaskRepository;
import com.rdpaas.task.strategy.Strategy;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import javax.annotation.PostConstruct;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.Callable;
import java.util.concurrent.DelayQueue;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

/**
 * 任務調度器
 * @author rongdi
 * @date 2019-03-13 21:15
 */
@Component
public class TaskExecutor {

    private static final Logger logger = LoggerFactory.getLogger(TaskExecutor.class);

    @Autowired
    private TaskRepository taskRepository;

    @Autowired
    private NodeRepository nodeRepository;

    @Autowired
    private EasyJobConfig config;

    /**
     * 創建任務到期延時隊列
      */
    private DelayQueue<DelayItem<Task>> taskQueue = new DelayQueue<>();

    /**
     * 可以明確知道最多只會運行2個線程,直接使用系統自帶工具就可以了
     */
    private ExecutorService bossPool = Executors.newFixedThreadPool(2);

    /**
     * 正在執行的任務的Future
     */
    private Map<Long,Future> doingFutures = new HashMap<>();

    /**
     * 聲明工作線程池
     */
    private ThreadPoolExecutor workerPool;
    
    /**
     * 獲取任務的策略
     */
    private Strategy strategy;


    @PostConstruct
    public void init() {
        /**
         * 根據配置選擇一個節點獲取任務的策略
         */
        strategy = Strategy.choose(config.getNodeStrategy());
        /**
         * 自定義線程池,初始線程數量corePoolSize,線程池等待隊列大小queueSize,當初始線程都有任務,並且等待隊列滿后
         * 線程數量會自動擴充最大線程數maxSize,當新擴充的線程空閑60s后自動回收.自定義線程池是因為Executors那幾個線程工具
         * 各有各的弊端,不適合生產使用
         */
        workerPool = new ThreadPoolExecutor(config.getCorePoolSize(), config.getMaxPoolSize(), 60, TimeUnit.SECONDS, new ArrayBlockingQueue<>(config.getQueueSize()));
        /**
         * 執行待處理任務加載線程
         */
        bossPool.execute(new Loader());
        /**
         * 執行任務調度線程
         */
        bossPool.execute(new Boss());
    
    }

    class Loader implements Runnable {

        @Override
        public void run() {
            for(;;) {
                try { 
                    /**
                     * 先獲取可用的節點列表
                     */
                    List<Node> nodes = nodeRepository.getEnableNodes(config.getHeartBeatSeconds() * 2);
                    if(nodes == null || nodes.isEmpty()) {
                        continue;
                    }
                    /**
                     * 查找還有指定時間(單位秒)才開始的主任務列表
                     */
                    List<Task> tasks = taskRepository.listNotStartedTasks(config.getFetchDuration());
                    if(tasks == null || tasks.isEmpty()) {
                        continue;
                    }
                    for(Task task:tasks) {
                        
                        boolean accept = strategy.accept(nodes, task, config.getNodeId());
                        /**
                         * 不該自己拿就不要搶
                         */
                        if(!accept) {
                            continue;
                        }
                        /**
                         * 先設置成待執行
                         */
                        task.setStatus(TaskStatus.PENDING);
                        task.setNodeId(config.getNodeId());
                        /**
                         * 使用樂觀鎖嘗試更新狀態,如果更新成功,其他節點就不會更新成功。如果其它節點也正在查詢未完成的
                         * 任務列表和當前這段時間有節點已經更新了這個任務,version必然和查出來時候的version不一樣了,這裏更新
                         * 必然會返回0了
                         */
                        int n = taskRepository.updateWithVersion(task);
                        Date nextStartTime = task.getNextStartTime();
                        if(n == 0 || nextStartTime == null) {
                            continue;
                        }
                        /**
                         * 封裝成延時對象放入延時隊列,這裏再查一次是因為上面樂觀鎖已經更新了版本,會導致後面結束任務更新不成功
                         */
                        task = taskRepository.get(task.getId());
                        DelayItem<Task> delayItem = new DelayItem<Task>(nextStartTime.getTime() - new Date().getTime(), task);
                        taskQueue.offer(delayItem);
                        
                    }
                    Thread.sleep(config.getFetchPeriod());
                } catch(Exception e) {
                    logger.error("fetch task list failed,cause by:{}", e);
                }
            }
        }
        
    }
    
    class Boss implements Runnable {
        @Override
        public void run() {
            for (;;) {
                try {
                     /**
                     * 時間到了就可以從延時隊列拿出任務對象,然後交給worker線程池去執行
                     */
                    DelayItem<Task> item = taskQueue.take();
                    if(item != null && item.getItem() != null) {
                        Task task = item.getItem();
                        /**
                         * 真正開始執行了設置成執行中
                         */
                        task.setStatus(TaskStatus.DOING);
                        /**
                         * loader線程中已經使用樂觀鎖控制了,這裏沒必要了
                         */
                        taskRepository.update(task);
                        /**
                         * 提交到線程池
                         */
                        Future future = workerPool.submit(new Worker(task));
                        /**
                         * 暫存在doingFutures
                         */
                        doingFutures.put(task.getId(),future);
                    }
                     
                } catch (Exception e) {
                    logger.error("fetch task failed,cause by:{}", e);
                }
            }
        }

    }

    class Worker implements Callable<String> {

        private Task task;

        public Worker(Task task) {
            this.task = task;
        }

        @Override
        public String call() {
            logger.info("Begin to execute task:{}",task.getId());
            TaskDetail detail = null;
            try {
                //開始任務
                detail = taskRepository.start(task);
                if(detail == null) return null;
                //執行任務
                task.getInvokor().invoke();
                //完成任務
                finish(task,detail);
                logger.info("finished execute task:{}",task.getId());
                /**
                 * 執行完后刪了
                 */
                doingFutures.remove(task.getId());
            } catch (Exception e) {
                logger.error("execute task:{} error,cause by:{}",task.getId(), e);
                try {
                    taskRepository.fail(task,detail,e.getCause().getMessage());
                } catch(Exception e1) {
                    logger.error("fail task:{} error,cause by:{}",task.getId(), e);
                }
            }
            return null;
        }

    }

    /**
     * 完成子任務,如果父任務失敗了,子任務不會執行
     * @param task
     * @param detail
     * @throws Exception
     */
    private void finish(Task task,TaskDetail detail) throws Exception {

        //查看是否有子類任務
        List<Task> childTasks = taskRepository.getChilds(task.getId());
        if(childTasks == null || childTasks.isEmpty()) {
            //當沒有子任務時完成父任務
            taskRepository.finish(task,detail);
            return;
        } else {
            for (Task childTask : childTasks) {
                //開始任務
                TaskDetail childDetail = null;
                try {
                    //將子任務狀態改成執行中
                    childTask.setStatus(TaskStatus.DOING);
                    childTask.setNodeId(config.getNodeId());
                    //開始子任務
                    childDetail = taskRepository.startChild(childTask,detail);
                    //使用樂觀鎖更新下狀態,不然這裏可能和恢複線程產生併發問題
                    int n = taskRepository.updateWithVersion(childTask);
                    if (n > 0) {
                        //再從數據庫取一下,避免上面update修改后version不同步
                        childTask = taskRepository.get(childTask.getId());
                        //執行子任務
                        childTask.getInvokor().invoke();
                        //完成子任務
                        finish(childTask, childDetail);
                    }
                } catch (Exception e) {
                    logger.error("execute child task error,cause by:{}", e);
                    try {
                        taskRepository.fail(childTask, childDetail, e.getCause().getMessage());
                    } catch (Exception e1) {
                        logger.error("fail child task error,cause by:{}", e);
                    }
                }
            }
            /**
             * 當有子任務時完成子任務后再完成父任務
             */
            taskRepository.finish(task,detail);

        }

    }

    /**
     * 添加任務
     * @param name
     * @param cronExp
     * @param invockor
     * @return
     * @throws Exception
     */
    public long addTask(String name, String cronExp, Invocation invockor) throws Exception {
        Task task = new Task(name,cronExp,invockor);
        return taskRepository.insert(task);
    }

    /**
     * 添加子任務
     * @param pid
     * @param name
     * @param cronExp
     * @param invockor
     * @return
     * @throws Exception
     */
    public long addChildTask(Long pid,String name, String cronExp, Invocation invockor) throws Exception {
        Task task = new Task(name,cronExp,invockor);
        task.setPid(pid);
        return taskRepository.insert(task);
    }

    /**
     * 立即執行任務,就是設置一下延時為0加入任務隊列就好了,這個可以外部直接調用
     * @param taskId
     * @return
     */
    public boolean startNow(Long taskId) {
        Task task = taskRepository.get(taskId);
        task.setStatus(TaskStatus.DOING);
        taskRepository.update(task);
        DelayItem<Task> delayItem = new DelayItem<Task>(0L, task);
        return taskQueue.offer(delayItem);
    }

    /**
     * 立即停止正在執行的任務,留給外部調用的方法
     * @param taskId
     * @return
     */
    public boolean stopNow(Long taskId) {
        Task task = taskRepository.get(taskId);
        if(task == null) {
            return false;
        }
        /**
         * 該任務不是正在執行,直接修改task狀態為已完成即可
         */
        if(task.getStatus() != TaskStatus.DOING) {
            task.setStatus(TaskStatus.STOP);
            taskRepository.update(task);
            return true;
        }
        /**
         * 該任務正在執行,使用節點配合心跳發布停用通知
         */
        int n = nodeRepository.updateNotifyInfo(NotifyCmd.STOP_TASK,String.valueOf(taskId));
        return n > 0;
    }

    /**
     * 立即停止正在執行的任務,這個不需要自己調用,是給心跳線程調用
     * @param taskId
     * @return
     */
    public boolean stop(Long taskId) {
        Task task = taskRepository.get(taskId);
        /**
         * 不是自己節點的任務,本節點不能執行停用
         */
        if(task == null || !config.getNodeId().equals(task.getNodeId())) {
            return false;
        }
        /**
         * 拿到正在執行任務的future,然後強制停用,並刪除doingFutures的任務
         */
        Future future = doingFutures.get(taskId);
        boolean flag =  future.cancel(true);
        if(flag) {
            doingFutures.remove(taskId);
            /**
             * 修改狀態為已停用
             */
            task.setStatus(TaskStatus.STOP);
            taskRepository.update(task);
        }
        /**
         * 重置通知信息,避免重複執行停用通知
         */
        nodeRepository.resetNotifyInfo(NotifyCmd.STOP_TASK);
        return flag;
    }
}

  好吧,其實實現很簡單,關鍵在於思路,不BB了,詳細代碼見: 在下告辭!

  

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理【其他文章推薦】

3c收購,鏡頭 收購有可能以全新價回收嗎?

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

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

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

賣IPHONE,iPhone回收,舊換新!教你怎麼賣才划算?

嵌入式、C語言位操作的一些技巧匯總

下面分享關於位操作的一些筆記:

一、位操作簡單介紹

首先,以下是按位運算符:

嵌入式編程中,常常需要對一些寄存器進行配置,有的情況下需要改變一個字節中的某一位或者幾位,但是又不想改變其它位原有的值,這時就可以使用按位運算符進行操作。下面進行舉例說明,假如有一個8位的TEST寄存器:

當我們要設置第0位bit0的值為1時,可能會這樣進行設置:

TEST = 0x01;

但是,這樣設置是不夠準確的,因為這時候已經同時操作到了高7位:bit1~bit7,如果這高7位沒有用到的話,這麼設置沒有什麼影響;但是,如果這7位正在被使用,結果就不是我們想要的了。

在這種情況下,我們就可以借用按位操作運算符進行配置。

對於二進制位操作來說,不管該位原來的值是0還是1,它跟0進行&運算,得到的結果都是0,而跟1進行&運算,將保持原來的值不變;不管該位原來的值是0還是1,它跟1進行|運算,得到的結果都是1,而跟0進行|運算,將保持原來的值不變。

所以,此時可以設置為:

TEST = TEST | 0x01;

其意義為:TEST寄存器的高7位均不變,最低位變成1了。在實際編程中,常改寫為:

TEST |= 0x01;

這種寫法可以一定程度上簡化代碼,是 C 語言常用的一種編程風格。設置寄存器的某一位還有另一種操作方法,以上的等價方法如:

TEST |= (0x01 << 0);

第幾位要置1就左移幾位。

同樣的,要給TEST的低4位清0,高4位保持不變,可以進行如下配置:

TEST &= 0xF0;

二、嵌入式中位操作一些常見用法

1、一個32bit數據的位、字節讀取操作

(1)獲取單字節:

#define GET_LOW_BYTE0(x)    ((x >>  0) & 0x000000ff)    /* 獲取第0個字節 */
#define GET_LOW_BYTE1(x)    ((x >>  8) & 0x000000ff)    /* 獲取第1個字節 */
#define GET_LOW_BYTE2(x)    ((x >> 16) & 0x000000ff)    /* 獲取第2個字節 */
#define GET_LOW_BYTE3(x)    ((x >> 24) & 0x000000ff)    /* 獲取第3個字節 */

示例:

(2)獲取某一位:

#define GET_BIT(x, bit) ((x & (1 << bit)) >> bit)   /* 獲取第bit位 */

示例:

2、一個32bit數據的位、字節清零操作

(1)清零某個字節:

#define CLEAR_LOW_BYTE0(x)  (x &= 0xffffff00)   /* 清零第0個字節 */
#define CLEAR_LOW_BYTE1(x)  (x &= 0xffff00ff)   /* 清零第1個字節 */
#define CLEAR_LOW_BYTE2(x)  (x &= 0xff00ffff)   /* 清零第2個字節 */
#define CLEAR_LOW_BYTE3(x)  (x &= 0x00ffffff)   /* 清零第3個字節 */

示例:

(2)清零某一位:

#define CLEAR_BIT(x, bit)   (x &= ~(1 << bit))  /* 清零第bit位 */

示例:

3、一個32bit數據的位、字節置1操作

(1)置某個字節為1:

#define SET_LOW_BYTE0(x)    (x |= 0x000000ff)   /* 第0個字節置1 */   
#define SET_LOW_BYTE1(x)    (x |= 0x0000ff00)   /* 第1個字節置1 */   
#define SET_LOW_BYTE2(x)    (x |= 0x00ff0000)   /* 第2個字節置1 */   
#define SET_LOW_BYTE3(x)    (x |= 0xff000000)   /* 第3個字節置1 */

示例:

(2)置位某一位:

#define SET_BIT(x, bit) (x |= (1 << bit))   /* 置位第bit位 */

4、判斷某一位或某幾位連續位的值

(1)判斷某一位的值

舉例說明:判斷0x68第3位的值。

也就是說,要判斷第幾位的值,if里就左移幾位(當然別過頭了)。在嵌入式編程中,可通過這樣的方式來判斷寄存器的狀態位是否被置位。

(2)判斷某幾位連續位的值

/* 獲取第[n:m]位的值 */
#define BIT_M_TO_N(x, m, n)  ((unsigned int)(x << (31-(n))) >> ((31 - (n)) + (m)))

示例:

這是一個查詢連續狀態位的例子,因為有些情況不止有0、1兩種狀態,可能會有多種狀態,這種情況下就可以用這種方法來取出狀態位,再去執行相應操作。

以上是對32bit數據的一些操作進行總結,其它位數的數據類似,可根據需要進行修改。

三、STM32寄存器配置

STM32有幾套固件庫,這些固件庫函數以函數的形式進行1層或者多層封裝(軟件開發中很重要的思想之一:分層思想),但是到了最裏面的一層就是對寄存器的配置。我們平時都比較喜歡固件庫來開發,大概是因為固件庫用起來比較簡單,用固件庫寫出來的代碼比較容易閱讀。最近一段時間一直在配置寄存器,越發地發現使用寄存器來進行一些外設的配置也是很容易懂的。使用寄存器的方式編程無非就是往寄存器的某些位置1、清零以及對寄存器一些狀態位進行判斷、讀取寄存器的內容等。

這些基本操作在上面的例子中已經有介紹,我們依舊以實例來鞏固上面的知識點(以STM32F1xx為例):

(1)寄存器配置

看一下GPIO功能的端口輸出數據寄存器 (GPIOx_ODR) (x=A..E) :

假設我們要讓PA10引腳輸出高、輸出低,可以這麼做:

方法一:

GPIOA->ODR |= 1 << 10;      /* PA10輸出高(置1操作) */
GPIOA->ODR &= ~(1 << 10);  /* PA10輸出低(清0操作) */

也可用我們上面的置位、清零的宏定義:

SET_BIT(GPIOA->ODR, 10);    /* PA10輸出高(置1操作) */
CLEAR_BIT(GPIOA->ODR, 10);  /* PA10輸出低(清0操作) */

方法二:

GPIOA->ODR |= (uint16_t)0x0400;   /* PA10輸出高(置1操作) */
GPIOA->ODR &= ~(uint16_t)0x0400;  /* PA10輸出低(清0操作) */

貌似第二種方法更麻煩?還得去細心地去構造一個數據。

但是,其實第二種方法其實是ST推薦我們用的方法,為什麼這麼說呢?因為ST官方已經把這些我們要用到的值給我們配好了,在stm32f10x.h中:

這個頭文件中存放的就是外設寄存器的一些位配置。

所以我們的方法二等價於:

GPIOA->ODR |= GPIO_ODR_ODR10;   /* PA10輸出高(置1操作) */
GPIOA->ODR &= ~GPIO_ODR_ODR10;  /* PA10輸出低(清0操作) */

兩種方法都是很好的方法,但方法一似乎更好理解。

配置連續幾位的方法也是一樣的,就不介紹了。簡單介紹配置不連續位的方法,以TIM1的CR1寄存器為例:

設置CEN位為1、設置CMS[1:0]位為01、設置CKD[1:0]位為10:

TIM1->CR1 |= (0x1 << 1)| (0x1 << 5) |(0x2 << 8);

這是組合的寫法。當然,像上面一樣拆開來寫也是可以的。

(2)判斷標誌位

以狀態寄存器(USART_SR) 為例:

判斷RXNE是否被置位:

/* 數據寄存器非空,RXNE標誌置位 */
if (USART1->SR & (1 << 5))
{
    /* 其它代碼 */
    
    USART1->SR &= ~(1 << 5);  /* 清零RXNE標誌 */
}

或者:

/* 數據寄存器非空,RXNE標誌置位 */
if (USART1->SR & USART_SR_RXNE)
{
    /* 其它代碼 */
    
    USART1->SR &= ~USART_SR_RXNE;  /* 清零RXNE標誌 */
}

四、總結

以上就是本次關於位操作的一點總結筆記,有必要掌握。雖然說在用STM32的時候有庫函數可以用,但是最接近芯片內部原理的還是寄存器。有可能之後有用到其它芯片沒有像ST這樣把寄存器相關配置封裝得那麼好,那就不得不直接操控寄存器了。

此外,使用庫函數的方式代碼佔用空間大,用寄存器的話,代碼佔用空間小。之前有個需求,我們能用的Flash的空間大小隻有4KB,遇到類似這樣的情況就不能那麼隨性的用庫函數了。

最後,應用的時候當然是怎麼簡單就怎麼用。學從“難”處學,用從易處用,與君共勉~

END:以上筆記中如有錯誤,歡迎指出!謝謝

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理【其他文章推薦】

3c收購,鏡頭 收購有可能以全新價回收嗎?

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

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

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

賣IPHONE,iPhone回收,舊換新!教你怎麼賣才划算?

關係型數據庫幾大範式的理解總結

範式的定義

  • 關係型數據庫中的關係是需要滿足一定條件的,滿足這些不同程度的規範化就叫做範式。

  • 範式按照規範化程度從低到高排序為第一範式,第二範式,第三範式,BC範式,第四範式,第五範式。

前導知識

函數依賴

R(U)是屬性集U的關係模型,X,Y是U的一個子集,對於R(U)中的任一個關係r,不可能存在兩個元組在X上屬性值相同,而在Y上屬性值不同。則稱X函數確定Y,或Y函數依賴X。

  • 說人話:U是表(可能不止一個表,可以是有關係的多個表)的所有列,X,Y分別是這些屬性列的一個子集,也就是若干個屬性,對於所有在X這些屬性上的值一樣的行,在Y上的屬性上也必須一樣,滿足這樣條件的這若干個屬性 X和Y叫稱其函數依賴。
  • X相同則Y必須相同,但X不同Y可以相同,也可以不同
  • 如果Y是X的子集,就叫平凡的函數依賴,一般不考慮這種,因為就是廢話,X整個都相同,子集肯定相同。
  • 如果Y不是X的子集,叫做非平凡的函數依賴
  • 如果Y函數依賴X,那麼X稱為決定因素。
  • 如果Y函數依賴X,但不依賴X的任何一個真子集,也就是X是極小的,那就稱Y完全函數依賴X,否則稱Y部分函數依賴X
  • 如果X決定Y,Y決定Z,且Y不決定X,那麼稱Z對X傳遞函數依賴

碼(鍵)

  • U是屬性全集,K是U的子集,若U完全函數依賴K,則稱K為候選碼,候選碼若有多個,任意選擇一個都可作為主碼,若U部分函數依賴K,則稱K為超碼。顯然,候選碼當然也是超碼,而且是最小的超碼。
  • 包含在任何一個候選碼的屬性都叫主屬性,其他都叫非主屬性
  • 在本書中主碼和候選碼統稱為,屬性集K不是該關係模式(表)的碼,而是另一個關係模式(表)的碼,則稱K為該關係模式(表)的外碼

求候選碼

例子:

舉例

有這樣一個配件管理表WPE(WNO,PNO,ENO,QNT),其中WNO表示倉庫號,PNO表示配件號,ENO表示職工號,QNT表示數量。

有以下約束要求:

(1)一個倉庫有多名職工;

(2)一個職工僅在一個倉庫工作;

(3)每個倉庫里一種型號的配件由專人負責,但一個人可以管理幾種配件;

(4)同一種型號的配件可以分放在幾個倉庫中。

分析表中的函數依賴關係,可以得到:

(1)ENO->WNO;

(2)(WNO,PNO)->QNT

(3)(WNO,PNO)->ENO

(4)(ENO,PNO)->QNT

觀察法?:

候選碼的定義就是一組能決定所有列(某一個元組)的屬性。

所以根據這4個函數依賴關係,(WNO,PNO)顯然肯定是,因為它可以決定QNT,也可以決定ENO,加上它本身,就是屬性全集U了。

而(ENO,PNO),雖然只有一個決定QNT,但是ENO可以單獨決定WNO,所以顯然(ENO,PNO)也就能一起決定QNT和WNO,因此也是候選碼。

六大範式?

第一範式

定義

滿足最基本的條件,每一個分量都是不可分的數據項。

  • 說人話,每一列對應只有一個值。

第二範式

定義

R屬於第一範式,且每一個非主屬性完全函數依賴於任何一個候選碼,則R屬於第二範式

  • 說人話,除了主碼候選碼之外的其他屬性都要完全函數依賴於主碼。
  • 因為任意一個候選碼都能作為主碼,所以,也就是說,如果存在某個屬性不是完全函數依賴於某一個候選碼,可能是部分函數依賴,那就沒了。
  • 比如主鍵是(學號,課程號),但是現在有一個屬性完全函數依賴於學號,而部分函數依賴於(學號,課程號),那就不滿足第二範式。

第三範式

定義

R屬於第二範式,若R中不存在碼X,屬性子集Y,非主屬性Z,使得X決定Y,Y不決定X,Y決定Z,則R屬於第三範式。

  • 說人話,非主屬性必須直接完全函數依賴於主鍵,中間不能有其他函數,即不能是傳遞函數依賴。

BC範式

定義

R屬於第一範式,若X決定Y,且Y不是X的子集時X必含有碼,即每一個決定因素都包含碼,則R屬於BC範式。

  • 說人話, 若R是第一範式,且每個屬性不部分函數依賴於候選碼也不傳遞函數依賴於候選碼,則R是BC範式,具體以下三點。
    • 所有非主屬性對每一個碼都是完全函數依賴。(也是第二範式要求)
    • 所有主屬性對每一個不包含它的碼也是完全函數依賴。(也就是排除了所有屬性對碼的部分依賴)
    • 沒有任何屬性完全函數依賴於非碼的任何一組屬性。(排除傳遞函數依賴)
  • 實際上,BC範式就是在第三範式的基礎上消除了主屬性的傳遞依賴

第四範式

多值依賴

  • 說人話,多值依賴就是一個表中多對多的關係,如果可以分成兩列,這兩列多對多,這就平凡的多值依賴,如果是分成三列,固定某一列的值,其他兩列多對多,這就是非平凡的多值依賴,第四範式要消除的就是非平凡的多值依賴。

  • 函數依賴是特殊的多值依賴,因為多對多其實也是一對多。

定義

R屬於第一範式,對應R的每一個非平凡多值依賴,X->->Y,X都含有碼,則R屬於第四範式。

  • 說人話,在滿足第三範式的基礎上,關係表中不能含有一個實體的兩個或多個相互獨立的多值因子。
  • 或者說,滿足第四範式即要求每個非平凡的多值依賴都含有碼,也就是實際上是函數依賴。

第五範式

定義

第五範式是指關係模式R依賴均由R候選碼所隱含。

這輩子應該不會用到的內容,就不管了。

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理【其他文章推薦】

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

平板收購,iphone手機收購,二手筆電回收,二手iphone收購-全台皆可收購

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

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

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

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

Spring中常見的設計模式——工廠模式

一、簡單工廠模式

  簡單工廠模式(Simple Factory Pattern)由一個工廠對象決定創建哪一種產品類的實例,簡單工廠模式適用於工廠類負責創建對象較少的情況,且客戶端只需要傳入工廠類的參數,對於如何創建對像不關心。

public  interface IBlog {
     // 寫隨筆
    public  void write();
}
public  class JavaBlog implements IBlog {
    @Override
    public  void write() {
        System.out.println( "寫java隨筆" );
    }
}
public  class WriteBlog {
     public  static  void main(String[] args) {
        IBlog blog = new JavaBlog();
        blog.write();
    }
}

  上述代碼中,父類 IBlog 指向子類JavaBlog 的引用,應用層需要依賴JavaBlog,如果增加PythonBlog等等更多的課程,客戶端就會越來越臃腫。因此要把依賴減弱,把創建細節隱藏。現在我們用簡單工廠優化:

public class BlogFactory {
    public IBlog create(Class<? extends IBlog> clazz) {
        if (null != clazz) {
            try {
                return clazz.newInstance();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        return null;
    }
}

客戶端改變:

  public static void main(String[] args) {
        BlogFactory blogFactory = new BlogFactory();
        IBlog blog = blogFactory.create(JavaBlog. class );
        blog.write();
    }

  簡單工廠模式在JDK中很常見,如Calender類(感興趣去看源碼),還有logback,LoggerFactory中有很多重載的方法getLogger()。但是簡單工廠也有缺點:工廠類的職責相對過重,不易於擴展過於複雜的產品結構。

二、工廠方法模式

   工廠方法模式(Factory Method Pattern)是指定義一個創建對象的接口,但讓實現這個接口的類來決定實例化哪個類,工廠方法模式讓類的實例化推遲到子類中進行。在工廠方法模式中,用戶只需要關心所需產品對應工廠,無須關心創建的細節,而且加入新產品時符合開閉原則。

  工廠方法模式主要解決產品擴展問題。在簡單工廠模式中,隨著產品的增多,如果不同語言書寫隨筆的邏輯各不相同,工廠職責越來越多,那工廠裏面就會亂搞一氣,狗屁不通。根據單一職責原則,我們將只能進行拆分,不同工廠做不同事,Java隨筆由Java工廠創建,Python隨筆由Python工廠創建,對工廠本身進行抽象。

先創建工廠類:

public  interface IBlogFactory {
    IBlog create();
}

再創建對應工廠:

public  class JavaBlogFactory implements IBlogFactory {
    @Override
    public IBlog create() {
        return new JavaBlog();
    }
}

public class PythonBlogFactory implements IBlogFactory {
    @Override
    public IBlog create() {
        return new PythonBlog();
    }
}

客戶端:

public class CreateBlog {
    public static void main(String[] args) {
        IBlogFactory factory = new PythonBlogFactory();
        IBlog blog = factory.create();
        blog.write();

        factory = new JavaBlogFactory();
        blog = factory.create();
        blog.write();
    }
}

總結來說就是:不同工廠抽像出一個工廠頭子,不同的工廠創建不同的實例。

工廠方法模式適用於以下場景:

1.創建對象需要大量重複代碼。

2.客戶端(應用層)不依賴於產品類實例如何被創建、如何被實現等細節。

3.一個類通過其子類來指定創建哪個對象。

缺點:

1.類的個數容易過多,增加複雜度。

2.增加了系統的抽象性和理解難度。

三、抽象工廠

  抽象工廠(Abstract Factory Pattern)提供一個黃健一系列相關或相互依賴對象的接口,無需指定具體類。客戶端(應用層)不依賴於產品類實例如何被創建、如何被實現等細節,強調的是一系列相關得產品對象(屬於同一產品族)一起使用創建對象需要大量重複代碼。需要提供一個產品類的庫,所有產品以同樣接口出現,從而是客戶端不依賴於具體實現。

產品族:同一家的不同產品,比如小米,華為,蘋果;

產品等級:不同種類的產品,比如手機,電視,電腦。

工廠要做的就是生產我們牌子的所有產品。以博客為例,java分類的博客有隨筆、文章、日記等。

首先創建文章和日記的抽象接口:

public  interface IDocument {
     void write();
}

public  interface INote {
     void make();
}

再創建抽象工廠:

public  interface BlogFactory {
    INote createNote();

    IDocument createDocument();
}

實現Java文章和日記:

public  class JavaDocument implements IDocument {
    @Override
    public  void write() {
        System.out.println( "寫Java文章" );
    }
}

public  class JavaNote implements INote {
    @Override
    public  void make() {
        System.out.println( "寫Java筆記" );
    }
}

實現Java產品族具體工廠:

public  class JavaBlogFactory implements BlogFactory {
    @Override
    public INote createNote() {
         return  new JavaNote();
    }

    @Override
    public IDocument createDocument() {
         return  new JavaDocument();
    }
}

實現Python文章和日記、實現Python具體工廠參考Java的。

客戶端調用:

public  class BlogTest {
     public  static  void main(String[] args) {
        JavaBlogFactory factory = new JavaBlogFactory();
        factory.createDocument().write();
        factory.createNote().make();
    }
}

  上述代碼描述了兩個產品族的工廠,如果想要擴展產品等級(就是再加點評啥的),要調整抽象工廠、具體工廠。由此可見抽象工廠模式的缺點:

1.規定所有可能被創建的產品集合,產品族(Java系列)中擴展新產品很困難,需要修改抽象工廠及實現;

2.增加系統抽象性和理解難度;

  我們可以利用工廠模式創建好數據源連接池並放到容器中,業務需要時再取出。就避免了用一次創建一次的尷尬。

 

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理
【其他文章推薦】

3c收購,鏡頭 收購有可能以全新價回收嗎?

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

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

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

賣IPHONE,iPhone回收,舊換新!教你怎麼賣才划算?

PL真有意思(五):數據類型

前言

現在大多數程序設計語言中都有表達式和/或對象的類型概念。類型起着兩種主要作用:

  • 為許多操作提供了隱含的上下文信息,使程序員可以在許多情況下不必显示的描述這種上下文。比如int類型的兩個對象相加就是整數相加、兩個字符串類型的對象相加就是拼接字符串、在Java和C#中new object()隱含在背後的就是要分配內存返回對象的引用等等。

  • 類型描述了其對象上一些合法的可以執行的操作集合。類型系統將不允許程序員去做一個字符和一個記錄的加法。編譯器可以使用這個合法的集合進行錯誤檢查,好的類型系統能夠在實踐中捕獲很多錯誤

類型系統

從編譯方面的知識我們可以知道,計算機硬件可以按多種不同的方式去解釋寄存器里的一組二進制位。處理器的不同功能單元可能把一組二進制位解釋為指令、地址、字符、各種長度的整數或者浮點數等。當然,二進制位本身是無類型的,對存儲器的哪些位置應該如何解釋,大部分硬件也無任何保留信息。彙編語言由於僅僅是對一些二進制指令的“助記符號”翻譯,它也是這種無類型情況。高級語言中則總是關聯值與其類型,需要這種關聯的一些原因和用途就如前面說到的上下文信息和錯誤檢測。

一般來說,一個類型系統包含一種定義類型並將它們與特定的語言結構關聯的機制;以及一些關於類型等價、類型相容、類型推理的規則。 必須具有類型的結構就是那些可以有值的,或者可以引用具有值得對象的結構。類型等價規則確定兩個值得類型何時相同;類型相容規則確定特定類型的值是否可以用在特定的上下文環境里;類型推理規則基於一個表達式的各部分組成部分的類型以及其外圍上下文來確定這個表達式的類型。

在一些多態性變量或參數的語言中,區分表達式(如一個名字)的類型與它所引用的那個對象的類型非常重要,因為同一個名字在不同時刻有可能引用不同類型的對象。

在一些語言中,子程序也是有類型的,如果子程序是一級或者二級值,其值是動態確定的子程序,這時語言就需要通過類型信息,根據特定的子程序接口(即參數的個數和類型)提供給這種結構的可接受的值集合,那麼子程序就必須具有類型信息。在那些不能動態創建子程序引用的靜態作用域語言(這種語言中子程序是三級值),編譯器時就能確定一個名字所引用的子程序,因此不需要子程序具有類型就可以保證子程序的正確調用。

類型檢查

類型檢查時一個處理過程,其目的就是保證程序遵循了語言的類型相容規則,違背這種規則的情況稱為類型衝突。說一個語言是強類型的,那麼就表示這個語言的實現遵循一種禁止把任何操作應用到不支持這種操作的類型對象上的規則。說一個語言是靜態類型化(statically type)的,那麼它就是強類型的,且所有的類型檢查都能在編譯時進行(現實中很少有語言是真正的靜態類型,通常這一術語是指大部分類型檢查可以在編譯器執行,其餘一小部分在運行時檢查)。如C#我們通常都認為它是靜態類型化的語言。

動態(運行時)類型檢查是遲約束的一種形式,把大部分的檢查操作都推遲到運行的時候進行。採用動態作用域規則的語言大部分都是動態類型語言,因為它的名字和對象的引用都是在運行時確定的,而確定引用對象的類型則更是要在引用確定之後才能做出的。

類型檢查是把雙刃劍,嚴格的類型檢查會使編譯器更早的發現一些程序上的錯誤,但是也會損失一部分靈活性;動態類型檢查靈活性大大的,但是運行時的代價、錯誤的推遲檢查,各種語言的實現也都在這種利弊上進行權衡。

多態性

多態性使得同一段代碼體可以對多個類型的對象工作。它意味着可能需要運行時的動態檢查,但也未必一定需要。在Lisp、Smalltalk以及一些腳本語言中,完全的動態類型化允許程序員把任何操作應用於任何對象,只有到了運行時採取檢查一個對象是否實現了具體的操作。由於對象的類型可以看作它們的一個隱式的(未明確聲明的,一個不恰當的比喻就如C#中的this)參數,動態類型化也被說成是支持隱式的參數多態性。

雖然動態類型化具有強大的威力(靈活性),但卻會帶來很大的運行時開銷,還會推遲錯誤報告。一些語言如ML採用了一種複雜的類型推理系統,設法通過靜態類型化支持隱式的參數多態性。

在面向對象語言里,子類型多態性允許類型T的變量X引用了從T派生的任何類型的對象,由於派生類型必定支持基類型的所有操作,因此編譯器完全可以保證類型T的對象能接受的任何操作,X引用的對象也都能接受。對於簡單的繼承模型,子類型多態的類型檢查就能完全在編譯時實現。採用了這種實現的大多數語言(如C++,JAVA和C#)都提供另一種显示的參數化類型(泛型),允許程序員定義帶有類型參數的類。泛型對於容器(集合)類型特別有用,如T的列表(List )和T的棧(Stack )等,其中T只是一個類型佔位符,在初始化的這個容器對象時提供具體的類型來代替它。與子類型多態類似,泛型也可以在編譯時完成類型檢查。比如C++的模板完全就是編譯期間的東西,編譯后就完全沒有了模板的痕迹;JAVA則是利用一種“擦除”的技術實現的泛型,需要在運行時做一些檢查。

類型的含義

現在至少存在三種不同的考慮類型問題的方式,分別稱之為指稱的、構造的和基於抽象的

  • 指稱的

按照指稱的觀點,一個類型就是一組值,一個值具有某個類型的條件是他屬於這個值集合,一個對象具有某個類型的條件是他的值保證屬於這個值集合

  • 構造的

從構造的觀點看,一個類型或者是以一小組內部類型,或者是通過對一個或幾個更簡單些的類型,應用某個類型的構造符構造出來的

  • 基於抽象的

從基於抽象的角度來看,一個類型就是一個接口,由一組定義良好而且具有相互協調的語義的操作組成。

類型的分類

在不同語言里,有關類型的術語也不相同,這裏說的通常都是常用的術語,大部分語言多提供的內部類型差不多就是大部分處理器所支持的類型:整數、字符、布爾和實數。

一般語言規範中都會規定數值類型的精度問題,以及一些字符的編碼規定。通常特殊的一個數值類型是枚舉類型,具體的語法在不同的語言中略有差異,但是其也都是一個目的(用一個字符友好的表示一個數值)。

關於枚舉類型,由一組命名元素組成。在C中可以這樣寫:

enum weekday { sun, mon, tue, wed, thu, fri, sat };

在C中這樣的寫法和直接對裏面的元素直接賦值除了語法上效果完全一樣。但是在之後的許多語言中,枚舉類型是一個真正的類型

還有一些語言中提供一種稱為子界的類型,它表示一種基於基本數值的一個連續的區間。比如Pascal中表示1到100:

type test_score = 0..100

複合類型:由一些簡單的基本類型組合成的一些類型稱為複合類型,比如常見的記錄、變體記錄、數組、集合、指針、表等,具體的都會在後面詳細介紹。

類型檢查

大多數的靜態類型語言中,定義一個對象都是需要描述清楚它的類型,進一步講,這些對象出現的上下文也都是有類型的,也就是說語言中的一些規則限制了這種上下文中可以合法出現的對象類型。

類型相容確定了一個特定類型的對象的能否用在一個特定上下文中。在最極端的情況下,對象可使用的條件就是它的類型與上下文所期望的類型等價。但是在大多數語言中,相容關係都比等價更寬鬆一些,即使對象與上下文的類型不同,它們也可以相容。

而類型推理想回答的是從一個簡單的表達式出發構造另一個表達式時,這整個的表達式的類型是什麼

類型等價

在用戶可以定義新類型的語言中,類型等價的定義一般基於兩種形式。

type R2 = record
    a : integer
    b : integer
end;

type R2 = record
    b : integer
    a : integer
end;
  • 結構等價

基於類型定義的內容,就是它們由同樣的組成部分且按照同樣的方式組合而成

它的準確定義在不同的語言中也不一樣,因為它們要決定類型之間的哪些潛在差異是重要的,哪些是可以接受的(比如上面的兩個定義,是否還認為是等價的)。結構等價是一種很直接的認識類型的方式,早期的一些語言(Algol 68、Modula-3、ML)有些事基於結構等價的,現在的大部分語言(Java、C#)大都是基於名字等價了,為何呢?因為從某種意義上看,結構等價是由底層、由實現決定的,屬於比較低級的思考方式。就如一個上下文,如果你傳遞了一個結構等價但是不是所期待對象,實施結構等價的編譯器是不會拒絕這種情況的(假如這不是你希望的,那麼你也不會得到任何提示或者錯誤信息,很難排查的)。

  • 名字等價

基於類型的詞法形式,可以認為是每一個名字都引進一個新的類型;

它基於一種假設,就是說程序員花時間定義了兩個類型,雖然它們的組成部分可能相同,但是程序員要表達的意思就是這是兩個不同的類型。名字等價的常規判斷就非常簡單了,看看聲明兩個對象的類型是否是一個就是了。但是也會有一些特殊的情況出現,比如類型別名(C、C++的程序員很熟悉這種東西吧),比如 typedef int Age; 就為int類型重新定義了一個別名”Age”。那些認為int不等價越Age的語言稱為嚴格名字等價,認為等價的稱為寬鬆名字等價。其實這兩種也是很容易區分的,只要能區分聲明和定義兩個概念的差異就可以區分。在嚴格名字等價中看待typedef int Age是認為定義了一個新類型Age,在寬鬆名字等價看來這就是一個類型聲明而已,int和Age共享同一個關於整數的定義。

類型變換和轉換

在靜態類型的語言中,如果“a=b”,那麼我們會期望b的類型和a的相同;現在假定所提供的類型和期望的類型和所提供的類型相同,那麼我們在要求某個類型的上下文中使用另外一個類型時就需要显示的寫出類型變換(或稱為類型轉換)。根據具體的變換的具體情況,在運行時執行這種變化會有以下三種主要的情況出現:

  • 所涉及的類型可以認為是結構等價的,這種情況裏面因為涉及的類型採用了相同的底層的表示,則這種變換純粹就是概念上的操作,不需要運行時執行任何代碼。

  • 所涉及的類型具有不同的值集合,但它們的值集合具有相同的表示形式。比如一個類型和它的子類型,一個整數和一個無符號的整數。拿無符號整數變換為整數來說,由於無符號整數的最大值是整數類型所容納不了的,則運行時就必須執行一些代碼來保證這種變換的合法性,如果合法則繼續下去,否則會產生一個動態語義錯誤。

  • 所涉及的類型具有不同的底層表示,但是我們可以在它們的值之間定義某種對應關係。比如32位整數可以變換到IEEE的雙精度浮點數,且不會丟失精度。浮點數也可以通過舍入或割斷的形式變換成整數,但是會丟失小數部分。

非變換的類型轉換

有這麼一種情況,我們需要改變一個值,但是不需要改變它的二進製表示形式,更通俗點說就是我們希望按照另外一個類型的方式去解釋某個類型的二進制位,這種情況稱為非變換類型轉換。最簡單的一個例子比如說,一個byte類型的數值65,按byte類型來解釋它是65,如果按照char類型來解釋它就是字符“A”。比如C++中的static_cast執行類型變換,reinterpret_cast執行非變換的類型轉換。c中出現的union形式的結構,就可以認為是這種非變換的類型轉換的合法的安全的語言結構。在比如下面C中一般性非變換類型轉換代碼:

r=*((float *) &n);

任何非變換的類型轉換都極其危險的顛覆了語言的類型系統。在弱類型系統的語言中,這種顛覆可能很難發現,在強類型系統的語言中显示的使用這種非變換的類型轉換,起碼從代碼上可以看得出來它是這麼一回事,或多或少的有利於排查問題。

類型相容

大多數語言的上下文中並不要求類型等價,相應的一般都是實施較為“寬鬆”的類型相容規則。比如賦值語句要求右值相容與左值、參數類型相容,實際返回類型與指定的返回類型相容。在語言中,只要允許把一個類型的值用到期望的另外一個類型的上下文中,語言都必須執行一個到所期望類型的自動隱式變換,稱為類型強制(比如int b;double a=b;)。就像前面說的显示的類型變換一樣,隱式的類型變換也可能需要執行底層代碼或者做一些動態類型檢查。

重載

一個重載的名字可能引用不同類型的對象,這種歧義性需要通過上下文信息進行解析。比如a+b這個表達式可以表示整數或者浮點數的加法運算,在沒有強制的語言中,a和b必須都是整數或都是浮點數。如果是有強制的語言,那麼在a或者b有一個是浮點數的情況下,編譯器就必須使用浮點數的加法運算(另外一個整數強制轉換為浮點數)。如果語言中+只是進行浮點數運算,那麼即使a和b都是整數,也會被全部轉成浮點數進行運算(這代價就高了好多了)。

通用引用類型

通用引用類型:一些語言根據實習需求,設計有通用的引用類型,比如C中的void*、C#中的Object,任意的值都可以賦值給通用引用類型的對象。但是問題是存進去容易取出來難,當通用引用類型是右值的時候,左值的類型可能支持某些操作,然而這些操作右值對象是不具備的。為了保證通用類型到具體類型的賦值安全,一種解決辦法是讓對象可以自描述(也就是這個對象包含其真實類型的描述信息),C++,JAVA,C#都是這種方式,C#中如果賦值的類型不匹配則會拋出異常,而C++則是使用dynamic_cast做這種賦值操作,具體的後果呢,也是C++程序員負責。

類型推理

通過前面的類型檢查我們可以保證表達式的各各組成部分具有合適的類型,那麼這整個表達式的類型是什麼來着?其實在大多數的語言中也是比較簡單的,算術表達式的類型與運算對象相同、比較表達式總是布爾類型、函數調用的結果在函數頭聲明、賦值結果就是其左值的類型。在一些特殊的數據類型中,這個問題並不是那麼清晰明了,比如子界類型、複合類型。比如下面的子界類型問題(Pascal):

type Atype=0..20;
type Btype=10..20;

var a: Atype;
var b: Btype;

那麼a+b什麼類型呢???它確實是不能是Atype或者Btype類型,因為它可能的結果是10-40。有人覺得那就新構造一個匿名的子界類型,邊界時10到40。實際情況是Pascal給的答案是它的基礎類型,也就是整數。

在Pascal中,字符串’abc’的類型是array[1..3] of char、而Ada則認為是一種未完全確定的類型,該類型與任何3個字符數組相容,比如在Ada中’abc’ & ‘defg’其結果是一個7字符的數組,那麼這個7字符數組的類型是array[1..7] of cahr呢還是某一個也是7個字符組成的類型array (weekday) of character呢,更或者是其他任意一個也是包含七個字符數組的另外一個類型。這種情況就必須依賴表達式所處的上下文信息才能推到出來具體的類型來。

記錄(結構)與變體(聯合)

一些語言中稱記錄為結構(struct),比如C語言。C++把結構定義為class的一種特殊形式(成員默認全局可見),Java中沒有struct的概念,而C#則對struct採用值模型,對class採用引用模型。

語法與運算

一個簡單的結構體在C中可以這樣定義:

struct element{
    char name[2];
    int number;
    double weight;
    Bool merallic;    
}; 

等價於Pascal中的:

 type two_chars=packed array [1..2] of char;
 type element - record
     name:two_chars;
     number:integer;
     weight:real;
     metallic:Boolean
 end

記錄裏面的成員(如name,number…)稱為域(field)。在需要引用記錄中的域時,大部分語言使用“.”記法形式。比如Pascal中:

 var copper:eement;
 copper.name=6.34;

大部分語言中還允許記錄的嵌套定義,比如在Pascal中:

 type short_string=packed array[1..30] of char;
 type ore=record
      name:short_string;
      element_yielded:record /*嵌套的記錄定義*/
          name:two_chars;
          number:integer;
          weight:real;
          metallic:Boolean
      end
 end

存儲布局及其影響

一個記錄的各個域通常被放入內存中的相鄰位置。編譯器在符號表中保存每個域的偏移量,裝載和保存的時候通過基址寄存器和偏移量即可得到域的內存地址。類型element在32位的機器中可能的布局如下:

此處有圖

(圖在最後面,因為markdown的這個畫表格不符合這個要求,又不想引圖了,就直接用html寫了,會被擠到最後去)

(table標籤和我博客園的樣式生成的時候會出bug,刪除了)

在對結構體的存儲布局方案上,如果使用正常排序,結構中的空洞會浪費空間。但是如果通過壓縮來節省空間,但是可能很帶來很嚴重的訪問時間的代價

數組

數組是最常見也是最重要的複合數據類型。記錄用於組合一些不同類型的域在一起;而數組則不同,它們總是同質的。從語義上看,可以把數組想象成從一個下標類型到成員(元素)類型的映射。

有些語言要求下標類型必須是integer,也有許多語言允許任何離散類型作為下標;有些語言要求數組的元素類型只能是標量,而大多數語言則允許任意類型的元素類型。也有一些語言允許非離散類型的下標,這樣產生的關聯數組只能通過散列表的方式實現,而無法使用高效的連續位置方式存儲,比如C++中的map,C#中的Dictionary。在本節中的討論中我們假定數組的下標是離散的。

語法和操作

大多數的語言都通過數組名后附加下標的方式(圓括號|方括號)來引用數組裡的元素。由於圓括號()一般用於界定子程序調用的實際參數,方括號在區分這兩種情況則有易讀的優勢。Fortran的數組用圓括號,是因為當時IBM的打卡片機器上沒有方括號

維數、上下界和分配

對於數組的形狀在聲明中就已經描述,對於這種有靜態形狀的數組,可以用通常的方式來管理內存:生存期是整個程序的數組使用棧分配,具有更一般的生存期的動態生成數組使用堆分配。但是對於在加工之前不知道其形狀的數組,或其形狀在執行期間可能改變的數組,存儲管理就會更複雜一點。

  • 內情向量

在編譯期間,符號表維護者程序中的每個數組的維度和邊界信息。對於每個記錄,它還維護着每個域的偏移量。如果數組維度的數目和邊界是靜態已知的,編譯器就可以在符號表中找出它們,以便計算數組元素的地址。如果這些值不是靜態已知的,則編譯器就必須生成代碼,在運行時從一個叫內情向量的數據結構來查找它

  • 棧分配

子程序參數是動態形狀數組最簡單的例子,其中數組的上下界在運行時才確定,調用方都會傳遞數組的數據和一個適當的內情向量,但是如果一個數組的形狀只能到加工時才知道,這種情況下仍可以在子程序的棧幀里為數組分配空間,但是需要多做一層操作

  • 堆分配

在任意時間都可以改變形狀的數組,有時被稱為是完全動態的。因為大小的變化不會以先進先出的順序進行,所以棧分配就不夠用了。完全動態的數組必須在堆中分配。比如Java中的ArrayList

#### 內存布局

大多數語言的實現里,一個數組都存放在內存的一批連續地址中,比如第二個元素緊挨着第一個,第三個緊挨着第二個元素。對於多維數組而言,則是一個矩陣,會出現行優先和列優先的選擇題,這種選擇題對於語言使用者而言是透明的,而對語言的實現者則需要考慮底層方面的優化問題了。

在一些語言中,還有另外一種方式,對於數組不再用連續地址分配,也不要求各行連續存放,而是允許放置在內存的任何地方,再創建一個指向各元素的輔助指針數組,如果數組的維數多於兩維,就再分配一個指向指針數組的指針數組。這種方式稱為行指針布局,這種方式需要更多的內存空間,但是卻有兩個優點:

  • 首先,可能加快訪問數組裡單獨元素的速度;
  • 其次,允許創建不用長度的行,而且不需要再各行的最後留下對齊所用的空洞空間,這樣節省下來的空間有時候可能會超過指針佔據的空間。C,C++和C#都支持連續方式或行指針方式組織多維數組,從技術上講,連續布局才是真正的多維數組,而行指針方式則只是指向數組的指針數組。

字符串

許多語言中,字符串也就是字符的數組。而在另一些語言中,字符串的情況特殊,允許對它們做一些其他數組不能用的操作,比如Icon以及一些腳本語言中就有強大的字符串操作功能。

字符串是編程中非常重要的一個數據類型,故而很多語言都對字符串有特殊的處理以便優化其性能以及存儲(比如C#中的字符串不可變性保證了性能,字符串駐留技術照顧了存儲方面的需要),由於這些特殊的處理,故而各各語言中為字符串提供的操作集合嚴重依賴語言設計者對於實現的考慮。

集合

程序設計語言中的一個集合,也就是具有某個公共類型的任意數目的一組值的一種無序彙集。集合的元素所具有的類型叫做元類型或者基類型。現在的大多數程序設計語言都對集合提供了很大的支持,為集合提供了很多相關的操作

指針和遞歸類型

所謂的遞歸類型,就是可以在其對象中包含一個或多個本類型對象的引用類型。遞歸類型用於構造各種各樣的“鏈接”數據結構,比如樹。在一些對變量採用引用模型的語言中,很容易在創建這種遞歸類型,因為每個變量都是引用;在一些對變量採用值模型的語言中,定義遞歸類型就需要使用指針的概念,指針就是一種變量,其值是對其他對象的引用。

對於任何允許在堆里分配新對象的語言,都存在一個問題:若這種對象不在需要了,何時以及以何種方式收回對象佔用的空間?對於那些活動時間很短的程序,讓不用的存儲留在那裡,可能還可以接受,畢竟在它不活動時系統會負責回收它所使用的任何空間。但是大部分情況下,不用的對象都必須回收,以便騰出空間,如果一個程序不能把不再使用的對象存儲回收,我們就認為它存在“內存泄漏”。如果這種程序運行很長一段時間,那麼它可能就會用完所有的空間而崩潰。許多早期的語言要求程序員显示的回收空間,如C,C++等,另一些語言則要求語言實現自動回收不再使用的對象,如Java,C#以及所有的函數式語言和腳本語言。显示的存儲回收可以簡化語言的實現,但會增加程序員忘記回收不再使用的對象(造成內存泄漏),或者不當的回收了不該回收的正在使用的對象(造成懸空引用)的可能性。自動回收可以大大簡化程序員的工作,但是為語言的實現帶來了複雜度。

語法和操作

對指針的操作包括堆中對象的分配和釋放,對指針間接操作以訪問被它們所指的對象,以及用一個指針給另一個指針賦值。這些操作的行為高度依賴於語言是函數式還是命令式,以及變量/名字使用的是引用模型還是值模型。

函數式語言一般對名字採用某種引用模型(純的函數式語言里根本沒有變量和賦值)。函數式語言里的對象傾向於採取根據需要自動分配的方式。

命令式語言里的變量可能採用值模型或引用模型,有時是兩者的某種組合。比如 A=B;

  • 值模型: 把B的值放入A。
  • 引用模型: 使A去引用B所引用的那個對象。

Java的實現方式區分了內部類型和用戶定義的類型,對內部類型採用值模型,對用戶定義的類型採用則採用引用模型,C#的默認方式與Java類似,另外還提供一些附加的語言特性,比如“unsafe”可以讓程序員在程序中使用指針。

懸空引用

在前兩篇的名字、作用域和約束中我們列舉了對象的3種存儲類別:靜態、棧和堆。靜態對象在程序的執行期間始終是活動的,棧對象在它們的聲明所在的子程序執行期間是活動的,而堆對象則沒有明確定義活動時間。

在對象不在活動時,長時間運行的程序就需要回收該對象的空間,棧對象的回收將作為子程序調用序列的一部分被自動執行。而在堆中的對象,由程序員或者語言的自動回收機制負責創建或者釋放,那麼如果一個活動的指針並沒有引用合法的活動對象,這種情況就是懸空引用。比如程序員显示的釋放了仍有指針引用着的對象,就會造成懸空指針,再進一步假設,這個懸空指針原來指向的位置被其他的數據存放進去了,但是實際卻不是這個懸空指針該指向的數據,如果對此存儲位置的數據進行操作,就會破壞正常的程序數據。

那麼如何從語言層面應對這種問題呢?Algol 68的做法是禁止任何指針指向生存周期短於這個指針本身的對象,不幸的是這條規則很難貫徹執行。因為由於指針和被指對象都可能作為子程序的參數傳遞,只有在所有引用參數都帶有隱含的生存周期信息的情況下,才有可能動態的去執行這種規則的檢查。

廢料收集

對程序員而已,显示釋放堆對象是很沉重的負擔,也是程序出錯的主要根源之一,為了追蹤對象的生存軌跡所需的代碼,會導致程序更難設計、實現,也更難維護。一種很有吸引力的方案就是讓語言在實現層面去處理這個問題。隨着時間的推移,自動廢料收集回收都快成了大多數新生語言的標配了,雖然它的有很高的代價,但也消除了去檢查懸空引用的必要性了。關於這方面的爭執集中在兩方:以方便和安全為主的一方,以性能為主的另一方。這也說明了一件事,編程中的很多地方的設計,架構等等方面都是在現實中做出權衡。

廢料收集一般有這兩種思想,就不詳細說了。

  • 引用計算
  • 追溯式收集

表具有遞歸定義的結構,它或者是空表,或者是一個有序對,有序對由一個對象和另一個表組成。表對於函數式或者邏輯式語言程序設計非常適用,因為那裡的大多數工作都是通過遞歸函數或高階函數來完成的。

在Lisp中:

(cons 'a '(b))  => (a b)
(car '(a b))    => a
(cdr '(a b c))  => (b c)

在Haskell和Python還由一個非常有用的功能,叫做列表推導。在Python中可以這樣推導出一個列表

[i * i for i in range(1, 100) if i % 2 == 1]

文件和輸入/輸出

輸入/輸出(I/O)功能使程序可以與外部世界通信。在討論這種通信時,將交互式I/O和文件I/O分開可能有些幫助。交互式IO通常意味着與人或物理設備通信,人或設備都與運行着的程序并行工作,送給程序的輸入可能依賴程序在此之前的輸出。文件通常對應於程序的地址空間之外的存儲器,由操作系統實現。

有些語言提供了內置的File數據類型,另外一些語言將IO工作完全委託給庫程序包,這些程序包導出一個file類型。所以IO也算作是一種數據類型

相等檢測和賦值

對於簡單的基本數據類型,如整數、浮點數和字符,相等檢測和賦值相對來說都是直截了當的操作。其語義和實現也很明確,可以直接按照二進制位方式比較或複製,但是,對於更加複雜或抽象的數據類型,就可能還需要其它的比較方式

  • 相互是別名?
  • 二進制位是否都相等?
  • 包含同樣的字符序列?
  • 如果打印出來,看起來完全一樣?

就許多情況下,當存在引用的情況下,只有兩個表達式引用相同的對象時它們才相等,這種稱為淺比較。而對於引用的對象本身存在相等的含義時,這種比較稱為深比較。對於複雜的數據結構,進行深比較可能要進行遞歸的遍歷。所以相對來說,賦值也有深淺之分。深賦值時是進行完整的拷貝。

大多數的語言都使用淺比較和淺賦值

小結

本文從語言為何需要類型系統出發,解釋了類型系統為語言提供了那些有價值的用途:1是為許多操作提供隱含的上下文,使程序員在許多情況下不必显示的描述這種上下文;2是使得編譯器可以捕捉更廣泛的各種各樣的程序錯誤。 然後介紹了類型系統的三個重要規則:類型等價、類型相容、類型推理。以此3個規則推導出的強類型(絕不允許把任何操作應用到不支持該操作的對象上)、弱類型以及靜態類型化(在編譯階段貫徹實施強類型的性質)、動態類型化的性質以及在對語言的使用方面的影響。以及後續介紹了語言中常見的一些數據類型的用途以及語言在實現這種類型方面所遇到的問題以及其大致的實現方式。

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理【其他文章推薦】

3c收購,鏡頭 收購有可能以全新價回收嗎?

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

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

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

賣IPHONE,iPhone回收,舊換新!教你怎麼賣才划算?