fix the zh-tw version of 复杂

This commit is contained in:
Gang Yin 2022-05-11 12:19:08 +08:00
parent 1101ae5a91
commit 92e51b4f33
8 changed files with 18 additions and 17 deletions

View File

@ -12,6 +12,7 @@ def convert(src_path, dst_path, cfg='s2twp.json'):
.replace('髮出', '發出')
.replace('嚐試', '嘗試')
.replace('線上性', '線性')
.replace('復雜', '複雜')
for line in src))
print("convert %s to %s" % (src_path, dst_path))

View File

@ -305,7 +305,7 @@
* 瞭解系統間的相互作用,以便在異常變更造成損失前進行規避。
* 預測未來的問題,並在問題出現之前加以解決(例如,容量規劃)。
* 建立部署、配置、管理方面的良好實踐,編寫相應工具。
* 執行雜的維護任務,例如將應用程式從一個平臺遷移到另一個平臺。
* 執行雜的維護任務,例如將應用程式從一個平臺遷移到另一個平臺。
* 當配置變更時,維持系統的安全性。
* 定義工作流程,使運維操作可預測,並保持生產環境穩定。
* 鐵打的營盤流水的兵,維持組織對系統的瞭解。

View File

@ -373,7 +373,7 @@ $$
此外,透過從不變的事件日誌中分離出可變的狀態,你可以針對不同的讀取方式,從相同的事件日誌中衍生出幾種不同的表現形式。效果就像一個流的多個消費者一樣([圖 11-5](../img/fig11-5.png)):例如,分析型資料庫 Druid 使用這種方式直接從 Kafka 攝取資料【55】Pistachio 是一個分散式的鍵值儲存,使用 Kafka 作為提交日誌【56】Kafka Connect 能將來自 Kafka 的資料匯出到各種不同的資料庫與索引【41】。這對於許多其他儲存和索引系統如搜尋伺服器來說是很有意義的當系統要從分散式日誌中獲取輸入時亦然請參閱 “[保持系統同步](#保持系統同步)”)。
新增從事件日誌到資料庫的顯式轉換,能夠使應用更容易地隨時間演進:如果你想要引入一個新功能,以新的方式表示現有資料,則可以使用事件日誌來構建一個單獨的、針對新功能的讀取最佳化檢視,無需修改現有系統而與之共存。並行執行新舊系統通常比在現有系統中執行雜的模式遷移更容易。一旦不再需要舊的系統你可以簡單地關閉它並回收其資源【47,57】。
新增從事件日誌到資料庫的顯式轉換,能夠使應用更容易地隨時間演進:如果你想要引入一個新功能,以新的方式表示現有資料,則可以使用事件日誌來構建一個單獨的、針對新功能的讀取最佳化檢視,無需修改現有系統而與之共存。並行執行新舊系統通常比在現有系統中執行雜的模式遷移更容易。一旦不再需要舊的系統你可以簡單地關閉它並回收其資源【47,57】。
如果你不需要擔心如何查詢與訪問資料,那麼儲存資料通常是非常簡單的。模式設計、索引和儲存引擎的許多複雜性,都是希望支援某些特定查詢和訪問模式的結果(請參閱 [第三章](ch3.md))。出於這個原因,透過將資料寫入的形式與讀取形式相分離,並允許幾個不同的讀取檢視,你能獲得很大的靈活性。這個想法有時被稱為 **命令查詢責任分離command query responsibility segregation, CQRS**【42,58,59】。

View File

@ -30,7 +30,7 @@
### 組合使用衍生資料的工具
例如,為了處理任意關鍵詞的搜尋查詢,將 OLTP 資料庫與全文搜尋索引整合在一起是很常見的的需求。儘管一些資料庫(例如 PostgreSQL包含了全文索引功能對於簡單的應用完全夠了【1】但更雜的搜尋能力就需要專業的資訊檢索工具了。相反的是,搜尋索引通常不適合作為持久的記錄系統,因此許多應用需要組合這兩種不同的工具以滿足所有需求。
例如,為了處理任意關鍵詞的搜尋查詢,將 OLTP 資料庫與全文搜尋索引整合在一起是很常見的的需求。儘管一些資料庫(例如 PostgreSQL包含了全文索引功能對於簡單的應用完全夠了【1】但更雜的搜尋能力就需要專業的資訊檢索工具了。相反的是,搜尋索引通常不適合作為持久的記錄系統,因此許多應用需要組合這兩種不同的工具以滿足所有需求。
我們在 “[保持系統同步](ch11.md#保持系統同步)” 中接觸過整合資料系統的問題。隨著資料不同表示形式的增加,整合問題變得越來越困難。除了資料庫和搜尋索引之外,也許你需要在分析系統(資料倉庫,或批處理和流處理系統)中維護資料副本;維護從原始資料中衍生的快取,或反規範化的資料版本;將資料灌入機器學習、分類、排名或推薦系統中;或者基於資料變更傳送通知。
@ -64,7 +64,7 @@
#### 全序的限制
對於足夠小的系統,構建一個完全有序的事件日誌是完全可行的(正如單主複製資料庫的流行所證明的那樣,它正好建立了這樣一種日誌)。但是,隨著系統向更大更雜的工作負載伸縮,限制開始出現:
對於足夠小的系統,構建一個完全有序的事件日誌是完全可行的(正如單主複製資料庫的流行所證明的那樣,它正好建立了這樣一種日誌)。但是,隨著系統向更大更雜的工作負載伸縮,限制開始出現:
* 在大多數情況下,構建完全有序的日誌,需要所有事件彙集於決定順序的 **單個領導者節點**。如果事件吞吐量大於單臺計算機的處理能力,則需要將其分割槽到多臺計算機上(請參閱 “[分割槽日誌](ch11.md#分割槽日誌)”)。然後兩個不同分割槽中的事件順序關係就不明確了。
* 如果伺服器分佈在多個 **地理位置分散** 的資料中心上,例如為了容忍整個資料中心掉線,你通常在每個資料中心都有單獨的主庫,因為網路延遲會導致同步的跨資料中心協調效率低下(請參閱 “[多主複製](ch5.md#多主複製)“)。這意味著源自兩個不同資料中心的事件順序未定義。
@ -138,7 +138,7 @@ Lambda 架構的核心思想是透過將不可變事件附加到不斷增長的
Lambda 架構是一種有影響力的想法,它將資料系統的設計變得更好,尤其是透過推廣這樣的原則:在不可變事件流上建立衍生檢視,並在需要時重新處理事件。但是我也認為它有一些實際問題:
* 在批處理和流處理框架中維護相同的邏輯是很顯著的額外工作。雖然像 Summingbird【13】這樣的庫提供了一種可以在批處理和流處理的上下文中執行的計算抽象。除錯、調整和維護兩個不同系統的操作複雜性依然存在【14】。
* 由於流管道和批處理管道產生獨立的輸出,因此需要合併它們以響應使用者請求。如果計算是基於滾動視窗的簡單聚合,則合併相當容易,但如果檢視基於更雜的操作(例如連線和會話化)而匯出,或者輸出不是時間序列,則會變得非常困難。
* 由於流管道和批處理管道產生獨立的輸出,因此需要合併它們以響應使用者請求。如果計算是基於滾動視窗的簡單聚合,則合併相當容易,但如果檢視基於更雜的操作(例如連線和會話化)而匯出,或者輸出不是時間序列,則會變得非常困難。
* 儘管有能力重新處理整個歷史資料集是很好的,但在大型資料集上這樣做經常會開銷巨大。因此,批處理流水線通常需要設定為處理增量批處理(例如,在每小時結束時處理一小時的資料),而不是重新處理所有內容。這引發了 “[時間推理](ch11.md#時間推理)” 中討論的問題,例如處理滯留事件和處理跨批次邊界的視窗。增量化批處理計算會增加複雜性,使其更類似於流式傳輸層,這與保持批處理層儘可能簡單的目標背道而馳。
#### 統一批處理和流處理
@ -259,7 +259,7 @@ Unix 和關係資料庫以非常不同的哲學來處理資訊管理問題。Uni
* 在機器學習系統中,我們可以將模型視作從訓練資料透過應用各種特徵提取、統計分析函式衍生的資料,當模型應用於新的輸入資料時,模型的輸出是從輸入和模型(因此間接地從訓練資料)中衍生的。
* 快取通常包含將以使用者介面UI顯示的形式的資料聚合。因此填充快取需要知道 UI 中引用的欄位UI 中的變更可能需要更新快取填充方式的定義,並重建快取。
用於次級索引的衍生函式是如此常用的需求,以致於它作為核心功能被內建至許多資料庫中,你可以簡單地透過 `CREATE INDEX` 來呼叫它。對於全文索引,常見語言的基本語言特徵可能內建到資料庫中,但更雜的特徵通常需要領域特定的調整。在機器學習中特徵工程是眾所周知的特定於應用的特徵通常需要包含很多關於使用者互動與應用部署的詳細知識【35】。
用於次級索引的衍生函式是如此常用的需求,以致於它作為核心功能被內建至許多資料庫中,你可以簡單地透過 `CREATE INDEX` 來呼叫它。對於全文索引,常見語言的基本語言特徵可能內建到資料庫中,但更雜的特徵通常需要領域特定的調整。在機器學習中特徵工程是眾所周知的特定於應用的特徵通常需要包含很多關於使用者互動與應用部署的詳細知識【35】。
當建立衍生資料集的函式不是像建立次級索引那樣的標準搬磚函式時,需要自定義程式碼來處理特定於應用的東西。而這個自定義程式碼是讓許多資料庫掙扎的地方,雖然關係資料庫通常支援觸發器、儲存過程和使用者定義的函式,可以用它們來在資料庫中執行應用程式碼,但它們有點像資料庫設計裡的事後反思。(請參閱 “[傳遞事件流](ch11.md#傳遞事件流)”)。
@ -399,7 +399,7 @@ Unix 和關係資料庫以非常不同的哲學來處理資訊管理問題。Uni
#### 多分割槽資料處理
對於只涉及單個分割槽的查詢,透過流來發送查詢與收集響應可能是殺雞用牛刀了。然而,這個想法開啟了分散式執行雜查詢的可能性,這需要合併來自多個分割槽的資料,利用了流處理器已經提供的訊息路由、分割槽和連線的基礎設施。
對於只涉及單個分割槽的查詢,透過流來發送查詢與收集響應可能是殺雞用牛刀了。然而,這個想法開啟了分散式執行雜查詢的可能性,這需要合併來自多個分割槽的資料,利用了流處理器已經提供的訊息路由、分割槽和連線的基礎設施。
Storm 的分散式 RPC 功能支援這種使用模式(請參閱 “[訊息傳遞和 RPC](ch11.md#訊息傳遞和RPC)”)。例如,它已經被用來計算瀏覽過某個推特 URL 的人數 —— 即,發推包含該 URL 的所有人的粉絲集合的並集【48】。由於推特的使用者是分割槽的因此這種計算需要合併來自多個分割槽的結果。
@ -414,7 +414,7 @@ MPP 資料庫的內部查詢執行圖有著類似的特徵(請參閱 “[Hadoo
我們希望構建可靠且 **正確** 的應用(即使面對各種故障,程式的語義也能被很好地定義與理解)。約四十年來,原子性、隔離性和永續性([第七章](ch7.md))等事務特性一直是構建正確應用的首選工具。然而這些地基沒有看上去那麼牢固:例如弱隔離級別帶來的困惑可以佐證(請參閱 “[弱隔離級別](ch7.md#弱隔離級別)”)。
事務在某些領域被完全拋棄,並被提供更好效能與可伸縮性的模型取代,但後者有更雜的語義(例如,請參閱 “[無主複製](ch5.md#無主複製)”)。**一致性Consistency** 經常被談起,但其定義並不明確(請參閱 “[一致性](ch7.md#一致性)” 和 [第九章](ch9.md))。有些人斷言我們應當為了高可用而 “擁抱弱一致性”,但卻對這些概念實際上意味著什麼缺乏清晰的認識。
事務在某些領域被完全拋棄,並被提供更好效能與可伸縮性的模型取代,但後者有更雜的語義(例如,請參閱 “[無主複製](ch5.md#無主複製)”)。**一致性Consistency** 經常被談起,但其定義並不明確(請參閱 “[一致性](ch7.md#一致性)” 和 [第九章](ch9.md))。有些人斷言我們應當為了高可用而 “擁抱弱一致性”,但卻對這些概念實際上意味著什麼缺乏清晰的認識。
對於如此重要的話題我們的理解以及我們的工程方法卻是驚人地薄弱。例如確定在特定事務隔離等級或複製配置下執行特定應用是否安全是非常困難的【51,52】。通常簡單的解決方案似乎在低併發性的情況下工作正常並且沒有錯誤但在要求更高的情況下卻會出現許多微妙的錯誤。

View File

@ -232,7 +232,7 @@ CODASYL 中的查詢是透過利用遍歷記錄列和跟隨訪問路徑表在資
文件資料庫對連線的糟糕支援可能是個問題也可能不是問題這取決於應用程式。例如如果某分析型應用程式使用一個文件資料庫來記錄何時何地發生了何事那麼多對多關係可能永遠也用不上。【19】。
但如果你的應用程式確實會用到多對多關係,那麼文件模型就沒有那麼誘人了。儘管可以透過反規範化來消除對連線的需求,但這需要應用程式程式碼來做額外的工作以確保資料一致性。儘管應用程式程式碼可以透過向資料庫發出多個請求的方式來模擬連線,但這也將複雜性轉移到應用程式中,而且通常也會比由資料庫內的專用程式碼更慢。在這種情況下,使用文件模型可能會導致更雜的應用程式碼與更差的效能【15】。
但如果你的應用程式確實會用到多對多關係,那麼文件模型就沒有那麼誘人了。儘管可以透過反規範化來消除對連線的需求,但這需要應用程式程式碼來做額外的工作以確保資料一致性。儘管應用程式程式碼可以透過向資料庫發出多個請求的方式來模擬連線,但這也將複雜性轉移到應用程式中,而且通常也會比由資料庫內的專用程式碼更慢。在這種情況下,使用文件模型可能會導致更雜的應用程式碼與更差的效能【15】。
我們沒有辦法說哪種資料模型更有助於簡化應用程式碼,因為它取決於資料項之間的關係種類。對高度關聯的資料而言,文件模型是極其糟糕的,關係模型是可以接受的,而選用圖形模型(請參閱 “[圖資料模型](#圖資料模型)”)是最自然的。

View File

@ -78,7 +78,7 @@ $ cat database
### 雜湊索引
讓我們從 **鍵值資料key-value Data** 的索引開始。這不是你可以索引的唯一資料型別,但鍵值資料是很常見的。對於更雜的索引來說,這也是一個有用的構建模組。
讓我們從 **鍵值資料key-value Data** 的索引開始。這不是你可以索引的唯一資料型別,但鍵值資料是很常見的。對於更雜的索引來說,這也是一個有用的構建模組。
鍵值儲存與在大多數程式語言中可以找到的 **字典dictionary** 型別非常相似,通常字典都是用 **雜湊對映hash map****散列表hash table** 實現的。雜湊對映在許多演算法教科書中都有描述【1,2】所以這裡我們不會討論它的工作細節。既然我們已經可以用雜湊對映來表示 **記憶體中** 的資料結構,為什麼不使用它來索引 **硬碟上** 的資料呢?
@ -242,7 +242,7 @@ Lucene 是 Elasticsearch 和 Solr 使用的一種全文搜尋的索引引擎,
B 樹的基本底層寫操作是用新資料覆寫硬碟上的頁面,並假定覆寫不改變頁面的位置:即,當頁面被覆寫時,對該頁面的所有引用保持完整。這與日誌結構索引(如 LSM 樹)形成鮮明對比,後者只追加到檔案(並最終刪除過時的檔案),但從不修改檔案中已有的內容。
你可以把覆寫硬碟上的頁面對應為實際的硬體操作。在磁性硬碟驅動器上,這意味著將磁頭移動到正確的位置,等待旋轉盤上的正確位置出現,然後用新的資料覆寫適當的扇區。在固態硬碟上,由於 SSD 必須一次擦除和重寫相當大的儲存晶片塊,所以會發生更雜的事情【19】。
你可以把覆寫硬碟上的頁面對應為實際的硬體操作。在磁性硬碟驅動器上,這意味著將磁頭移動到正確的位置,等待旋轉盤上的正確位置出現,然後用新的資料覆寫適當的扇區。在固態硬碟上,由於 SSD 必須一次擦除和重寫相當大的儲存晶片塊,所以會發生更雜的事情【19】。
而且,一些操作需要覆寫幾個不同的頁面。例如,如果因為插入導致頁面過滿而拆分頁面,則需要寫入新拆分的兩個頁面,並覆寫其父頁面以更新對兩個子頁面的引用。這是一個危險的操作,因為如果資料庫在僅有部分頁面被寫入時崩潰,那麼最終將導致一個損壞的索引(例如,可能有一個孤兒頁面不是任何父項的子項) 。
@ -306,7 +306,7 @@ B 樹在資料庫架構中是非常根深蒂固的,為許多工作負載都提
索引中的鍵是查詢要搜尋的內容,而其值可以是以下兩種情況之一:它可以是實際的行(文件,頂點),也可以是對儲存在別處的行的引用。在後一種情況下,行被儲存的地方被稱為 **堆檔案heap file**,並且儲存的資料沒有特定的順序(它可以是僅追加的,或者它可以跟蹤被刪除的行以便後續可以用新的資料進行覆蓋)。堆檔案方法很常見,因為它避免了在存在多個次級索引時對資料的複製:每個索引只引用堆檔案中的一個位置,實際的資料都儲存在一個地方。
在不更改鍵的情況下更新值時,堆檔案方法可以非常高效:只要新值的位元組數不大於舊值,就可以覆蓋該記錄。如果新值更大,情況會更因為它可能需要移到堆中有足夠空間的新位置。在這種情況下要麼所有的索引都需要更新以指向記錄的新堆位置或者在舊堆位置留下一個轉發指標【5】。
在不更改鍵的情況下更新值時,堆檔案方法可以非常高效:只要新值的位元組數不大於舊值,就可以覆蓋該記錄。如果新值更大,情況會更因為它可能需要移到堆中有足夠空間的新位置。在這種情況下要麼所有的索引都需要更新以指向記錄的新堆位置或者在舊堆位置留下一個轉發指標【5】。
在某些情況下從索引到堆檔案的額外跳躍對讀取來說效能損失太大因此可能希望將被索引的行直接儲存在索引中。這被稱為聚集索引clustered index。例如在 MySQL 的 InnoDB 儲存引擎中表的主鍵總是一個聚集索引次級索引則引用主鍵而不是堆檔案中的位置【31】。在 SQL Server 中可以為每個表指定一個聚集索引【32】。
@ -598,7 +598,7 @@ C-Store 中引入了這個想法的一個巧妙擴充套件,並在商業資料
日誌結構的儲存引擎是相對較新的技術。他們的主要想法是,透過系統性地將隨機訪問寫入轉換為硬碟上的順序寫入,由於硬碟驅動器和固態硬碟的效能特點,可以實現更高的寫入吞吐量。
關於 OLTP我們最後還介紹了一些更雜的索引結構,以及針對所有資料都放在記憶體裡而最佳化的資料庫。
關於 OLTP我們最後還介紹了一些更雜的索引結構,以及針對所有資料都放在記憶體裡而最佳化的資料庫。
然後,我們暫時放下了儲存引擎的內部細節,查看了典型資料倉庫的高階架構,並說明了為什麼分析工作負載與 OLTP 差別很大:當你的查詢需要在大量行中順序掃描時,索引的重要性就會降低很多。相反,非常緊湊地編碼資料變得非常重要,以最大限度地減少查詢需要從硬碟讀取的資料量。我們討論了列式儲存如何幫助實現這一目標。

View File

@ -174,7 +174,7 @@ SELECT COUNT*FROM emails WHERE recipient_id = 2 AND unread_flag = true
這些問題非常讓人頭大,故儲存引擎一個幾乎普遍的目標是:對單節點上的單個物件(例如鍵值對)上提供原子性和隔離性。原子性可以透過使用日誌來實現崩潰恢復(請參閱 “[讓 B 樹更可靠](ch3.md#讓B樹更可靠)”),並且可以使用每個物件上的鎖來實現隔離(每次只允許一個執行緒訪問物件) 。
一些資料庫也提供更雜的原子操作 [^iv],例如自增操作,這樣就不再需要像 [圖 7-1](../img/fig7-1.png) 那樣的讀取 - 修改 - 寫入序列了。同樣流行的是 **[比較和設定CAS, compare-and-set](#比較並設定CAS)** 操作,僅當值沒有被其他併發修改過時,才允許執行寫操作。
一些資料庫也提供更雜的原子操作 [^iv],例如自增操作,這樣就不再需要像 [圖 7-1](../img/fig7-1.png) 那樣的讀取 - 修改 - 寫入序列了。同樣流行的是 **[比較和設定CAS, compare-and-set](#比較並設定CAS)** 操作,僅當值沒有被其他併發修改過時,才允許執行寫操作。
[^iv]: 嚴格地說,**原子自增atomic increment** 這個術語在多執行緒程式設計的意義上使用了原子這個詞。 在 ACID 的情況下,它實際上應該被稱為 **隔離的isolated** 的或 **可序列的serializable** 的增量。 但這就太吹毛求疵了。
@ -827,7 +827,7 @@ WHERE room_id = 123 AND
事務是一個抽象層,允許應用程式假裝某些併發問題和某些型別的硬體和軟體故障不存在。各式各樣的錯誤被簡化為一種簡單情況:**事務中止transaction abort**,而應用需要的僅僅是重試。
在本章中介紹了很多問題,事務有助於防止這些問題發生。並非所有應用都易受此類問題影響:具有非常簡單訪問模式的應用(例如每次讀寫單條記錄)可能無需事務管理。但是對於更雜的訪問模式,事務可以大大減少需要考慮的潛在錯誤情景數量。
在本章中介紹了很多問題,事務有助於防止這些問題發生。並非所有應用都易受此類問題影響:具有非常簡單訪問模式的應用(例如每次讀寫單條記錄)可能無需事務管理。但是對於更雜的訪問模式,事務可以大大減少需要考慮的潛在錯誤情景數量。
如果沒有事務處理,各種錯誤情況(程序崩潰,網路中斷,停電,磁碟已滿,意外併發等)意味著資料可能以各種方式變得不一致。例如,非規範化的資料可能很容易與源資料不同步。如果沒有事務處理,就很難推斷複雜的互動訪問可能對資料庫造成的影響。

View File

@ -34,7 +34,7 @@
然而,這是一個非常弱的保證 —— 它並沒有說什麼時候副本會收斂。在收斂之前讀操作可能會返回任何東西或什麼都沒有【1】。例如如果你寫入了一個值然後立即再次讀取這並不能保證你能看到剛才寫入的值因為讀請求可能會被路由到另外的副本上。請參閱 “[讀己之寫](ch5.md#讀己之寫)” )。
對於應用開發人員而言,最終一致性是很困難的,因為它與普通單執行緒程式中變數的行為有很大區別。對於後者,如果將一個值賦給一個變數,然後很快地再次讀取,不可能讀到舊的值,或者讀取失敗。資料庫表面上看起來像一個你可以讀寫的變數,但實際上它有更雜的語義【3】。
對於應用開發人員而言,最終一致性是很困難的,因為它與普通單執行緒程式中變數的行為有很大區別。對於後者,如果將一個值賦給一個變數,然後很快地再次讀取,不可能讀到舊的值,或者讀取失敗。資料庫表面上看起來像一個你可以讀寫的變數,但實際上它有更雜的語義【3】。
在與只提供弱保證的資料庫打交道時,你需要始終意識到它的侷限性,而不是意外地作出太多假設。錯誤往往是微妙的,很難找到,也很難測試,因為應用可能在大多數情況下執行良好。當系統出現故障(例如網路中斷)或高併發時,最終一致性的邊緣情況才會顯現出來。
@ -103,7 +103,7 @@
[圖 9-3](../img/fig9-3.png) 中的箭頭說明了這個時序依賴關係。客戶端 A 是第一個讀取新的值 `1` 的位置。在 A 的讀取返回之後B 開始新的讀取。由於 B 的讀取嚴格在發生於 A 的讀取之後,因此即使 C 的寫入仍在進行中,也必須返回 `1`(與 [圖 9-1](../img/fig9-1.png) 中的 Alice 和 Bob 的情況相同:在 Alice 讀取新值之後Bob 也希望讀取新的值)。
我們可以進一步細化這個時序圖,展示每個操作是如何在特定時刻原子性生效的。[圖 9-4](../img/fig9-4.png) 顯示了一個更雜的例子【10】。
我們可以進一步細化這個時序圖,展示每個操作是如何在特定時刻原子性生效的。[圖 9-4](../img/fig9-4.png) 顯示了一個更雜的例子【10】。
在 [圖 9-4](../img/fig9-4.png) 中,除了讀寫之外,還增加了第三種類型的操作: