TT electronics推出用於混合動力汽車的大功率電阻

 

TT electronics公司推出WPRT系列線繞功率徑向端子(Wirewound Power Radial Terminal)電阻,符合AEC-Q200標準,設計用於滿足包括混合動力汽車(Hybrid Electric Vehicle, HEV)的大功率應用的特殊要求。   隨著WPRT系列的推出,TT electronics現在已提供業界最廣泛的規格和配置。新電阻系列由高純度陶瓷棒和壓配 (force-fit) 端帽(end-cap)構成,其上纏繞著導線元件,而後使用防火絕緣接合劑將其放置在陶瓷容器內,因此,在規定的溫度和超載狀況下電阻能夠完全防火。   這款電阻產品採用針對浪湧性能而優化的獨特設計,使之成為電動汽車和電機驅動的理想選擇。WPRT系列根據最高容差標準製造,可以很好地適合汽車保護裝置,大大改進了裝配性能。   TT electronics高級應用工程師 Stephen Oxley 評論說:“WPRT系列瞄準工業、汽車和能源領域的特定需求,通過開發最廣泛的此種類別功率電阻,TT electronics正在實踐承諾,不但支援客戶的節能設計,同時消除開發定制功率電阻解決方案的風險、成本和複雜性。”   WPRT系列可用於HEV預充電和BDU放電應用,以及保護電機避免浪湧電流的損壞。此外,該系列採用了浪湧保護設計,也適用於UPS系統。   WPRT系列包含了從10W至50W的六種額定功率,對於WPRT50 (50W)器件,可耐受最大250W超載。TT electronics提供的這些電阻都符合E24標準電阻值。

本站聲明:網站內容來源於EnergyTrend https://www.energytrend.com.tw/ev/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

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

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

※回頭車貨運收費標準

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

※超省錢租車方案

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

2013自行車展-自行車輛更來電

2013自行車展盛大展出,根據集邦科技旗下綠能事業部EnergyTrend的現場了解,除了汽車廠豐田汽車提出了鋰電池在電動載具的應用看法外,自行車方面,包括了變速器廠以及電池廠也紛紛對於電動自行車提出各自的最新的商品展示,無論是零組件廠以及整車廠,都讓參觀者對於車輛電動化的延伸發展,產生的極大期待,讓電動自行車從過去的代步、健身兼具延伸續航距離,發展到未來將整合隨身電子裝置,讓人們對電動車量的普及充滿樂觀。

豐田汽車鋰電池電動車開賣

雖然全球電動汽車銷售市況不如預期,以Nissan leaf來看,銷量逐步好轉,目前在日本每月銷售約800輛、北美則有1,600輛,截至目前累計約兩萬多輛。根據自行車研討會的演說分享,豐田汽車所寄予厚望的重量級車款Prius α 預計在2013年底上市(全球),並且為首度配置的鋰電池中階車款,電池位置也因為體積輕巧而配置於車室內。豐田目前使用NCA材料做為正極材料,電池芯單元為3.7V, 5Ah,總電池容量約4.4kWh,2014年將推出新款車款IQ,12kWh,預計仍是使用NCA材料(圖一)。
 
圖一 豐田汽車

 
   
變速器、系統整合、電池模組熱鬧參與

作為自行車變速系統龍頭大廠,Shimano也將電子化與變速系統做了創新結合。在Shimano E tube計畫裡頭,將以變速系統Di2做為變速系統電子化為主要訴求,初期將落實在維修體系以及變速資訊的便利性上,未來更有可能藉著電池系統的搭配,提供車主更多的資訊來源(圖二),進而成為讓消費者改變使用習慣的革命。
 
圖二 Shimano E tube應用展示

 
三星SDI做為目前全球第二大的電池芯廠(僅次於合併後的Sanyo + Panasonic),除了在消費型應用的鋰電池產業著墨甚多外,近年也積極切入電動工具以及電動自行車等利基市場,更從過去的電池芯供應,逐步跨足到電池模組(圖三)。

以電動自行車來看,此次展出全方位的自行車電池,可提供後架、坐管、下管等不同位置的應用模組。目前全球各地電動自行車因使用族群不同而衍生出不同擺放位置的設計,中國市場大多以坐管以及後架位置為主要設計,主要訴求在荷重與價格;歐洲則集中在下管及座管位置,用於強調於設計外觀一體性,根據會場的展示DM,目前三星主推2.1Ah以及2.8Ah兩顆自行車專用三元材料電池芯,最大放電率可達到3C(C-rate),已可滿足各種路況的電動自行車的使用需求。
 
圖三 三星自行車電池解決方案展示


   
中華車在配合工業局補助政策,全力推廣電動機車銷售,並且在2012年繳出了六千輛的銷售佳績,未來也將從整車品牌退居幕後,藉著曾經扮演整車零組件整合的豐富經驗,轉型成為關鍵零組件提供者,本次特別展示了關鍵零組件品牌GreenTrans,提供中華車的Power Kit(圖四),包含了扭力感測器、LCD顯示、以及馬達驅動系統,2012年起已與台灣多家自行車廠合作,包括永祺、世同、達生、紹凱、吉安等,都採用中華的Power Kit系統組裝成車。

圖四 中華電動車套件供應分類展示


   
台灣電池模組大廠新普科技也展示了已耕耘三年的電動自行車商品,本次特別針對歐洲市場,與美國品牌Specialize、法國品牌BTwin進行合作(圖五),分別展出下管以及後座電池組產品。

與Specialize合作的登山車,下管產品特色在於電池與車架進行整合,呈現高度客製化,與客戶的關係也將因此而更形緊密。另外BTwin所推出的tilt折疊車系列,目前定位在堅固耐用,為了維持車體的強度,電池模組僅在延伸支架上做結合。新普科技憑藉著對於電池芯的品質掌握度,再加上電源管理的豐富經驗,相信未來在電動自行車市場後市可期。

圖五 新普科技電池模組展示

 
   
另一家作風一向低調的台灣電池模組大廠順達科技也積極跨足各種電池類型應用,於2012年與台達電合作的備用電源產品首次發表即得到設計大獎的殊榮外,此次自行車展也不遑多讓,搭配合作的車廠於現場展出了電動自行車模組的電池模組應用,與中國第一大馬達與控制器廠安乃達做配合,搭配了恪萊博(climbull)各系列整車系統(圖六),這樣的策略合作與全系列搭配的組合,也產生令人期待的商品。

恪萊博的產品策略主打戶外運動客層,屬於高階客群,有別於一般通勤族,由於戶外運動屬於白領階級活動,產品單價較高,且一般通勤族需要載重負荷,容易導致產品壽命偏低,對品牌形象反而產生負面影響,因而改以戶外運動客層為主要訴求對象為主,從應用定位面來扭轉可能造成的問題。而整車產品賣點在於搭配新型中置高速馬達,結合其馬達減速系統,可與外變速系統做最有效益的齒輪比搭配,進而提升產品的壽命與動力表現。

圖六 順達科技電池模組展示

本站聲明:網站內容來源於EnergyTrend https://www.energytrend.com.tw/ev/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※超省錢租車方案

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

※回頭車貨運收費標準

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

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

Elasticsearch系列—生產集群的索引管理

概要

索引是我們使用Elasticsearch里最頻繁的部分日常的操作都與索引有關,本篇從運維人員的視角,來玩一玩Elasticsearch的索引操作。

基本操作

在運維童鞋的視角里,索引的日常操作除了CRUD,還是打開關閉、壓縮、alias重置,我們來了解一下。

創建索引

[esuser@elasticsearch02 ~]$curl -XPUT 'http://elasticsearch02:9200/music?pretty' -H 'Content-Type: application/json' -d '
{
    "settings" : {
        "index" : {
            "number_of_shards" : 3, 
            "number_of_replicas" : 2 
        }
    },
    "mappings" : {
        "type1" : {
            "properties" : {
                "name" : { "type" : "text" }
            }
        }
    }
}'

{
    "acknowledged": true,
    "shards_acknowledged": true
}

默認情況下,索引創建命令會在每個primary shard的replica shard 開始進行複製后,或者是請求超時之後,返迴響應消息,如上。

acknowledged表示這個索引是否創建成功,shards_acknowledged表明了每個primary shard有沒有足夠數量的replica開始進行複製。

可能這兩個參數會為false,但是索引依然可以創建成功。因為這些參數僅僅是表明在請求超時之前,這兩個操作有沒有成功,也有可能請求超時了,在超時前都沒成功,但是實際上Elasticsearch Server端接收到了消息,並且都執行了,只是響應前還沒來得及執行,所以響應的是false。

刪除索引

curl -XDELETE 'http://elasticsearch02:9200/music?pretty'

查詢索引設置信息

curl -XGET 'http://elasticsearch02:9200/music?pretty'

打開/關閉索引

curl -XPOST 'http://elasticsearch02:9200/music/_close?pretty'
curl -XPOST 'http://elasticsearch02:9200/music/_open?pretty'

如果一個索引關閉了,那麼這個索引就沒有任何的性能開銷了,只要保留這個索引的元數據即可,然後對這個索引的讀寫操作都不會成功。一個關閉的索引可以接着再打開,打開以後會進行shard recovery過程。

如果集群數據定時有備份,在執行恢復的操作之前,必須將待恢復的索引關閉,否則恢復會報失敗。

壓縮索引

我們知道索引的primary shard數量在創建時一旦指定,後期就不能修改了,但是有一個這樣的情況:預估的shard數量在實際生產之後,發現估算得有點高,比如原來設置number_of_shards為8,結果生產上線后發現數據量沒那麼大,我想把這個索引的primary shard壓縮一下,該如何操作呢?

shrink命令的作用就是對索引進行壓縮的,不過有個限制:壓縮后的shard數量必須可以被原來的shard數量整除。如我們的8個primary shard的index可以只能被壓縮成4個,2個,或者1個primary shard的index。

shrink命令的工作流程:
  1. 創建一個跟source index的定義一樣的target index,但是唯一的變化就是primary shard變成了指定的數量。
  2. 將source index的segment file直接用hard-link的方式連接到target index的segment file,如果操作系統不支持hard-link,那麼就會將source index的segment file都拷貝到target index的data dir中,會很耗時。如果用hard-link會很快。
  3. target index進行shard recovery恢復。
案例演示
  1. 我們創建一個number_of_shards為8的索引,名稱為music8
curl -XPUT 'http://elasticsearch02:9200/music8?pretty' -H 'Content-Type: application/json' -d '
{
    "settings" : {
        "index" : {
            "number_of_shards" : 8, 
            "number_of_replicas" : 2 
        }
    },
    "mappings" : {
        "children" : {
            "properties" : {
                "name" : { "type" : "text" }
            }
        }
    }
}'
  1. 在索引內灌點數據進去
  2. 將索引的shard都移到一個node上去,如node1
curl -XPUT 'http://elasticsearch02:9200/music8/_settings?pretty' -H 'Content-Type: application/json' -d '
{
  "settings": {
    "index.routing.allocation.require._name": "node-1", 
    "index.blocks.write": true 
  }
}'

這個過程叫shard copy relocate,使用

`curl -XGET ‘http://elasticsearch02:9200/_cat/recovery?v’

可以查看該過程的進度。

  1. 執行shrink命令,新的索引名稱為music9
curl -XPOST 'http://elasticsearch02:9200/music8/_shrink/music9?pretty' -H 'Content-Type: application/json' -d '
{
  "settings": {
	"index.number_of_shards": 2, 
    "index.number_of_replicas": 1,
    "index.codec": "best_compression" 
  }
}'

執行完成后,可以看到music9的shard數據變化了,並且擁有music8所有的數據。

  1. 將別名指向新的music9索引,客戶端訪問無感知。

rollover索引

我們最常見的日誌索引,需要每天創建一個新的帶日期的索引,但客戶端又使用同一個alias進行寫入,此時可以用rollover命令將alias重置到這個新的索引上。

假設log_write別名已經存在,示例命令:

curl -XPOST 'http://elasticsearch02:9200/log_write/_rollover/log-20120122
-H 'Content-Type: application/json' -d '
{
  "conditions": {
    "max_age":   "1d"
  }
}'

用crontab定時每天執行一次,並且將日期部分用shell腳本進行參數化,這樣每天都創建一個帶日期的索引名字,而客戶端那邊一直使用log_write別名作寫入操作,對日誌系統非常實用。

索引mapping管理

索引的mapping管理是非常基礎的操作,我們可以在創建索引時定義mapping信息,也可以在索引創建成功后執行增加字段操作。

列舉以下幾個常用示例:

查看索引的mapping信息

curl -XGET 'http://elasticsearch02:9200/music/_mapping/children?pretty'

查看索引指定field的mapping信息

curl -XGET 'http://elasticsearch02:9200/music/_mapping/children/field/content?pretty'

創建索引時帶上mapping信息

# 節省篇幅,省略大部分字段
curl -XPUT 'http://elasticsearch02:9200/music?pretty' -H 'Content-Type: application/json' -d ' 
{
  "mappings": {
    "children": {
      "properties": {
        "content": {
          "type": "text",
          "fields": {
            "keyword": {
              "type": "keyword",
              "ignore_above": 256
            }
          }
		}
      }
    }
  }
}'

為索引增加一個字段name,類型為text

curl -XPUT 'http://elasticsearch02:9200/music/_mapping/children?pretty' -H 'Content-Type: application/json' -d ' 
{
  "properties": {
    "name": {
      "type": "text"
    }
  }
}'

索引別名

客戶端訪問Elasticsearch的索引時,規範化操作都不會直接使用索引名稱,而是使用索引別名,索引別名能夠起到封裝Elasticsearch真實索引的作用,像上面的rollover操作,索引重建操作,別名起到了非常關鍵的作用。

我們來簡單看一下索引的基本操作:

# 創建索引別名
curl -XPOST 'http://elasticsearch02:9200/_aliases?pretty' -H 'Content-Type: application/json' -d '
{
    "actions" : [
        { "add" : { "index" : "music", "alias" : "music_prd" } }
    ]
}'
# 刪除索引別名
curl -XPOST 'http://elasticsearch02:9200/_aliases?pretty' -H 'Content-Type: application/json' -d '
{
    "actions" : [
        { "remove" : { "index" : "music", "alias" : "music_prd" } }
    ]
}'
# 重命名別名:先刪掉后添加
curl -XPOST 'http://elasticsearch02:9200/_aliases?pretty' -H 'Content-Type: application/json' -d '
{
    "actions" : [
        { "remove" : { "index" : "music", "alias" : "music_prd" } },
        { "add" : { "index" : "music2", "alias" : "music_prd" } }
    ]
}'
# 多個索引綁定一個別名
curl -XPOST 'http://elasticsearch02:9200/_aliases?pretty' -H 'Content-Type: application/json' -d '
{
    "actions" : [
        { "add" : { "indices" : ["music1", "music2"], "alias" : "music_prd" } }
    ]
}'

索引setting修改

查看索引setting信息:

curl -XGET 'http://elasticsearch02:9200/music/_settings?pretty'

修改setting信息:

curl -XPUT 'http://elasticsearch02:9200/music/_settings?pretty' -H 'Content-Type: application/json' -d '
{
    "index" : {
        "number_of_replicas" : 1
    }
}'

setting最常見的修改項就是replicas的數量,其他的參數修改的場景不是特別多。

索引template

假設我們正在設計日誌系統的索引結構,日誌數據量較大,可能每天創建一個新的索引,索引名稱按日期標記,但別名是同一個,這種場景就比較適合使用index template。

我們舉個示例,先創建一個索引模板:

curl -XPUT 'http://elasticsearch02:9200/_template/template_access_log?pretty' -H 'Content-Type: application/json' -d '
{
  "template": "access-log-*",
  "settings": {
    "number_of_shards": 2
  },
  "mappings": {
    "log": {
      "_source": {
        "enabled": false
      },
      "properties": {
        "host_name": {
          "type": "keyword"
        },
		"thread_name": {
          "type": "keyword"
        },
        "created_at": {
          "type": "date",
          "format": "YYYY-MM-dd HH:mm:ss"
        }
      }
    }
  },
  "aliases" : {
      "access-log" : {}
  }
}'

索引名稱符合”access-log-*”將使用該模板,我們創建一個索引:

curl -XPUT 'http://elasticsearch02:9200/access-log-01?pretty'

查看該索引:

curl -XGET 'http://elasticsearch02:9200/access-log-01?pretty'

可以看到如下結構:

[esuser@elasticsearch02 bin]$ curl -XGET 'http://elasticsearch02:9200/access-log-01?pretty'
{
  "access-log-01" : {
    "aliases" : {
      "access-log" : { }
    },
    "mappings" : {
      "log" : {
        "_source" : {
          "enabled" : false
        },
        "properties" : {
          "created_at" : {
            "type" : "date",
            "format" : "YYYY-MM-dd HH:mm:ss"
          },
          "host_name" : {
            "type" : "keyword"
          },
          "thread_name" : {
            "type" : "keyword"
          }
        }
      }
    },
    "settings" : {
      "index" : {
        "creation_date" : "1581373546223",
        "number_of_shards" : "2",
        "number_of_replicas" : "1",
        "uuid" : "N8AHh3wITg-Zh4T6umCS2Q",
        "version" : {
          "created" : "6030199"
        },
        "provided_name" : "access-log-01"
      }
    }
  }
}

說明使用了模板的內容。

當然也有命令可以查看和刪除template:

curl -XGET 'http://elasticsearch02:9200/_template/template_access_log?pretty'

curl -XDELETE 'http://elasticsearch02:9200/_template/template_access_log?pretty'

索引常用查詢

索引操作統計查詢

發生在索引上的所有CRUD操作,Elasticsearch都是會做統計的,而且統計的內容非常翔實,我們可以使用這條命令:

curl -XGET 'http://elasticsearch02:9200/music/_stats?pretty'

內容非常詳細,有好幾百行,從doc的數據和佔用的磁盤字節數,到get、search、merge、translog等底層數據應有盡有。

segment信息查詢

索引下的segment信息,可以使用這條命令進行查詢:

curl -XGET 'http://elasticsearch02:9200/music/_segments?pretty'

內容也同樣挺多,我們摘抄出關鍵的部分做個示例:

"segments" : {
  "_1" : {
    "generation" : 1,
    "num_docs" : 1,
    "deleted_docs" : 0,
    "size_in_bytes" : 7013,
    "memory_in_bytes" : 3823,
    "committed" : true,
    "search" : true,
    "version" : "7.3.1",
    "compound" : true,
    "attributes" : {
      "Lucene50StoredFieldsFormat.mode" : "BEST_SPEED"
    }
  }
}

這個片段表示名稱為_1的segment的信息。詳細如下:

  • _1:segment的名稱
  • generation:segment的自增長ID
  • num_docs:segment中沒有被刪除的document的數量
  • deleted_docs:segment中被刪除的document數量
  • size_in_bytes:segment佔用的磁盤空間
  • memory_in_bytes:segment會將一些數據緩存在內存中,這個數值就是segment佔用的內存的空間大小
  • committed:segment是否被sync到磁盤上去了
  • search:segment是否可被搜索,如果這個segment已經被sync到磁盤上,但是還沒有進行refresh,值為false
  • version:lucene的版本號
  • compound:true表示lucene已將這個segment所有的文件都merge成了一個文件
shard存儲信息

查看索引下shard的存儲情況,分佈在哪個node上,這條命令還是挺有用處的:

curl -XGET 'http://elasticsearch02:9200/music/_shard_stores?status=green&pretty'

摘抄了一個片段,3表示shard的id:

"3" : {
  "stores" : [
    {
      "A1s1uus7TpuDSiT4xFLOoQ" : {
        "name" : "node-2",
        "ephemeral_id" : "Q3uoxLeJRnWQrw3E2nOq-Q",
        "transport_address" : "192.168.17.137:9300",
        "attributes" : {
          "ml.machine_memory" : "3954196480",
          "rack" : "r1",
          "xpack.installed" : "true",
          "ml.max_open_jobs" : "20",
          "ml.enabled" : "true"
        }
      },
      "allocation_id" : "o-t-AwGZRrWTflYLP030jA",
      "allocation" : "primary"
    },
    {
      "RGw1IXzZR4CeZh9FUrGHDw" : {
        "name" : "node-1",
        "ephemeral_id" : "B1pv6c4TRuu1vQNvL40iPg",
        "transport_address" : "192.168.17.138:9300",
        "attributes" : {
          "ml.machine_memory" : "3954184192",
          "rack" : "r1",
          "ml.max_open_jobs" : "20",
          "xpack.installed" : "true",
          "ml.enabled" : "true"
        }
      },
      "allocation_id" : "SaXqL8igRUmLAoBBQyQNqw",
      "allocation" : "replica"
    }
  ]
},
補充幾個操作
  1. 清空索引緩存

curl -XPOST 'http://elasticsearch02:9200/music/_cache/clear?pretty'

  1. 強制flush

強行將os cache里的數據強制fsync到磁盤上去,同時還會清理掉translog中的日誌

curl -XPOST 'http://elasticsearch02:9200/music/_flush?pretty'

  1. refresh操作

顯式地刷新索引,讓在自動refresh前的所有操作變成可見

curl -XPOST 'http://elasticsearch02:9200/music/_flush?pretty'

  1. force merge

強制合併segment file,可以減小segment的數量
curl -XPOST 'http://elasticsearch02:9200/music/_forcemerge?pretty'

以上4個操作,一般是由Elasticsearch自動去執行,非特殊情況下不需要人工干預。

小結

本篇從運維角度簡單介紹了一下索引的一些日常操作與管理,能夠熟練應用的話,可以提升操縱索引的效率。

專註Java高併發、分佈式架構,更多技術乾貨分享與心得,請關注公眾號:Java架構社區
可以掃左邊二維碼添加好友,邀請你加入Java架構社區微信群共同探討技術

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

【其他文章推薦】

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

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

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

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

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

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

.Net Core微服務入門全紀錄(六)——EventBus-事件總線

前言

上一篇【.Net Core微服務入門全紀錄(五)——Ocelot-API網關(下)】中已經完成了Ocelot + Consul的搭建,這一篇簡單說一下EventBus。

EventBus-事件總線

  • 首先,什麼是事件總線呢?

貼一段引用:

事件總線是對觀察者(發布-訂閱)模式的一種實現。它是一種集中式事件處理機制,允許不同的組件之間進行彼此通信而又不需要相互依賴,達到一種解耦的目的。

如果沒有接觸過EventBus,可能不太好理解。其實EventBus在客戶端開發中應用非常廣泛(android,ios,web前端等),用於多個組件(或者界面)之間的相互通信,懂的人都懂。。。

  • 那麼,我們為什麼要用EventBus呢?

就拿當前的項目舉例,我們有一個訂單服務,一個產品服務。客戶端有一個下單功能,當用戶下單時,調用訂單服務的下單接口,那麼下單接口需要調用產品服務的減庫存接口,這涉及到服務與服務之間的調用。那麼服務之間又怎麼調用呢?直接RESTAPI?或者效率更高的gRPC?可能這兩者各有各的使用場景,但是他們都存在一個服務之間的耦合問題,或者難以做到異步調用。

試想一下:假設我們下單時調用訂單服務,訂單服務需要調用產品服務,產品服務又要調用物流服務,物流服務再去調用xx服務 等等。。。如果每個服務處理時間需要2s,不使用異步的話,那這種體驗可想而知。

如果使用EventBus的話,那麼訂單服務只需要向EventBus發一個“下單事件”就可以了。產品服務會訂閱“下單事件”,當產品服務收到下單事件時,自己去減庫存就好了。這樣就避免了兩個服務之間直接調用的耦合性,並且真正做到了異步調用。

既然涉及到多個服務之間的異步調用,那麼就不得不提分佈式事務。分佈式事務並不是微服務獨有的問題,而是所有的分佈式系統都會存在的問題。
關於分佈式事務,可以查一下“CAP原則”和“BASE理論”了解更多。當今的分佈式系統更多的會追求事務的最終一致性。

下面使用國人開發的優秀項目“CAP”,來演示一下EventBus的基本使用。之所以使用“CAP”是因為它既能解決分佈式系統的最終一致性,同時又是一個EventBus,它具備EventBus的所有功能!
作者介紹:https://www.cnblogs.com/savorboard/p/cap.html

CAP使用

  • 環境準備

在Docker中準備一下需要的環境,首先是數據庫,數據庫我使用PostgreSQL,用別的也行。CAP支持:SqlServer,MySql,PostgreSql,MongoDB。
關於在Docker中運行PostgreSQL可以看我的另一篇博客:https://www.cnblogs.com/xhznl/p/13155054.html

然後是MQ,這裏我使用RabbitMQ,Kafka也可以。
Docker運行RabbitMQ:

docker pull rabbitmq:management
docker run -d -p 15672:15672 -p 5672:5672 --name rabbitmq rabbitmq:management

默認用戶:guest,密碼:guest

環境準備就完成了,Docker就是這麼方便。。。

  • 代碼修改:

為了模擬以上業務,需要修改大量代碼,下面代碼如有遺漏的直接去github找。

NuGet安裝:

Microsoft.EntityFrameworkCore
Microsoft.EntityFrameworkCore.Tools
Npgsql.EntityFrameworkCore.PostgreSQL

CAP相關:

DotNetCore.CAP
DotNetCore.CAP.RabbitMQ
DotNetCore.CAP.PostgreSql

Order.API/Controllers/OrdersController.cs增加下單接口:

[Route("[controller]")]
[ApiController]
public class OrdersController : ControllerBase
{
    private readonly ILogger<OrdersController> _logger;
    private readonly IConfiguration _configuration;
    private readonly ICapPublisher _capBus;
    private readonly OrderContext _context;

    public OrdersController(ILogger<OrdersController> logger, IConfiguration configuration, ICapPublisher capPublisher, OrderContext context)
    {
        _logger = logger;
        _configuration = configuration;
        _capBus = capPublisher;
        _context = context;
    }

    [HttpGet]
    public IActionResult Get()
    {
        string result = $"【訂單服務】{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")}——" +
            $"{Request.HttpContext.Connection.LocalIpAddress}:{_configuration["ConsulSetting:ServicePort"]}";
        return Ok(result);
    }

    /// <summary>
    /// 下單 發布下單事件
    /// </summary>
    /// <param name="order"></param>
    /// <returns></returns>
    [Route("Create")]
    [HttpPost]
    public async Task<IActionResult> CreateOrder(Models.Order order)
    {
        using (var trans = _context.Database.BeginTransaction(_capBus, autoCommit: true))
        {
            //業務代碼
            order.CreateTime = DateTime.Now;
            _context.Orders.Add(order);

            var r = await _context.SaveChangesAsync() > 0;

            if (r)
            {
                //發布下單事件
                await _capBus.PublishAsync("order.services.createorder", new CreateOrderMessageDto() { Count = order.Count, ProductID = order.ProductID });
                return Ok();
            }
            return BadRequest();
        }

    }

}

Order.API/MessageDto/CreateOrderMessageDto.cs:

/// <summary>
/// 下單事件消息
/// </summary>
public class CreateOrderMessageDto
{
    /// <summary>
    /// 產品ID
    /// </summary>
    public int ProductID { get; set; }

    /// <summary>
    /// 購買數量
    /// </summary>
    public int Count { get; set; }
}

Order.API/Models/Order.cs訂單實體類:

public class Order
{
    [Key]
    [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
    public int ID { get; set; }

    /// <summary>
    /// 下單時間
    /// </summary>
    [Required]
    public DateTime CreateTime { get; set; }

    /// <summary>
    /// 產品ID
    /// </summary>
    [Required]
    public int ProductID { get; set; }

    /// <summary>
    /// 購買數量
    /// </summary>
    [Required]
    public int Count { get; set; }
}

Order.API/Models/OrderContext.cs數據庫Context:

public class OrderContext : DbContext
{
    public OrderContext(DbContextOptions<OrderContext> options)
       : base(options)
    {

    }

    public DbSet<Order> Orders { get; set; }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {

    }
}

Order.API/appsettings.json增加數據庫連接字符串:

"ConnectionStrings": {
  "OrderContext": "User ID=postgres;Password=pg123456;Host=host.docker.internal;Port=5432;Database=Order;Pooling=true;"
}

Order.API/Startup.cs修改ConfigureServices方法,添加Cap配置:

public void ConfigureServices(IServiceCollection services)
{
    services.AddControllers();

    services.AddDbContext<OrderContext>(opt => opt.UseNpgsql(Configuration.GetConnectionString("OrderContext")));

    //CAP
    services.AddCap(x =>
    {
        x.UseEntityFramework<OrderContext>();

        x.UseRabbitMQ("host.docker.internal");
    });
}

以上是訂單服務的修改。

Product.API/Controllers/ProductsController.cs增加減庫存接口:

[Route("[controller]")]
[ApiController]
public class ProductsController : ControllerBase
{
    private readonly ILogger<ProductsController> _logger;
    private readonly IConfiguration _configuration;
    private readonly ICapPublisher _capBus;
    private readonly ProductContext _context;

    public ProductsController(ILogger<ProductsController> logger, IConfiguration configuration, ICapPublisher capPublisher, ProductContext context)
    {
        _logger = logger;
        _configuration = configuration;
        _capBus = capPublisher;
        _context = context;
    }

    [HttpGet]
    public IActionResult Get()
    {
        string result = $"【產品服務】{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")}——" +
            $"{Request.HttpContext.Connection.LocalIpAddress}:{_configuration["ConsulSetting:ServicePort"]}";
        return Ok(result);
    }

    /// <summary>
    /// 減庫存 訂閱下單事件
    /// </summary>
    /// <param name="message"></param>
    /// <returns></returns>
    [NonAction]
    [CapSubscribe("order.services.createorder")]
    public async Task ReduceStock(CreateOrderMessageDto message)
    {
        //業務代碼
        var product = await _context.Products.FirstOrDefaultAsync(p => p.ID == message.ProductID);
        product.Stock -= message.Count;

        await _context.SaveChangesAsync();
    }

}

Product.API/MessageDto/CreateOrderMessageDto.cs:

/// <summary>
/// 下單事件消息
/// </summary>
public class CreateOrderMessageDto
{
    /// <summary>
    /// 產品ID
    /// </summary>
    public int ProductID { get; set; }

    /// <summary>
    /// 購買數量
    /// </summary>
    public int Count { get; set; }
}

Product.API/Models/Product.cs產品實體類:

public class Product
{
    [Key]
    [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
    public int ID { get; set; }

    /// <summary>
    /// 產品名稱
    /// </summary>
    [Required]
    [Column(TypeName = "VARCHAR(16)")]
    public string Name { get; set; }

    /// <summary>
    /// 庫存
    /// </summary>
    [Required]
    public int Stock { get; set; }
}

Product.API/Models/ProductContext.cs數據庫Context:

public class ProductContext : DbContext
{
    public ProductContext(DbContextOptions<ProductContext> options)
       : base(options)
    {

    }

    public DbSet<Product> Products { get; set; }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        base.OnModelCreating(modelBuilder);
        
        //初始化種子數據
        modelBuilder.Entity<Product>().HasData(new Product
        {
            ID = 1,
            Name = "產品1",
            Stock = 100
        },
        new Product
        {
            ID = 2,
            Name = "產品2",
            Stock = 100
        });
    }
}

Product.API/appsettings.json增加數據庫連接字符串:

"ConnectionStrings": {
  "ProductContext": "User ID=postgres;Password=pg123456;Host=host.docker.internal;Port=5432;Database=Product;Pooling=true;"
}

Product.API/Startup.cs修改ConfigureServices方法,添加Cap配置:

public void ConfigureServices(IServiceCollection services)
{
    services.AddControllers();

    services.AddDbContext<ProductContext>(opt => opt.UseNpgsql(Configuration.GetConnectionString("ProductContext")));

    //CAP
    services.AddCap(x =>
    {
        x.UseEntityFramework<ProductContext>();

        x.UseRabbitMQ("host.docker.internal");
    });
}

以上是產品服務的修改。

訂單服務和產品服務的修改到此就完成了,看着修改很多,其實功能很簡單。就是各自增加了自己的數據庫表,然後訂單服務增加了下單接口,下單接口會發出“下單事件”。產品服務增加了減庫存接口,減庫存接口會訂閱“下單事件”。然後客戶端調用下單接口下單時,產品服務會減去相應的庫存,功能就這麼簡單。

關於EF數據庫遷移之類的基本使用就不介紹了。使用Docker重新構建鏡像,運行訂單服務,產品服務:

docker build -t orderapi:1.1 -f ./Order.API/Dockerfile .
docker run -d -p 9060:80 --name orderservice orderapi:1.1 --ConsulSetting:ServicePort="9060"
docker run -d -p 9061:80 --name orderservice1 orderapi:1.1 --ConsulSetting:ServicePort="9061"
docker run -d -p 9062:80 --name orderservice2 orderapi:1.1 --ConsulSetting:ServicePort="9062"

docker build -t productapi:1.1 -f ./Product.API/Dockerfile .
docker run -d -p 9050:80 --name productservice productapi:1.1 --ConsulSetting:ServicePort="9050"
docker run -d -p 9051:80 --name productservice1 productapi:1.1 --ConsulSetting:ServicePort="9051"
docker run -d -p 9052:80 --name productservice2 productapi:1.1 --ConsulSetting:ServicePort="9052"

最後 Ocelot.APIGateway/ocelot.json 增加一條路由配置:

好了,進行到這裏,整個環境就有點複雜了。確保我們的PostgreSQL,RabbitMQ,Consul,Gateway,服務實例都正常運行。

服務實例運行成功后,數據庫應該是這樣的:

產品表種子數據:

cap.published表和cap.received表是由CAP自動生成的,它內部是使用本地消息表+MQ來實現異步確保。

運行測試

這次使用Postman作為客戶端調用下單接口(9070是之前的Ocelot網關端口):

訂單庫published表:

訂單庫order表:

產品庫received表:

產品庫product表:

再試一下:

OK,完成。雖然功能很簡單,但是我們實現了服務的解耦,異步調用,和最終一致性。

總結

注意,上面的例子純粹是為了說明EventBus的使用,實際中的下單流程絕對不會這麼做的!希望大家不要較真。。。

可能有人會說如果下單成功,但是庫存不足導致減庫存失敗了怎麼辦,是不是要回滾訂單表的數據?如果產生這種想法,說明還沒有真正理解最終一致性的思想。首先下單前肯定會檢查一下庫存數量,既然允許下單那麼必然是庫存充足的。這裏的事務是指:訂單保存到數據庫,和下單事件保存到cap.published表(保存到cap.published表理論上就能夠發送到MQ)這兩件事情,要麼一同成功,要麼一同失敗。如果這個事務成功,那麼就可以認為這個業務流程是成功的,至於產品服務的減庫存是否成功那就是產品服務的事情了(理論上也應該是成功的,因為消息已經確保發到了MQ,產品服務必然會收到消息),CAP也提供了失敗重試,和失敗回調機制。

如果非要數據回滾也是能實現的,CAP的ICapPublisher.Publish方法提供一個callbackName參數,當減庫存時,可以觸發這個回調。其本質也是通過發布訂閱完成,這是不推薦的做法,就不詳細說了,有興趣自己研究一下。
另外,CAP無法保證消息不重複,實際使用中需要自己考慮一下消息的重複過濾和冪等性。

這一篇內容有點多,不知道有沒有表達清楚,有問題歡迎評論交流,如有不對之處還望大家指出。

下一篇計劃寫一下授權認證相關的內容。

代碼放在:https://github.com/xiajingren/NetCoreMicroserviceDemo

未完待續…

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

【其他文章推薦】

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

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

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

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

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

※超省錢租車方案

再看rabbitmq的交換器和隊列的關係

最近又要用到rabbitmq,業務上要求服務器只發一次消息,需要多個客戶端都去單獨消費。但我們知道rabbitmq的機制里,每個隊列里的消息只能消費一次,所以客戶端要單獨消費信息,就必須得每個客戶端單獨監聽一個queue。所以我最終想實現的是服務端只聲明exchange,客戶端來創建queue和綁定exchange。但是在看各種rabbitmq博文和討論的時候,我覺得對exchange的模式和queue間的關係講的都不是很清楚。所以我決定自己驗證一下

fanout模式和direct模式

本文主要驗證fanout模式和direct模式下以上猜想是否可行。fanout模式就是大名鼎鼎的廣播模式了,只要queue綁定了fanout的交換器,就可以直接的收到消息,無需routingkey的參与。而direct模式就是通過routing key直接發送到綁定了同樣routing key的隊列中。那麼,在這兩種exchange的模式下,是否都可以實現服務端僅創建exchange,客戶端創建queue並綁定exchange呢?

Direct模式驗證

我們先把交換器、routingkey、隊列的名稱定義好:

  1. 交換器為directTest
  2. routingkey為direct_routing_key
  3. 隊列測試3個,首先測試Direct_test_queue_1,再行測試Direct_test_queue_2,再行測試Direct_test_queue_3

代碼使用spring boot框架快速搭建。我們先規劃好需要幾個類來完成這個事情:

  1. 針對生產者,需要RabbitmqConfig,用來配置exchange的
  2. 針對生產者,需要DirectRabbitSender,用來實現Direct模式的消息發送
  3. 針對消費者,需要DirectConsumerOne,來測試第一個隊列Direct_test_queue_1生成和消息接收
  4. 針對消費者,需要DirectConsumerTwo,來測試第二個隊列Direct_test_queue_2生成和消息接收
  5. 針對消費者,需要DirectConsumerThree,來測試第三個隊列Direct_test_queue_3生成和消息接收
  6. 我們還需要一個測試類RabbitmqApplicationTests,用於測試消息的發送和接收

rabbitmq先配置一個DirectExchange

@Bean
DirectExchange directExchange(){
    return new DirectExchange("directTest", true, false);
}

我們可以看到Direct交換器的名稱定義為了directTest,這時候還未綁定任何的隊列。啟動程序,若我們的設想沒錯,則rabbitmq中應該已經生成了directTest的exchange。

Bingo!directTest交換器成功創建。接下來,我們去編寫DirectRabbitSender的代碼

@Component
public class DirectRabbitSender{

    @Autowired
    private RabbitTemplate rabbitTemplate;

    private final String EXCHANGE_NAME = "directTest";
    private final String ROUTING_KEY = "direct_routing_key";

    public void send(Object message) {
        rabbitTemplate.convertAndSend(EXCHANGE_NAME, ROUTING_KEY, message);
    }

}

我們可以看到代碼中,通過rabbitTemplate發送消息到了交換器為directTest,routingkey為direct_routing_key的地方。但這時候我們沒有任何隊列了,自然接不到消息。現在我們去編寫第一個消費者DirectConsumerOne來接受消息。

@Component
@RabbitListener(bindings = @QueueBinding(
        value = @Queue(value = "Direct_test_queue_1", durable = "true"),
        exchange = @Exchange(value = "directTest"),
        key = "direct_routing_key"
))
public class DirectConsumerOne {

    @RabbitHandler
    private void onMessage(String message){
        System.out.println("監聽隊列Direct_test_queue_1接到消息" + message);
    }

}

通過代碼可以看到,我們通過@QueueBinding把Direct_test_queue_1隊列綁定到了directTest和direct_routing_key上。Direct_test_queue_1並沒有在rabbitmq創建,這並沒有關係。一般來說,@RabbitListener會自動去創建隊列。啟動程序,我們去看一下rabbitmq里隊列是不是創建了。

Bingo!再次驗證成功。我們去看看綁定關係是不是正確。這時候Direct_test_queue_1應該綁定到了名為directTest的交換器,而綁定的routingkey為direct_routing_key

biubiubiu!綁定關係完全正確。到了這裏,我們進行最後一步,寫了單元測試去發送消息,查看控制台中消費者是否成功收到消息。RabbitmqApplicationTests的代碼如下:

@SpringBootTest
class RabbitmqApplicationTests {

    @Autowired
    private DirectRabbitSender directRabbitSender;

    @Test
    void contextLoads() {
    }

    @Test
    public void directSendTest(){
        directRabbitSender.send("direct-sender");
        directRabbitSender.send("direct-sender_test");
    }

}

啟動測試類,然後去查看控制台。

沒錯,這就是我們想要達到的效果!基本可以宣布Direct模式驗證成功。服務端生成exchange,客戶端去生成隊列綁定的方式在direct模式下完全可行。為了保險起見,再驗證一下生成多個消費者綁定到同一個隊列是否可行。

DirectConsumerTwo代碼如下:

@Component
@RabbitListener(bindings = @QueueBinding(
        value = @Queue(value = "Direct_test_queue_2", durable = "true"),
        exchange = @Exchange(value = "directTest"),
        key = "direct_routing_key"
))
public class DirectConsumerTwo {

    @RabbitHandler
    private void onMessage(String message){
        System.out.println("監聽隊列Direct_test_queue_2接到消息" + message);
    }

}

DirectConsumerThree代碼如下:

@Component
@RabbitListener(bindings = @QueueBinding(
        value = @Queue(value = "Direct_test_queue_3", durable = "true"),
        exchange = @Exchange(value = "directTest"),
        key = "direct_routing_key"
))
public class DirectConsumerThree {

    @RabbitHandler
    private void onMessage(String message){
        System.out.println("監聽隊列Direct_test_queue_3接到消息" + message);
    }

}

啟動測試類,我們去看兩個地方:

  1. rabbitmq是否創建了客戶端綁定的三個隊列Direct_test_queue_1、Direct_test_queue_2、Direct_test_queue_3
  2. 消費者應該各自收到2條消息(Test中發送了兩條,參看上面 RabbitmqApplicationTests 的代碼)。那3個隊列,控制台中應該打印了6條消息。

hohohoho!創建成功,並且綁定關係我看了也全都正確。我們去看控制台

6條!沒有任何毛病,至此,可以宣布Direct模式下,完全支持我們最初的想法:服務端生成exchange,客戶端去生成隊列綁定的方式在direct模式下完全可行。

fanout模式驗證

接下來我們驗證一下fanout的方式,基本操作流程和Direct模式一致。代碼的結構也差不多:

  1. 針對生產者,需要RabbitmqConfig,直接在Direct模式下的rabbitmqConfig里直接添加Fanout的交換器配置
  2. 針對生產者,需要FanoutRabbitSender,用來實現Fanout模式的消息發送
  3. 針對消費者,需要FanoutConsumerOne,來測試第一個隊列Fanout_test_queue_1生成和消息接收
  4. 針對消費者,需要FanoutConsumerTwo,來測試第二個隊列Fanout_test_queue_2生成和消息接收
  5. 針對消費者,需要FanoutConsumerThree,來測試第三個隊列Fanout_test_queue_3生成和消息接收
  6. 測試類RabbitmqApplicationTests也直接復用Direact模式下測試的類

我就不多BB,直接上代碼了。

RabbitmqConfig代碼如下

@Configuration
public class RabbitmqConfig {

    @Bean
    DirectExchange directExchange(){
        return new DirectExchange("directTest", true, false);
    }

    @Bean
    FanoutExchange fanoutExchange(){
        return new FanoutExchange("fanoutTest", true, false);
    }

}

FanoutRabbitSender的代碼如下,此處和direct模式的區別是Fanout中沒有routingkey,所以代碼里也沒定義routingkey:

@Component
public class FanoutRabbitSender{

    @Autowired
    private RabbitTemplate rabbitTemplate;

    private final String EXCHANGE_NAME = "fanoutTest";

    public void send(Object message) {
        rabbitTemplate.convertAndSend(EXCHANGE_NAME, null, message);
    }

}

我們到這裏先啟動程序試試,看看fanoutTest的交換器在沒有綁定隊列的情況下是否生成了。

棒棒棒!和我們想的一樣,那接下來去寫完所有的消費者,這裏和Direct模式最重要的區別是@Exchange中必須要指定type為fanout。direct模式的代碼里沒指定是因為@Exchange的type默認值就是direct。我直接上代碼了:

/**
 * 監聽器主動去聲明queue=fanout_test_queue_1,並綁定到fanoutTest交換器
 */
@Component
@RabbitListener(bindings = @QueueBinding(
        value = @Queue(value = "fanout_test_queue_1", durable = "true"),
        exchange = @Exchange(value = "fanoutTest", type = ExchangeTypes.FANOUT)
))
public class FanoutConsumerOne {

    @RabbitHandler
    private void onMessage(String message){
        System.out.println("監聽隊列fanout_test_queue_1接到消息" + message);
    }

}

@Component
@RabbitListener(bindings = @QueueBinding(
        value = @Queue(value = "fanout_test_queue_2", durable = "true"),
        exchange = @Exchange(value = "fanoutTest", type = ExchangeTypes.FANOUT)
))
public class FanoutConsumerTwo {

    @RabbitHandler
    private void onMessage(String message){
        System.out.println("監聽隊列fanout_test_queue_2接到消息" + message);
    }

}

@Component
@RabbitListener(bindings = @QueueBinding(
        value = @Queue(value = "fanout_test_queue_3", durable = "true"),
        exchange = @Exchange(value = "fanoutTest", type = ExchangeTypes.FANOUT)
))
public class FanoutConsumerThree {

    @RabbitHandler
    private void onMessage(String message){
        System.out.println("監聽隊列fanout_test_queue_3接到消息" + message);
    }

}

接着去測試類RabbitmqApplicationTests中加上fanout的發送測試,然後註釋掉direct的單元測試,以便一會造成干擾

@SpringBootTest
class RabbitmqApplicationTests {

    @Autowired
    private DirectRabbitSender directRabbitSender;

    @Autowired
    private FanoutRabbitSender fanoutRabbitSender;

    @Test
    void contextLoads() {
    }

//    @Test
//    public void directSendTest(){
//        directRabbitSender.send("direct-sender");
//        directRabbitSender.send("direct-sender_test");
//    }

    @Test
    public void fanoutSendTest(){
        fanoutRabbitSender.send("fanout-sender_1");
        fanoutRabbitSender.send("fanout-sender_2");
    }

}

代碼都完成了,現在我們啟動測試類,看看控制台是否正常收到了消息

看圖看圖,fanout模式下也完全認證成功!!!那我們可以宣布,文章開頭的猜想完全可以實現。

總結

服務端只聲明exchange,客戶端來創建queue和綁定exchange的方式完全可行。並且在Direct和Fanout模式下都可行。

那我們可以推測在Header模式的交換器和Topic模式的交換器下應該也大差不差。具體各位可自行驗證,基本流程和上面direct和fanout的流程差不多。

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

【其他文章推薦】

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

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

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

※南投搬家公司費用需注意的眉眉角角,別等搬了再說!

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

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

Swagger之外的選擇

今天給大家安利一款接口文檔生成器——JApiDocs。

swagger想必大家都用過吧,非常方便,功能也十分強大。如果要說swaager有什麼缺點,想必就是註解寫起來比較麻煩。如果我說有一款不用寫註解,就可以生成文檔的工具,你心動了嗎?他就是我們今天的主角——JApiDocs。

下面我們一起來看看如何使用!

一、添加依賴

<dependency>
  <groupId>io.github.yedaxia</groupId>
  <artifactId>japidocs</artifactId>
  <version>1.3</version>
</dependency>

二、配置生成參數

我們新建一個項目,然後隨便寫一個main方法,增加生成文檔的配置,然後運行main方法。

DocsConfig config = new DocsConfig();
config.setProjectPath("F:\\Java旅途\\japi-docs"); // 項目根目錄
config.setProjectName("japi-docs"); // 項目名稱
config.setApiVersion("V1.0");       // 聲明該API的版本
config.setDocsPath("F:\\test"); // 生成API 文檔所在目錄
config.setAutoGenerate(Boolean.TRUE);  // 配置自動生成
Docs.buildHtmlDocs(config); // 執行生成文檔

三、編碼規範

由於JApiDocs是通過解析Java源碼來實現的,因此如果要想實現想要的文檔,還是需要遵循一定的規範。

3.1 類註釋、方法註釋和屬性註釋

如果我們想生成類的註釋,我們可以直接在類上加註釋,也可以通過加@description來生成。

/**
 * 用戶接口類
 */
@RequestMapping("/api/user")
@RestController
public class UserController {}

/**
 * @author Java旅途
 * @Description 用戶接口類
 * @Date 2020-06-15 21:46
 */
@RequestMapping("/api/user")
@RestController
public class UserController {}

如果我們想生成方法的註釋,只能直接加註釋,不能通過加@description來生成。

/**
 * 查詢用戶
 * @param age 年齡
 * @return R<User>
*/
@GetMapping("/list")
public R<User> list(@RequestParam int age){

    User user = new User("Java旅途", 18);
    return R.ok(user);
}

JApiDocs可以自動生成實體類,關於實體類屬性的註釋有三種方式,生成的效果都是一樣的,如下:

/**
 * 用戶名稱
 */
private String name;
/**
 * 用戶年齡
 */
private int age;
// 用戶名稱
private String name;
// 用戶年齡
private int age;
private String name;// 用戶名稱
private int age;// 用戶年齡

他除了支持咱們常用的model外,還支持IOS的model生成效果如下:

​

3.2 請求參數

如果提交的表單是 application/x-www-form-urlencoded 類型的key/value格式,則我們通過@param註解來獲取參數,在參數後面添加註釋,示例如下:

/**
  * @param age 年齡
  */
@GetMapping("/list")
public R<User> list(@RequestParam int age){
    User user = new User("Java旅途", 18);
    return R.ok(user);
}

生成的文檔效果如下:

請求參數

參數名 類型 必須 描述
age int 否 年齡

如果提交的表單是 application/json 類型的json數據格式,如下:

/**
  * @param user
  * @return
  */
@PostMapping("/add")
public R<User> add(@RequestBody User user){
    return R.ok(user);
}

生成的文檔效果如下:

請求參數

{
  "name": "string //用戶名稱",
  "age": "int //用戶年齡"
}

3.3 響應結果

我們知道,如果Controller聲明了@RestController,SpringBoot會把返回的對象直接序列成Json數據格式返回給前端。 JApiDocs也利用了這一特性來解析接口返回的結果,但由於JApiDocs是靜態解析源碼的,因此你要明確指出返回對象的類型信息,JApiDocs支持繼承、泛型、循環嵌套等複雜的類解析。

因此我們不需要再寫註釋,它會根據我們的返回結果進行解析,效果如下:

返回結果:

{
  "code": "int",
  "msg": "string",
  "data": {
    "name": "string //用戶名稱",
    "age": "int //用戶年齡"
  }
}

最終,我們生成的接口文檔,如下:

四、高級配置

4.1 @ApiDoc

如果你不希望把所有的接口都導出,我們可以在配置中設置config.setAutoGenerate(Boolean.FALSE);然後再想要生成的接口上添加@ApiDoc。

@ApiDoc有以下三個屬性:

  • result: 這個可以直接聲明返回的對象類型,如果你聲明了,將會覆蓋SpringBoot的返回對象
  • url: 請求URL,擴展字段,用於支持非SpringBoot項目
  • method: 請求方法,擴展字段,用於支持非SpringBoot項目
@ApiDoc(result = User.class, url = "/api/user/view", method = "post")

4.2 @Ignore

如果你不想導出對象裏面的某個字段,可以給這個字段加上@Ignore註解,這樣JApiDocs導出文檔的時候就會自動忽略掉了。

public class User {
    @Ignore
    private int age;
}

五、總結

JApiDocs就介紹到這裏了,優勢劣勢大家很容易就看出來了。幾乎不需要註釋即可生成接口文檔,僅有的幾個註釋我們也可以通過ide來自動生成。但是JApiDocs不具備swagger在線調試功能。如果有一天JApiDocs支持在線調試后,那時候肯定會有一大波追隨者,畢竟寫代碼的誰喜歡寫多餘的註解!~

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

【其他文章推薦】

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

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

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

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

※超省錢租車方案

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

我們是如何做go語言系統測試覆蓋率收集的?

工程效能領域,測試覆蓋率度量總是繞不開的話題,我們也不例外。在七牛雲,我們主要使用go語言構建雲服務,在考慮系統測試覆蓋率時,最早也是通過圍繞原生go test -c -cover的能力來構建。這個方案,筆者還曾在 MTSC2018大會上有過專項分享。其實我們當時已經做了不少自動化工作,能夠針對很多類型的代碼庫,自動插樁服務,自動生成TestMain()等方法,但隨着接入項目越來越多,以及後面使用場景的不斷複雜化,我們發現這套還是有其先天局限,會讓後面越來越難受:

  • 程序必須關閉才能收集覆蓋率。如果將這套系統僅定位在收集覆蓋率數據上,這個痛點倒也能忍受。但是如果想進一步做精準測試等方向,就很受局限。
  • 因為不想污染被測代碼庫,我們採取了自動化的方式,在編譯階段給每個服務生成類似main_test.go文件。但這種方式,其最難受的地方在於flag的處理,要知道go test命令本身會調用flag.Parse方法,所以這裏需要自動化的修改源碼,保證被測程序的flag定義,要先於go test調用flag.Parse之前。但是,隨着程序自己使用flag姿勢的複雜化,我們發現越來越難有通用方案來處理這些flag,有點難受。
  • 受限於go test -c命令的先天缺陷,它會給被測程序注入一些測試專屬的flag,比如-test.coverprofile, -test.timeout等等。這個是最難受的,因為它會破壞被測程序的啟動姿勢。我們知道系統測試面對是完整被測集群,如果你需要專門維護一套測試集群來做覆蓋率收集時,就會顯得非常浪費。好鋼就應該用在刀刃上,在七牛雲,我們倡導極客文化,追求用工程師思維解決重複問題。而作為業務效率部門,我們自己更應該走在前列。

也是因為以上的種種考量,我們內部一直在優化這一套系統,到今天這一版,我們已從架構和實現原理上完成了顛覆,能夠做到無損插樁,運行時分析覆蓋率,當屬非常優雅。

Goc – A Comprehensive Coverage Testing System for The Go Programming Language

一圖勝千言:

使用goc run .的姿勢直接運行被測程序,就能在運行時,通過goc profile命令方便的得到覆蓋率結果。是不是很神奇?是不是很優雅?

這個系統就是goc, 設計上希望完全兼容go命令行工具核心命令(go buld/install/run)。使用體驗上,也希望向go命令行工具靠攏:

以下是goc 1.0版本支持的功能:

系統測試覆蓋率收集方案

有了goc,我們再來看如何收集go語言系統測試覆蓋率。整體比較簡單,大體只需要三步:

  • 首先通過goc server命令部署一個服務註冊中心,它將會作為樞紐服務跟所有的被測服務通信。

  • 使用goc build --center="<server>" 命令編譯被測程序。goc不會破壞被測程序的啟動方式,所以你可以直接將編譯出的二進制發布到集成測試環境。

  • 環境部署好之後,就可以做執行任意的系統測試。而在測試期間,可以在任何時間,通過goc profile --center="<server>"拿到當前被測集群的覆蓋率結果。
    是不是很優雅?

goc 核心原理及未來

goc在設計上,拋棄老的go test -c -cover模式,而是直接與go tool cover工具交互,避免因go test命令引入的一系列弊端。goc同樣沒有選擇自己做插樁,也是考慮go語言的兼容性,以及性能問題,畢竟go tool cover工具,原生採用結構體來定義counter收集器,每個文件都有單獨的結構體,性能相對比較可靠。goc旨在做go語言領域綜合性的覆蓋率工具以及精準測試系統,其還有很長的路要走:

  • 基於PR的單測/集測/系統覆蓋率增量分析
  • 精準測試方向,有一定的產品化設計體驗,方便研發與測試日常使用
  • 擁抱各種CICD系統

當前goc已經開源了,歡迎感興趣的同學,前往代碼倉庫查看詳情並Star支持。當然,我們更歡迎有志之士,能夠參与貢獻,和我們一起構建這個有意思的系統。

最後,父親節快樂!

Contact me ?

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

【其他文章推薦】

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

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

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

※超省錢租車方案

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

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

C# 9.0 新特性之只讀屬性和記錄

閱讀本文大概需要 2 分鐘。

大家好,這是 C# 9.0 新特性系列的第 4 篇文章。

熟悉函數式編程的童鞋一定對“只讀”這個詞不陌生。為了保證代碼塊自身的“純潔”,函數式編程是不能隨便“弄髒”外來事物(參數、變量等)的,所以“只讀”對函數式編程非常重要。

為了豐富 C# 對函數式編程支持,較新的 C# 版本引入了一些很有用的新特性。比如 C# 8 中就對 struct 類型的方法增加了 readonly 修飾符支持,被 readonly 修飾的方法是不能修改該方法所在類的屬性的。舉個例子:

public struct FooValue
{
    private int A { get; set; }
    public readonly int IncreaseA()
    {
        A = A + 1; // 報錯
        return A;
    }
}

而 C# 9 又進一步增加了對“只讀”的支持,此次增加了 init-only 屬性和 record 相關特性,下面一一介紹。

Init-only 屬性

我們知道類的屬性有 set 和 get 兩種訪問器,現在 C# 9 增加一種屬性訪問器:init。init 是 set 訪問器的變體,它的作用是使屬性只能在對象初始化的時候對其賦值,之後該屬性就是只讀的,因此叫 init-only 屬性。使用方式如下:

public class Foo
{
    public string PropA { get; init; }
    public string PropB { get; init; }
}

賦值操作:

var foo = new Foo {  PropA = "A", PropB = "B" };
foo.PropA = "AA"; // 報錯,PropA 此時是只讀的!

由於 init 是在初始化階段賦值,所以它可以在類內部修改 readonly 修飾的字段。比如:

public class Foo
{
    private readonly string propA;
    private readonly string propB;

    public string PropA
    {
        get => propA;
        init => propA = (value ?? throw new ArgumentNullException(nameof(propA)));
    }
    public string PropA
    {
        get => propB;
        init => propB = (value ?? throw new ArgumentNullException(nameof(propB)));
    }
}

如果你知道在構造函數中可以對只讀字段/屬性賦值就自然也理解這一點。

記錄 (Record)

做過財務系統的人都知道交易記錄一旦入賬是不能修改的,如果錄入錯誤,就要新錄入一筆負的記錄把之前的紅衝掉,再錄入正確的記錄。應對類似這種只讀記錄的場景,C# 9 引入了 Record(記錄,下文均使用中文的“記錄”)的概念,它用來支持整個對象的只讀特性(即實例化後為只讀)。使用方式如下:

public data class Foo
{
    public string PropA { get; init; }
    public string PropB { get; init; }
}

這裏用了一個 data 關鍵字,表示該類的對象只是純粹的記錄值,它不是可修改的狀態(在函數式編程中,所有的數據修改都是狀態在發生變化)。

上面的太麻煩了,可以這樣簡寫:

public data class Foo
{
    string PropA;
    string PropB;
}

默認屬性都是 public 的,如果實在要改為 private,可以在屬性定義前面加上 private 修飾符。

定位記錄 (Positional Record)

有時候為了初始化更方便,可以定義構造函數來給屬性賦值,初始化時只需要把屬性值按順序傳給構造函數即可,這個操作稱為定位構造(Positional Construction)。同樣,也可以使用解構函數(Deconstructor)來實現屬性的解構,即按照解構函數的參數順序從對象中提取屬性的值,被稱為定位解構(Positional Deconstructor)。實現了定位構造或定位解構的記錄稱為定位記錄(Positional Record)。下面是一個定位記錄的實現:

public data class Foo
{
    string PropA;
    string PropB;
    public Foo(string propA, string propB)
      => (PropA, PropB) = (propA, propB);
    public void Deconstruct(out string propA, out string propB)
      =>  (propA, propB) = (PropA, PropB);
}

這個寫法太麻煩了,可以直接簡寫為:

public data class Foo(string PropA, string PropB);

這樣簡短一句代碼,其內部默認實現了 init-only 自動屬性,且同時為所有屬性定義了構造函數和解構函數。

使用示例:

var foo = new Foo("AA", "BB");  // 構造定位
var (a, b) = foo;               // 解構定位

可以想象,記錄的大部分使用場景,以上簡寫的寫法能滿足需求。若有特殊場景,就不能簡單,需要進行自定義修改其默認行為。

with 表達式

當處理不可變數據時,若要生成不同的狀態,一個常見的場景是在一條舊記錄基礎上拷貝一條新的記錄。比如我們要修改 Foo 對象的 PropA 屬性,我們就要拷貝該對象生成一個新的對象。這個操作在函數式編程中被稱為“非破壞性修改 (non-destructive mutation)”。為了支持記錄這個操作,C# 9 引入了 with 表達式,它可以很方便在一條原有記錄基礎上創建一條新記錄。示例:

var other = foo with { PropA = "AA" };

with 表達式內部其實是通過一個默認的 protected 構造函數來實現的,大致如下:

protected Foo(Foo original)
{
    // 拷貝 original 的所有字段
}

如果默認實現的字段拷貝不符合你的需求,你也可以手動實現這個構造函數。

今天就分享到這裏,敬請期待一下篇關於 C# 9 新特性的文章!

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

【其他文章推薦】

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

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

※回頭車貨運收費標準

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

※超省錢租車方案

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

十萬同時在線用戶,需要多少內存?——Newbe.Claptrap 框架水平擴展實驗

Newbe.Claptrap 項目是筆者正在構建以反應式、Actor模式和事件溯源為理論基礎的一套服務端開發框架。本篇我們將來了解一下框架在水平擴展方面的能力。

前情提要

時隔許久,今日我們再次見面。首先介紹一下過往的項目情況:

第一次接觸本框架的讀者,可以先點擊此處閱讀本框架相關的基礎理論和工作原理。

日前,我們也編寫了一些預熱文章和工具,讀者可以通過以下鏈接進行了解:

  • 談反應式編程在服務端中的應用,數據庫操作優化,從 20 秒到 0.5 秒
  • docker-mcr 助您全速下載 dotnet 鏡像
  • Newbe.Claptrap 項目周報 1 – 還沒輪影,先用輪跑

今日主題

今天,我們來做一套實驗預演,來驗證 Newbe.Claptrap 框架,如何通過水平擴展的形式來適應逐漸增長的同時在線用戶數。

由於此次實驗涉及的內容很多,因此筆者將內容進行了歸類,讀者可以按照自己的興趣閱讀相關的章節:

  • 業務需求說明
  • 調用時序關係
  • 物理結構設計
  • 實際測試數據
  • 源碼構建說明
  • 常見問題解答

業務需求說明

先看看今天要實現的業務場景:

  • 用戶通過 API 登錄後生成一個 JWT token
  • 用戶調用 API 時驗證 JWT token 的有效性
  • 沒有使用常規的 JWS 公私鑰方式進行 JWT token 頒發,而是為每個用戶單獨使用 secret 進行哈希驗證
  • 驗證看不同的在線用戶需要消耗的內存情況
  • 用戶登錄到生成 token 所消耗時間不得超過 200 ms
  • tokn 的驗證耗時不得超過 10 ms

吹牛先打草稿

筆者沒有搜索到於 “在線用戶數” 直接相關的理論定義,因此,為了避免各位的理解存在差異。筆者先按照自己的理解來點明:在線用戶數到底意味着什麼樣的技術要求?

未在線用戶若上線,不應該受到已在線用戶數的影響

如果一個用戶登錄上線需要消耗 100 ms。那麼不論當前在線的用戶數是十人還是百萬人。這個登錄上線所消耗的時間都不會明顯的超過 100 ms。

當然,有限的物理硬件肯定會使得,當在線用戶數超過一個閾值(例如兩百萬)時,新用戶登錄上線會變慢甚至出錯。

但是,增加物理機器就能提高這個閾值,我們就可以認為水平擴展設計是成功的。

對於任意一個已在線用戶,得到的系統性能反饋應當相同

例如已在線的用戶查詢自己的訂單詳情,需要消耗 100 ms。那麼當前任何一個用戶進行訂單查詢的平均消耗都應該穩定在 100 ms。

當然,這裏需要排除類似於 “搶購” 這種高集中性能問題。此處主要還是討論日常穩定的容量增加。(我們以後會另外討論 “搶購” 這種問題)

具體一點可以這樣理解。假設我們做的是一個雲筆記產品。

那麼,如果增加物理機器就能增加同時使用雲筆記產品的用戶數,而且不犧牲任何一個用戶的性能體驗,我們就認為水平擴展設計是成功的。

在此次的實驗中,若用戶已經登錄,則驗證 JWT 有效性的時長大約為 0.5 ms。

調用時序關係

簡要說明:

  1. 客戶端發起登錄請求將會逐層傳達到 UserGrain 中
  2. UserGrain 將會在內部激活一個 Claptrap 來進行維持 UserGrain 中的狀態數據。包括用戶名、密碼和用於 JWT 簽名的 Secret。
  3. 隨後的生成 JWT 生成和驗證都將直接使用 UserGrain 中的數據。由於 UserGrain 中的數據是在一段時間內是 “緩存” 在內存中的。所以之後的 JWT 生成和驗證將非常快速。實測約為 0.5 ms。

物理結構設計

如上圖所示,便是此次進行測試的物理組件:

名稱 說明
WebAPI 公開給外部調用 WebAPI 接口。提供登錄和驗證 token 的接口。
Orleans Cluster 託管 Grain 的核心進程.
Orleans Gateway 於 Orleans Cluster 基本相同,但是 WebAPI 只能與 Gateway 進行通信
Orleans Dashboard 於 Orleans Gateway 基本相同,但增加了 Dashboard 的展示,以查看整個 Orleans 集群的情況
Consul 用於 Orleans 集群的集群發現和維護
Claptrap DB 用於保存 Newbe.Claptrap 框架的事件和狀態數據
Influx DB & Grafana 用於監控 Newbe.Claptrap 相關的性能指標數據

此次實驗的 Orleans 集群節點的數量實際上是 Cluster + Gateway + Dashboard 的總數。以上的劃分實際上是由於功能設定的不同而進行的區分。

此次測試 “水平擴展” 特性的物理節點主要是 Orleans Cluster 和 Orleans Gateway 兩個部分。將會分別測試以下這些情況的內存使用情況。

Orleans Dashboard Orleans Gateway Orleans Cluster
1 0 0
1 1 1
1 3 5

此次實驗採用的是 Windows Docker Desktop 結合 WSL 2 進行的部署測試。

以上的物理結構實際上是按照最為此次實驗最為複雜的情況設計的。實際上,如果業務場景足夠簡單,該物理結構可以進行裁剪。詳細可以查看下文 “常見問題解答” 中的說明。

實際測試數據

以下,分別對不同的集群規模和用戶數量進行測試

0 Gateway 0 Cluster

默認情況下,剛剛啟動 Dashboard 節點時,通過 portainer 可以查看 container 佔用的內存約為 200 MB 左右,如下圖所示:

通過測試控制台,向 WebAPI 發出 30,000 次請求。每批 100 個請求,分批發送。

經過約兩分鐘的等待后,再次查看內存情況,約為 9.2 GB,如下圖所示:

因此,我們簡單的估算每個在線用戶需要消耗的內存情況約為 (9.2*1024-200)/30000 = 0.3 MB。

另外,可以查看一些輔助數據:

CPU 使用情況

網絡吞吐量

Orleans Dashboard 情況。左上角的 TOTAL ACTIVATIONS 中 30,000 即表示當前內存中存在的 UserGrain 數量,另外的 3 個為 Dashboard 使用的 Grain。

Grafana 中查看 Newbe.Claptrap 的事件平均處理時長約為 100-600 ms。此次測試的主要是內存情況,處理時長的採集時間為 30s 一次,因此樣本數並不多。關於處理時長我們將在後續的文章中進行詳細測試。

Grafana 中查看 Newbe.Claptrap 的事件的保存花費的平均時長約為 50-200 ms。事件的保存時長是事件處理的主要部分。

Grafana 中查看 Newbe.Claptrap 的事件已處理總數。一種登錄了三萬次,因此事件總數也是三萬。

1 Gateway 1 Cluster

接下來,我們測試額外增加兩個節點進行測試。

還是再提一下,Orleans 集群節點的數量實際上是 Cluster + Gateway + Dashboard 的總數。因此,對比上一個測試,該測試的節點數為 3。

測試得到的內存使用情況如下:

用戶數 節點平均內存 內存總佔用
10000 1.8 GB 1.8*3 = 5.4 GB
20000 3.3 GB 3.3*3 = 9.9 GB
30000 4.9 GB 4.9*3 = 14.7 GB

那麼,以三萬用戶為例,平均每個用戶佔用的內存約為 (14.7*1024-200*3)/30000 = 0.48 MB

為什麼節點數增加了,平均消耗內存上升了呢?筆者推測,沒有進行過驗證:節點增加,實際上節點之間的通訊還需要消耗額外的內存,因此平均來說有所增加。

3 Gateway 5 Cluster

我們再次增加節點。總結點數為 1 (dashboard) + 3 (cluster) + 5 (gateway) = 9 節點

測試得到的內存使用情況如下:

用戶數 節點平均內存 內存總佔用
20000 1.6 GB 3.3*9 = 14.4 GB
30000 2 GB 4.9*9 = 18 GB

那麼,以三萬用戶為例,平均每個用戶佔用的內存約為 (18*1024-200*9)/30000 = 0.55 MB

十萬用戶究竟要多少內存?

以上所有的測試都是以三萬為用戶數進行的測試,這是一個特殊的数字。因為繼續增加用戶數的話,內存將會超出測試機的內存余量。(求贊助兩條 16G)

如果繼續增加用戶數,將會開始使用操作系統的虛擬內存。雖然可以運行,但是運行效率會降低。原來登錄可能只需要 100 ms。使用到虛擬內存的用戶則需要 2 s。

因此,速度降低的情況下,在驗證需要多少內存意義可能不大。

但是,這不意味着不能夠繼續登錄,以下便是 1+1+1 的情況下,十萬用戶全部登錄后的情況。(有十萬用戶同時在線,加點內存吧,不差錢了。)

源碼構建說明

此次測試的代碼均可以在文末的樣例代碼庫中找到。為了方便讀者自行實驗,主要採用的是 docker-compose 進行構建和部署。

因此對於測試機的唯一環境需求就是要正確的安裝好 Docker Desktop 。

可以從以下任一地址獲取最新的樣例代碼:

  • https://github.com/newbe36524/Newbe.Claptrap.Examples
  • https://gitee.com/yks/Newbe.Claptrap.Examples

快速啟動

使用控制台進入 src/Newbe.Claptrap.Auth/LocalCluster 文件夾。運行以下命令便可以在本地啟動所有的組件:

1
docker-compose up -d

途中需要拉取一些託管於 Dockerhub 上的公共鏡像,請確保本地已經正確配置了相關的加速器,以便您可以快速構建。可以參看這篇文檔進行設置

成功啟動之後可以通過 docker ps 查看到所有的組件。

1
2
3
4
5
6
7
8
9
10
11
12
13
PS>docker ps
CONTAINER ID        IMAGE                                                                            COMMAND                  CREATED             STATUS              PORTS                                                                                                                              NAMES
66470e5393e2        registry.cn-hangzhou.aliyuncs.com/newbe36524/newbe-claptrap-auth-webapi          "dotnet Newbe.Claptr…"   4 hours ago         Up About an hour    0.0.0.0:10080->80/tcp                                                                                                              localcluster_webapi_1
3bbaf5538ab9        registry.cn-hangzhou.aliyuncs.com/newbe36524/newbe-claptrap-auth-backendserver   "dotnet Newbe.Claptr…"   4 hours ago         Up About an hour    80/tcp, 443/tcp, 0.0.0.0:19000->9000/tcp, 0.0.0.0:32785->11111/tcp, 0.0.0.0:32784->30000/tcp                                       localcluster_dashboard_1
3f60f51e4641        registry.cn-hangzhou.aliyuncs.com/newbe36524/newbe-claptrap-auth-backendserver   "dotnet Newbe.Claptr…"   4 hours ago         Up About an hour    80/tcp, 443/tcp, 9000/tcp, 0.0.0.0:32787->11111/tcp, 0.0.0.0:32786->30000/tcp                                                      localcluster_cluster_gateway_1
7d516ada2b26        registry.cn-hangzhou.aliyuncs.com/newbe36524/newbe-claptrap-auth-backendserver   "dotnet Newbe.Claptr…"   4 hours ago         Up About an hour    80/tcp, 443/tcp, 9000/tcp, 30000/tcp, 0.0.0.0:32788->11111/tcp                                                                     localcluster_cluster_core_1
fc89fcd973f9        grafana/grafana                                                                  "/run.sh"                4 hours ago         Up 6 seconds        0.0.0.0:23000->3000/tcp                                                                                                            localcluster_grafana_1
1f10ed0eb25f        postgres                                                                         "docker-entrypoint.s…"   4 hours ago         Up About an hour    0.0.0.0:32772->5432/tcp                                                                                                            localcluster_claptrap_db_1
d5d2bec74311        adminer                                                                          "entrypoint.sh docke…"   4 hours ago         Up About an hour    0.0.0.0:58080->8080/tcp                                                                                                            localcluster_adminer_1
4c4be69f2f41        bitnami/consul                                                                   "/opt/bitnami/script…"   4 hours ago         Up About an hour    8300-8301/tcp, 8500/tcp, 8301/udp, 8600/tcp, 8600/udp                                                                              localcluster_consulnode3_1
88811d3aa0d2        influxdb                                                                         "/entrypoint.sh infl…"   4 hours ago         Up 6 seconds        0.0.0.0:29086->8086/tcp                                                                                                            localcluster_influxdb_1
d31c73b62a47        bitnami/consul                                                                   "/opt/bitnami/script…"   4 hours ago         Up About an hour    8300-8301/tcp, 8500/tcp, 8301/udp, 8600/tcp, 8600/udp                                                                              localcluster_consulnode2_1
72d4273eba2c        bitnami/consul                                                                   "/opt/bitnami/script…"   4 hours ago         Up About an hour    0.0.0.0:8300-8301->8300-8301/tcp, 0.0.0.0:8500->8500/tcp, 0.0.0.0:8301->8301/udp, 0.0.0.0:8600->8600/tcp, 0.0.0.0:8600->8600/udp   localcluster_consulnode1_1

啟動完成之後,便可以通過以下鏈接來查看相關的界面

地址 說明
http://localhost:19000 Orleans Dashboard 查看 Orleans 集群中各節點的狀態
http://localhost:10080 Web API 基地址,此次使用所測試的 API 基地址
http://localhost:23000 Grafana 地址,查看 Newbe.Claptrap 相關的性能指標情況

源碼構建

使用控制台進入 src/Newbe.Claptrap.Auth 文件夾。運行以下命令便可以在本地完成代碼的構建:

1
2
./LocalCluster/pullimage.cmd
docker-compose build

pullimage.cmd 使用了筆者編寫的 docker-mcr 加速器功能。您可以通過該文檔來了解其工作原理

等待構建完畢之後,本地便生成好了相關的鏡像。接下來便可以初次嘗試在本地啟動應用:

使用控制台進入 src/Newbe.Claptrap.Auth/LocalCluster 文件夾。運行以下命令便可以啟動相關的容器:

1
docker-compose up -d

常見問題解答

文中為何沒有說明代碼和配置的細節?

本文主要為讀者展示該方案的實驗可行性,具體應該如何應用 Newbe.Claptrap 框架編寫代碼,並非本文的主旨,因此沒有提及。

當然,另外一點就是目前框架沒有最終定版,所有內容都有可能發生變化,講解代碼細節意義不大。

但可以提前說明的是:編寫非常簡單,由於本樣例的業務需求非常簡單,因此代碼內容也不多。全部都可以在示例倉庫中找到。

用 Redis 存儲 Token 也可以實現上面的需求,為什麼要選擇這個框架?

目前來說,筆者沒有十足的理由說服讀者必須使用哪種方案,此處也只是提供一種可行方案,至於實際應該選擇哪種方案,應該有讀者自己來考量,畢竟工具是否趁手還是需要試試才知道。

如果是最多 100 個在線用戶,那怎麼裁剪系統?

必要的組件只有 Orleans Dashboard 、 WebAPI 和 Claptrap Db。其他的組件全部都是非必要的。而且如果修改代碼, Orleans Dashboard 和 WebAPI 是可以合併的。

所以最小規模就是一個進程加一個數據庫。

Grafana 為什麼沒有報表?

Grafana 首次啟動之後需要手動的創建 DataSource 和導入 Dashboard.

本實驗相關的參數如下:

DataSource

  • URL: http://influxdb:8086
  • Database: metricsdatabase
  • User: claptrap
  • Password: claptrap

點擊此處獲取 Dashboard 定義文件

測試機的物理配置是什麼?

沒有專門騰內存,未開始測試前已佔用 16GB 內存。以下是測試機的身材數據(洋垃圾,3500 元左右):

處理器 英特爾 Xeon (至強) E5-2678 v3 @ 2.50GHz 12 核 24 線程
主板 HUANANZHI X99-AD3 GAMING (Wellsburg)
顯卡 Nvidia GeForce GTX 750 Ti (2 GB / Nvidia)
內存 32 GB (三星 DDR3L 1600MHz) 2013 年產 高齡內存
主硬盤 金士頓 SA400S37240G (240 GB / 固態硬盤)

如果您有更好的物理配置,相信可以得出更加優秀的數據。

即使是 0.3 MB 平均每用戶的佔用的我也覺得太高了

框架還在優化。未來會更好。

最後但是最重要!

最近作者正在構建以反應式、Actor模式和事件溯源為理論基礎的一套服務端開發框架。希望為開發者提供能夠便於開發出 “分佈式”、“可水平擴展”、“可測試性高” 的應用系統 ——Newbe.Claptrap

本篇文章是該框架的一篇技術選文,屬於技術構成的一部分。如果讀者對該內容感興趣,歡迎轉發、評論、收藏文章以及項目。您的支持是促進項目成功的關鍵。

GitHub 項目地址:https://github.com/newbe36524/Newbe.Claptrap

Gitee 項目地址:https://gitee.com/yks/Newbe.Claptrap

如果你對該項目感興趣,你可以通過 github issues 提交您的看法。

如果您無法正常訪問 github issue,您也可以發送郵件到 newbe-claptrap@googlegroups.com 來參与我們的討論。

點擊鏈接 QQ 交流【Newbe.Claptrap】:https://jq.qq.com/?_wv=1027&k=5uJGXf5。

​​​​​​​

    • 本文作者: newbe36524
    • 本文鏈接: https://www.newbe.pro/Newbe.Claptrap/How-Many-RAMs-In-Used-While-There-Are-One-Hundred-Thousand-Users-Online/
    • 版權聲明: 本博客所有文章除特別聲明外,均採用 BY-NC-SA 許可協議。轉載請註明出處!

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

【其他文章推薦】

※超省錢租車方案

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

※回頭車貨運收費標準

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

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

HTML躬行記(1)——SVG

  <svg>是矢量圖的根元素,通過xmlns屬性聲明命名空間,從而告訴用戶代理標記名稱屬於哪個XML方言。在下面的示例中,為<svg>元素聲明了寬度和高度(默認以像素為單位),其子元素<title>可作為提示,在<desc>中可聲明一段描述性純文本,這兩個元素都不會在頁面中呈現。而<rect>是一個矩形,將被繪製到頁面中。

<svg xmlns="http://www.w3.org/2000/svg" width="150" height="100">
  <title>SVG example</title>
  <desc>SVG contains a rectangle</desc>
  <rect width="50" height="50" fill="#F60" />
</svg>

  SVG作為一種圖像格式,可以包含在<img>元素內(如下所示)或CSS樣式中(例如background-image屬性)。

<img src="demo.svg" />

一、坐標系統

1)視口

  在SVG中,有一張無限大的畫布,而畫布區域被稱為視口(viewport)。通過<svg>元素的width和height兩個屬性可定義視口的尺寸,視口的原點在其左上角。

  而在視口中,還包含一個坐標系統,由viewBox屬性控制。它能包含四個數值(以逗號或空格分隔),分別是用戶坐標系統的最小橫坐標(x軸)和縱坐標(y軸),以及寬和高。

  下面的兩個<svg>元素,第一個採用了默認的坐標系統,第二個聲明了新的坐標系統,並且寬高比相同,如圖1所示,第二個矩形縮小了。

<svg xmlns="http://www.w3.org/2000/svg" width="150" height="100">
  <rect width="50" height="50" fill="#F60" />
</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="150" height="100" viewBox="0 0 300 200">
  <rect width="50" height="50" fill="#F60" />
</svg>

圖 1

  接下來修改兩個最小坐標(如圖2所示),第一個<svg>元素聲明的最小坐標為(20,20),雖然矩形的坐標是(0,0),但是比最小坐標要小,所以就會往左上角偏移;第二個<svg>元素聲明了負數坐標,與前一個正好相反;在第三個<svg>元素中,修改了矩形的坐標,正好在左上角。

<svg xmlns="http://www.w3.org/2000/svg" width="150" height="100" viewBox="20 20 300 200">
  <rect width="50" height="50" fill="#F60" />
</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="150" height="100" viewBox="-20 -20 300 200">
  <rect width="50" height="50" fill="#F60" />
</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="150" height="100" viewBox="20 20 300 200">
  <rect width="50" height="50" fill="#F60" x="20" y="20" />
</svg>

圖 2

2)preserveAspectRatio屬性

  當viewBox屬性中聲明的尺寸與視圖的寬高比不一致時,可以使用preserveAspectRatio屬性指定圖形的縮放比例和對齊方式,語法如下,默認值是“xMidYMid meet”。

preserveAspectRatio: <align> [<meetOrSlice>]

  其中<align>的屬性值由兩個軸(x和y)以及三個位置(min、mid和max)組合而成,如表1所列。

表 1

值 說明
xMin viewport與viewBox的左側對齊
xMid viewport與viewBox的x軸中點對齊
xMax viewport與viewBox的右側對齊
YMin viewport與viewBox的頂部對齊
YMid viewport與viewBox的y軸中點對齊
YMax viewport與viewBox的底部對齊

  在下面的示例中,由於三個<svg>元素寬高比是3:2,而viewBox的寬高比是3:1,因此矩形會等比縮小。對它們分別應用xMinYMin、xMidYMid和xMinYMax,效果如圖3所示。

<svg xmlns="http://www.w3.org/2000/svg" width="150" height="100" viewBox="0 0 300 100" preserveAspectRatio="xMinYMin">
  <rect width="50" height="50" fill="#F60" />
</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="150" height="100" viewBox="0 0 300 100" preserveAspectRatio="xMidYMid">
  <rect width="50" height="50" fill="#F60" />
</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="150" height="100" viewBox="0 0 300 100" preserveAspectRatio="xMinYMax">
  <rect width="50" height="50" fill="#F60" />
</svg>

圖 3

  注意,因為寬度正好適配,所以對於x軸的對齊方式,效果都是相同的。

  <meetOrSlice>可控製圖形的適配或裁剪,可選的值如表2所列。

表 2

值 說明
meet 保留圖形的寬高比,並且縮放viewBox以適應viewport
slice 保留圖形的寬高比,並且放大viewBox以覆蓋viewport

  在下面的示例中,兩個矩形的高度比視口要大,對它們分別應用meet和slice,效果如圖4所示,第二個矩形將整個視口給填滿了。

<svg xmlns="http://www.w3.org/2000/svg" width="150" height="100" viewBox="0 0 300 100" preserveAspectRatio="xMinYMin meet">
  <rect width="150" height="150" fill="#F60" />
</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="150" height="100" viewBox="0 0 300 100" preserveAspectRatio="xMinYMin slice">
  <rect width="150" height="150" fill="#F60" />
</svg>

圖 4

二、形狀

  SVG的基本形狀包括線段(line)、矩形(rect)、圓形(circle)、橢圓(ellipse)、多邊形(polygon)和折線(polyline)。

1)<line>

  線段元素需要指定起止點的坐標,如下所示,效果如圖5所示。

<svg xmlns="http://www.w3.org/2000/svg" width="150" height="100" viewBox="0 0 300 200">
  <line x1="20" y1="100"  x2="100" y2="20" stroke="black" stroke-width="2"/>
</svg>

圖 5

  其中stroke和stroke-width是筆畫屬性,可指定筆畫的顏色和寬度,相關屬性如表3所列,部分屬性的效果如圖6所示。

表 3

值 說明
stroke 筆畫顏色
stroke-dasharray 筆畫的外觀(實線、點線或虛線),由一系列逗號分隔的数字組成,表示長度和空隙長度
stroke-dashoffset 相對繪圖起點的偏移值
stroke-linecap 描邊的展現形狀
stroke-linejoin 路徑轉角處的形狀
stroke-miterlimit 斜接長度與線寬的最大比例
stroke-opacity 筆畫不透明度,取值範圍0~1,其中0表示透明
stroke-width 筆畫寬度

圖 6

2)<rect>

  除了直角矩形之外,還可以聲明圓角矩形,如下所示,效果如圖7所示。

<svg xmlns="http://www.w3.org/2000/svg" width="150" height="100" viewBox="0 0 300 200">
  <rect x="10" y="10" width="100" height="100" rx="15" ry="15" fill="#F60"/>
</svg>

圖 7

3)<circle>和<ellipse>

  利用圓形,可非常便捷的畫出圓環,效果如圖8所示。將<circle>元素的stroke-dasharray聲明為圓的周長(2πR),例如半徑為50,周長就是314。如果為stroke-dashoffset屬性定義一個值,就能得到圓環缺失一段的效果,從而就能模擬出圓環型的進度條了。

<svg xmlns="http://www.w3.org/2000/svg" width="150" height="100" viewBox="0 0 300 200">
  <circle cx="100" cy="100" r="50" stroke="black" stroke-width="2" 
    stroke-dasharray="314" stroke-dashoffset="40" fill="transparent" />
</svg>

圖 8

  橢圓和圓形類似,只是需要聲明兩個方向的半徑,如圖9所示。

<svg xmlns="http://www.w3.org/2000/svg" width="150" height="100" viewBox="0 0 300 200">
  <ellipse cx="100" cy="100" rx="100" ry="50" />
</svg>

圖 9

4)<polygon>

  <polygon>可畫出任意形狀的封閉圖形,例如平行四邊形、梯形等。在points屬性中,可聲明各個點的坐標,每組坐標用逗號隔開,下面繪製了一個五角星,如圖10所示。

<svg xmlns="http://www.w3.org/2000/svg" width="150" height="100" viewBox="0 0 300 200">
  <polygon points="100,0 160,180 10,60 190,60 40,180" fill="#F60"/>
</svg>

圖 10

5)<polyline>

  折線不需要閉合,與<polygon>元素類似,也是由points屬性繪製,如下所示,得到的折線如圖11所示。

<svg xmlns="http://www.w3.org/2000/svg" width="150" height="100" viewBox="0 0 300 200">
  <polyline points="20,100 40,60 70,80 100,20" fill="none" stroke="black" />
</svg>

圖 11

6)<path>

  上述基本形狀都可以由<path>元素來繪製,並且通過<path>元素還可繪製出不規則的圖形,例如心形,如下所示。

<svg xmlns="http://www.w3.org/2000/svg" width="150" height="100" viewBox="0 0 300 200">
  <path d="M 10,30
           A 20,20 0,0,1 50,30
           A 20,20 0,0,1 90,30
           Q 90,60 50,90
           Q 10,60 10,30 z" />
</svg>

  其中d屬性可聲明一系列路徑,包含多個指令,如表4所列。

表 4

指令 效果
M、m 移動到指定的坐標
L、l 繪製一條直線
H、h 繪製一條水平線
V、v 繪製一條垂直線
Z、z 關閉路徑
Q、q 繪製一條二次貝塞爾曲線
T、t 繪製一條平滑的二次貝塞爾曲線
C、c 繪製一條三次貝塞爾曲線
S、s 繪製一條平滑的三次貝塞爾曲線
A、a 繪製弧形曲線

三、文檔結構

1)內部樣式

  SVG允許在其內部嵌入<style>元素,如下所示,其中CDATA區段中的文本會被解析器忽略,但不影響渲染。

<svg xmlns="http://www.w3.org/2000/svg" width="150" height="100" viewBox="0 0 300 200">
  <style>
    <![CDATA[
    line {
      stroke: black;
      stroke-width: 2;
    }
    ]]>
  </style>
  <line x1="20" y1="100" x2="100" y2="20" />
</svg>

2)<g>

  <g>元素相當於一個容器,能將其所有的子元素組合在一起。應用於<g>元素中的屬性會被其子元素所繼承,如下所示,兩個圓的筆畫顏色都是綠色,而寬度都是10,如圖12所示。

<svg xmlns="http://www.w3.org/2000/svg" width="150" height="100" viewBox="0 0 300 200">
  <g id="ring" fill="white" stroke="green" stroke-width="10">
    <circle cx="80" cy="80" r="50" />
    <circle cx="120" cy="120" r="50" />
  </g>
</svg>

圖 12

3)<use>

  <use>元素可引用其它圖形,以及<g>元素,類似於複製黏貼的功能。在下面的示例中,通過<use>元素的xlink:href屬性引用了id為ring的<g>元素,並且將<use>元素上聲明的屬性傳給了<circle>元素。

<svg xmlns="http://www.w3.org/2000/svg" width="150" height="100" viewBox="0 0 300 200">
  <g id="ring">
    <circle cx="80" cy="80" r="50" />
    <circle cx="120" cy="120" r="50" />
  </g>
  <use x="100" y="0" fill="white" stroke="green" stroke-width="10" xlink:href="#ring" />
</svg>

  注意,當給<use>元素定義坐標后,<circle>元素會與其相加計算出最終的坐標,如圖13所示。

圖 13

4)<defs>

  可將需要復用的圖形抽象到<defs>元素中,在其內部定義的圖形不會直接呈現到頁面中。在上面那個示例的基礎上,將<g>元素整個放置到<defs>中,效果如圖14所示,沒有渲染<g>元素中的圓。

<svg xmlns="http://www.w3.org/2000/svg" width="150" height="100" viewBox="0 0 300 200">
  <defs>
    <g id="ring">
      <circle cx="80" cy="80" r="50" />
      <circle cx="120" cy="120" r="50" />
    </g>
  </defs>
  <use x="100" y="0" fill="white" stroke="green" stroke-width="10" xlink:href="#ring" />
</svg>

圖 14

5)<symbol>

  <symbol>提供了另一種組合圖形的方式,但與<g>元素不同,它的圖形不會呈現,並且它可以聲明viewBox和preserveAspectRatio兩個屬性,如下所示。

<svg xmlns="http://www.w3.org/2000/svg" width="150" height="100">
  <symbol id="ring" viewBox="0 0 300 200">
    <circle cx="80" cy="80" r="50" />
    <circle cx="120" cy="120" r="50" />
  </symbol>
  <use fill="white" stroke="green" stroke-width="10" xlink:href="#ring" />
</svg>

6)<image>

  <use>元素可以引用SVG文件的某個部分,而<image>元素可以引用整個SVG文件或柵格圖像(如下所示),並且能控制引用文件的尺寸和preserveAspectRatio屬性,效果如圖15所示。

<svg xmlns="http://www.w3.org/2000/svg" width="150" height="100" viewBox="0 0 300 200">
  <image xlink:href="img/avatar.png" width="150" />
</svg>

圖 15

四、變形、圖案和漸變

1)變形

  transform屬性定義了一系列圖形元素變形的規則,包括位移(translate)、縮放(scale)、旋轉(rotate)和傾斜(skew)。注意,與CSS3中的transform屬性不同,SVG中的transform屬性作用的對象是坐標系統,而不是元素本身。

  translate()函數會接收兩個參數,沿x軸和y軸位移坐標系統,如圖16所示。

<svg xmlns="http://www.w3.org/2000/svg" width="150" height="100" viewBox="0 0 300 200">
  <rect width="50" height="50" fill="#F60" />
</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="150" height="100" viewBox="0 0 300 200">
  <rect width="50" height="50" fill="#F60" transform="translate(50, 30)" />
</svg>

圖 16

  scale()函數也會接收兩個參數(這點與CSS3中的scale()不同),沿x軸和y軸縮放坐標系統,如圖17所示。

<svg xmlns="http://www.w3.org/2000/svg" width="150" height="100" viewBox="0 0 300 200">
  <rect width="50" height="50" fill="#F60" />
</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="150" height="100" viewBox="0 0 300 200">
  <rect width="50" height="50" fill="#F60" transform="scale(2, 3)" />
</svg>

圖 17

  rotate()函數可接收三個參數,第一個是旋轉角度,另外兩個是原點坐標,也就是坐標系統按照該原點旋轉,如圖18所示,第三個矩形指定了原點。

<svg xmlns="http://www.w3.org/2000/svg" width="150" height="100" viewBox="0 0 300 200">
  <rect width="50" height="50" fill="#F60" />
</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="150" height="100" viewBox="0 0 300 200">
  <rect width="50" height="50" fill="#F60" transform="rotate(30)" />
</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="150" height="100" viewBox="0 0 300 200">
  <rect width="50" height="50" fill="#F60" transform="rotate(30, 50, 30)" />
</svg>

圖 18

  傾斜分為兩個函數:skewX()和skewY(),分別會沿着x軸和y軸傾斜,如圖19所示。

<svg xmlns="http://www.w3.org/2000/svg" width="150" height="100" viewBox="0 0 300 200">
  <rect width="50" height="50" fill="#F60" />
</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="150" height="100" viewBox="0 0 300 200">
  <rect width="50" height="50" fill="#F60" transform="skewX(30)" />
</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="150" height="100" viewBox="0 0 300 200">
  <rect width="50" height="50" fill="#F60" transform="skewY(30)" />
</svg>

圖 19

2)圖案

  要創建一個圖案,就得使用<pattern>元素包裹圖形元素,再利用patternUnits屬性確定平鋪圖案的方式。它有兩個關鍵字可選,默認的objectBoundingBox會讓<pattern>元素的尺寸以百分數或0~1之間的小數表示,參照對象分別是引用<pattern>的圖形元素的寬和高。

  以下面的圖案為例,它的寬和高都是10%,參照的圓形的寬高都是200,計算出的實際值就都是20,效果如圖20所示。

<svg xmlns="http://www.w3.org/2000/svg" width="250" height="200">
  <defs>
    <pattern id="star1" width="10%" height="10%" patternUnits="objectBoundingBox">
      <circle cx="10" cy="10" r="10" />
    </pattern>
  </defs>
  <circle cx="100" cy="100" r="100" fill="url(#star1)" />
</svg>

圖 20

  另一個userSpaceOnUse會讓<pattern>元素的尺寸以絕對值表示,如下所示,因為<pattern>元素的寬高都為25,所以圖案會有空白間隙,得到的效果如圖21所示。

<svg xmlns="http://www.w3.org/2000/svg" width="250" height="200">
  <defs>
    <pattern id="star2" width="25" height="25" patternUnits="userSpaceOnUse">
      <circle cx="10" cy="10" r="10" />
    </pattern>
  </defs>
  <circle cx="100" cy="100" r="100" fill="url(#star2)" />
</svg>

圖 21

  <pattern>元素還包含另一個patternContentUnits屬性,用於處理圖案內部的排列方式,它的兩個關鍵字也是objectBoundingBox和userSpaceOnUse,後者是該屬性的默認值。

  objectBoundingBox會讓<pattern>中的圖形元素以小數定義,如下所示。三個屬性值都是0.1(不能用百分數),參照的仍然是引用<pattern>的圖形元素,計算得到的實際值都是20,效果如圖22所示,每個圖案只显示四分之一個圓。

<svg xmlns="http://www.w3.org/2000/svg" width="250" height="200">
  <defs>
    <pattern id="star3" width="10%" height="10%" patternContentUnits="objectBoundingBox">
      <circle cx=".1" cy=".1" r=".1" />
    </pattern>
  </defs>
  <circle cx="100" cy="100" r="100" fill="url(#star3)" />
</svg>

圖 22

3)漸變

  <linearGradient>元素用於繪製線性漸變,其子元素<stop>可指定某處的色標(即漸變點),如下所示,在起點、中點和止點處聲明了三種顏色,其中stop-opacity用於聲明不透明度(取值範圍0~1),1表示完全不透明,得到的效果如圖23所示。

<svg xmlns="http://www.w3.org/2000/svg" width="250" height="200">
  <defs>
    <linearGradient id="gradient">
      <stop offset="0" stop-color="#F60" />
      <stop offset="50%" stop-color="#CCC" stop-opacity=".5"/>
      <stop offset="100%" stop-color="#FC0" />
    </linearGradient>
  </defs>
  <rect width="200" height="100" fill="url(#gradient)" />
</svg>

圖 23

  如果要改變線性漸變的方向,可通過修改起點和終點的坐標實現,例如沿垂直線漸變,代碼如下,得到的效果如圖24所示。

<svg xmlns="http://www.w3.org/2000/svg" width="250" height="200">
  <defs>
    <linearGradient id="gradientVertical" x1="0" y1="0" x2="0" y2="100%">
      <stop offset="0" stop-color="#F60" />
      <stop offset="50%" stop-color="#CCC" stop-opacity=".5" />
      <stop offset="100%" stop-color="#FC0" />
    </linearGradient>
  </defs>
  <rect width="200" height="100" fill="url(#gradientVertical)" />
</svg>

圖 24

  <linearGradient>元素還有一個spreadMethod屬性,可指定在漸變範圍之外的填充方式,它有三個值可選,如下所列。

  (1)pad:默認值,用起點和終點的顏色填充。

  (2)reflect:按終點到起點,起點到終點的方式重複填充。

  (3)repeat:按起點到終點的方式重複填充。

  下面用一個示例來演示spreadMethod屬性,如圖25所示,從左往右,spreadMethod屬性的值依次是pad、reflect和repeat。

<svg xmlns="http://www.w3.org/2000/svg" width="800" height="200">
  <defs>
    <linearGradient id="spreadMethod" x1="20%" y1="30%" x2="60%" y2="70%">
      <stop offset="0" stop-color="#F60" />
      <stop offset="40%" stop-color="#CCC" />
      <stop offset="80%" stop-color="#FC0" />
    </linearGradient>
    <linearGradient id="gradientPad" href="#spreadMethod" spreadMethod="pad" />
    <linearGradient id="gradientReflect" href="#spreadMethod" spreadMethod="reflect" />
    <linearGradient id="gradientRepeat" href="#spreadMethod" spreadMethod="repeat" />
  </defs>
  <rect width="200" height="100" fill="url(#gradientPad)" />
  <rect width="200" height="100" fill="url(#gradientReflect)" x="210" />
  <rect width="200" height="100" fill="url(#gradientRepeat)" x="420" />
</svg>

圖 25

  另外一種徑向漸變由<radialGradient>元素控制,具體可參考官方文檔。

五、文本

1)<text>

  <text>元素用於處理SVG文件中的文本,在該元素中可修改字體樣式,包括字體名稱、大小、顏色、外觀等。在下面的示例中,聲明了四個<text>元素,併為它們添加了各自的樣式,效果如圖26所示。

<svg xmlns="http://www.w3.org/2000/svg" width="250" height="100">
  <style>
    .small {
      font: italic 13px sans-serif;
    }
    .heavy {
      font: bold 30px sans-serif;
    }
    .Rrrrr {
      font: italic 40px serif;
      fill: red;
    }
  </style>
  <text x="20" y="35" class="small">My</text>
  <text x="40" y="35" class="heavy">name</text>
  <text x="55" y="55" class="small">is</text>
  <text x="65" y="55" class="Rrrrr">Strick!</text>
</svg>

圖 26

2)對齊

  在<text>元素中對齊文本得用text-anchor屬性,它的對齊方式為起點(start)、居中(middle)和終點(end)。在下面的示例中,為了便於觀察這三個關鍵字的行為,畫了一條參考線,如圖27所示。

<svg xmlns="http://www.w3.org/2000/svg" width="120" height="150">
  <path d="M60,15 L60,110 M30,40 L90,40 M30,75 L90,75 M30,110 L90,110" stroke="grey" />
  <text text-anchor="start" x="60" y="40">Start</text>
  <text text-anchor="middle" x="60" y="75">Middle</text>
  <text text-anchor="end" x="60" y="110">End</text>
</svg>

圖 27

  SVG中的文本對齊與CSS中的略有不同,當起點對齊時,第一個字符會貼着參考線;當居中對齊時,文本的中間位置會被參考線貫穿;當終點對齊時,文本的最後一個字符會貼着參考線。

3)<tspan>

  在<text>元素中,通過其子元素<tspan>可調整文本屬性,從而實現一段文本呈現不同的效果,如下所示,這段文本為斜體,而<tspan>元素中的文本被加粗並且賦予了紅色(如圖28所示)。

<svg xmlns="http://www.w3.org/2000/svg" width="200" height="100">
  <style>
    text {
      font: italic 18px serif;
    }
    tspan {
      font: bold 20px sans-serif;
      fill: red;
    }
  </style>
  <text x="10" y="30" class="small">
    My name is
    <tspan>Strick</tspan>!
  </text>
</svg>

圖 28

4)長度
  textLength屬性用於定義文本的長度,lengthAdjust屬性用於設置字符間距和字形,如下所示,默認值spacing只會控制字符間距,而spacingAndGlyphs還能控制字形,如圖29所示。

<svg xmlns="http://www.w3.org/2000/svg" width="200" height="60">
  <text y="20" textLength="90%" lengthAdjust="spacing">Big text length</text>
  <text y="40" textLength="90%" lengthAdjust="spacingAndGlyphs">Big text length</text>
</svg>

圖 29

5)文本路徑

  將文本放置在<textPath>元素中,就可讓文本按任意的路徑排列,而不是以往的水平或垂直排列。在下面的示例中,文本按一條螺旋曲線排列,如圖30所示。

<svg xmlns="http://www.w3.org/2000/svg" width="150" height="120">
  <defs>
    <path id="curve" d="M10,90 Q90,90 90,45 Q90,10 50,10 Q10,10 10,40 Q10,70 45,70 Q70,70 75,50" />
  </defs>
  <text>
    <textPath href="#curve">
      My name is Strick! What is your name?
    </textPath>
  </text>
</svg>

圖 30

六、動畫

1)<animate>

  <animate>是一個動畫元素,它可以包含在圖形元素中,多個<animate>元素可以實現多重動畫。例如將矩形先沿着水平方向,再沿着垂直方向位移,最後在位移結束后切換背景色的動畫,代碼如下所示,效果如圖31。

<svg xmlns="http://www.w3.org/2000/svg" width="200" height="100">
  <rect id="move" width="50" height="50" fill="#F60">
    <animate attributeType="XML" dur="1s" repeatCount="1" attributeName="x"
        from="0" to="50" fill="freeze" begin="1s" id="line" />
    <animate attributeType="XML" dur="1s" repeatCount="1" attributeName="y"
        from="0" to="30" fill="freeze" begin="2s" />
    <animate attributeType="XML" dur="3s" repeatCount="indefinite" attributeName="fill"
        values="#F60;#FC0;#CCC" begin="line.end" />
  </rect>
</svg>

圖 31

  <animate>元素包含多個動畫屬性,此處只使用了其中的幾個,具體說明如下,其中fill=”freeze”是指動畫結束后保持最後的動作。

  (1)attributeName:執行動畫的屬性。

  (2)attributeType:屬性類型,可選值包括XML和CSS。

  (3)from:屬性的起始值。

  (4)to:屬性的結束值。

  (5)dur:動畫持續時間。

  (6)repeatCount:動畫執行次數,可以是有限次的整數,也可以是無限次的indefinite。

  (7)begin:動畫開始時機,可以是秒數,也可以是某個動作,例如上面第一個動畫結束后執行顏色的切換。

  <set>元素是對<animate>元素的補充,可為那些不能過渡的屬性提供動畫,例如在某個時刻显示文本,如下所示,點擊矩形可显示“change color”。

<svg xmlns="http://www.w3.org/2000/svg" width="200" height="100">
  <!--省略矩形代碼-->
  <text x="100" y="20" text-anchor="middle" style="display:none">
    <set attributeName="display" attributeType="CSS" to="block" begin="move.click" dur="1s" fill="freeze" />
    change color
  </text>
</svg>

2)<animateTransform>

  對於transform屬性的動畫,得用<animateTransform>元素完成。下面的示例演示了旋轉矩形(如圖32所示),其中type屬性用於指定變形的動作,可選的值有translate、scale、rotate、skewX和skewY。

<svg xmlns="http://www.w3.org/2000/svg" width="180" height="160">
  <rect id="move" width="50" height="50" fill="#F60" x="80" y="80">
    <animateTransform attributeName="transform" attributeType="XML" type="rotate"
        from="0 80 80" to="360 80 80" dur="5s" repeatCount="indefinite" />
  </rect>
</svg>

圖 32

3)<animateMotion>

  <animateMotion>元素可讓圖形沿着任意路徑運動,無論是直線還是曲線,都能實現。在下面的示例中,橙色矩形會沿着S型曲線來回運動,如圖33所示。

<svg xmlns="http://www.w3.org/2000/svg" width="200" height="100">
  <path fill="none" stroke="#000"
        d="M20,50 C20,-50 180,150 180,50 C180-50 20,150 20,50 z" />
  <rect width="20" height="10" fill="#F60">
    <animateMotion dur="10s" repeatCount="indefinite"
        path="M20,50 C20,-50 180,150 180,50 C180-50 20,150 20,50 z" />
  </rect>
</svg>

圖 33

  如果要讓矩形始終沿着平行於曲線的方向運動,那麼可以將rotate屬性設為auto,如圖34所示。

<svg xmlns="http://www.w3.org/2000/svg" width="200" height="100">
  <path fill="none" stroke="#000"
        d="M20,50 C20,-50 180,150 180,50 C180-50 20,150 20,50 z" />
  <rect width="20" height="10" fill="#F60">
    <animateMotion dur="10s" repeatCount="indefinite" rotate="auto"
        path="M20,50 C20,-50 180,150 180,50 C180-50 20,150 20,50 z" />
  </rect>
</svg>

圖 34

 

【參考資料】
SVG元素參考

[技術分享] 理解 SVG 中的 Viewport 和 ViewBox-拖曳與縮放功能實做(上)

理解SVG viewport,viewBox,preserveAspectRatio縮放

SVG圖像的viewport和viewBox用於設置圖像可見區域的大小

SVG:理解stroke-dasharray和stroke-dashoffset屬性

SVG 研究之路 (16) – Stroke-miterlimit

如何使用SVG圖案

SVG 研究之路 (26) – 有趣的 Patterns

 

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

【其他文章推薦】

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

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

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

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

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

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