一起玩轉微服務(11)——一切從簡單開始

介紹

使用Spring Bboot是快樂並且簡單的,不需要繁瑣的配置就能夠完成一套非常強大的應用。

spring boot 2.3.1

Spring Boot 2.3.1 發佈於:2020/06/12,現在已經提交到 Spring 倉庫和 Maven 中央倉庫了。

這個版本包括 127 個 bug 修復、Spring Boot 文檔改進增強、依賴升級等,另外還新增了一些新特性:

•提供基於新的 Maven 坐標 com.oracle.database 對 Oracle JDBC driver 的依賴管理;

•優化 Spring Cloud 的 CachedRandomPropertySource 不能正確適配的問題;•限制使用定製的 YAML 類型;

•增強對 NoSuchMethodErrors 異常失敗分析,能显示基類從哪被加載的;•提供更佳的錯誤消息,如果 Docker 停止運行了;

•優化 SystemEnvironmentPropertyMapper 類;

•提供更佳的診斷信息,當構建 OCI 鏡像失敗時 Docker 響應的 500 錯誤;

•支持通過 alwaysUseFullPath=true 參數來配置 UrlPathHelper;•支持在 Elasticsearch URIs 中使用用戶信息;

•支持在 Spring WebFlux 框架中使用歡迎頁面;

這個小版本還增加了蠻多東西的,大家也沒有必要跟着版本走,可以根據需要進行升級。疫情也擋不住外國友人更新的熱情。

實現

使用STS,可以去官方網站下載最新版。網站地址 https://Spring.io/tools/sts/ Spring Tool Suite™是基於eclipse開發的專門為Spring開發使用的工具包。

新建工程

選擇Spring Starter Project,

輸入工程名 對應的Name 打包方式 對應的Packaging,可以選擇jar或者war的方式。

輸入組織名 對應的Group 輸入描述 對應的Description

輸入包名 對應的Package 點擊next,然後選擇web和mysql

這裏的版本用的是2.3.1 如果沒有本地maven庫或者私庫會下載很長時間。

添加默認請求

進入 Chapter0301Application 添加

@RestController
@SpringBootApplication
public class Chapter0301Application {
    
    @RequestMapping("/")
    String home() {
         return "歡迎使用Spring Boot!";
    }

    public static void main(String[] args) {
        SpringApplication.run(Chapter0301Application.class, args);
    }

}

使用@RestController 相當於@Controller 和 @RequestBody。是Sspring Bboot 基於Sspring MVC的基礎上進行了改進, 將@Controller 與@ResponseBody 進行了合併形成的一個新的註解。 @EnableAutoConfiguration 作用 從classpath中搜索所有META-INF/spring.factories配置文件然後,將其中org.springframework.boot.autoconfigure.EnableAutoConfiguration key對應的配置項加載到spring容器 只有spring.boot.enableautoconfiguration為true(默認為true)的時候,才啟用自動配置 @EnableAutoConfiguration還可以進行排除,排除方式有2種,一是根據class來排除(exclude),二是根據class name(excludeName)來排除 其內部實現的關鍵點有

1.ImportSelector 該接口的方法的返回值都會被納入到spring容器管理中

2.SpringFactoriesLoader 該類可以從classpath中搜索所有META-INF/spring.factories配置文件,並讀取配置

啟動spring boot

  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::        (v2.3.1.RELEASE)

2020-06-23 13:30:11.611  INFO 9916 --- [           main] com.cloud.sky.Chapter0301Application     : Starting Chapter0301Application on DADI-PC with PID 9916 (D:\java\microservice\chapter0301\target\classes started by Administrator in D:\java\microservice\chapter0301)
2020-06-23 13:30:11.614  INFO 9916 --- [           main] com.cloud.sky.Chapter0301Application     : No active profile set, falling back to default profiles: default
2020-06-23 13:30:12.415  INFO 9916 --- [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat initialized with port(s): 8080 (http)
2020-06-23 13:30:12.423  INFO 9916 --- [           main] o.apache.catalina.core.StandardService   : Starting service [Tomcat]
2020-06-23 13:30:12.424  INFO 9916 --- [           main] org.apache.catalina.core.StandardEngine  : Starting Servlet engine: [Apache Tomcat/9.0.36]
2020-06-23 13:30:12.512  INFO 9916 --- [           main] o.a.c.c.C.[Tomcat].[localhost].[/]       : Initializing Spring embedded WebApplicationContext
2020-06-23 13:30:12.512  INFO 9916 --- [           main] w.s.c.ServletWebServerApplicationContext : Root WebApplicationContext: initialization completed in 830 ms
2020-06-23 13:30:12.665  INFO 9916 --- [           main] o.s.s.concurrent.ThreadPoolTaskExecutor  : Initializing ExecutorService 'applicationTaskExecutor'
2020-06-23 13:30:12.809  INFO 9916 --- [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat started on port(s): 8080 (http) with context path ''
2020-06-23 13:30:12.818  INFO 9916 --- [           main] com.cloud.sky.Chapter0301Application     : Started Chapter0301Application in 1.492 seconds (JVM running for 3.109)
2020-06-23 13:30:20.675  INFO 9916 --- [nio-8080-exec-1] o.a.c.c.C.[Tomcat].[localhost].[/]       : Initializing Spring DispatcherServlet 'dispatcherServlet'
2020-06-23 13:30:20.676  INFO 9916 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet        : Initializing Servlet 'dispatcherServlet'
2020-06-23 13:30:20.680  INFO 9916 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet        : Completed initialization in 4 ms

打開瀏覽器訪問 http://localhost:8080/ 可以得到如下頁面

遇到問題

構建的過程中遇到問題

[INFO] Scanning for projects...
[ERROR] [ERROR] Some problems were encountered while processing the POMs:
[FATAL] Non-parseable POM D:\java\apache-maven-3.1.1\repo\org\jetbrains\kotlin\kotlin-bom\1.3.72\kotlin-bom-1.3.72.pom: entity reference names can not start with character ')' (position: START_TAG seen ...ost,s="";function qs(n){var u=D.URL;var t=u.match(eval(\'/(\\?|#|&)... @1:243)  @ D:\java\apache-maven-3.1.1\repo\org\jetbrains\kotlin\kotlin-bom\1.3.72\kotlin-bom-1.3.72.pom, line 1, column 243
 @ 
[ERROR] The build could not read 1 project -> [Help 1]
[ERROR]   
[ERROR]   The project com.cloudskyme:chapter0301:0.0.1 (D:\java\microservice\chapter0301\pom.xml) has 1 error
[ERROR]     Non-parseable POM D:\java\apache-maven-3.1.1\repo\org\jetbrains\kotlin\kotlin-bom\1.3.72\kotlin-bom-1.3.72.pom: entity reference names can not start with character ')' (position: START_TAG seen ...ost,s="";function qs(n){var u=D.URL;var t=u.match(eval(\'/(\\?|#|&)... @1:243)  @ D:\java\apache-maven-3.1.1\repo\org\jetbrains\kotlin\kotlin-bom\1.3.72\kotlin-bom-1.3.72.pom, line 1, column 243 -> [Help 2]
[ERROR] 
[ERROR] To see the full stack trace of the errors, re-run Maven with the -e switch.
[ERROR] Re-run Maven using the -X switch to enable full debug logging.
[ERROR] 
[ERROR] For more information about the errors and possible solutions, please read the following articles:
[ERROR] [Help 1] http://cwiki.apache.org/confluence/display/MAVEN/ProjectBuildingException
[ERROR] [Help 2] http://cwiki.apache.org/confluence/display/MAVEN/ModelParseException

1. 解決

修改maven默認源配置
我使用的是阿里的maven倉庫,國外的東西沒個代理還真麻煩。

<repositories>
        <repository>  
            <id>alimaven</id>
            <name>aliyun maven</name>
            <url>http://maven.aliyun.com/nexus/content/groups/public/</url>
        </repository> 
        <repository>
            <id>sonatype-nexus-snapshots</id>
            <url>https://oss.sonatype.org/content/repositories/snapshots</url>
            <releases>
                <enabled>false</enabled>
            </releases>
            <snapshots>
                <enabled>true</enabled>
            </snapshots>
        </repository>
    </repositories>

然後執行 mvn help:system
成功可以看到如下界面:

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

【其他文章推薦】

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

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

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

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

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

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

Jmeter系列(27)- 詳解正則提取器

如果你想從頭學習Jmeter,可以看看這個系列的文章哦

https://www.cnblogs.com/poloyy/category/1746599.html

 

有了 JSON 提取器為啥還要用正則提取器?

  • JSON 提取器只針對接口返回的響應內容
  • 如果想提取的是響應頭、請求頭的值,而非響應內容的值呢?
  • 這個時候正則提取器的作用就出來了,它可以提取請求任一部分的值

 

需知

  • 正則表達式很多內容,在這篇文章中不會展開詳細說的哦,主要還是說提取器的使用
  • 想詳細學習正則表達式可以看這篇文章:待補充

 

正則提取器

我們通過實際栗子去講述理論知識點

 

正則提取器界面介紹

 

字段含義

字段 含義
Apply to 應用範圍,選默認的 main sample only 就行了
Field to check

可提取的字段

Names of created variables
  • 接收提取值的變量名
  • 必傳
Regular Expression

正則表達式

Template 從找到的匹配項中創建字符串的模板
Match No.(0 for Random)
  • 取第幾個值
  • 0:隨機,默認
  • -1:所有
  • 1:第一個值
  • 非必傳
Default Value
  • 缺省值,匹配不到值的時候取該值
  • 非必傳
Use empty default value

勾選后,提取不到值時,則返回空字符串

 

Template

  • 如果一條正則表達式有多個提取結果,則提取結果是數組形式
  • 模板 $1$、$2$…..表示把解析到的第幾個值賦給變量,從 1 開始匹配
  • $0$ 表示整個表達式匹配的內容(後續具體看栗子)
  • 若只有一個結果,只能是$1$

 

Field to check

屬性 含義
Body 響應體,不包括響應頭;最常用
Body (unescaped) 響應體,替換了所有HTML轉義符;不建議使用
Body as a Document 從不同類型的文件中提取文本;影響性能
Request Headers 請求頭
Response Headers 響應頭
URL URL
Response Code 響應碼
Response Message 響應信息

 

Body

 

Request Headers

 

Response Headers

 

URL

 

Response Code、Message

 

入門栗子

栗子的前提

這個栗子,我都會以這個地址的接口來完成 JSON 提取器的實戰慄子,大家可以註冊個賬號玩一玩哦

http://api.yesapi.cn/docs.php?keyword=%E4%BC%9A%E5%91%98&channel=api

 

測試計劃樹結構

下面多個栗子都以這個測試計劃為基礎哦

 

提取某個特定的值的栗子

登錄接口響應

登錄是執行其他接口的前置接口,所以要獲取用戶登錄后的 token、uuid

 

提取 token

 

提取 uuid

 

其他接口調用 token、uuid

 

知識點

  • 提其他接口可以通過  ${var}  這種格式,來獲取提取到的值
  •  ( ) 裏面寫匹配規則,用於解析正則表達式
  •  .*? 表示匹配任意長度的任意字符,這也是最常用的正則表達式
  • 一般 (.+?) 和 (.*?) 能夠滿足我們 80%的使用場景

 

一般正則表達式都可以寫成下面兩種

  •  左邊界(.+?)右邊界 
  •  左邊界(.*?)右邊界 

 

舉更多栗子前的一些話

  • 上面講的是使用正則提取器時的一個流程,也是實際工作中最簡單的栗子
  • 在實際項目中,我們可能會出現一條正則表達式有多個提取結果的情況

 

JSON 字符串

下面的栗子都以這個 JSON 字符串為基礎,从里面提取結果

這 JSON 字符串也是某個接口的響應內容,貨真價實,感興趣也可以自己玩一玩:http://api.yesapi.cn/docs-api-App.User.GetList.html

{
    "ret": 200,
    "msg": "V2.5.1 YesApi App.User.GetList",
    "data": {
        "total": 4,
        "err_msg": "",
        "err_code": 0,
        "users": [
            {
                "role": "user",
                "status_desc": "正常",
                "reg_time": "2020-06-22 20:45:05",
                "role_desc": "普通會員",
                "ext_info": {
                    "yesapi_nickname": "",
                    "yesapi_points": 0
                },
                "uuid": "0564CE592B4CE914365D8922F6FC4CEC",
                "username": "luojunjiess286",
                "status": 0
            },
            {
                "role": "user",
                "status_desc": "正常",
                "reg_time": "2020-06-22 14:27:17",
                "role_desc": "普通會員",
                "ext_info": {
                    "yesapi_nickname": "",
                    "yesapi_points": 0
                },
                "uuid": "0164DC0680F84DCE40D3DD4A36640ECA",
                "username": "luojunjiessa",
                "status": 0
            },
            {
                "role": "admin",
                "status_desc": "正常",
                "reg_time": "2020-03-23 22:48:32",
                "role_desc": "管理員",
                "ext_info": {
                    "yesapi_nickname": "",
                    "yesapi_points": 0
                    "yesapi_reg_source": ""
                },
                "uuid": "079BF6BB82AFCFC7084F96AECAF0519F",
                "username": "luojunjiess",
                "status": 0
            }
        ]
    }
}

 

一條正則表達式只有一個提取結果的栗子

什麼叫只有一個提取結果

就是正則表達式里只有一個 ( ) ,且  Match No. 不是 -1

 

未填寫模板

提取器

 

測試結果

uuid1=
uuid1_g=1
uuid1_g0="uuid":"0564CE592B4CE914365D8922F6FC4CEC"
uuid1_g1=0564CE592B4CE914365D8922F6FC4CEC

 

知識點

如果正則匹配到值,但是沒有填模板,則返回

 

$0$

提取器

 

測試結果

uuid2="uuid":"0564CE592B4CE914365D8922F6FC4CEC"
uuid2_g=1
uuid2_g0="uuid":"0564CE592B4CE914365D8922F6FC4CEC"
uuid2_g1=0564CE592B4CE914365D8922F6FC4CEC

 

知識點

  •  $0$ 模板其實返回的就是 uuid2_g0 的值
  • 返回了整個正則表達式,不只是 ( ) 內匹配到的值

 

$1$

提取器

 

測試結果

uuid3=0564CE592B4CE914365D8922F6FC4CEC
uuid3_g=1
uuid3_g0="uuid":"0564CE592B4CE914365D8922F6FC4CEC"
uuid3_g1=0564CE592B4CE914365D8922F6FC4CEC

 

知識點

  •  $1$  模板其實返回的就是 uuid2_g1 的值
  • 僅返回 ( ) 內匹配到的值

 

$2$

提取器

 

測試結果

uuid4=null
uuid4_g=1
uuid4_g0="uuid":"0564CE592B4CE914365D8922F6FC4CEC"
uuid4_g1=0564CE592B4CE914365D8922F6FC4CEC

 

知識點

 $2$ 模板並不存在,其實就是 uuid4_g2 變量不存在,即使勾了使用空默認值,也返回 null,

 

總結

  • 其實 uuid 在 JSON 字符串中有三個可匹配到的值,如果不填寫匹配数字 Match No. ,則會隨機取一個 uuid 並返回
  • 像上述的幾個栗子,都填了 1 ,所以都返回了第一個匹配到的 uuid

 

一條正則表達式有多個提取結果的栗子

什麼叫有多個提取結果

有兩種情況

  • 一條表達式有多個 ( ) 
  • 一個 ( ) 匹配到多個值,且 Match No 填了 -1

 

一個 ( ) 匹配到多個值 

提取器

 

測試結果

手動分成四部分

uuid1_1=0564CE592B4CE914365D8922F6FC4CEC
uuid1_1_g=1
uuid1_1_g0="uuid":"0564CE592B4CE914365D8922F6FC4CEC"
uuid1_1_g1=0564CE592B4CE914365D8922F6FC4CEC

uuid1_2=0164DC0680F84DCE40D3DD4A36640ECA
uuid1_2_g=1
uuid1_2_g0="uuid":"0164DC0680F84DCE40D3DD4A36640ECA"
uuid1_2_g1=0164DC0680F84DCE40D3DD4A36640ECA

uuid1_3=079BF6BB82AFCFC7084F96AECAF0519F
uuid1_3_g=1
uuid1_3_g0="uuid":"079BF6BB82AFCFC7084F96AECAF0519F"
uuid1_3_g1=079BF6BB82AFCFC7084F96AECAF0519F

uuid1_matchNr=3

 

知識點

  • 一個 ( ) 匹配到多個值的場景 ,一般會結合 ForEach控制器,可以循環將提取到的值賦予到 HTTP 請求中
  • 可以看看下圖的小栗子,這裏不展開講,後面會再詳細講解

 

結構樹 + ForEach 控制器

 

查看結果樹

 

一條表達式有多個( ),且模板為空

提取器

 

測試結果

info1=
info1_g=2
info1_g0="uuid":"0564CE592B4CE914365D8922F6FC4CEC","username":"luojunjiess286"
info1_g1=0564CE592B4CE914365D8922F6FC4CEC
info1_g2=luojunjiess286

 

一條表達式有多個( ),且只有一個模板

提取器

 

測試結果

info2=0564CE592B4CE914365D8922F6FC4CEC
info2_g=2
info2_g0="uuid":"0564CE592B4CE914365D8922F6FC4CEC","username":"luojunjiess286"
info2_g1=0564CE592B4CE914365D8922F6FC4CEC
info2_g2=luojunjiess286

 

知識點

  • info2 拿的就是 info2_g1 的值
  •  $1$ 獲取的是第一個 ( ) 匹配到的值, $2$ 獲取的是第二個 ( ) 匹配到的值,以此類推     

 

一條表達式有多個( ),且有兩個模板

提取器

 

測試結果

info3=0564CE592B4CE914365D8922F6FC4CECluojunjiess286
info3_g=2
info3_g0="uuid":"0564CE592B4CE914365D8922F6FC4CEC","username":"luojunjiess286"
info3_g1=0564CE592B4CE914365D8922F6FC4CEC
info3_g2=luojunjiess286

info4=0564CE592B4CE914365D8922F6FC4CEC,luojunjiess286
info4_g=2
info4_g0="uuid":"0564CE592B4CE914365D8922F6FC4CEC","username":"luojunjiess286"
info4_g1=0564CE592B4CE914365D8922F6FC4CEC
info4_g2=luojunjiess286

 

正則表達式中有多個 ( ) 時的總結

  • 如果其中一個 ( ) 匹配不到元素,那也無法獲取到值
  • 引用名稱、匹配数字、缺省值三個字段也只需要填一個值即可,不需要跟 ( ) 的數量一致
  • 多個模板( $1$$2$ )的時候,可以用空格、, 、. 、  連接模板,最終會显示在變量上,如:info4

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

【其他文章推薦】

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

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

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

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

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

※超省錢租車方案

向強大的SVG邁進

作者:凹凸曼 – 暖暖

SVG 即 Scalable Vector Graphics 可縮放矢量圖形,使用XML格式定義圖形。

一、SVG印象

SVG 的應用十分廣泛,得益於 SVG 強大的各種特性。

1.1、 矢量

可利用 SVG 矢量的特點,描出深圳地鐵的輪廓:

1.2、iconfont

SVG 可依據一定的規則,轉成 iconfont 使用:

1.3、 foreignObject

利用 SVG 的 foreignObject 標籤實現截圖功能,原理:foreignObject 內部嵌入 HTML 元素:

<svg xmlns="http://www.w3.org/2000/svg">
	<foreignObject width="120" height="60">
		<p style="font-size:20px;margin:0;">凹凸實驗室 歡迎您</p>
	</foreignObject>
</svg>

截圖實現流程:

  1. 首先聲明一個基礎的 svg 模版,這個模版需要一些基礎的描述信息,最重要的,它要有 <foreignObject></foreignObject> 這對標籤;
  2. 將要渲染的 DOM 模版模版嵌入 foreignObject 即可;
  3. 利用 Blob 構建 svg 對象;
  4. 利用 URL.createObjectURL(svg) 取出 URL。

1.4、SVG SMIL

由於微信編輯器不允許嵌入 <style><script><a> 標籤,利用SVG SMIL 可進行微信公眾號極具創意的圖文排版設計,包括動畫與交互。
但是也要注意,標籤里不允許有id,否則會被過濾或替換掉。

點擊 “凹凸實驗室” 后,圍繞 “凹凸實驗室” 中心旋轉 360度,點擊0.5秒后 出現 https://aotu.io/ ,動畫只運行一次。

下圖為 GIF循環演示:

代碼如下:

<svg width="360" height="300" xmlns="http://www.w3.org/2000/svg">
    <g>
        <!-- 點擊后 運行transform旋轉動畫,restart="never"表示只運行一次 -->
        <animateTransform attributeName="transform" type="rotate" begin="click" dur="0.5s" from="0 100 80" to="360 100 80"  fill="freeze" restart="never" />
        <g>
            <text font-family="microsoft yahei" font-size="20" x="50" y="80">
                凹凸實驗室
            </text>
        </g>
        <g style="opacity: 0;">
            <!-- 同一個初始位置以及大致的寬高,觸發點擊事件 -->
            <text font-family="microsoft yahei" font-size="20" x="50" y="80">https://aotu.io/</text>
            <!-- 點擊后 運行transform移動動畫,改變文本的位置 -->
            <animateTransform attributeName="transform" type="translate" begin="click" dur="0.1s" to="0 40"  fill="freeze" restart="never" />
            <!-- 點擊0.5秒后 運行opacity显示動畫 -->
            <animate attributeName="opacity" begin="click+0.5s" from="0" to="1" dur="0.5s" fill="freeze" restart="never" />
        </g>
    </g>
</svg>

以上是鄙人對SVG的大致印象,最近的需求開發再次刷新了我的認知,那就是 SVG實現非比例縮放 以及 小程序不支持SVG標籤的處理,下面容我來講述一番。

二、SVG 實現非比例縮放

我們熟知的 iconfont,可通過改變字體大小縮放,但是這是 比例縮放,那如何實現 SVG 的非比例縮放呢?
如下圖所示,如何將 一隻兔子 非比例縮放?

划重點:實現非比例縮放主要涉及三個知識點:viewport、viewBox和preserveAspectRatio,viewport 與viewBox 結合可實現縮放的功能,viewBox 與 preserveAspectRatio 結合可實現非比例的功能。

2.1、viewport

viewport 表示SVG可見區域的大小。
viewport 就像是我們的显示器屏幕大小,超出區域則隱藏,原點位於左上角,x 軸水平向右,y 軸垂直向下。

通過類似CSS的屬性 widthheight 指定視圖大小:

<svg width="400" height="200"></svg>

2.2、viewBox

viewBox值有4個数字:x, y, width, height 。
其中 x:左上角橫坐標,y:左上角縱坐標,width:寬度,height:高度。
原點默認位於左上角,x 軸水平向右,y 軸垂直向下。

<svg width="400" height="200" viewBox="0 0 200 100"></svg>

显示器屏幕的畫面,可以特寫,可以全景,這就是 viewBox
viewBox 可以想象成截屏工具選中的那個框框,和 viewport 作用的結果就是 把框框中的截屏內容再次在 显示器 中全屏显示。

(圖片來源:SVG 研究之路 (23) – 理解 viewport 與 viewbox)

2.3、preserveAspectRatio

上圖的紅色框框和藍色框框,恰好和显示器的比例相同,如果是下圖的綠色框框,怎樣在显示器屏幕中显示呢?

2.3.1、 定義

preserveAspectRatio 作用的對象是 viewBox,使用方法如下:

preserveAspectRatio="[defer] <align> [<meetOrSlice>]"
// 例如 preserveAspectRatio="xMidYMid meet"

其中 defer 此時不是重點,暫且忽略,主要了解 alignmeetOrSlice 的 用法:

  • align:由兩個名詞組成,分別代表 viewbox 與 viewport 的 x 方向、y方向的對齊方式。
含義
xMin viewport 和 viewBox 左邊對齊
xMid viewport 和 viewBox x軸中心對齊
xMax viewport 和 viewBox 右邊對齊
YMin viewport 和 viewBox 上邊緣對齊。注意Y是大寫。
YMid viewport 和 viewBox y軸中心點對齊。注意Y是大寫。
YMax viewport 和 viewBox 下邊緣對齊。注意Y是大寫。
  • meetOrSlice:表示如何維持高寬的比例,有三個值 meet、slice、none。
    • meet – 默認值,保持縱橫比縮放 viewBox 適應 viewport,可能會有餘留的空白。
    • slice – 保持縱橫比同時比例小的方向放大填滿 viewport,超出的部分被剪裁掉。
    • none – 扭曲縱橫比以充分適應 viewport。
2.3.2、 例子

例子1:preserveAspectRatio="xMidYMid meet" 表示 綠色框框 與 显示器的 x 方向、y方向的 中心點 對齊;

例子2:preserveAspectRatio="xMidYMin slice" 表示 綠色框框 與 显示器的 x 方向 中心點 對齊,Y 方向 上邊緣對齊,保持比例放大填滿 显示屏 后超出部分隱藏;

例子3:preserveAspectRatio="xMidYMid slice" 表示 綠色框框 與 显示器的 x 方向、y方向的 中心點 對齊,保持比例放大填滿显示屏 后超出部分隱藏;

例子4:preserveAspectRatio="none" 不管三七二十一,隨意縮放綠色框框,填滿 显示屏即可;這就是非比例縮放的答案了。

三、小程序不支持svg標籤怎麼辦

微信小程序官方不支持 SVG 標籤的,但是決定曲線救國,相當於自己實現了一個SVG標籤:使用小程序內置的 Canvas 渲染器, 在 Cax 中實現 SVG 標準的子集,使用 JSX 或者 HTM(Hyperscript Tagged Markup) 描述 SVG 結構行為表現。

但是今天我想講講其他的。
我們知道,小程序雖然不支持 SVG 標籤,但是支持 svg 轉成 base64 後作為 background-imageurl,如 background-image: url("data:image/svg+xml.......)

但是我這邊還有個需求,隨時更改 SVG 每個路徑的顏色,即 顏色可配置:

來迴轉 Base64 肯定是比較麻煩的,有沒有更好的方式呢?
直接貼答案:對於SVG圖形,還有更好的實現方式,就是直接使用SVG XML格式代碼,無需進行base64轉換。

3.1、URL 編碼

直接使用 SVG XML 格式代碼,首先要了解 Data URI的格式。
划重點:base64非必選項,不指定的時候,後面的 <data> 將使用 URL編碼。

3.1.1、入門

百分號編碼(Percent-encoding), 也稱作URL編碼(URL encoding),是特定上下文的統一資源定位符 (URL)的編碼機制。

原理:ASCII 字符 = % + 兩位 ASCII 碼(十六進制)。
例如,字符 a 對應的 ASCII 碼為 0x61,那麼 URL 編碼后得到 %61 。

3.1.2、URL 編碼壓縮

前言:

Data URI 的格式中的 <data> 完全使用URL 編碼也是可以的,如 encodeURIComponent('<svg version="1.1" viewBox= …</svg>')
但是和轉義前原始SVG相比,可讀性差了很多,而且佔用體積也變大了。
如果深入了解URL 編碼的話,<data> 沒必要全部編碼的。

正文:

RFC3986文檔規定,URL中只允許包含 未保留字符 以及 所有保留字符。

  • 未保留字符:包含英文字母(a-zA-Z)、数字(0-9)、-_.~ 4個特殊字符。對於未保留字符,不需要百分號編碼。
  • 保留字符:具有特殊含義的字符 :/?#[]@ (分隔Url的協議、主機、路徑等組件) 和 !$&'()*+,;= (用於在每個組件中起到分隔作用的,如&符號用於分隔查詢多個鍵值對)。
  • 受限字符或不安全字符:直接放在Url中的時候,可能會引起解析程序的歧義,因此這部分需要百分號編碼,如%空格雙引號"尖號 <>等等。

綜上所述,只需要對 受限字符或不安全字符 進行編碼即可。

  • JS 處理比較簡單,利用 replace 將 需要編碼的字符 替換掉 即可,基本替換 以下的符號 就夠用了:
svgToUrl (svgData) {
    encoded = encoded
      .replace(/<!--(.*)-->/g, '') // 親測必須去掉註釋
      .replace(/[\r\n]/g, ' ') // 親測最好去掉換行
      .replace(/"/g, `'`) // 單引號是保留字符,雙引號改成單引號減少編碼
      .replace(/%/g, '%25')
      .replace(/&/g, '%26')
      .replace(/#/g, '%23')
      .replace(/{/g, '%7B')
      .replace(/}/g, '%7D')
      .replace(/</g, '%3C')
      .replace(/>/g, '%3E')
    return `data:image/svg+xml,${encoded}`
  }
  • 如果使用在 CSS 中,可利用 SASS版本3.3以上 的 三個API 對 SVG字符串做替換處理。

    1. str_insert(string, insert, index): 從 $string$index 插入字符 $insert
    2. str_index(string, substring): 返回 $substring$string 中第一個位置;
    3. str_slice(string, start_at, end_at = nil): 返回從字符 $string 中第 $start_at 開始到 $end_at 結束的一個新字符串。

    前人已有總結,可前往 https://github.com/leeenx/sass-svg/blob/master/sass-encodeuri.scss 查看完整代碼。

3.2、SVG 壓縮

一般從 Sketch 導出 SVG ,冗餘代碼比較多,有條件的話建議使用 SVGO 壓縮SVG的原本體積,比如清除換行、重複空格;刪除文檔聲明;刪除註釋;刪除desc描述等等。

四、總結

SVG強大的地方在於,出其不意,炫酷,與眾不同。

無論是微信公眾號花式排版,foreignObject 標籤實現截圖,實現非比例縮放,或者 背景圖直接使用 SVG XML 格式代碼,還是上文沒有提及的路徑動畫、描邊動畫、圖形裁剪、濾鏡等等,都可以玩出新的花樣。

SVG 一個屬性可成就一篇文章,學習 SVG 可以說是在挑戰自己,歡迎加入 SVG 的學習隊列。

五、參考內容 · 推薦閱讀

三看 SVG Web 動效
URL編碼的奧秘
學習了,CSS中內聯SVG圖片有比Base64更好的形式
超級強大的SVG SMIL animation動畫詳解
詳細教你微信公眾號正文頁SVG交互開發
SVG 簡介與截圖等應用

歡迎關注凹凸實驗室博客:aotu.io

或者關注凹凸實驗室公眾號(AOTULabs),不定時推送文章:

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

【其他文章推薦】

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

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

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

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

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

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

01MySQL內核分析-The Skeleton of the Server Code

摘要

這個官方文檔一段對MySQL內核分析的一個嚮導。是對MySQL一條insert語句寫入到MySQL數據庫的分析。
但是,對於MySQL 5.7版本來說,基本上都是寫入到innodb引擎。但也還是有借鑒意義,大的框架沒有太大變化。
後面的文檔,會通過mysqld –debug 和gdb等工具,通過分析mysqld.trace來分析insert語句在MySQL 5.7中怎麼寫入數據庫。

官方文檔給出的一段結構,如下:

/sql/mysqld.cc
/sql/sql_parse.cc
/sql/sql_prepare.cc
/sql/sql_insert.cc
/sql/ha_myisam.cc
/myisam/mi_write.c

上述梳理一個過程,是說從客戶段執行一條簡單的insert語句,然後到達MySQL服務器端,並通過MyISAM存儲層。寫入到MyISAM文件的過程。

由於,我們現在的主流都是InnoDB存儲引擎,所以我們分析的寫入到存儲層應該是InnoDB的源代碼。但是上述的一個框架也有借鑒意義。雖然,走的是InnoDB存儲引擎插入數據,但是也還是需要通過SQL層的ha_*這樣的接口進行接入。

正題開始!!!!!!!!!!!!!!!!!!!!!!!

第一步,進入MySQL大門的地方。夢開始的地方。眾所周知,C語言都是需要main方法作為主入口。而MySQL的主入口如下:

代碼位置 /sql/mysqld.cc

  int main(int argc, char **argv)
  {
    _cust_check_startup();
    (void) thr_setconcurrency(concurrency);
    init_ssl();
    server_init();                             // 'bind' + 'listen'
    init_server_components();
    start_signal_handler();
    acl_init((THD *)0, opt_noacl);
    init_slave();
    create_shutdown_thread();
    create_maintenance_thread();
    handle_connections_sockets(0);             // !  這裏也代表着我們進入下一個門的地方
    DBUG_PRINT("quit",("Exiting main thread"));
    exit(0);
  }

這裏可以看到很多的init_*或者server_init()。通過名字我們可以猜測出,這裏做了很多初始化的工作。例如:啟動過程中一些初始化的檢查和MySQL配置變量的加載和一些組件的初始化等。

這裏重要的函數是handle_connections_sockets

繼續跟蹤 /sql/mysqld.cc

 handle_connections_sockets (arg __attribute__((unused))
  {
     if (ip_sock != INVALID_SOCKET)
     {
       FD_SET(ip_sock,&clientFDs);
       DBUG_PRINT("general",("Waiting for connections."));
       while (!abort_loop)
       {
         new_sock = accept(sock, my_reinterpret_cast(struct sockaddr*)
           (&cAddr),             &length);
         thd= new THD;
         if (sock == unix_sock)
         thd->host=(char*) localhost;
         create_new_thread(thd);            // !
         }

從簡易的思維,忽視其他的判斷語句。可以看到這裏做的是典型的client/server架構。服務器有一個主線程,它總是偵聽來自新客戶機的請求。一旦它接收到這樣的請求,它將分配資源。特別是,主線程將生成一個新線程來處理連接。然後主服務器將循環並偵聽新連接——但我們將保留它並跟蹤新線程。

這裏創建新線程的方法是:create_new_thread(thd);

繼續跟蹤 /sql/mysqld.cc

  create_new_thread(THD *thd)
  {
    pthread_mutex_lock(&LOCK_thread_count);
    pthread_create(&thd->real_id,&connection_attrib,
        handle_one_connection,                        // !
        (void*) thd));
    pthread_mutex_unlock(&LOCK_thread_count);
  }

可以看到這裏獲得一個新線程加入一個互斥鎖,避免衝突。

繼續跟蹤 /sql/mysqld.cc

handle_one_connection(THD *thd)
  {
    init_sql_alloc(&thd->mem_root, MEM_ROOT_BLOCK_SIZE, MEM_ROOT_PREALLOC);
    while (!net->error && net->vio != 0 && !thd->killed)
    {
      if (do_command(thd))            // !
        break;
    }
    close_connection(net);
    end_thread(thd,1);
    packet=(char*) net->read_pos;

從這裏開始,我們即將脫離mysqld.cc文件,因為我們獲得了thread,且分配一小段內存資源,給與我們來處理我們的SQL語句了。

我們會走向何方呢,可以開始觀察do_command(thd)方法。

繼續跟蹤/sql/sql_parse.cc

bool do_command(THD *thd)
{
  net_new_transaction(net);
  packet_length=my_net_read(net);
  packet=(char*) net->read_pos;
  command = (enum enum_server_command) (uchar) packet[0];
  dispatch_command(command,thd, packet+1, (uint) packet_length);
// !
}

其中從這裏可以看到,do_command(THD *thd)把它串聯起來的是一個叫作THD的東西,也就是thread。所以後面的工作和行為,基本都是通過thread進行牽線搭橋的。

my_net_read函數位於另一個名為net_servlet .cc的文件中。該函數從客戶端獲取一個包,解壓縮它,並去除頭部。

一旦完成,我們就得到了一個名為packet的多字節變量,它包含客戶端發送的內容。第一個字節很重要,因為它包含標識消息類型的代碼。

說明了packet第一個字節很重要。debug也有證據進行一個佐證。

packet_header: Memory: 0x7f7fc000a4b0  Bytes: (4)
21 00 00 00

然後把packet第一個字節和餘下的部分傳遞給dispatch_command

繼續跟蹤/sql/sql_parse.cc

bool dispatch_command(enum enum_server_command command, THD *thd,
       char* packet, uint packet_length)
{
  switch (command) {
    case COM_INIT_DB:          ...
    case COM_REGISTER_SLAVE:   ...
    case COM_TABLE_DUMP:       ...
    case COM_CHANGE_USER:      ...
    case COM_EXECUTE:
         mysql_stmt_execute(thd,packet);
    case COM_LONG_DATA:        ...
    case COM_PREPARE:
         mysql_stmt_prepare(thd, packet, packet_length);   // !
    /* and so on for 18 other cases */
    default:
     send_error(thd, ER_UNKNOWN_COM_ERROR);
     break;
    }

這裏sql_parser .cc中有一個非常大的switch語句

switch語句中代碼有:code for prepare, close statement, query, quit, create database, drop database, dump binary log, refresh, statistics, get process info, kill process, sleep, connect, and several minor commands

除了COM_EXECUTE和COM_PREPARE兩種情況外,我們刪除了所有情況下的代碼細節。

可以看到

  • COM_EXECUTE 會調用mysql_stmt_execute(thd,packet);

  • COM_PREPARE 會調用mysql_stmt_prepare(thd, packet, packet_length);

這裏就像一个中轉站一般,看我們去向什麼地方。這裏去的門是:COM_PREPARE:mysql_stmt_prepare

跟蹤 /sql/sql_prepare.cc

下面是一段prepare的註釋

"Prepare:
Parse the query
Allocate a new statement, keep it in 'thd->prepared statements' pool
Return to client the total number of parameters and result-set
metadata information (if any)"

繼續回到主線COM_EXECUTE

跟蹤/sql/sql_parse.cc

  bool dispatch_command(enum enum_server_command command, THD *thd,
       char* packet, uint packet_length)
  {
  switch (command) {
    case COM_INIT_DB:          ...
    case COM_REGISTER_SLAVE:   ...
    case COM_TABLE_DUMP:       ...
    case COM_CHANGE_USER:      ...
    case COM_EXECUTE:
         mysql_stmt_execute(thd,packet);                   // !
    case COM_LONG_DATA:        ...
    case COM_PREPARE:
         mysql_stmt_prepare(thd, packet, packet_length);
    /* and so on for 18 other cases */
    default:
     send_error(thd, ER_UNKNOWN_COM_ERROR);
     break;
    }

現在“COM_EXECUTE 中的mysql_stmt_execute`是我們關注的重點,我們來看看

跟蹤/sql/sql_prepare.cc代碼

  void mysql_stmt_execute(THD *thd, char *packet)
  {
    if (!(stmt=find_prepared_statement(thd, stmt_id, "execute")))
    {
      send_error(thd);
      DBUG_VOID_RETURN;
    }
    init_stmt_execute(stmt);
    mysql_execute_command(thd);           // !
  }

這裏做一個判斷,看是否是execute,然後初始化語句,並開始執行mysql_execute_command(thd);可以看到,是通過thread來調用動作。

跟蹤/sql/sql_parse.cc代碼

  void mysql_execute_command(THD *thd)
       switch (lex->sql_command) {
       case SQLCOM_SELECT: ...
       case SQLCOM_SHOW_ERRORS: ...
       case SQLCOM_CREATE_TABLE: ...
       case SQLCOM_UPDATE: ...
       case SQLCOM_INSERT: ...                   // !
       case SQLCOM_DELETE: ...
       case SQLCOM_DROP_TABLE: ...
       }

lex 解析sql語句。然後進入SQLCOM_INSERT。

跟蹤/sql/sql_parse.cc代碼

case SQLCOM_INSERT:
{
  my_bool update=(lex->value_list.elements ? UPDATE_ACL : 0);
  ulong privilege= (lex->duplicates == DUP_REPLACE ?
                    INSERT_ACL | DELETE_ACL : INSERT_ACL | update);
  if (check_access(thd,privilege,tables->db,&tables->grant.privilege))
    goto error;
  if (grant_option && check_grant(thd,privilege,tables))
    goto error;
  if (select_lex->item_list.elements != lex->value_list.elements)
  {
    send_error(thd,ER_WRONG_VALUE_COUNT);
    DBUG_VOID_RETURN;
  }
  res = mysql_insert(thd,tables,lex->field_list,lex->many_values,
                     select_lex->item_list, lex->value_list,
                     (update ? DUP_UPDATE : lex->duplicates));
// !
  if (thd->net.report_error)
    res= -1;
  break;
}

對於插入數據,我們要做的第一件事情是:檢查用戶是否具有對錶進行插入的適當特權,服務器通過調用check_access和check_grant函數在這裏進行檢查。

有了權限才可以做【插入】動作。

我們可以導航 /sql 目錄,如下:

Program Name          SQL statement type
------------          ------------------
sql_delete.cc         DELETE
sql_do.cc             DO
sql_handler.cc        HANDLER
sql_help.cc           HELP
sql_insert.cc         INSERT            // !
sql_load.cc           LOAD
sql_rename.cc         RENAME
sql_select.cc         SELECT
sql_show.cc           SHOW
sql_update.cc         UPDATE

sql_insert.cc是具體執行插入的操作。

上面的mysql_insert() 的方法具體實現,在sql_insert.cc文件中。

跟蹤 /sql/sql_insert.cc代碼

 int mysql_insert(THD *thd,TABLE_LIST *table_list, List<Item> &fields,
        List<List_item> &values_list,enum_duplicates duplic)
  {
    table = open_ltable(thd,table_list,lock_type);
    if (check_insert_fields(thd,table,fields,*values,1) ||
      setup_tables(table_list) ||
      setup_fields(thd,table_list,*values,0,0,0))
      goto abort;
    fill_record(table->field,*values);
    error=write_record(table,&info);                 // !
    query_cache_invalidate3(thd, table_list, 1);
    if (transactional_table)
      error=ha_autocommit_or_rollback(thd,error);
    query_cache_invalidate3(thd, table_list, 1);
    mysql_unlock_tables(thd, thd->lock);
    }

這裏就要開始,打開一張表。然後各種檢查,看插入表的字段是否有問題。不行就abort。

然後,開始填充記錄數據。最終調用write_record 寫記錄的方法。

由於write_record 會對應不同的存儲引擎,所以這裡有分支的。我這裏講解兩種

繼續跟蹤/sql/sql_insert.cc

  int write_record(TABLE *table,COPY_INFO *info)
  {
    table->file->write_row(table->record[0];           // !
  }

終於,要寫文件了。調用那個存儲引擎呢?看handler.h

  /* The handler for a table type.
     Will be included in the TABLE structure */

  handler(TABLE *table_arg) :
table(table_arg),active_index(MAX_REF_PARTS),
    ref(0),ref_length(sizeof(my_off_t)),
block_size(0),records(0),deleted(0),
    data_file_length(0), max_data_file_length(0),
index_file_length(0),
    delete_length(0), auto_increment_value(0), raid_type(0),
    key_used_on_scan(MAX_KEY),
    create_time(0), check_time(0), update_time(0), mean_rec_length(0),
    ft_handler(0)
    {}
  ...
  virtual int write_row(byte * buf)=0;

寫入之MyISAM的代碼路徑

官方文檔默認調用的是 ha_myisam::write_row

代碼 /sql/ha_myisam.cc

如下:

int ha_myisam::write_row(byte * buf)
{
  statistic_increment(ha_write_count,&LOCK_status);
   /* If we have a timestamp column, update it to the current time */
   if (table->time_stamp)
    update_timestamp(buf+table->time_stamp-1);
   /*
  If we have an auto_increment column and we are writing a changed row
    or a new row, then update the auto_increment value in the record.
  */
  if (table->next_number_field && buf == table->record[0])
    update_auto_increment();
  return mi_write(file,buf);     // !
}

這些以字母ha開頭的程序是處理程序的接口,而這個程序是myisam處理程序的接口。我們這裏就開始調用MyISAM了。

可以看到這裏調用了mi_write(file,buf);

跟蹤/myisam/mi_write.c

int mi_write(MI_INFO *info, byte *record)
{
  _mi_readinfo(info,F_WRLCK,1);
  _mi_mark_file_changed(info);
  /* Calculate and check all unique constraints */
  for (i=0 ; i < share->state.header.uniques ; i++)
  {
    mi_check_unique(info,share->uniqueinfo+i,record,
      mi_unique_hash(share->uniqueinfo+i,record),
      HA_OFFSET_ERROR);
  }

  ... to be continued in next snippet

這裡有很多唯一性的校驗,繼續看下面

 ... continued from previous snippet

  /* Write all keys to indextree */
  for (i=0 ; i < share->base.keys ; i++)
  {
    share->keyinfo[i].ck_insert(info,i,buff,
      _mi_make_key(info,i,buff,record,filepos)
  }
  (*share->write_record)(info,record);
  if (share->base.auto_key)
    update_auto_increment(info,record);
}

這裏就是我們寫入到文件的地方。至此,MySQL的插入操作結束。

路徑為:

main in /sql/mysqld.cc
handle_connections_sockets in /sql/mysqld.cc
create_new_thread in /sql/mysqld.cc
handle_one_connection in /sql/sql_parse.cc
do_command in /sql/sql_parse.cc
dispatch_command in /sql/sql_parse.cc
mysql_stmt_execute in /sql/sql_prepare.cc
mysql_execute_command in /sql/sql_parse.cc
mysql_insert in /sql/mysql_insert.cc
write_record in /sql/mysql_insert.cc
ha_myisam::write_row in /sql/ha_myisam.cc
mi_write in /myisam/mi_write.c

1.進入主函數入口

2.建立socket connection的請求

3.創建一個新的線程

4.處理線程,分配內存資源

5.do_command,是獲取packet第一字節,看做什麼操作,並接受餘下字節。

6.dispatch_command,分發操作,這裏分發的是insert。

7.mysql_stmt_execute,檢查是否為execute,初始化,準備做execute動作。

8.mysql_execute_command ,lex解析SQL語句,進入到SQLCOM_INSERT

9.mysql_insert ,開始做插入操作。調用write_record

10.write_record,準備寫入,看調用哪個存儲引擎,寫入前期準備工作

11.ha_myisam::write_row,ha_myisam進行插入寫入。

12.mi_write,最後做寫入操作。

文獻參考:https://dev.mysql.com/doc/internals/en/guided-tour-skeleton.html

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

【其他文章推薦】

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

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

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

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

※超省錢租車方案

.Net Core Configuration源碼探究

前言

    上篇文章我們演示了為Configuration添加Etcd數據源,並且了解到為Configuration擴展自定義數據源還是非常簡單的,核心就是把數據源的數據按照一定的規則讀取到指定的字典里,這些都得益於微軟設計的合理性和便捷性。本篇文章我們將一起探究Configuration源碼,去了解Configuration到底是如何工作的。

ConfigurationBuilder

    相信使用了.Net Core或者看過.Net Core源碼的同學都非常清楚,.Net Core使用了大量的Builder模式許多核心操作都是是用來了Builder模式,微軟在.Net Core使用了許多在傳統.Net框架上並未使用的設計模式,這也使得.Net Core使用更方便,代碼更合理。Configuration作為.Net Core的核心功能當然也不例外。
    其實並沒有Configuration這個類,這隻是我們對配置模塊的代名詞。其核心是IConfiguration接口,IConfiguration又是由IConfigurationBuilder構建出來的,我們找到IConfigurationBuilder源碼大致定義如下

public interface IConfigurationBuilder
{
    IDictionary<string, object> Properties { get; }

    IList<IConfigurationSource> Sources { get; }

    IConfigurationBuilder Add(IConfigurationSource source);

    IConfigurationRoot Build();
}

Add方法我們上篇文章曾使用過,就是為ConfigurationBuilder添加ConfigurationSource數據源,添加的數據源被存放在Sources這個屬性里。當我們要使用IConfiguration的時候通過Build的方法得到IConfiguration實例,IConfigurationRoot接口是繼承自IConfiguration接口的,待會我們會探究這個接口。
我們找到IConfigurationBuilder的默認實現類ConfigurationBuilder大致代碼實現如下

public class ConfigurationBuilder : IConfigurationBuilder
{
    /// <summary>
    /// 添加的數據源被存放到了這裏
    /// </summary>
    public IList<IConfigurationSource> Sources { get; } = new List<IConfigurationSource>();

    public IDictionary<string, object> Properties { get; } = new Dictionary<string, object>();

    /// <summary>
    /// 添加IConfigurationSource數據源
    /// </summary>
    /// <returns></returns>
    public IConfigurationBuilder Add(IConfigurationSource source)
    {
        if (source == null)
        {
            throw new ArgumentNullException(nameof(source));
        }
        Sources.Add(source);
        return this;
    }

    public IConfigurationRoot Build()
    {
        //獲取所有添加的IConfigurationSource里的IConfigurationProvider
        var providers = new List<IConfigurationProvider>();
        foreach (var source in Sources)
        {
            var provider = source.Build(this);
            providers.Add(provider);
        }
        //用providers去實例化ConfigurationRoot
        return new ConfigurationRoot(providers);
    }
}

這個類的定義非常的簡單,相信大家都能看明白。其實整個IConfigurationBuilder的工作流程都非常簡單就是將IConfigurationSource添加到Sources中,然後通過Sources里的Provider去構建IConfigurationRoot。

Configuration

通過上面我們了解到通過ConfigurationBuilder構建出來的並非是直接實現IConfiguration的實現類而是另一個接口IConfigurationRoot

ConfigurationRoot

通過源代碼我們可以知道IConfigurationRoot是繼承自IConfiguration,具體定義關係如下

public interface IConfigurationRoot : IConfiguration
{
    /// <summary>
    /// 強制刷新數據
    /// </summary>
    /// <returns></returns>
    void Reload();

    IEnumerable<IConfigurationProvider> Providers { get; }
}

public interface IConfiguration
{
    string this[string key] { get; set; }

    /// <summary>
    /// 獲取指定名稱子數據節點
    /// </summary>
    /// <returns></returns>
    IConfigurationSection GetSection(string key);

    /// <summary>
    /// 獲取所有子數據節點
    /// </summary>
    /// <returns></returns>
    IEnumerable<IConfigurationSection> GetChildren();
    
    /// <summary>
    /// 獲取IChangeToken用於當數據源有數據變化時,通知外部使用者
    /// </summary>
    /// <returns></returns>
    IChangeToken GetReloadToken();
}

接下來我們查看IConfigurationRoot實現類ConfigurationRoot的大致實現,代碼有刪減

public class ConfigurationRoot : IConfigurationRoot, IDisposable
{
    private readonly IList<IIConfigurationProvider> _providers;
    private readonly IList<IDisposable> _changeTokenRegistrations;
    private ConfigurationReloadToken _changeToken = new ConfigurationReloadToken();

    public ConfigurationRoot(IList<IConfigurationProvider> providers)
    {
        _providers = providers;
        _changeTokenRegistrations = new List<IDisposable>(providers.Count);
        //通過便利的方式調用ConfigurationProvider的Load方法,將數據加載到每個ConfigurationProvider的字典里
        foreach (var p in providers)
        {
            p.Load();
            //監聽每個ConfigurationProvider的ReloadToken實現如果數據源發生變化去刷新Token通知外部發生變化
            _changeTokenRegistrations.Add(ChangeToken.OnChange(() => p.GetReloadToken(), () => RaiseChanged()));
        }
    }

    //// <summary>
    /// 讀取或設置配置相關信息
    /// </summary>
    public string this[string key]
    {
        get
        {
            //通過這個我們可以了解到讀取的順序取決於註冊Source的順序,採用的是後來者居上的方式
            //后註冊的會先被讀取到,如果讀取到直接return
            for (var i = _providers.Count - 1; i >= 0; i--)
            {
                var provider = _providers[i];
                if (provider.TryGet(key, out var value))
                {
                    return value;
                }
            }
            return null;
        }
        set
        {
            //這裏的設置只是把值放到內存中去,並不會持久化到相關數據源
            foreach (var provider in _providers)
            {
                provider.Set(key, value);
            }
        }
    }

    public IEnumerable<IConfigurationSection> GetChildren() => this.GetChildrenImplementation(null);

    public IChangeToken GetReloadToken() => _changeToken;

    public IConfigurationSection GetSection(string key)
        => new ConfigurationSection(this, key);

    //// <summary>
    /// 手動調用該方法也可以實現強制刷新的效果
    /// </summary>
    public void Reload()
    {
        foreach (var provider in _providers)
        {
            provider.Load();
        }
        RaiseChanged();
    }

    //// <summary>
    /// 強烈推薦不熟悉Interlocked的同學研究一下Interlocked具體用法
    /// </summary>
    private void RaiseChanged()
    {
        var previousToken = Interlocked.Exchange(ref _changeToken, new ConfigurationReloadToken());
        previousToken.OnReload();
    }
}

上面展示了ConfigurationRoot的核心實現其實主要就是兩點

  • 讀取的方式其實是循環匹配註冊進來的每個provider里的數據,是後來者居上的模式,同名key后註冊進來的會先被讀取到,然後直接返回
  • 構造ConfigurationRoot的時候才把數據加載到內存中,而且為註冊進來的每個provider設置監聽回調

ConfigurationSection

其實通過上面的代碼我們會產生一個疑問,獲取子節點數據返回的是另一個接口類型IConfigurationSection,我們來看下具體的定義

public interface IConfigurationSection : IConfiguration
{
    string Key { get; }

    string Path { get; }

    string Value { get; set; }
}

這個接口也是繼承了IConfiguration,這就奇怪了分明只有一套配置IConfiguration,為什麼還要區分IConfigurationRoot和IConfigurationSection呢?其實不難理解因為Configuration可以同時承載許多不同的配置源,而IConfigurationRoot正是表示承載所有配置信息的根節點,而配置又是可以表示層級化的一種結構,在根配置里獲取下來的子節點是可以表示承載一套相關配置的另一套系統,所以單獨使用IConfigurationSection去表示,會顯得結構更清晰,比如我們有如下的json數據格式

{
  "OrderId":"202005202220",
  "Address":"銀河系太陽系火星",
  "Total":666.66,
  "Products":[
    {
      "Id":1,
      "Name":"果子狸",
      "Price":66.6,
      "Detail":{
          "Color":"棕色",
          "Weight":"1000g"
      }
    },
    {
      "Id":2,
      "Name":"蝙蝠",
      "Price":55.5,
      "Detail":{
          "Color":"黑色",
          "Weight":"200g"
      }
    }
  ]
}

我們知道json是一個結構化的存儲結構,其存儲元素分為三種一是簡單類型,二是對象類型,三是集合類型。但是字典是KV結構,並不存在結構化關係,在.Net Corez中配置系統是這麼解決的,比如以上信息存儲到字典中的結構就是這種

Key Value
OrderId 202005202220
Address 銀河系太陽系火星
Products:0:Id 1
Products:0:Name 果子狸
Products:0:Detail:Color 棕色
Products:1:Id 2
Products:1:Name 蝙蝠
Products:1:Detail:Weight 200g

如果我想獲取Products節點下的第一條商品數據直接

IConfigurationSection productSection = configuration.GetSection("Products:0")

類比到這裏的話根配置IConfigurationRoot里存儲了訂單的所有數據,獲取下來的子節點IConfigurationSection表示了訂單里第一個商品的信息,而這個商品也是一個完整的描述商品信息的數據系統,所以這樣可以更清晰的區分Configuration的結構,我們來看一下ConfigurationSection的大致實現

public class ConfigurationSection : IConfigurationSection
{
    private readonly IConfigurationRoot _root;
    private readonly string _path;
    private string _key;

    public ConfigurationSection(IConfigurationRoot root, string path)
    {
        _root = root;
        _path = path;
    }

    public string Path => _path;

    public string Key
    {
        get
        {
            return _key;
        }
    }

    public string Value
    {
        get
        {
            return _root[Path];
        }
        set
        {
            _root[Path] = value;
        }
    }

    public string this[string key]
    {
        get
        {
            //獲取當前Section下的數據其實就是組合了Path和Key
            return _root[ConfigurationPath.Combine(Path, key)];
        }
        set
        {
            _root[ConfigurationPath.Combine(Path, key)] = value;
        }
    }
    
    //獲取當前節點下的某個子節點也是組合當前的Path和子節點的標識Key
    public IConfigurationSection GetSection(string key) => _root.GetSection(ConfigurationPath.Combine(Path, key));
    //獲取當前節點下的所有子節點其實就是在字典里獲取包含當前Path字符串的所有Key
    public IEnumerable<IConfigurationSection> GetChildren() => _root.GetChildrenImplementation(Path);
    public IChangeToken GetReloadToken() => _root.GetReloadToken();
}

這裏我們可以看到既然有Key可以獲取字典里對應的Value了,為何還需要Path?通過ConfigurationRoot里的代碼我們可以知道Path的初始值其實就是獲取ConfigurationSection的Key,說白了其實就是如何獲取到當前IConfigurationSection的路徑。比如

//當前productSection的Path是 Products:0
IConfigurationSection productSection = configuration.GetSection("Products:0");
//當前productDetailSection的Path是 Products:0:Detail
IConfigurationSection productDetailSection = productSection.GetSection("Detail");
//獲取到pColor的全路徑就是 Products:0:Detail:Color
string pColor = productDetailSection["Color"];

而獲取Section所有子節點
GetChildrenImplementation來自於IConfigurationRoot的擴展方法

internal static class InternalConfigurationRootExtensions
{
    //// <summary>
    /// 其實就是在數據源字典里獲取Key包含給定Path的所有值
    /// </summary>
    internal static IEnumerable<IConfigurationSection> GetChildrenImplementation(this IConfigurationRoot root, string path)
    {
        return root.Providers
            .Aggregate(Enumerable.Empty<string>(),
                (seed, source) => source.GetChildKeys(seed, path))
            .Distinct(StringComparer.OrdinalIgnoreCase)
            .Select(key => root.GetSection(path == null ? key : ConfigurationPath.Combine(path, key)));
    }
}

相信講到這裏,大家對ConfigurationSection或者是對Configuration整體的思路有一定的了解,細節上的設計確實不少。但是整體實現思路還是比較清晰的。關於Configuration還有一個比較重要的擴展方法就是將配置綁定到具體POCO的擴展方法,該方法承載在ConfigurationBinder擴展類了,由於實現比較複雜,也不是本篇文章的重點,有興趣的同學可以自行查閱,這裏就不做探究了。

總結

    通過以上部分的講解,其實我們可以大概的將Configuration配置相關總結為兩大核心抽象接口IConfigurationBuilder,IConfiguration,整體結構關係可大致表示成如下關係

    配置相關的整體實現思路就是IConfigurationSource作為一種特定類型的數據源,它提供了提供當前數據源的提供者ConfigurationProvider,Provider負責將數據源的數據按照一定的規則放入到字典里。IConfigurationSource添加到IConfigurationBuilder的容器中,後者使用Provide構建出整個程序的根配置容器IConfigurationRoot。通過獲取IConfigurationRoot子節點得到IConfigurationSection負責維護子節點容器相關。這二者都繼承自IConfiguration,然後通過他們就可以獲取到整個配置體系的數據數據操作了。

    以上講解都是本人通過實踐和閱讀源碼得出的結論,可能會存在一定的偏差或理解上的誤區,但是我還是想把我的理解分享給大家,希望大家能多多包涵。如果有大家有不同的見解或者更深的理解,可以在評論區多多留言。

歡迎掃碼關注我的公眾號 本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

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

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

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

※超省錢租車方案

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

探索ADC的原理(自製3位并行比較型ADC)

摘要

      本文通過列舉歷史中出現的產品,梳理了模數轉換器在20世紀30年代~~20世紀80年代末的發展歷史。接下來,簡要介紹模數轉換器的原理、技術指標、分類和未來發展方向。最後,提供了一種自製3位FLASH型ADC的方法(該方法經過了作者的測試且價格在20元以下)。

 

參考文獻

    涉及到的數據手冊(eyg7)

    Flash ADC_Chapter 13 – Digital-Analog Conversion

    ZepToBars

    《Analog-Digital Conversion》 Chapter I Walt Kester

    《数字电子技術》第六版 康華光

    数字电子技術 西南石油大學課程中心

 

ADC的歷史

     世界上記載的第一個”純电子“的A/D轉換器於1939年被亞歷克·哈利·里夫斯(Alec Harley Reeves)發明,該設計的採樣率為6KSPS,分辨率為5位。

亞歷克·哈利·里夫斯設計的A/D轉換器原理圖,《Analog-Digital Conversion》 Chapter I Walt Kester

     1947年,鍺晶體管於貝爾實驗室誕生。

     1946年,ENIAC問世,現代数字計算機的鼻祖,為A/D的蓬勃發展做鋪墊。

     1948年,貝爾實驗室發明了5位、8KSPS的逐次逼近型A/D轉換器。

     得力於电子束編碼管技術,在1960年左右出現了12MSPS、9位的編碼器(A/D)。

 电子束編碼管原理圖,《Analog-Digital Conversion》 Chapter I Walt Kester

     1954年,硅晶體管於德州儀器誕生。

     1954年,伯納德·M·戈登(Bernard M. Gordon)發明了11位、50KSPS的基於真空管的A/D,這被認為是世界上第一個商業化的A/D轉換器。“Datrac”功率500W,售價8000~~9000美元。

伯納德·M·戈登發明的 “Datrac”,《Analog-Digital Conversion》 Chapter I Walt Kester

     1958/1959,集成電路問世,德州儀器(1958),仙童半導體(1959)。

     1963~1965年,為了給美國軍方的雷達提供高速A/D,貝爾實驗室的John M. Eubanks和Robert C. Bedingfield研發了8位、10MSPS的A/D,其功率為150W、售價10000美元。

 John M. Eubanks和Robert C. Bedingfield研發的A/D,《Analog-Digital Conversion》 Chapter I Walt Kester

      1969年,Pastoriza公司利用分立元件製造了12位、10us、2.3W的逐次逼近型A/D樣機—-“ADC-12U”,售價800美元。

 “ADC-12U”原型機,《Analog-Digital Conversion》 Chapter I Walt Kester

     1978年,Paul Brokaw設計了第一個完整的單芯片ADC,型號為AD571,使用了雙極型工藝,參數為:10位、25us、SAR結構。同年,誕生了最具重要意義的SAR ADC–AD574。這時的A/D可以說開始走向現代。

AD571,源

AD571框圖,《Analog-Digital Conversion》 Chapter I Walt Kester

     1988年,Crystal Semiconductor推出了世界上第一個單芯片商業化的ε-Δ ADC–CSZ5316,參數:16位、20KSPS,可以用於語音處理。

     接下來的歷史中,各廠商不斷改進ADC的性能、推出更多不同用途的ADC。總而言之,就是讓ADC進入千家萬戶。

 

ADC的原理

     ADC(Analog to Digital Converter)是一類將模擬信號(連續信號)轉換為数字信號(離散信號)的器件,按原理可分為:并行比較型A/D轉換器(FLASH ADC)、逐次比較型A/D轉換器(SAR ADC)和雙積分式A/D轉換器(Double Integral ADC)。

     模擬信號,下圖中的ui(t)是一個輸入的模擬電壓信號,可以想象成從一個麥克風輸出的音頻信號。

     数字信號,現代計算機能夠處理的信號,表現為下圖中的“n位数字量輸出”。

     香農-奈奎斯特(Shannon & Nyquist)採樣定理規定,使恢復出的信號不失真的條件:採樣頻率大於原始信號頻率的兩倍,即 Fs >= 2Fi

      一個連續的電壓信號ui(t)通過一個由方波CPs控制的開關S之後施加到電容C上,由於電容兩端的電壓不會突變,可知在S斷開時C將維持ui(t)在開關斷開瞬間的電壓一段時間,直到開關S再次打開。這樣,一個模擬的電壓信號就轉換成了採樣展寬信號us(t),其中CPs的頻率就是採樣頻率Fs。然後,由ADC的数字編碼電路將採樣展寬信號us(t)轉換成n位的数字量dn-1 : d0並輸出。

     通過上述步驟,一個連續的電壓信號就轉換成了n位的数字量,而實現該過程的器件叫做模擬-数字轉換器(ADC)。

AD轉換的一般原理,”数字电子技術”  SWPU

TLC5540I,8位、40MSPS、CMOS工藝的并行比較型A/D轉換器的版圖,https://zeptobars.com/,license: CC BY 3.0,未修改

 

 ADC的主要性能指標

     分辨率:ADC能分辨的最小電壓,通常用位數表示,例如:8位。一個n=8位的ADC,參考電壓為5V,則其能分辨的最小電壓為 5 / 2^n = 19.53mV

     轉換時間:ADC從控制信號到來開始,到輸出端得到穩定的数字信號所經歷的時間。

     轉換精度:ADC輸出的数字量所表示的模擬值與實際輸入的模擬量之間的偏差。

 

ADC的分類

     并行比較型A/D轉換器:這是本文嘗試構建的ADC,其由電阻分壓器、電壓比較器(運算放大器)、D觸發器和優先級編碼器構成。其原理簡單,將在後文介紹。

                            優點:1.轉換時間最短,其轉換周期為通過比較器、觸發器和優先級編碼器的時間總和(見下式),這個數值通常很小。

T轉 = T比 + T寄 + T編

                            缺點:1.造價高昂,隨着分辨位數的提高,所需的元件幾乎按幾何級數增長,如:一個n位的并行比較型ADC,需要2^n – 1個比較器和2^n – 1個觸發器,假如n=12,那麼一共需要8190個比較器和觸發器!

                                    2.對集成電路的工藝要求很高。

                    常見的型號:AD9012,TTL工藝,分辨率為8位,採樣率為100MSPS,模擬輸入電壓範圍 -Vs~~+0.5V(Vs為芯片供電電壓)。

                                     AD9002,ECL工藝(射極耦合邏輯),分辨率為8位,採樣率為150MSPS,模擬輸入電壓範圍 -Vs~~+0.5V(Vs為芯片供電電壓)。

                                     AD9020,TTL工藝,分辨率為10位,採樣率為60MSPS,雙極性模擬輸入(+-1.75V)。

3位并行比較型A/D轉換器原理圖,《数字电子技術》第六版 康華光

AD9012原理圖,Analog Devices 

AD9002原理圖,Analog Devices  

AD9020原理圖,Analog Devices 

1107PV2,蘇聯,8位、20MSPS,典型的并行比較型A/D轉換器的版圖https://zeptobars.com/,license: CC BY 3.0,未修改

1107PV2,蘇聯,8位、20MSPS,典型的并行比較型A/D轉換器的比較器的版圖https://zeptobars.com/,license: CC BY 3.0,未修改

 

     逐次比較型A/D轉換器:原理像天平,對輸入的模擬電壓信號與不同權值的電壓做多次比較,使得轉換所得的数字量在數值上不斷逼近輸入的模擬量。通常由控制邏輯電路、數據寄存器、移位寄存器、D/A轉換器(Digital Analog Converter)和電壓比較器構成。

                            優點:1.轉換速度快。其轉換周期等於 分辨率 * 時鐘周期(見下式),如一個8位的逐次比較型A/D轉換器,時鐘周期為10us,則其轉換周期為80us。

T轉 = n * Tclk  (n為分辨率)

                   常見的型號:1.ADC0808/ADC0809,8位逐次比較型A/D轉換器,轉換時間100us,輸入電壓範圍0~~5V,可接入8個模擬量輸入。

                                    2.ADC0803/ADC0804,8位逐次比較型A/D轉換器,在1MHz的時鐘頻率下,轉換時間在66~~73us之間,支持一對差分模擬電壓輸入。

逐次比較型A/D轉換器原理圖 ,《数字电子技術》第六版 康華光

 

 ADC0808/ADC0809原理圖,National Semiconductor

ADC0803/ADC0804原理圖,Philips Semiconductors

 

     雙積分式A/D轉換器:一種間接的A/D轉換器,其分別對輸入電壓和參考電壓進行兩次積分,將輸入電壓平均值變換成與之成正比的時間間隔,然後利用時鐘脈衝和計數器測出此時間間隔,進而在輸出端得到與模擬量相應的数字量。通常由積分器(運算放大器及相應的外部電路)、過零比較器(運算放大器)、時鐘脈衝控制門和計數器等構成。

                         優點:1.抗工頻干擾能力強。通過對輸入電壓的平均值進行變換來實現抗干擾。 

                         缺點:1.轉換速度最慢。

                常見的型號:TLC7135,4.5位雙積分式A/D轉換器,CMOS工藝,差分電壓輸入。

雙積分式A/D轉換器原理圖,《数字电子技術》第六版 康華光

TLC7135数字部分原理圖,Texas Instruments

 

ADC的未來發展方向

     ADC在未來會變得性能更強、價格更低、功耗更低、通用性和專業性更強。

     性能:從歷史上看,對ADC性能的改進主要集中在改進架構改善製造工藝兩個方面。ADC有很多架構,典型的包括:FLASH、SAR和雙積分;其他的有:流水線等。在集成電路發展的過程中,出現了許多的工藝:雙極性、ECL、CMOS、CB、BiCMOS、GaAs……這些工藝可以幫助改進ADC的性能。

     價格:隨着集成電路工藝的不斷成熟,價格變低只是時間問題。

     功耗:得力於集成電路工藝的改善,如:使用更低線寬的IC的功耗會低於高線寬的IC。功耗同時也取決於ADC架構。

 

元件清單(” * “為可選)

————————————————————時鐘發生器部分——————————————————————–

  NE555           *1

  *DIP-8芯片座  *1

  8位撥碼開關    *1

  *3pin排針       *1

  3.9K電阻        *1

  68K電阻         *1

  10uF無極電容   *1

  1uF無極電容     *1

  100nF無極電容 *1

  10nF無極電容   *2

  1nF無極電容     *1

  100pF無極電容 *1

  10pF無極電容   *1

  1pF無極電容     *1

所有元件合照(時鐘發生器部分)

———————————————————————————————————————————————–

————————————————————數模轉換器部分——————————————————————–

  MCP6004        *1(可以使用LM324替換)

  *DIP-14芯片座 *1

  CD4042B *1

  CD4532B *1

  *DIP-16芯片座 *2

  2K可調電阻器   *1

  330R電阻   *3

  390R電阻   *1

  1K電阻       *5

  LED-G       *3

  *Pin-3排母 *1

  *Pin-2排針 *1

所有元件合照(數模轉換器部分,不含Pin-2排針)

———————————————————————————————————————————————–

 

電路原理

 

總原理圖(1)

 

總原理圖(2)

————————————————————時鐘發生器部分——————————————————————–

     555定時器工作在多諧振蕩器模式,通過撥碼開關選擇不同的電容來產生不同頻率的方波。

 

基於555定時器的時鐘發生器原理圖

————————————————————————————————————————————————

————————————————————數模轉換器部分——————————————————————–

     比較器:左側的電阻分壓網絡為右側的四個比較器的反相輸入端提供階梯狀的參考電壓(4V、3V、2V、1V),可調電阻模擬輸入到四個比較器同相端的模擬電壓(0~~5V)。比較器通過比較同相輸入端與反相輸入端電壓的大小,輸出0V(Vp<Vn)或5V(Vp>Vn)給後面的D鎖存器。

 

運放的特性圖

     四路D鎖存器:在時鐘的每一個上升沿,將四個運放輸出的電壓(比較結果)存儲起來並交給後面的編碼器。

     優先級編碼器:對來自鎖存器的四個比較結果進行編碼,並輸出給計算機處理(如果有計算機的話)。

———————————————————————————————————————————————–

 

集成電路簡介

     MCP6004:微芯公司生產的低功耗1MHz帶寬的4路運算放大器,本項目的運算放大器均工作在飽和區。

 

MCP6004實物圖

MCP6004引腳定義

     CD4042B:CMOS四路D鎖存器,在本項目中使用上升沿觸發,時鐘由555定時器提供,用於保存MCP6004輸出的4位數據。

 

CD4042B實物圖

CD4042B引腳定義

CD4042B真值表

     CD4532B: CMOS的8位優先級編碼器,用於對CD4042B鎖存的數據進行編碼。

 

CD4532B實物圖

CD4532B引腳定義

CD4532B真值表

 

測試

 ————————————————————時鐘發生器部分——————————————————————–

     此555時鐘發生電路,實際測試可以產生1Hz、10Hz、100Hz、1KHz、10KHz、100KHz、0.4MHz、0.7MHz的方波信號。實測中,產生的0.4MHz和0.7MHz與設計的1MHz、10MHz存在較大的誤差,可能是電容的問題(這兩個頻率對應所使用的是貼片電容)。

時鐘發生器(正面)

時鐘發生器(反面)

實測產生的最大頻率的波形(Vcc=5V下,Vpp=4.7V)

————————————————————————————————————————————————

————————————————————數模轉換器部分——————————————————————–

     在時鐘為400KHz下,此并行比較型A/D可以正常工作;使用700KHz的時鐘會導致轉換故障。

     主要參數:A/D分辨率為3位(嚴格來說只有2位,可以在不改變架構的情況下通過增加4個比較器拓增至3位)

                   採樣率為400KSPS~~700KSPS。

     功耗:20mA@5V = 100mW (包含時鐘發生器部分)

 

當輸入電壓為2.5V時,輸出的情況(可以看出LED指示“101”,正好是對“1100”編碼的結果)

 

正面(1)

 

正面(2)

 

反面

———————————————————————————————————————————————–

 

聲明

     此教程未經DLHC允許,禁止轉載。所有引用均註明了出處。DLHC保留所有權利。

     由於本人學識有限且整理較為倉促,如有錯誤或不妥,請指正。

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

【其他文章推薦】

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

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

※回頭車貨運收費標準

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

※超省錢租車方案

《Java核心技術(卷1)》筆記:第7章 異常、斷言和日誌

異常

  1. (P 280)異常處理需要考慮的問題:

    • 用戶輸入錯誤
    • 設備錯誤
    • 物理限制
    • 代碼錯誤
  2. (P 280)傳統的處理錯誤的方法是:返回一個特殊的錯誤碼,常見的是返回-1或者null引用

  3. (P 280)在Java中,方法出現錯誤時,它會立即退出,不返回任何值,而是拋出一個封裝了錯誤信息的對象

  4. (P 280)Java中所有的異常都是由Throwable繼承而來,它下面又分解為兩個分支:ErrorException

    • Error:描述了Java運行時系統的內部錯誤資源耗盡錯誤(對於處理這種錯誤,你幾乎無能為力
    • Exception:又分解為兩個分支:RuntimeException(由編程錯誤導致,如果出現該異常,那一定是你的問題)和其他異常(諸如IO錯誤這類問題)
      • 派生於RuntimeException的異常:
        • 錯誤的強制類型轉換
        • 數組訪問越界
        • 訪問null指針
      • 不是派生於RuntimeException的異常:
        • 試圖超越文件末尾繼續讀取數據
        • 試圖打開一個不存在的文件
        • 試圖根據特定的字符串查找Class對象,而這個字符串表示的類並不存在

    graph TD Throwable[Throwable]–>Error[Error] Throwable[Throwable]–>Exception[Exception] Error[Error]–>OtherError1[…] Error[Error]–>OtherError2[…] Error[Error]–>OtherError3[…] Exception[Exception]–>RuntimeException[RuntimeException] Exception[Exception]–>IOException[IOException] Exception[Exception]–>OtherException[…] IOException[IOException]–>OtherIOException1[…] IOException[IOException]–>OtherIOException2[…] IOException[IOException]–>OtherIOException3[…] RuntimeException[RuntimeException]–>OtherRuntimeException1[…] RuntimeException[RuntimeException]–>OtherRuntimeException2[…] RuntimeException[RuntimeException]–>OtherRuntimeException3[…]

  5. (P 281)派生於ErrorRuntimeException的所有異常稱為非檢查型異常,所有其他異常稱為檢查型異常

  6. (P 282)如果沒有處理器捕獲異常對象,那麼當前執行的線程就會終止

  7. (P 283)必須在方法的首部列出所有檢查型異常類型,但是不需要聲明從Error繼承的異常,也不應該聲明從RuntimeException繼承的那些非檢查型異常

  8. (P 283)如果在子類中覆蓋了超類的一個方法,子類方法中聲明的檢查型異常不能比超類方法中聲明的異常更通用(子類方法可以拋出更特定的異常,或者根本不拋出任何異常)。如果超類方法沒有拋出任何檢查型異常,子類也不能拋出任何檢查型異常

  9. (P 288)同一個catch子句中可以捕獲多個異常類型,如果一些異常的處理邏輯是一樣的,就可以合併catch子句。只有當捕獲的異常類型彼此之間不存在子類關係時才需要這個特性

    try {
        ...
    } catch (FileNotFoundException | UnknownHostException e) {
        ...
    } catch (IOException e) {
        ...
    }
    
  10. (P 289)可以在catch子句中拋出一個異常,此時,可以把原始異常設置為新異常的“原因”

    try {
        ...
    } catch (SQLException original) {
        var e = new ServletException("database error");
        e.initCause(original);
        throw e;
    }
    

    捕獲異常時,獲取原始異常

    Throwable original = caughtException.getCause();
    
  11. (P 292)一種推薦的異常捕獲寫法:內層try語句塊只有一個職責,就是確保釋放資源外層try語句塊也只有一個職責,就是確保報告出現的錯誤

    try {
        try {
            ...
        } finally {
            // 釋放資源
        }
    } catch (Exception e) {
        // 報告錯誤
    }
    
  12. (P 293)Java 7中,對於實現了AutoCloseable接口的類,可以使用帶資源的try語句(try-with-resources):

    try (Resources res = ...) {
        // Work with res
        ...
    }
    

    Java 9中,可以在try首部中提供之前聲明的事實最終變量(effectively final variable):

    try (res) {
        // Work with res
        ...
    } // res.close() called here
    
  13. (P 294)在try-with-resources語句中,如果try塊拋出一個異常,而且close方法也拋出一個異常,則原來的異常會重新拋出,而close方法拋出的異常會“被抑制”(可以通過getSuppressed方法得到這些被抑制的異常)

  14. (P 294)可以通過StackWalker類處理堆棧軌跡

    var walker = StackWalker.getInstance();
    walker.forEach(frame -> ...); // 例如:walker.forEach(System.out::println);
    
  15. (P 298)使用異常的一些技巧:

    • 異常處理不能代替簡單的測試(捕獲處理異常的成本很高,只在異常情況下使用異常
    • 不要過分的細化異常(有必要將整個任務包在一個try語句塊中,將正常處理與錯誤處理分開
    • 充分利用異常層次結構
    • 不要壓制異常(異常非常重要時,應該適當地進行處理)
    • 在檢測錯誤時,“苛刻”要比放任更好
    • 不要羞於傳遞異常(最好繼續傳遞異常,而不是自己捕獲)

斷言

  1. (P 301)Java中引入了關鍵字assert,其有如下兩種形式:

    assert condition;
    assert condition : expression;
    

    這兩個語句都會計算條件,如果結果為false,則拋出一個AssertionError異常。在第二個語句中,表達式將傳入AssertionError對象的構造器,並轉換為一個消息字符串

  2. (P 301)默認情況下,斷言是禁用的,可以使用-enableassertions或者-ea選項啟用斷言:

    java -enableassertions MyApp
    

    禁用斷言可以使用-disableassertions-da

  3. (P 302)斷言只應該用於在測試階段確定程序內部錯誤的位置

日誌

  1. (P 305)基本日誌的使用:

    • 生成簡單的日誌記錄

      Logger.getGlobal().info("hello world!");
      
    • 取消所有日誌

      Logger.getGlobal().setLevel(Level.OFF);
      
  2. (P 305)高級日誌的使用:

    • 創建或獲取日誌記錄器

      private static final Logger myLogger = Logger.getLogger("className"); // className是全限定類名
      
    • 設置日誌級別

      logger.setLevel(Level.FINE); // FINE以及更高級別的日誌都會被記錄
      
    • 記錄日誌

      // 調用相應級別的日誌記錄方法
      logger.warning(message);
      logger.fine(message);
      
      // 使用log方法並指定級別
      logger.log(Level.FINE, message);
      
      // 跟蹤執行流的方法
      logger.entering("className", "methodName", new Object[]{ params... });
      logger.exiting("className", "methodName", result);
      
      // 在日誌記錄中包含異常的描述
      logger.throwing("className", "methodName", exception);
      logger.log(Level.WARNING, message, exception);
      
  3. (P 305)7個日誌級別:

    • SEVERE
    • WARNING
    • INFO
    • CONFIG
    • FINE
    • FINER
    • FINEST
  4. (P 307)可以通過配置文件修改日誌系統的各個屬性,默認情況下,配置文件位於:

    conf/logging.properties
    

    指定特定位置的配置文件:

    java -Djava.util.logging.config.file=configFile MainClass
    

    指定日誌記錄器的日誌級別:在日誌記錄器名後面追加後綴.level,例如

    com.mycompany.myapp.level=FINE
    
  5. (P 313)日誌技巧

    • 對一個簡單的應用,選擇一個日誌記錄器,可以把日誌記錄器命名為與主應用包一樣的名字
    • 默認的日誌配置會把級別等於或高於INFO的所有消息記錄到控制台,用戶可以覆蓋這個默認配置,最好在你的應用中安裝一個更合適的默認配置
    • 所有級別為INFO、WARNING和SEVERE的消息都將显示到控制台上
      • 只將對程序用戶有意義的消息設置為以上這幾個級別
      • 程序員想要的日誌消息設定為FINE級別是一個很好的選擇
  6. (P 321)調試技巧

    • 打印或日誌記錄變量的值

    • 在每一個類中放置一個單獨的main方法,以便獨立地測試類

    • 使用JUnit

    • 日誌代理,它是一個子類的對象,可以截獲方法調用,記錄日誌,然後調用超類中的方法

      var generator = new Random() {
          public double nextDouble() {
              double result = super.nextDouble();
              Logger.getGlobal().info("nextDouble: " + result);
              return result;
          }
      }
      
    • 利用Throwable類的printStackTrace方法,可以從任意的異常對象獲得堆棧軌跡

    • 一般來說,堆棧軌跡显示在System.err上。如果想要記錄或显示堆棧軌跡,可以將它捕獲到一個字符串中

      var out = new StringWriter();
      new Throwable().printStackTrace(new PrintWriter(out));
      String description = out.toString();
      
    • 通常,將程序錯誤記入一個文件會很有用:

      java MyProgram > errors.txt        # 錯誤,錯誤被發送到System.err而不是System.out
      java MyProgram 2> errors.txt       # 正確,只輸出System.err
      java MyProgram 1> errors.txt 2>&1  # 同時捕獲System.out和System.err
      
    • 將未捕獲的異常的堆棧軌跡記錄到一個文件中,而不是直接輸出到System.err,可以使用靜態方法Thread.setDefaultUncaughtExceptionHandler改變未捕獲異常的處理器

      Thread.setDefaultUncaughtExceptionHandler(
          new Thread.UncaughtExceptionHandler() {
              public void uncaughtException(Thread t, Throwable e) {
                  // save information in log file
              }
          }
      )
      
    • 要想觀察類的加載過程,啟動Java虛擬機時可以使用-verbose標誌

    • -Xlint選項告訴編譯器找出常見的代碼問題

      javac -Xlint sourceFiles
      
    • Java虛擬機增加了對Java應用程序的監控和管理支持,允許在虛擬機中安裝代理來跟蹤內存消耗、線程使用、類加載等情況。jconsole工具可以显示有關虛擬機性能的統計結果

    • Java任務控制器:一個專業級性能分析和診斷工具

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

【其他文章推薦】

※超省錢租車方案

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

※回頭車貨運收費標準

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

跳躍表確定不了解下

redis源碼分析系列文章

[Redis源碼系列]在Liunx安裝和常見API 

為什麼要從Redis源碼分析 

String底層實現——動態字符串SDS 

Redis的雙向鏈表一文全知道

面試官:說說Redis的Hash底層 我:……(來自閱文的面試題)

前言

hello,大家好,周五見了。前面幾周我們一起看了Redis底層數據結構,如動態字符串SDS,雙向鏈表Adlist,字典Dict,如果有對Redis常見的類型或底層數據結構不明白的請看上面傳送門。

今天我們來看下ZSET的底層架構,如果不知道ZSET是什麼的,可以看上面傳送門第一篇。簡單來說,ZSET是Redis提供的根據數據和分數來判斷其排名的數據結構。最常見的就是微信運動的排名,每個用戶對應自己的步數,每天晚上可以給出用戶的排名。

有小夥伴可能會想,如果是實現排名的話,各種排序方法都可以實現的,沒必要引入Redis的ZSET結構啊?

當然,如果是採用排序方法的話,是可以實現相同功能的,但是代碼裏面需要硬編碼,會添加工作量,還會提供代碼的Bug哦,哈哈哈。而且Redis的底層是C實現的,直接操作內存,速度也會比Java方法實現提升。

綜上,使用Redis的ZSET結構,好處多多。那話不多說,開始把。在正式開始之前,我們需要引入下跳躍表的概念,其是ZSET結構的底層實現。以下可能有點枯燥,我盡量說的簡單點哈。

什麼是跳躍表?

對於數據量大的鏈表結構,插入和刪除比較快,但是查詢速度卻很慢。那是因為無法直接獲取某個節點,需要從頭節點開始,藉助某個節點的next指針來獲取下一節點。即使數據是有序排放的,想要查詢某個數據,只能從頭到尾遍歷變量,查詢效率會很低,時間複雜度為O(n)。

如果我們需要快速查詢鏈表有啥辦法呢?有同學說用數組存放,但是如果不改數據結構呢?

我們可以先想想在有序數組結構中有二分法,每次將範圍都縮小一半,這樣查詢速度提升了很多,那麼在鏈表中能不能也使用這種思想。

這就到了今天講的主角——跳躍表。(一點也生硬的引出概念)

步驟一  新建有序單項鏈表

先看下圖有序單向鏈表,存放了1,2,3,4,5,6,7這7個元素。

步驟二 抽取二級索引節點

我們可以在鏈表中抽取部分節點,下圖抽取了1,3,5,7四個節點,也就是每兩個節點提取了一個節點到上級,抽取出來的叫做索引。

注意不是每次都能抽取到這麼完美,這其實就跟拋硬幣一樣,每個硬幣的正反兩面的概率是一樣的,都是1/2。當數據量小的時候,正反的概率可能差別較大。但是隨着數據量的加大,正反的概率越來越接近於1/2。類比過來是一個意思,每個節點的機會都是一樣的,要麼停留原級,要麼提取到上級,概率都是1/2。但是隨着節點數量的增加,抽取的節點越來越接近與1/2。

步驟三 抽取三級索引節點

我們可以在鏈表中抽取部分節點,下圖抽取了1,5兩個節點,也就是每兩個節點提取了一個節點到上級,抽取出來的叫做索引。

步驟四 類二分法查詢

我們假設要查找值為6的節點,先從三級索引開始,找到值為1的節點,發現比5小,根據值為1節點的next指針,找到值為5的節點,5後面沒有其他的三級索引啦。

於是順着往下找,到了二級索引,根據值為5的節點的next指針找到值為7的節點,發現比6小,說明要找到的節點6在此範圍內。

再接着到了一級索引位置,根據值為5的節點next指針指向值為6的節點,發現是想要查詢的數據,所以查詢過程結束。

根據上面的查詢過程(下圖的藍色連線),我們發現其採用的核心思想是二分法,不斷縮小查詢範圍,如果在上層索引找到區間,則順延深入到下一層找到真正的數據。

總結

從上面的整個過程中可以看出,數據量小的時候,這種拿空間換時間,消耗內存方法的並不是最優解。所以Redis的zset結構在數據量小的時候採用壓縮表,數據量大的時候採用跳躍表。

像這種鏈表加多級索引的結構,就是跳躍表。這名字起的形象,過程是跳躍着來查詢的。

Redis中跳躍表圖解

下圖簡單來說是對跳躍表的改進和再封裝,首先引入了表頭的概念,這與雙向鏈表,字典結構一樣,都是對數據的封裝,因為他們都是採用的指針,而指針必然導致在計算長度,獲取最後節點的數據問題上會產生查詢太慢的性能問題,所以封裝表頭是為了在這些問題上提升速度,浪費的只是添加,刪除等操作的時間,與此對比,是可以忽略的。

其次是引入管理所有節點的層數數組,我們可以看到有32層,即32個數組,這和後面的數據節點結構是一樣的。引入它是為了便於直接根據此數組的層數定位到每個元素。

再其次是數據節點的每個level都有層級和span(也就是下圖箭頭指針上的数字,其是為了方便統計兩個節點相距多少長度)。

最後就是數據節點的後退指針backward,引入目的是Level數組只有前指針,即只能指向下一個節點地址,而後退指針是為了能往回找節點。

上圖主要分為3大塊:(這邊大致看下就行,下面將對各模塊進行代碼詳細解釋)

表頭

主要包括四個屬性,分別是頭指針header,尾指針tail,節點長度length,所有節點的最大level。

header:指向跳躍表的表頭節點,通過這個指針地址可以直接找到表頭,時間複雜度為O(1)。

tail:指向跳躍表的表尾節點,通過這個指針可以直接找到表尾,時間複雜度為O(1)。

length:記錄跳躍表的長度,即不包含表頭節點,整個跳躍表中有多少個元素。

level:記錄當前跳躍表內,所有節點層數最大的level(排除表頭節點)。

管理所有節點層數level的數組

其對象值為空,level數組為32層,目的是為了管理真正的數據節點。關於具體的level有哪些屬性放在數據節點來說。

數據節點

主要包括四個屬性對象值obj,分數score,後退指針backward和level數組。每個數據的Level數組有多少層,是隨機產生的,這跟上面說過的跳躍表是一樣的。

成員對象obj:真正的實際數據,每個節點的數據都是唯一的,但是節點的分數可能相同。兩個相同分數的節點是按照成員對象在字典中的大小進行排序的,成員對象較小的節點會排在前面,成員對象較大的節點會排在後面。

分數score:各個節點中的数字是節點所保存的分數,在跳躍表中,節點按各自所保存的分數從小到大排列。

後退指針backward:用於從表尾向表頭遍歷,每個節點只有一個後退指針,即每次只能後退一步。

層級level:節點中用1,2,3等字樣標記節點的各個層,L1代表第一層,L2代表第二層,L3代表第三層,並以此類推。

跳躍表的定義

表頭結構zskiplist

 

typedef struct zskiplist {
    //表頭的頭指針header和尾指針tail
    struct zskiplistNode *header, *tail;
    //一共有多少個節點length
    unsigned long length;
    // 所有節點最大的層級level
   int level;
} zskiplist;

具體數據節點zskiplistNode

 

//跳錶的具體節點 
typedef struct zskiplistNode {
    sds ele; //具體的數據,對應張三
    double score;//分數,對應70
    struct zskiplistNode *backward;//後退指針backward
     //層級數組    struct zskiplistLevel {
        struct zskiplistNode *forward;//前進指針forward
        unsigned int span;//跨度span
    } level[];
} zskiplistNode; 

 

跳躍表的實現(源碼分析)

redis關於跳躍表的API都定義在t_zset.c文件中。

千萬不要看到源碼分析就跑開了,一定要看哦。

創建跳躍表

創建空的跳躍表,其實就是創建表頭和管理所有的節點的level數組。首先,定義一些變量,嘗試分配內存空間。其次是初始化表頭的level和length,分別賦值1和0。接着創建管理所有節點的Level的數組,是調用zslCreateNode函數,輸入參數為數組大小宏常量ZSKIPLIST_MAXLEVEL(32),分數為0,對象值為NULL。(此為跳躍表得以實現重點)。再接着就是為此數組每個元素的前指針forword和跨度span初始化。最後初始化尾指針並返回值。

可以參照下面的圖解和源碼:

 

//創建一個空表頭的跳躍表
zskiplist *zslCreate(void) {
    int j;
    zskiplist *zsl;
    //嘗試分配內存空間
    zsl = zmalloc(sizeof(*zsl));
    //初始化level和length
    zsl->level = 1;
    zsl->length = 0;
    //調用下面的方法zslCreateNode,傳入的參數有數組長度ZSKIPLIST_MAXLEVEL 32
    //分數0,對象值NuLL
    //這一步就是創建管理所有節點的數組
    //並且設置表頭的頭頭指針為此對象的地址
    zsl->header = zslCreateNode(ZSKIPLIST_MAXLEVEL,0,NULL);
    //為這32個數組賦值前指針forward和跨度span
    for (j = 0; j < ZSKIPLIST_MAXLEVEL; j++) {
        zsl->header->level[j].forward = NULL;
        zsl->header->level[j].span = 0;
    }
    //設置尾指針
    zsl->header->backward = NULL;
    zsl->tail = NULL;
    //返回對象
    return zsl;
}
zskiplistNode *zslCreateNode(int level, double score, sds ele) {
    zskiplistNode *zn =
        zmalloc(sizeof(*zn)+level*sizeof(struct zskiplistLevel));
    zn->score = score;
    zn->ele = ele;
    return zn;
}

 

插入節點

比如有下圖6個元素,需要插入值為趙六,分數為101的元素,我們大致想一想,大致的步驟包括找到要插入的位置新建一個數據節點,然後調整與之相關的頭尾指針的level數組。那就看看redis咋做的,和我們想的一樣不一樣呢?

噔噔噔噔,答案揭曉。當然了大框架是相同的。

正文開始了:(先來圖片)

1.遍歷管理所有節點的level數組,從最大的level開始,即3,挨個對比值,如果有分數比他大的值或者分數相同,但是數據的值比他大,記錄到數組裡面,同時記錄跨度。

這樣說太抽象了。拿上圖舉個例子,從表頭的level即3開始,首先到張三的L3,發現分數70,比目標分數101小跳過,根據其前指針找到趙六的L3,發現分數102,比目標分數101大,將趙六L3記錄在待更新數組update中,同時記錄跨度span為4。接着到下一層,張三的L2層,發現分數70比目標分數101小跳過,根據前指針找到王五的L2,發現分數90,比目標分數101小跳過,根據前指針找到趙六的L2,發現分數102比目標分數101大,將趙六的L2記錄到待更新數組update中,同時記錄跨度span為2。最後到下一層,張三的L1層,邏輯和剛才一樣的,也是記錄趙六的L1層和跨度span為1。

2.為新節點隨機生成層級數level(通過位運算),如果生成的level大於目前level最大值3,則將將大於部分挨個遍歷,並將跨度等信息記錄到上面update表中。

比如,新節點生成的level為5,目前level最大值為3,說明這個節點只會有一個,並且跨越了之前的所有節點,那麼我們將從第四層和第五層都遍歷下,記錄到待更新數組update中。

3.準備工作都做好了,找到了該節點將插入到哪一位置,處於哪一層,每層對應的跨度是多少,下面就要新增數據節點了。把上兩步的信息都添加到新節點上,並且調整位置前後指針即可。

4.最後就是一些收尾工作,比如修改表頭的層級level,節點大小length和尾指針tail等屬性。

綜上,整個流程就已經結束了。可能看着有點複雜,可以對照下面代碼來。

 

//插入節點,輸入參數為
//zsl:表頭
//score:插入元素的分數score
//ele:插入元素的具體數據ele
zskiplistNode *zslInsert(zskiplist *zsl, double score, sds ele) {
    //使用update數組記錄每層待插入元素的前一個元素
    zskiplistNode *update[ZSKIPLIST_MAXLEVEL], *x;
    //記錄前置節點與第一個節點之間的跨度,即元素在列表中的排名-1
    unsigned int rank[ZSKIPLIST_MAXLEVEL];
    int i, level;

    serverAssert(!isnan(score));
    x = zsl->header;
    //從最大的level開始遍歷,從頂到底,找到每一層待插入的位置
    for (i = zsl->level-1; i >= 0; i--) {
        /* store rank that is crossed to reach the insert position */
        rank[i] = i == (zsl->level-1) ? 0 : rank[i+1];
    //直接找到第一個分數比該元素大的位置
    //或者分數與該元素相同但是對象的ASSICC碼比該元素大的位置
        while (x->level[i].forward &&
                (x->level[i].forward->score < score ||
                    (x->level[i].forward->score == score &&
                    sdscmp(x->level[i].forward->ele,ele) < 0)))
        {
            //將已走過元素的跨越元素進行計數,得到元素在列表中排名,或者是已搜尋的路徑長度
            rank[i] += x->level[i].span;
            x = x->level[i].forward;
        }
    //記錄待插入位置
        update[i] = x;
    }
     //隨機產生一個層數,在1到32之間,層數越高,生成的概率越低
    level = zslRandomLevel();
    //如果產生的層數大於現有的最高層數,則超出層數都需要初始化
    if (level > zsl->level) {
        //開始循環
        for (i = zsl->level; i < level; i++) {
            rank[i] = 0;
            //該元素作為這些層的第一個節點,前節點就是header
            update[i] = zsl->header;
            //初始化后這些層每層有兩個元素,走一步就是跨越所有元素
            update[i]->level[i].span = zsl->length;
        }
        zsl->level = level;
    }
    //創建節點
    x = zslCreateNode(level,score,ele);
    for (i = 0; i < level; i++) {
        //將新節點插入到各層鏈表中
        x->level[i].forward = update[i]->level[i].forward;
        update[i]->level[i].forward = x;

        // rank[0]是第0層的前置節點P1(也就是底層插入節點前面那個節點)與第一個節點的跨度
        // rank[i]是第i層的前置節點P2(這一層里在插入節點前面那個節點)與第一個節點的跨度
        // 插入節點X與後置節點Y的跨度f(X,Y)可由以下公式計算
        // 關鍵在於f(P1,0)-f(P2,0)+1等於新節點與P2的跨度,這是因為跨度呈扇形形向下延伸到最底層
        // 記錄節點各層跨越元素情況span, 由層與層之間的跨越元素總和rank相減而得
        x->level[i].span = update[i]->level[i].span - (rank[0] - rank[i]);
               // 插入位置前一個節點的span在原基礎上加1即可(新節點在rank[0]的后一個位置)

 update[i]->level[i].span = (rank[0] - rank[i]) + 1;
    }

    /* increment span for untouched levels */
    for (i = level; i < zsl->level; i++) {
        update[i]->level[i].span++;
    }
    // 第0層是雙向鏈表, 便於redis常支持逆序類查找
    x->backward = (update[0] == zsl->header) ? NULL : update[0];
    if (x->level[0].forward)
        x->level[0].forward->backward = x;
    else
        zsl->tail = x;
    zsl->length++;
    return x;
}

 

int zslRandomLevel(void) {
    int level = 1;
    while ((random()&0xFFFF) < (ZSKIPLIST_P * 0xFFFF))
        level += 1;
    return (level<ZSKIPLIST_MAXLEVEL) ? level : ZSKIPLIST_MAXLEVEL;
}

 

獲取節點排名

擔心大家忘了這張圖,再粘貼一遍。如下圖,這部分邏輯比較簡單,就不寫了,具體參考代碼分析。

 

//得到節點的排名
//輸入參數為表頭結構zsl,分數score,真正的數據ele
unsigned long zslGetRank(zskiplist *zsl, double score, sds ele) {
    zskiplistNode *x;
    unsigned long rank = 0;
    int i;
    //先獲取表頭的頭指針,即找到管理所有節點的level數組
    x = zsl->header;
     //從表頭的level,即最大值開始循環遍歷
    for (i = zsl->level-1; i >= 0; i--) {
        //如果找到分數小於目標分數的,排名加上其跨度
        //或者分數相同,但是具體數據小於目標數據的,排名也加上跨度
        while (x->level[i].forward &&
            (x->level[i].forward->score < score ||
                (x->level[i].forward->score == score &&
                sdscmp(x->level[i].forward->ele,ele) <= 0))) {
            rank += x->level[i].span;
            x = x->level[i].forward;
        }

        //確保在第i層找到分值相同,且對象相同時才會返回排位值
        if (x->ele && sdscmp(x->ele,ele) == 0) {
            return rank;
        }
    }
    return 0;
}

 

結語

該篇主要講了Redis的ZSET數據類型的底層實現跳躍表,先從跳躍表是什麼,引出跳躍表的概念和數據結構,剖析了其主要組成部分,進而通過多幅過程圖解釋了Redis是如何設計跳躍表的,最後結合源碼對跳躍表進行描述,如創建過程,添加節點過程,獲取某個節點排名過程,中間穿插例子和過程圖。

如果覺得寫得還行,麻煩給個贊,您的認可才是我寫作的動力!

如果覺得有說的不對的地方,歡迎評論指出。

好了,拜拜咯。

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

【其他文章推薦】

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

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

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

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

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

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

必知必會的8個Python列表技巧

原作者:Nik Piepenbreier

翻譯&內容補充:費弗里

原文地址:https://towardsdatascience.com/advanced-python-list-techniques-c6195fa699a3

  列表(List)是你使用Python過程中接觸最為頻繁的數據結構,也是功能最為強大的幾種數據結構之一。Python列表非常的萬能且蘊含着許多隱藏技巧,下面我們就來探索一些常用的列表技巧。

1 列表元素的過濾

1.1 filter()的使用

  filter()函數接受2個參數:1個函數對象以及1個可迭代的對象,接下來我們定義1個函數然後對1個列表進行過濾。

  首先我們創建1個列表,並且剔除掉小於等於3的元素:

圖1

  回顧一下發生了什麼:

  1. 我們定義了列表original_list
  2. 接着我們定義了一個接受數值型參數number的函數filter_three,當傳入的參數值大於3時會返回True,反之則會返回False
  3. 我們定義了filter對象filtered,其中filter()接受的第一個參數是函數對象,第二個參數是列表對象
  4. 最終我們將filter對象轉化為列表,最終得到經filter_three過濾后original_list內留下的元素。

1.2 使用列表推導式

  類似的,我們也可以利用列表推導式來過濾列表元素,作為一種生成和修改列表優雅的方式,列表推導式想必大家都比較熟悉了,下面是使用列表推導完成同樣任務的過程:

圖2

2 修改列表

2.1 map()的使用

  Python中內置的map()函數使得我們可以將某個函數應用到可迭代對象內每一個元素之上。

  比方說我們想獲取到一個列表對象中每一個元素的平方,就可以使用到map()函數,就像下面的例子一樣:

圖3

  類似filter()的工作過程,下面我們來看看發生了什麼:

  1. 首先我們定義了列表original_list,以及接受數值型參數並返回其平方值的函數square()
  2. 接着我們定義了map對象squares,類似filter()map()接受的第一個參數是函數對象,第二個參數是列表對象
  3. 最終我們將map對象squares列表化,就得到了想要的結果

2.2 使用列表推導式

  同樣的我們也可以使用列表推導式完成同樣的任務:

圖4

3 利用zip()來組合列表

  有些情況下我們需要將兩個或以上數量的列表組合在一起,這類需求使用zip()來完成非常方便。

  zip()函數接收多個列表作為參數傳入,進而得到每個位置上一一對應的元素組合,就像下面的例子一樣:

圖5

4 顛倒列表

  Python中的列表是有序的數據結構,正因如此,列表中元素的順序很重要,有些時候我們需要翻轉列表中所有元素的順序,可以通過Python中的切片操作,用::-1來快捷地實現:

圖6

5 檢查列表中元素的存在情況

  有些情況下我們想要檢查列表中是否存在某個元素,這種時候就可以使用到Python中的in運算符,譬如說我們有一個記錄了所有比賽獲勝隊伍名稱的列表,當我們想查詢某個隊名是否已獲勝時,可以像下面的例子一樣:

圖7

6 找出列表中出現次數最多的元素

  有些情況下我們想要找出列表中出現次數最多的元素,譬如對記錄若干次拋硬幣結果的列表,找出哪一種結果出現次數最多,就可以參考下面的例子:

圖8

7 展平嵌套列表

  有些情況下我們會遇到一些嵌套的列表,其每個元素又是各自不同的列表,這種時候我們就可以利用列表推導式來把這種嵌套列表展平,如下面2層嵌套的例子:

圖9

額外補充

  原作者這裏只考慮到兩層嵌套的列表,如果是更多層嵌套,就需要有多少層寫多少for循環,比較麻煩,其實還有一種更好的方法,我們可以使用pip install dm-tree來安裝tree這個專門用於展平嵌套結構的庫,可以展平任意層嵌套列表,使用例子如下:

圖10

8 檢查唯一性

  如果你想要查看列表中的值是否都是唯一值,可以使用Python中的set數據結構的特點,譬如下面的例子:

圖11

  以上就是本文的全部內容,如有疑問歡迎在評論區討論~

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

【其他文章推薦】

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

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

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

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

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

※超省錢租車方案

基於雲服務的個人網站架構設計

本文介紹如何基於各種雲服務優雅且低成本地搭建個人網站,涉及的雲產品有雲服務器、SSL、企業郵箱、對象存儲、CDN、雲函數、API網關、雲監控等。

概述

如今雲服務提供商們提供了大量涵蓋計算、網絡、存儲等方面的雲服務,其中一些雲產品功能強大,如果能善加利用可以大幅降低開發和運維的成本。下面以基於騰訊雲搭建的個人網站為例,對網站整體的架構進行介紹。

網站目前的主要功能是個人博客,後續可以擴展如個人網盤等其他應用。當前架構圖如下:

一、基礎設施

1.雲服務器CVM

雲服務器使用的是CVM,1核2G,下行帶寬1Mbps,這個配置用來搭建起步階段的個人博客是完全夠用了,購買學生機或者在活動時購買價格也比較便宜。

有了服務器資源就可以開始博客搭建,我選的博客系統是極簡主義的Typecho,安裝過程可以參考這篇博文,主要是在服務器上安裝nginx、mysql、php以及typecho的源碼。

2.域名

註冊 – 備案 – 解析

服務器創建后同時會分配一個公網ip,但是為了便於分享和傳播,建議進行域名註冊。註冊后需要進行備案,現在的備案流程也已經簡化為在小程序上操作,省去了原有的幕布拍照環節,前後大概1-2周時間就可以完成備案。之後在控制台進行域名解析,即綁定域名和服務器ip,注意對帶或不帶www前綴的域名都要進行解析,完成解析后就可以在瀏覽器通過域名來訪問網頁了。

主域名的確定

為了便於SEO,建議根據個人喜好確定一個主域名,因為搜索引擎對於帶www和不帶www前綴的地址是當成兩個網站分開計算權重的。國內網站一般帶www,而國外網站(如github、stackoverflow、leetcode)等是不帶www的。我這裡是選擇不帶www的地址(zhayujie.com),並在nginx中配置對帶www的訪問301重定向到不帶www上,以集中權重。

企業郵箱

擁有域名后,還可以註冊以自己域名為後綴的企業郵箱,基礎版免費使用且賬號數量無上限,再也不用擔心郵箱號不夠用了(如微信公眾平台註冊),郵箱格式類似於 zyj@zhayujie.com

3.全站HTTPS

為了網站安全以及利於SEO,建議支持https協議訪問網站。可以申請免費的SSL證書,將證書和私鑰放置到服務器,並在nginx中開啟並配置SSL。同樣為了避免分散權重,可以把http訪問的請求301重定向到https上。以我的網站為例,帶不帶www以及是否使用https都會統一訪問https://zhayujie.com/。

二、基於COS和CDN的圖床

1.對象存儲COS

由於服務器下行帶寬有限,如果圖片存儲於我們自己的服務器,出現併發訪問時可能導致帶寬超限,訪問速度下降。所以可以把圖片存儲到 COS(Cloud Object Storage)中,搭建自己的圖床,這樣當博客同步到其他博客平台時,也便於對圖片資源進行統一管理。

COS的使用比較簡單,類似於網盤,在存儲桶中可以建立樹狀目錄結構,每個存儲桶(bucket)會分配一個公網域名,其下的文件通過https://{bucket}/{dir}/{filename}的形式進行訪問。但在博客中直接使用該鏈接是不妥的,因為一旦我們遷移到其他雲服務商或者切換其他的存儲方式了,原有的鏈接就失效了,一一修改成本太高。好在cos支持配置自定義域名,可以通過類似http://{domain}/{dir}/{filename}的地址進行訪問。

2.內容分髮網絡CDN

COS的自定義源站域名不支持https訪問,為了不影響我們的全站https,並且同時提升訪問速度和減少流量成本,可以配合CDN服務,開啟自定義CDN加速域名,具體步驟見文檔。

可以選取一個子域名作為cdn自定義域名,添加CNAME解析,這樣通過自定義域名會首先訪問cdn邊緣服務器,如果未命中則回源到cos。例如上面的圖片我配置的地址是https://blog.cos.zhayujie.com/web/blog-cloud-arch.jpg。

三、基於Serverless的消息服務

1.雲函數SCF

在博客開發過程中會遇到一些發送消息的功能,比如讀者回復文章時給筆者發送通知,筆者回複評論時給讀者發送通知,博文發布時給訂閱的讀者發送通知等等。這種消息通知的功能是很適合單獨拆分出來形成一個消息服務的,如果寫在博客源碼中則復用性差(網站下其他應用要發送消息時需要重寫),而單獨部署服務又會增加運維的成本(如果服務掛掉怎麼辦),這時候可以考慮serverless(無服務器)的架構,僅將我們的核心代碼片段託管給雲服務商。

騰訊雲提供了雲函數SCF(Serverless Cloud Function),是一種FaaS技術。對於消息通知這種異步、無狀態的功能,很適合使用雲函數編寫,比如接收到請求後向指定接收人發送一封郵件。

2.API網關

雲函數的觸發方式有多種,最常用的有定時任務和API網關。由於消息通知是通過事件觸發而不是定時觸發,所以選擇API網關,創建了觸發器后便可從公網直接訪問該函數,與Nginx反向代理的作用類似。

API網關的域名是隨機生成的,不利於對未來變化的擴展,故同樣綁定自定義域名,使用https://{domain}/{function}形式的地址觸發函數。例如我的郵件發送函數地址配置為https://apigw.zhayujie.com/commentNotice,在業務代碼中只需向該地址發送POST請求即可觸發郵件投遞。

四、監控、快照和統計

1.監控告警

服務器的監控和告警同樣很重要,有助於我們及時發現並排查問題。監控部分一般直接在控制台的 雲服務器 – 實例 – 監控 中進行查看,有對不同時間周期和時間粒度下的CPU、內存、帶寬、磁盤等的詳細數據。

告警部分則在雲監控中配置,可以配置多種報警策略如對cpu、內存、帶寬等指標超出閾值後進行告警,以及一些機器故障事件(如ping不可達、機器重啟等)。對COS的報警同樣可以在此配置。告警渠道可以是微信、郵件和短信。

2.快照

為了防止服務器硬盤中的數據遭到攻擊或被誤刪,可以在 雲服務器 – 快照 控制台中設置進行快照備份,並且支持定期快照策略,設置每隔一段時間自動創建新的快照。

3.訪問統計

對網站的訪問情況進行統計分析有利於我們優化網站內容和體驗。對於訪問數據統計使用的是百度統計,使用埋點方式接入,可以查看每一個訪客的地域,來源,搜索詞,轉化等信息,統計訪問量趨勢。

對於搜索引擎工具使用的是百度站長工具,用於提交頁面收錄,查看索引量、抓取頻次等數據。

總結

以上就是一個功能齊全的個人博客的搭建過程,大致計算一下成本,雲服務器活動期購買一百一年,域名一般幾十塊一個,而COS、CDN、SCF等產品都有大量的免費額度,且在建站初期流量費用同樣是微乎其微,所以總體算下來成本是極低的。個人開發者可以把個人網站當做一個產品來做,思考如何利用好公有雲的各種雲產品資源來提升用戶體驗,提高開發效率,降低運維成本。

原文鏈接:https://zhayujie.com/blog-cloud-arch.html

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

【其他文章推薦】

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

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

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

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

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

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