SQL鎖機制高級篇

SQL鎖機制高級篇

在看這篇文章(翻譯)之前,簡單介紹一下鎖,順便也帶出幾個專用詞彙的翻譯。

什麼是鎖

SQL Server 2000使用鎖來實現多用戶同時修改數據庫同一數據時的同步控制。

死鎖

多個會話同時訪問數據庫一些資源時,當每個會話都需要別的會話正在使用的資源時,死鎖就有可能發生。 死鎖在多線程系統中都有可能出現,並不僅僅侷限於於關係數據庫管理系統。

鎖的類型

一個數據庫系統在許多情況下都有可能鎖數據項。其可能性包括:

  • Rows—數據庫表中的一整行
  • Pages—行的集合(通常爲幾kb)
  • Extents—通常是幾個頁的集合
  • Table—整個數據庫表
  • Database—被鎖的整個數據庫表

除非有其它的說明,數據庫根據情況自己選擇最好的鎖方式。不過值得感謝的是,SQL Server提供了一種避免默認行爲的方法。這是由鎖提示來完成的。

鎖提示

Tansact-SQL提供了一系列不同級別的鎖提示,你可以在SELECT,INSERT,UPDATE和DELETE中使用它們來告訴SQL Server你需要如何通過重設鎖。可以實現的提示包括:

FASTFIRSTROW—選取結果集中的第一行,並將其優化 HOLDLOCK—持有一個共享鎖直至事務完成 NOLOCK—不允許使用共享鎖或獨享鎖。這可能會造成數據重寫或者沒有被確認就返回的情況; 因此,就有可能使用到髒數據。這個提示只能在SELECT中使用。 PAGLOCK—鎖表格 READCOMMITTED—只讀取被事務確認的數據。這就是SQL Server的默認行爲。 READPAST—跳過被其它進程鎖住的行,所以返回的數據可能會忽略行的內容。這也只能在SELECT中使用。 READUNCOMMITTED—等價於NOLOCK. REPEATABLEREAD—在查詢語句中,對所有數據使用鎖。這可以防止其它的用戶更新數據, 但是新的行可能被其它的用戶插入到數據中,並且被最新訪問該數據的用戶讀取。 ROWLOCK—按照行的級別來對數據上鎖。SQL Server通常鎖到頁或者表級別來修改行, 所以當開發者使用單行的時候,通常要重設這個設置。 SERIALIZABLE—等價於HOLDLOCK. TABLOCK—按照表級別上鎖。在運行多個有關表級別數據操作的時候,你可能需要使用到這個提示。 UPDLOCK—當讀取一個表的時候,使用更新鎖來代替共享鎖,並且保持一直擁有這個鎖直至事務結束。 它的好處是,可以允許你在閱讀數據的時候可以不需要鎖,並且以最快的速度更新數據。 XLOCK—給所有的資源都上獨享鎖,直至事務結束。 微軟將提示分爲兩類:granularity和isolation-level。Granularity提示包括PAGLOCK, NOLOCK, ROWLOCK和TABLOCK。而isolation-level提示包括HOLDLOCK, NOLOCK, READCOMMITTED, REPEATABLEREAD和SERIALIZABLE。

 

可以在Transact-SQL聲明中使用這些提示。它們被放在聲明的FROM部分中,位於WITH之後。WITH聲明在SQL Server 2000中是可選部分,但是微軟強烈要求將它包含在內。這就使得許多人都認爲在未來的SQL Server發行版中,就可能會包含這個聲明。下面是提示應用於FROM從句中的例子: [ FROM { < table_source > } [ ,...n ] ] < table_source > ::= table_name [ [ AS ] table_alias ] [ WITH ( < table_hint > [ ,...n ] ) ]   < table_hint > ::=   { INDEX ( index_val [ ,...n ] )   | FASTFIRSTROW   | HOLDLOCK   | NOLOCK   | PAGLOCK   | READCOMMITTED   | READPAST   | READUNCOMMITTED   | REPEATABLEREAD   | ROWLOCK   | SERIALIZABLE   | TABLOCK   | TABLOCKX   | UPDLOCK   | XLOCK }

詞彙表

會話 (session)

English Query 中由 English Query 引擎執行的操作序列。會話在用戶登錄時開始,在用戶註銷時結束。 會話期間的所有操作構成一個事務作用域,並受由登錄用戶名和密碼決定的權限的支配。 堆表 (heap table)

如果一個表沒有索引,數據行以隨機的順序存儲,這種結構稱爲堆。這種表稱爲堆表。 意向鎖 (intent lock)

放置在資源層次結構的一個級別上的鎖,以保護較低級別資源上的共享或排它鎖。例如,在 SQL Server 2000 數據庫引擎任務應用表內的共享或排它行鎖之前,在該表上放置意向鎖。如果另一個任務試圖在該表級別上應用共享或排它鎖,則受到由第一個任務控制的表級別意向鎖的阻塞。第二個任務在鎖定該表前不必檢查各個頁或行鎖,而只需檢查表上的意向鎖。 排它鎖(exclusive lock)

一種鎖,它防止任何其它事務獲取資源上的鎖,直到在事務的末尾將資源上的原始鎖釋放爲止。在更新操作(INSERT、UPDATE 或 DELETE)過程中始終應用排它鎖。 隔離級別 (isolation level)

控制隔離數據以供一個進程使用並防止其它進程干擾的程度的事務屬性。設置隔離級別定義了 SQL Server 會話中所有 SELECT 語句的默認鎖定行爲。 擴展(盤)區 (extent)

每當 SQL Server 對象(如表或索引)需要更多空間時分配給該對象的空間的單元。在 SQL Server 2000 中,一個擴展是八個鄰接的頁。 鎖粒度(lock granularity)

SQL Server中數據以8KB爲一頁(page)的單位保存,連續的8個頁組成一個擴展(extent)。創建數據庫時, 按這種方式來分配磁盤空間。當數據庫容量增加時,意味着要創建更多的頁和擴展。按照數據的存儲結構 (row,page,extent)進行加鎖,就是鎖粒度。

SQL Server 2000裏,最低的鎖粒度是行(row)鎖。SQL Server可以單獨鎖行,數據頁,擴展,表。 假設在UPDATE操作中隻影響一行記錄,SQL Server會將該行記錄鎖定,其他用戶只有等該行記錄的 更新操作完畢後才能修改。另一方面,對於沒有鎖定的行記錄,其他用戶是可以進行修改的。 因此行級鎖對於併發是最佳的。

現在假設UPDATE操作影響1000行記錄,SQL Server是否一次鎖定一行?那就意味着如果有一個這樣的選項,在 內存允許前提下,需要1000個鎖。實際上,SQL Server會根據這些數據是否分佈在連續的頁,來決定是否用 幾個頁面鎖,或者擴展鎖,或者是表鎖。如果SQL Server加了頁面鎖,那麼這些頁面上的記錄其它用戶就無法 訪問或者修改,即使頁面上有些數據並非屬於這1000行記錄。這就是一種追求併發性能和資源消耗之間的平衡策略。

SQL Server對鎖需要的資源十分敏感,也就是說,SQL Server查詢優化器檢測到可用內存較低時,就會使用頁鎖來 替代多個行鎖。同樣,在內存消耗更低的判斷下,會優先選擇表鎖而幾個擴展鎖。 鎖信息的標識

鎖類型:

  • RID :行標識符。用於在表中單獨鎖定一行。
  • KEY :鍵, 索引內部的行鎖。用於保護可串行事務中的鍵範圍。
  • PAG :數據或索引頁。
  • EXT :相鄰的八個數據頁或索引頁構成的一組。
  • TAB :包括所有數據和索引在內的整個表。
  • DB :數據庫。

 

^_^,說是簡單介紹,其實我覺得已經對鎖介紹也蠻多了,也許有寫得不對的地方,有心人幫忙指點一下。詞彙的中文翻譯 是從SQL Server聯機幫助(books online)上搬用的。下面開始正文,好歹人家也是發表在堂堂DBA大網站上的Article,呵呵。


原文:Advanced SQL Server Locking 來源:SQL-Server-Performance.com 作者:Andrés Taylor

I thought I knew SQL Server pretty well. I've been using the product for more than 6 years now, and I like to know my tools from the inside out.......


使用SQL Server 6年多了,在下自認爲對SQL Server還是比較熟悉的,而且我喜歡將SQL Server內部的一些 東西搞清楚。

當我在教一門SQL Server編程課程時,我注意到微軟MSDN中提到了鎖兼容性,在MSDN 列舉了一個兼容性關係的表格。

看過這張關係表格,我就想知道是否存在用於更新的意向鎖(Intent Update lock)?於是我開始閱讀相關的資料。 這篇文章也是我研究的結果。這篇文章的適用讀者是那些對隔離級別(isolation level),意向鎖,死鎖和鎖粒度有所瞭解的。 如果你對這些領域還不瞭解,那麼我建議你在讀這篇文章前,應該先去了解和閱讀相關資料。

希望這篇文章能夠加深你對SQL Server鎖的理解,也許有些技巧還能夠在SQL Server編程中帶來幫助。

必須指出,即使不知道鎖是如何工作的,你也能長時間愉快地使用SQL Server,並且能創建高質量的代碼和數據庫設計。 不過如果你象我那樣喜歡探究事情的內部機理,或者你的工作需要你掌握一些性能方面的知識,我很樂意能教你一些有用的東西。

更新鎖(Update Locks)

死鎖的典型情況是SPID X鎖住了資源A,並在等待對資源B進行加鎖,而SPID Y鎖住了資源B,在等待對資源A加鎖,如此就 形成了死鎖。如果不理解,查詢 MSDN 或者相關的資料。

現在來假想更多情形下的死鎖。假設:SPID X在資源A上加了共享鎖,SPID Y也在資源A上加了共享鎖,因爲是共享鎖, 所以這樣沒有問題。現在X想把共享鎖升級爲排它鎖(exclusive lock)以用於更新資源。X就必須等Y釋放共享鎖才能辦到, 當X在等待時,Y也想做同樣的事情。這樣,X在等Y釋放,Y同時在等待X釋放,死鎖產生了。這種死鎖被稱爲 轉換死鎖(conversion deadlock)。

這種情況會很常見,爲避免這種死鎖,就引入了更新鎖機制。更新鎖允許連接讀取資源,同時宣告它因爲要編輯數據而要開始 鎖住資源了。SQL Server並無法提前知道一個事務要把共享鎖轉換成排它鎖了,當然有一個情況特殊,即只在一個SQL語句中 完成讀取然後更新的操作,比如說UPDATE XXX (SELECT YYY ....)這種類型。對於一般的SELECT語句,我們必須顯示地 使用UPDLOCK提示。

下面是代碼示例:

USE Northwind GO SET TRANSACTION ISOLATION LEVEL REPEATABLE READ GO BEGIN TRAN SELECT * FROM Orders (UPDLOCK) WHERE OrderID = 10633

 

注意到我打開了事務,但並沒有關閉事務。這樣鎖就始終存在。 如果另外一個連接視圖在相同的記錄上獲取更新鎖,就只有等待第一個事務結束後才行。這樣就可以演示,在相同資源上, 兩個更新鎖不相容的效果。

運行SP_LOCK,會顯示和上面的操作相關的記錄行,字段以及鎖的情況:

at_sql_locking1

如我們預想那樣,主鍵OrderID被更新鎖鎖住了。圖中Resource列裏面那個(89003da47679)的值,表示的是 主鍵10633的哈希值。SQL Server使用哈希表的方式來存儲鎖信息。

包含那行的記錄行,如我們所期望的那樣,被更新意向鎖鎖住了。在resource那列的數值(1:242)表示該數據頁面是 數據庫的第1個文件,頁面編號是#242。而意外的是,SQL Server添加了一個IX的表鎖。由於SQL Server不會在 表鎖上使用U/IU類型鎖,所以在表鎖級別上,只能看到X/IX類型鎖。

當更新操作中帶有where語法,SQL Server會掃描整個表,並且/或者掃描索引,以決定那些記錄會被改變。 在從表/索引讀取信息之前,SQL Server首先把對象鎖住。既然SQL Server知道你提交的是更新事務,那麼它 就會選擇更新鎖,而不是共享鎖。這樣做就是爲了避免前面所提到的死鎖情況--轉換死鎖(conversion deadlock)。

當SQL Server確定那些記錄行需要改變後,在這些記錄上,它會把更新鎖進一步升級爲排它鎖,如果是堆表(heap table),那麼鎖加 在RID(行標識符)上,如果是聚集索引表,鎖加在主鍵上。這就意味着更新鎖會立刻升級爲排它鎖,因此當你執行UPDATE 操作時,幾乎不可能看到這個更新過程。

不過,也有例外。如果SQL Server使用一個索引來定位記錄行,它就會鎖住索引頁,在索引上加的就是更新鎖。 如果不改變任何包含在該索引中的數據列,更新鎖不會升級爲排它鎖。下面是一個例子:

BEGIN TRAN UPDATE Region SET RegionDescription = 'South' WHERE RegionID = 4

 

Region是一個堆表,在RegionId上只有非聚集唯一索引主鍵。因此完成上面查詢時,SQL Server在RegionId上掃描索引, 鎖住索引頁和索引鍵。當發現要改變得記錄行後,因爲更新查詢並不改變RegionId的值,因此不會升級到排它鎖。 運行SP_LOCK後可以得到以下信息:

at_sql_locking2

我們看到,在RID上有一個IX鎖。該鎖位於RegionId索引上。還可以看到在表上有一個IX鎖,RID上有一個X鎖。 KEY鎖在RegionId索引上,證據可以從Indid列上可以得到。在索引上還有一個更新鎖,這是更新鎖激活的一個瞬間之一。

當查詢結束後,仍然存在兩個頁面鎖 –- 一個在索引頁 (1:306)上, 另一個在堆(heap) (1:300)上。這是因爲 堆的Indid(Index id)爲0。

鎖粒度(Lock Granularity)

SQL Server有幾種鎖類型,每種類型都可以選擇不同的粒度。

如果運行SP_LOCK,或者查看企業管理器中"當前激活"信息,就可以看到至少四,五中不同的鎖類型。下面簡單 回顧一下這些類型:

  • Database (DB): 這是一種會話(session)鎖。例如,它不涉及任何事務,僅僅是一個用戶和數據庫之間的連接。 這樣就可以防止有用戶連接到數據庫時,該數據庫被卸載了。值得注意的是,雖然SQL Server的master和tempdb是 不能卸載的,但是在這兩個數據庫上是沒有DB鎖的。
  • Table (TAB): 這是SQL Server中最粗略的邏輯鎖。在表級別上經常加的是意向鎖 (覺得意向鎖不安全嗎? 這裏有更詳細的信息。)
  • Extent (EXT): 這種鎖一般發生在SQL Server創建新表,或者已有表容量增加時,而並非用於鎖住記錄行。因此 當文件容量變化時,經常會看到這種鎖的存在。
  • Page (PAG): 當SQL Server要同時鎖住很多記錄行,而可用的鎖槽(slot)較少時,頁面鎖將會被採用。頁面級別 上的意向鎖更常見。目前爲止的SQL Server版本(包括SQL Server 6.5在內),這種類型的鎖是最佳性能的。
  • Key (KEY): 和RID鎖一樣,可能是SQL Server中最佳級別的鎖。KEY鎖用於索引上,而RID鎖用在堆表上。 (譯者注:行鎖包括KEY鎖和RID鎖,從鎖的級別上 考慮對併發是最佳的,但是從性能考慮,行鎖會大量佔用資源,相關資料可見前面的blog)。

在研究SQL Server 2000的鎖行爲中,我認爲SQL Server在大多數情況下,和速度相比,更看重併發性能。 較高的併發性能,意味着很多用戶能同時對數據庫進行操作。所以鎖儘可能的小,不必要地鎖住別人也需要的數據的可能性 就越小;另一方面,如果使用較大的鎖,將獲取更高的速度。(譯者注:這句話的理解 應該以平衡性能的前提下考查。)

當SQL Server 2000發現操作將鎖住越來越多的記錄行時,就會提高鎖的級別。 例如SQL Server 2000會升級到表鎖,丟掉單獨的pages/keys/RIDs級別鎖。注意:提高鎖的級別肯定是升級到 表鎖,而不會將RID/KEY鎖升級到頁面鎖。

那麼SQL Server2000什麼時候升級鎖呢?它無法知道你將鎖住的表的比例,因此它唯一在意的就是產生的鎖的數量。 當鎖使用了較高比例的內存時,SQL Server2000就開始升級所有連接事務上的鎖了。當鎖槽使用將盡時,也會開始 升級工作。你可以用SP_CONFIGURE來配置SQL Server可用的鎖槽數,例如降低該數值,從而來觀察鎖的升級情況。

SQL Server會儘可能使用較小的鎖來保證較高的併發性能。但是有時候SQL Server並不知道數據將會發生怎麼樣的改變, 從而它會按照它的規則來改變鎖的級別,而這種改變並非你想要的。例如一個很大的查找表(lookup table),僅僅是讀取 數據。那麼你可以直接用一個表鎖來替代很多KEY鎖。使用的方法是使用鎖提示或者SP_INDEXOPTION。

鎖提示很普通,在 聯機幫助(BOL) 有大量關於此的文檔,因此在本文就不重複介紹了。系統存儲過程SP_INDEXOPTION是強迫SQL Server使用特定大小的鎖 的好辦法。

使用SP_INDEXOPTION,你可以關閉行或者頁面級別的鎖。也就是說,你可以不需要鎖提示--所有表或者索引上 的鎖都是你指定的大小。即使BOL宣稱,該存儲過程用於索引上的鎖粒度,其實它也能用戶堆上。一個好的實現 方法是使用表名來替代@IndexNamePattern變量,這種方法很少人知道。

關於這方面的研究並沒有結束。如果你使用了兩個更高隔離級別中的一個,且在檢索規則中沒有任何可用的索引,那麼 SQL Server即使不鎖住整個表,也會儘可能多的記錄來滿足你的查詢。下面是一個例子:

USE Northwind GO SET TRANSACTION ISOLATION LEVEL     SERIALIZABLE GO BEGIN TRAN UPDATE dbo.Orders SET Freight = Freight * 1.25 WHERE Freight BETWEEN 100 AND 200

 

在另一個窗口運行SP_LOCK。在我這裏運行的時候,我看到該連接上有853個鎖。數據庫Northwind中的Orders表上 有830行,每行上都有一個鎖。回滾該UPDATE事務,然後進行改寫,在UPDATE前先創建索引,如下所示:

USE Northwind GO CREATE NONCLUSTERED INDEX     FreightTest ON     Orders(Feight) GO SET TRANSACTION ISOLATION LEVEL     SERIALIZABLE GO BEGIN TRAN UPDATE dbo.Orders SET Freight = Freight * 1.25 WHERE Freight BETWEEN 100 AND 200

 

現在,運行SP_LOCK只顯示25個鎖。這在性能調試時經常被忽視。即使你是用缺省的READ COMMITED隔離級別, 和創建索引相比,也是巨大的差別--136個鎖和24個鎖。

鎖的跟蹤標記(Locking Trace Flags)

有一些跟蹤標記可以幫助我們調試鎖,發現死鎖問題。

跟蹤標記用於打開或者關閉SQL Server的行爲方式。DBCC TRACEON命令來設置跟蹤標記,如果希望SQL Server啓動 時就打開跟蹤標記,只要在啓動參數加'-T'就可以了。

  • 1200: 顯示所有連接的所有的鎖。這個選項將會有巨量的輸出信息,因此我建議只在可控制的環境下使用,例如在 同一時刻只有一個連接在工作。
  • 1204: 輸入和死鎖相關的信息。下面是這種信息的一個示例:

    Node:1 KEY: 6:885578193:2 (010086470766) CleanCnt : 1 Mode : U Flags : 0x0  Grant List 0:   Owner: 0x42c0b2e0 Mode: U Flg: 0x0 Ref: 2 Life: 02000000 SPID: 53 ECID: 0   SPID: 53 ECID: 0 Statement Type: UPDATE Line #: 1   Input Buf: Language Event: update Region set RegionDescription = 'aa' where RegionID = 1 Requested By   ResType: LockOwner Stype : 'OR' Mode: U SPID: 51 ECID: 0 Ec:(0x42E25568) Value : 0x42c0b220 Cost: (0/0) Node: 2 RID: 6:1:300:0 CleanCnt: 1 Mode: U Flags: 0x2  Grant List: 0   Owner: 0x42c0b320 Mode: S Flg: 0x0 Ref: 1 Life: 02000000 SPID: 51 ECID: 0   SPID: 51 ECID: 0 Statement Type: UPDATE Line #: 1   Input Buf: Language Event: update Region set RegionDescription = 'aa' where RegionID = 1 Requested By   ResType: LockOwner Stype : 'OR' Mode: X SPID: 53 ECID: 0 Ec:(0x434A1568) Value : 0x42c0b240 Cost: (0/0) Victim Resource Owner:  ResType: LockOwner Stype: 'OR' Mode: X SPID: 53 ECID: 0 Ec(0x434A1568) Value: 0x42c0b240 Cost: (0/0)

     

    KEY: 表示死鎖中涉及到的索引信息。當然你也可以用類似的參數來指定任何其它的鎖信息,例如page,RID,table等等。

    ECID從master.dbo.sysprocesses得到,用於區分不同線程產生的鎖。Mode是死鎖的請求模式,例如S, X 或者 U。

    字符串"6:885578193:2"表示:數據庫id爲6,對象id爲885578193,索引id爲2。後面圓括號內的數值是標識鎖的哈希值, 該值存儲在master.dbo.syslockinfo表的rsc_text列內。遺憾的是,這個數值是單向哈希,也就是說僅靠它是無法找出被 鎖住的記錄行。Spid是鎖的系統進程ID。

    Node 1 & 2顯示進入了死鎖狀態。兩個鎖都處於等待隊列中,“Requested By:”說明了這一點。

  • 1205: 打印鎖管理工作的相關信息。每次死鎖搜索工作初始化後,跟蹤標記就通知鎖管理打印出搜索的信息。該 選項工作的前提示跟蹤標記1024必須給出。
  • 1211: 禁止所有鎖的升級。這個標記通知鎖管理不要升級任何鎖,即使鎖資源被用完也一樣。

列鎖(Column Locks)

正如你所知道的,SQL Server 2000中最下的鎖是行鎖。SQL Server 並不直接提供列鎖。下面我們通過索引鎖來模擬 實現列級別的鎖。

列鎖通常被認爲在某些情況下會很慢,SQL Server也不例外。但是既然行鎖並不自動鎖表的索引,因此你總是可以 在索引頁上使用那些被鎖住的數據。我們再用數據庫Northwind的Region表來舉例。

表Region是堆表,有兩個字段:RegionDescription和RegionId。RegionId字段上有一個唯一性非聚集索引。

我們用一個簡單的UPDATE操作,來更新RegionDescription字段的內容。

USE Northwind GO BEGIN TRAN UPDATE Region SET RegionDescription = 'South' WHERE RegionDescription = 'Southern'

 

該查詢,SQL Server不會用到索引,因爲在RegionDescription字段上並沒有索引。 因此SQL Server會掃描整表以找到需要更新的記錄行。一旦找到,那些記錄上的更新鎖就會升級到排它鎖。 要確定這點,可以在另一個窗口運行SP_LOCK即可。因此那些對應數據上應該有RID鎖。在運行SP_LOCK的那個窗口中 輸入一個SELECT查詢:

SELECT * FROM Region

 

此時,我們不會進入等待狀態。如果你象我,就會喜歡去看一下執行計劃,因爲執行計劃會告訴我們爲什麼此時我們不會 進入等待狀態。

at_sql_locking3

正如上面看到的,SQL Server要完成上面的SELECT,需要選擇一個索引掃描以獲取數據。既然SLECT *可以用索引來完成, 因此它就沒有必要去讀取堆上的數據了。我們稱這種查詢爲覆蓋查詢(covering query)。

需要注意上面過程中的兩個準則。第一個準則是查詢中涉及到的數據必須是索引能照顧到的。記住如果表有一個聚集索引, 所有的非聚集索引會有一個index字段,字段內就是那個聚集索引字段的值

第二個準則是早先的那個UPDATE操作不能改變任何索引包含的字段的值。如果被改變了(即索引值也被改變了),它就會 升級到排它鎖,因此上面的技巧也失效了。

擴展鎖能力的表格(Extended Lock Capability Table)

該表可以在聯機幫助和MSDN中找到,它標識了那些鎖之間是相互兼容的。我這裏列出一個更復雜的表格,希望 對大家有用:

at_sql_locking4

結束語

我的確找到了難以掌握的更新意向鎖,對此進行了大量的研究。鎖和鎖行爲在聯機幫助中的資料很少,因此也增加了對此的研究。 我在大量研究後寫下此文,希望能和你們分享相關的知識。

發佈了56 篇原創文章 · 獲贊 10 · 訪問量 12萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章