[{"data":1,"prerenderedAt":869},["ShallowReactive",2],{"article-\u002Farticles\u002Frabbitmq-exchange-queue-binding":3},{"id":4,"title":5,"body":6,"date":856,"description":475,"excerpt":857,"extension":858,"meta":859,"navigation":596,"path":860,"seo":861,"stem":862,"tags":863,"__hash__":868},"articles\u002Farticles\u002Frabbitmq-exchange-queue-binding.md","RabbitMQ 的 Exchange、Queue、Binding：訊息到底是怎麼被送到佇列的？",{"type":7,"value":8,"toc":841},"minimark",[9,14,18,66,73,84,98,101,164,167,193,197,203,208,215,219,222,226,233,254,261,368,378,382,393,397,408,414,428,434,438,441,461,468,478,481,538,541,680,778,781,801,805,824,831,834,837],[10,11,13],"h2",{"id":12},"先記住一句話producer-不直接送到-queue","先記住一句話：Producer 不直接送到 Queue",[15,16,17],"p",{},"第一次接觸 RabbitMQ 時，很直覺會以為它的運作方式是「我把訊息放進某個佇列，另一邊再從佇列拿出來」。但 RabbitMQ（更精確地說，是它實作的 AMQP 0-9-1 協定）的模型多了一層：",[19,20,21,36,45,56],"ul",{},[22,23,24,28,29,32,33],"li",{},[25,26,27],"strong",{},"Producer"," 把訊息發布（Publish）給 ",[25,30,31],{},"Exchange","，並附上一個 ",[25,34,35],{},"Routing Key",[22,37,38,40,41,44],{},[25,39,31],{}," 本身",[25,42,43],{},"不儲存訊息","，它只負責「分派」：依照自己的類型和 Binding 規則，決定這則訊息要複製到哪些 Queue",[22,46,47,50,51,55],{},[25,48,49],{},"Binding"," 是 Exchange 和 Queue 之間的一條規則，例如「Routing Key 是 ",[52,53,54],"code",{},"a"," 的訊息，送到 Queue A」",[22,57,58,61,62,65],{},[25,59,60],{},"Queue"," 才是真正存放訊息的地方，",[25,63,64],{},"Consumer"," 從 Queue 取出訊息處理",[15,67,68],{},[69,70],"img",{"alt":71,"src":72},"RabbitMQ 核心模型示意圖：Producer 帶著 Routing Key 發布訊息到 Exchange，Exchange 依 Binding 規則把訊息分派到符合的 Queue，沒有符合 Binding 的 Queue 不會收到；Consumer 再從各自的 Queue 消費","\u002Fimages\u002Farticles\u002Frabbitmq-core-model.svg",[15,74,75,76,79,80,83],{},"這也解釋了最常見的「訊息不見了」：如果一則訊息送到 Exchange 時，",[25,77,78],{},"沒有任何一條 Binding 符合","，Exchange 就會直接把它丟掉（除非發布時設定了 ",[52,81,82],{},"mandatory"," 或 Exchange 設定了 Alternate Exchange）。訊息沒有進任何 Queue，自然也沒有人收到。",[85,86,87],"blockquote",{},[15,88,89,90,93,94,97],{},"你可能看過「直接送到 Queue」的範例程式碼，例如 ",[52,91,92],{},"BasicPublish(exchange: \"\", routingKey: \"my-queue\")","。這其實是送到一個名稱為空字串的",[25,95,96],{},"預設 Exchange（Default Exchange）","，它是一個 Direct Exchange，而且每個 Queue 建立時都會自動以「自己的名字」綁定到它上面。所以看起來像是直接送進 Queue，本質上還是經過了 Exchange。",[10,99,100],{"id":100},"三者的關係",[102,103,104,123],"table",{},[105,106,107],"thead",{},[108,109,110,114,117,120],"tr",{},[111,112,113],"th",{},"元件",[111,115,116],{},"負責什麼",[111,118,119],{},"會儲存訊息嗎",[111,121,122],{},"誰決定它的存在",[124,125,126,140,152],"tbody",{},[108,127,128,131,134,137],{},[129,130,31],"td",{},[129,132,133],{},"依類型與 Binding 分派訊息",[129,135,136],{},"不會",[129,138,139],{},"通常由 Producer 端宣告",[108,141,142,144,147,149],{},[129,143,49],{},[129,145,146],{},"描述「什麼樣的訊息要進哪個 Queue」",[129,148,136],{},[129,150,151],{},"通常由 Consumer 端宣告",[108,153,154,156,159,162],{},[129,155,60],{},[129,157,158],{},"存放訊息，等待 Consumer 取用",[129,160,161],{},"會",[129,163,151],{},[15,165,166],{},"幾個重要的關係：",[19,168,169,180,186],{},[22,170,171,172,175,176,179],{},"一個 Exchange 可以綁定",[25,173,174],{},"多個"," Queue，一則訊息符合幾條 Binding，就會",[25,177,178],{},"複製","幾份到對應的 Queue",[22,181,182,183,185],{},"一個 Queue 也可以綁定到",[25,184,174],{}," Exchange，或是在同一個 Exchange 上有多條不同 Routing Key 的 Binding",[22,187,188,189,192],{},"Producer 只需要知道 Exchange 的名稱，",[25,190,191],{},"完全不需要知道有哪些 Queue、誰在消費","——這正是用 RabbitMQ 做解耦的關鍵",[10,194,196],{"id":195},"四種-exchange-類型","四種 Exchange 類型",[15,198,199],{},[69,200],{"alt":201,"src":202},"RabbitMQ Exchange 類型示意圖：Direct 只送到 Routing Key 完全相同的佇列；Fanout 忽略 Routing Key，送一份到每個綁定的佇列；Topic 以萬用字元比對 Routing Key，house.created.franchise 會送到 house.*.franchise、house.created.# 與 # 三個佇列，但不會送到 house.price_changed.#","\u002Fimages\u002Farticles\u002Frabbitmq-exchange-types.svg",[204,205,207],"h3",{"id":206},"directrouting-key-完全相同","Direct：Routing Key 完全相同",[15,209,210,211,214],{},"Binding 時指定一個 Binding Key，訊息的 Routing Key 和它",[25,212,213],{},"完全相同","才會送進去。適合「這種工作交給那個佇列」的一對一分派，例如報表匯出、寄信這種背景任務。",[204,216,218],{"id":217},"fanout全部送一份","Fanout：全部送一份",[15,220,221],{},"完全忽略 Routing Key，所有綁定在這個 Exchange 上的 Queue 都會收到一份。適合「廣播」：有一件事發生了，所有人都要知道。",[204,223,225],{"id":224},"topic萬用字元比對","Topic：萬用字元比對",[15,227,228,229,232],{},"Routing Key 是以 ",[52,230,231],{},"."," 分隔的多個字，Binding Key 可以使用兩種萬用字元：",[19,234,235,245],{},[22,236,237,240,241,244],{},[52,238,239],{},"*","：剛好比對",[25,242,243],{},"一個","字",[22,246,247,250,251,244],{},[52,248,249],{},"#","：比對",[25,252,253],{},"零到多個",[15,255,256,257,260],{},"例如 Routing Key 為 ",[52,258,259],{},"house.created.franchise"," 時：",[102,262,263,276],{},[105,264,265],{},[108,266,267,270,273],{},[111,268,269],{},"Binding Key",[111,271,272],{},"是否符合",[111,274,275],{},"原因",[124,277,278,296,312,328,339,356],{},[108,279,280,285,288],{},[129,281,282],{},[52,283,284],{},"house.*.franchise",[129,286,287],{},"✓",[129,289,290,292,293],{},[52,291,239],{}," 比對到 ",[52,294,295],{},"created",[108,297,298,303,305],{},[129,299,300],{},[52,301,302],{},"house.created.#",[129,304,287],{},[129,306,307,292,309],{},[52,308,249],{},[52,310,311],{},"franchise",[108,313,314,319,321],{},[129,315,316],{},[52,317,318],{},"house.#",[129,320,287],{},[129,322,323,292,325],{},[52,324,249],{},[52,326,327],{},"created.franchise",[108,329,330,334,336],{},[129,331,332],{},[52,333,249],{},[129,335,287],{},[129,337,338],{},"什麼都符合，等於 Fanout",[108,340,341,346,349],{},[129,342,343],{},[52,344,345],{},"house.*",[129,347,348],{},"✗",[129,350,351,353,354],{},[52,352,239],{}," 只能比對一個字，後面還多一個 ",[52,355,311],{},[108,357,358,363,365],{},[129,359,360],{},[52,361,362],{},"house.price_changed.#",[129,364,348],{},[129,366,367],{},"第二個字不同",[15,369,370,371,373,374,377],{},"Topic 是最有彈性的類型：Binding Key 全部用完整字串時，它就是 Direct；用 ",[52,372,249],{}," 時，它就是 Fanout。如果一開始不確定未來的訂閱需求，",[25,375,376],{},"用 Topic 通常是最不會後悔的選擇","。",[204,379,381],{"id":380},"headers依訊息標頭比對","Headers：依訊息標頭比對",[15,383,384,385,388,389,392],{},"不看 Routing Key，而是依訊息的 Headers 比對（可以設定全部符合 ",[52,386,387],{},"x-match: all"," 或任一符合 ",[52,390,391],{},"x-match: any","）。實務上比較少用，大部分需求 Topic 都能滿足，而且 Topic 的規則一眼就看得懂。",[10,394,396],{"id":395},"同一個-queue-還是各自一個-queue","同一個 Queue 還是各自一個 Queue？",[15,398,399,400,403,404,407],{},"這是設計時最關鍵的決定，因為它決定了訊息是「",[25,401,402],{},"分工","」還是「",[25,405,406],{},"每個人都收到","」：",[15,409,410],{},[69,411],{"alt":412,"src":413},"同一個 Queue 多個 Consumer 與每個服務各自一個 Queue 的比較：左邊三個同步服務 Pod 共用 sync-queue，訊息 1、2、3 各自只被一個 Pod 拿走；右邊 house.events Exchange 綁定 sync-queue 與 notify-queue，同一則事件同步服務與通知中心各收到一份","\u002Fimages\u002Farticles\u002Frabbitmq-share-vs-copy.svg",[19,415,416,422],{},[22,417,418,421],{},[25,419,420],{},"同一個 Queue、多個 Consumer（Competing Consumers）","：一則訊息只會被其中一個 Consumer 拿走。同一個服務開了三個 Pod，三個 Pod 應該共用同一個 Queue，這樣訊息會分散到三個 Pod 處理，處理量跟著 Pod 數量水平擴展",[22,423,424,427],{},[25,425,426],{},"每個服務各自一個 Queue","：不同的服務要對同一個事件做不同的事，就應該各自宣告自己的 Queue，綁到同一個 Exchange 上。Exchange 會複製一份到每個 Queue，每個服務各自處理、互不影響",[15,429,430,431],{},"一個常見的錯誤是讓「同步服務」和「通知中心」共用同一個 Queue，結果一則事件只被其中一邊收到，另一邊莫名其妙漏資料。",[25,432,433],{},"判斷原則很簡單：同一個服務的多個實例共用 Queue；不同的服務各自一個 Queue。",[10,435,437],{"id":436},"使用情境房仲物件事件的路由設計","使用情境：房仲物件事件的路由設計",[15,439,440],{},"以房仲網站為例：主力平台上的物件會新增、改價、下架，而好幾個系統都在等這些事件：",[19,442,443,449,455],{},[22,444,445,448],{},[25,446,447],{},"加盟平台","：只關心「加盟品牌」的物件，要同步到加盟網站",[22,450,451,454],{},[25,452,453],{},"通知中心","：有新物件刊登時，通知訂閱了這個條件的使用者",[22,456,457,460],{},[25,458,459],{},"稽核紀錄","：所有物件異動都要留下紀錄",[15,462,463,464,467],{},"如果用 Fanout，每個系統都會收到全部的事件，再自己在程式裡過濾——加盟平台會收到大量和它無關的直營物件事件。改用 ",[25,465,466],{},"Topic Exchange","，把「事件類型」和「品牌類別」放進 Routing Key，讓 RabbitMQ 幫忙過濾：",[469,470,476],"pre",{"className":471,"code":473,"language":474,"meta":475},[472],"language-text","Exchange：house.events（topic）\nRouting Key 格式：house.{事件}.{品牌類別}\n\nhouse.created.franchise        加盟物件新增\nhouse.price_changed.main       直營物件改價\nhouse.removed.franchise        加盟物件下架\n","text","",[52,477,473],{"__ignoreMap":475},[15,479,480],{},"各系統依自己的需求綁定：",[102,482,483,494],{},[105,484,485],{},[108,486,487,489,491],{},[111,488,60],{},[111,490,269],{},[111,492,493],{},"收到的事件",[124,495,496,510,524],{},[108,497,498,503,507],{},[129,499,500],{},[52,501,502],{},"franchise-sync",[129,504,505],{},[52,506,284],{},[129,508,509],{},"所有加盟物件的異動",[108,511,512,517,521],{},[129,513,514],{},[52,515,516],{},"notify-new-listing",[129,518,519],{},[52,520,302],{},[129,522,523],{},"所有新刊登的物件",[108,525,526,531,535],{},[129,527,528],{},[52,529,530],{},"audit-log",[129,532,533],{},[52,534,249],{},[129,536,537],{},"全部",[15,539,540],{},"宣告的程式碼（以 RabbitMQ.Client 為例）：",[469,542,546],{"className":543,"code":544,"language":545,"meta":475,"style":475},"language-csharp shiki shiki-themes github-dark","\u002F\u002F Producer 端：只需要宣告 Exchange\nchannel.ExchangeDeclare(\"house.events\", ExchangeType.Topic, durable: true);\n\nvar body = JsonSerializer.SerializeToUtf8Bytes(houseCreatedEvent);\nvar props = channel.CreateBasicProperties();\nprops.Persistent = true;\n\nchannel.BasicPublish(\"house.events\", \"house.created.franchise\", props, body);\n","csharp",[52,547,548,557,591,598,620,639,654,659],{"__ignoreMap":475},[549,550,553],"span",{"class":551,"line":552},"line",1,[549,554,556],{"class":555},"sAwPA","\u002F\u002F Producer 端：只需要宣告 Exchange\n",[549,558,560,564,568,571,575,578,581,584,588],{"class":551,"line":559},2,[549,561,563],{"class":562},"s95oV","channel.",[549,565,567],{"class":566},"svObZ","ExchangeDeclare",[549,569,570],{"class":562},"(",[549,572,574],{"class":573},"sU2Wk","\"house.events\"",[549,576,577],{"class":562},", ExchangeType.Topic, ",[549,579,580],{"class":566},"durable",[549,582,583],{"class":562},": ",[549,585,587],{"class":586},"sDLfK","true",[549,589,590],{"class":562},");\n",[549,592,594],{"class":551,"line":593},3,[549,595,597],{"emptyLinePlaceholder":596},true,"\n",[549,599,601,605,608,611,614,617],{"class":551,"line":600},4,[549,602,604],{"class":603},"snl16","var",[549,606,607],{"class":566}," body",[549,609,610],{"class":603}," =",[549,612,613],{"class":562}," JsonSerializer.",[549,615,616],{"class":566},"SerializeToUtf8Bytes",[549,618,619],{"class":562},"(houseCreatedEvent);\n",[549,621,623,625,628,630,633,636],{"class":551,"line":622},5,[549,624,604],{"class":603},[549,626,627],{"class":566}," props",[549,629,610],{"class":603},[549,631,632],{"class":562}," channel.",[549,634,635],{"class":566},"CreateBasicProperties",[549,637,638],{"class":562},"();\n",[549,640,642,645,648,651],{"class":551,"line":641},6,[549,643,644],{"class":562},"props.Persistent ",[549,646,647],{"class":603},"=",[549,649,650],{"class":586}," true",[549,652,653],{"class":562},";\n",[549,655,657],{"class":551,"line":656},7,[549,658,597],{"emptyLinePlaceholder":596},[549,660,662,664,667,669,671,674,677],{"class":551,"line":661},8,[549,663,563],{"class":562},[549,665,666],{"class":566},"BasicPublish",[549,668,570],{"class":562},[549,670,574],{"class":573},[549,672,673],{"class":562},", ",[549,675,676],{"class":573},"\"house.created.franchise\"",[549,678,679],{"class":562},", props, body);\n",[469,681,683],{"className":543,"code":682,"language":545,"meta":475,"style":475},"\u002F\u002F 加盟平台（Consumer 端）：宣告自己的 Queue 與 Binding\nchannel.ExchangeDeclare(\"house.events\", ExchangeType.Topic, durable: true);\nchannel.QueueDeclare(\"franchise-sync\", durable: true, exclusive: false, autoDelete: false);\nchannel.QueueBind(\"franchise-sync\", \"house.events\", routingKey: \"house.*.franchise\");\n",[52,684,685,690,710,751],{"__ignoreMap":475},[549,686,687],{"class":551,"line":552},[549,688,689],{"class":555},"\u002F\u002F 加盟平台（Consumer 端）：宣告自己的 Queue 與 Binding\n",[549,691,692,694,696,698,700,702,704,706,708],{"class":551,"line":559},[549,693,563],{"class":562},[549,695,567],{"class":566},[549,697,570],{"class":562},[549,699,574],{"class":573},[549,701,577],{"class":562},[549,703,580],{"class":566},[549,705,583],{"class":562},[549,707,587],{"class":586},[549,709,590],{"class":562},[549,711,712,714,717,719,722,724,726,728,730,732,735,737,740,742,745,747,749],{"class":551,"line":593},[549,713,563],{"class":562},[549,715,716],{"class":566},"QueueDeclare",[549,718,570],{"class":562},[549,720,721],{"class":573},"\"franchise-sync\"",[549,723,673],{"class":562},[549,725,580],{"class":566},[549,727,583],{"class":562},[549,729,587],{"class":586},[549,731,673],{"class":562},[549,733,734],{"class":566},"exclusive",[549,736,583],{"class":562},[549,738,739],{"class":586},"false",[549,741,673],{"class":562},[549,743,744],{"class":566},"autoDelete",[549,746,583],{"class":562},[549,748,739],{"class":586},[549,750,590],{"class":562},[549,752,753,755,758,760,762,764,766,768,771,773,776],{"class":551,"line":600},[549,754,563],{"class":562},[549,756,757],{"class":566},"QueueBind",[549,759,570],{"class":562},[549,761,721],{"class":573},[549,763,673],{"class":562},[549,765,574],{"class":573},[549,767,673],{"class":562},[549,769,770],{"class":566},"routingKey",[549,772,583],{"class":562},[549,774,775],{"class":573},"\"house.*.franchise\"",[549,777,590],{"class":562},[15,779,780],{},"這樣設計的好處：",[19,782,783,792,798],{},[22,784,785,788,789],{},[25,786,787],{},"主力平台完全不知道加盟平台的存在","，它只負責把事件發到 ",[52,790,791],{},"house.events",[22,793,794,795],{},"之後多一個系統要訂閱物件事件，只要新增一個 Queue 和 Binding，",[25,796,797],{},"主力平台一行程式都不用改",[22,799,800],{},"過濾在 Broker 端完成，加盟平台不會收到、也不用處理直營物件的事件",[204,802,804],{"id":803},"fanout-的情境通知所有實例清快取","Fanout 的情境：通知所有實例清快取",[15,806,807,808,811,812,815,816,819,820,823],{},"Fanout 最適合「每個實例都要做同一件事」的場景。例如物件資料更新後，所有 Web 實例的本機快取都要清掉：每個實例啟動時宣告一個",[25,809,810],{},"自己專屬、斷線就自動刪除","的 Queue（",[52,813,814],{},"exclusive: true","、",[52,817,818],{},"autoDelete: true","），綁到 ",[52,821,822],{},"cache.invalidate"," 這個 Fanout Exchange 上。發一則訊息，所有實例都會收到。",[15,825,826,827,830],{},"注意這和前面「同一個服務共用 Queue」剛好相反：清快取是",[25,828,829],{},"每個實例都要做","，所以每個實例要有自己的 Queue。",[10,832,833],{"id":833},"一句話總結",[15,835,836],{},"Producer 把訊息交給 Exchange，Exchange 依 Binding 決定複製到哪些 Queue，Consumer 從 Queue 拿訊息；想讓同一個服務分工就共用 Queue，想讓不同服務各自收到就各自建 Queue，而 Topic Exchange 搭配設計良好的 Routing Key，可以讓 Broker 幫你完成大部分的過濾工作。",[838,839,840],"style",{},"html pre.shiki code .sAwPA, html code.shiki .sAwPA{--shiki-default:#6A737D}html pre.shiki code .s95oV, html code.shiki .s95oV{--shiki-default:#E1E4E8}html pre.shiki code .svObZ, html code.shiki .svObZ{--shiki-default:#B392F0}html pre.shiki code .sU2Wk, html code.shiki .sU2Wk{--shiki-default:#9ECBFF}html pre.shiki code .sDLfK, html code.shiki .sDLfK{--shiki-default:#79B8FF}html pre.shiki code .snl16, html code.shiki .snl16{--shiki-default:#F97583}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}",{"title":475,"searchDepth":559,"depth":559,"links":842},[843,844,845,851,852,855],{"id":12,"depth":559,"text":13},{"id":100,"depth":559,"text":100},{"id":195,"depth":559,"text":196,"children":846},[847,848,849,850],{"id":206,"depth":593,"text":207},{"id":217,"depth":593,"text":218},{"id":224,"depth":593,"text":225},{"id":380,"depth":593,"text":381},{"id":395,"depth":559,"text":396},{"id":436,"depth":559,"text":437,"children":853},[854],{"id":803,"depth":593,"text":804},{"id":833,"depth":559,"text":833},"2022-06-14",null,"md",{},"\u002Farticles\u002Frabbitmq-exchange-queue-binding",{"title":5,"description":475},"articles\u002Frabbitmq-exchange-queue-binding",[864,865,866,867],"RabbitMQ","Message Queue","事件驅動","系統設計","is8HIfimNVmoPE03Xu0wnF_KkToge8X8CLtuEzy2me4",1792959150569]