數據庫兩大神器【索引和鎖】

本文轉載自https://juejin.im/post/5b55b842f265da0f9e589e79。 html

前言

索引和鎖在數據庫中能夠說是很是重要的知識點了,在面試中也會常常會被問到的。mysql

本文力求簡單講清每一個知識點,但願你們看完能有所收穫git

聲明:若是沒有說明具體的數據庫和存儲引擎,默認指的是MySQL中的InnoDB存儲引擎程序員

1、索引

在以前,我對索引有如下的認知:github

  • 索引能夠加快數據庫的檢索速度面試

  • 常常進行INSERT/UPDATE/DELETE操做就不要創建索引了,換言之:索引會下降插入、刪除、修改等維護任務的速度。算法

  • 索引須要佔物理和數據空間sql

  • 瞭解過索引的最左匹配原則數據庫

  • 知道索引的分類:彙集索引和非彙集索引segmentfault

  • Mysql支持Hash索引和B+樹索引兩種

看起來好像啥都知道,但面試讓你說的時候可能就GG了:

  • 使用索引爲何能夠加快數據庫的檢索速度啊?

  • 爲何說索引會下降插入、刪除、修改等維護任務的速度。

  • 索引的最左匹配原則指的是什麼?

  • Hash索引和B+樹索引有什麼區別?主流的使用哪個比較多?InnoDB存儲都支持嗎?

  • 彙集索引和非彙集索引有什麼區別?

  • ........

1.1聊聊索引的基礎知識

首先Mysql的基本存儲結構是(記錄都存在頁裏邊):


1



1


  • 各個數據頁能夠組成一個雙向鏈表

  • 每一個數據頁中的記錄又能夠組成一個單向鏈表

    • 每一個數據頁都會爲存儲在它裏邊兒的記錄生成一個頁目錄,在經過主鍵查找某條記錄的時候能夠在頁目錄中使用二分法快速定位到對應的槽,而後再遍歷該槽對應分組中的記錄便可快速找到指定的記錄

    • 其餘列(非主鍵)做爲搜索條件:只能從最小記錄開始依次遍歷單鏈表中的每條記錄

因此說,若是咱們寫select * from user where username = 'Java3y'這樣沒有進行任何優化的sql語句,默認會這樣作:

  • 定位到記錄所在的頁

    • 須要遍歷雙向鏈表,找到所在的頁

  • 從所在的頁內中查找相應的記錄

    • 因爲不是根據主鍵查詢,只能遍歷所在頁的單鏈表了

很明顯,在數據量很大的狀況下這樣查找會很慢

1.2索引提升檢索速度

索引作了些什麼可讓咱們查詢加快速度呢?

其實就是將無序的數據變成有序(相對)


1


要找到id爲8的記錄簡要步驟:


1


很明顯的是:沒有用索引咱們是須要遍歷雙向鏈表來定位對應的頁,如今經過**「目錄」**就能夠很快地定位到對應的頁上了!

其實底層結構就是B+樹,B+樹做爲樹的一種實現,可以讓咱們很快地查找出對應的記錄。

參考資料:

1.3索引下降增刪改的速度

B+樹是平衡樹的一種。

平衡樹:它是一棵空樹或它的左右兩個子樹的高度差的絕對值不超過1,而且左右兩個子樹都是一棵平衡二叉樹。

若是一棵普通的樹在極端的狀況下,是能退化成鏈表的(樹的優勢就不復存在了)


1


B+樹是平衡樹的一種,是不會退化成鏈表的,樹的高度都是相對比較低的(基本符合矮矮胖胖(均衡)的結構)【這樣一來咱們檢索的時間複雜度就是O(logn)】!從上一節的圖咱們也能夠看見,創建索引實際上就是創建一顆B+樹。

  • B+樹是一顆平衡樹,若是咱們對這顆樹增刪改的話,那確定會破壞它的原有結構

  • 要維持平衡樹,就必須作額外的工做。正由於這些額外的工做開銷,致使索引會下降增刪改的速度

B+樹刪除和修改具體可參考:

1.4哈希索引

除了B+樹以外,還有一種常見的是哈希索引。

哈希索引就是採用必定的哈希算法,把鍵值換算成新的哈希值,檢索時不須要相似B+樹那樣從根節點到葉子節點逐級查找,只需一次哈希算法便可馬上定位到相應的位置,速度很是快

  • 本質上就是把鍵值換算成新的哈希值,根據這個哈希值來定位


1


看起來哈希索引很牛逼啊,但其實哈希索引有好幾個侷限(根據他本質的原理可得):

  • 哈希索引也沒辦法利用索引完成排序

  • 不支持最左匹配原則

  • 在有大量重複鍵值狀況下,哈希索引的效率也是極低的---->哈希碰撞問題。

  • 不支持範圍查詢

參考資料:

1.5InnoDB支持哈希索引嗎?

主流的仍是使用B+樹索引比較多,對於哈希索引,InnoDB是自適應哈希索引的(hash索引的建立由InnoDB存儲引擎引擎自動優化建立,咱們干預不了)!


1


參考資料:

1.6彙集和非彙集索引

簡單歸納:

  • 彙集索引就是以主鍵建立的索引

  • 非彙集索引就是以非主鍵建立的索引

區別:

  • 彙集索引在葉子節點存儲的是表中的數據

  • 非彙集索引在葉子節點存儲的是主鍵和索引列

  • 使用非彙集索引查詢出數據時,拿到葉子上的主鍵再去查到想要查找的數據。(拿到主鍵再查找這個過程叫作回表)

非彙集索引也叫作二級索引,不用糾結那麼多名詞,將其等價就好了~

非彙集索引在創建的時候也未必是單列的,能夠多個列來建立索引。

  • 此時就涉及到了哪一個列會走索引,哪一個列不走索引的問題了(最左匹配原則-->後面有說)

  • 建立多個單列(非彙集)索引的時候,會生成多個索引樹(因此過多建立索引會佔用磁盤空間)


1


在建立多列索引中也涉及到了一種特殊的索引-->覆蓋索引

  • 咱們前面知道了,若是不是彙集索引,葉子節點存儲的是主鍵+列值

  • 最終仍是要「回表」,也就是要經過主鍵查找一次。這樣就會比較慢

  • 覆蓋索引就是把要查詢出的列和索引是對應的,不作回表操做!

好比說:

  • 如今我建立了索引(username,age),在查詢數據的時候:select username , age from user where username = 'Java3y' and age = 20

  • 很明顯地知道,咱們上邊的查詢是走索引的,而且,要查詢出的列在葉子節點都存在!因此,就不用回表了~

  • 因此,能使用覆蓋索引就儘可能使用吧~

1.7索引最左匹配原則

最左匹配原則

  • 索引能夠簡單如一個列(a),也能夠複雜如多個列(a, b, c, d),即聯合索引

  • 若是是聯合索引,那麼key也由多個列組成,同時,索引只能用於查找key是否存在(相等),遇到範圍查詢(>、<、between、like左匹配)等就不能進一步匹配了,後續退化爲線性查找。

  • 所以,列的排列順序決定了可命中索引的列數

例子:

  • 若有索引(a, b, c, d),查詢條件a = 1 and b = 2 and c > 3 and d = 4,則會在每一個節點依次命中a、b、c,沒法命中d。(很簡單:索引命中只能是相等的狀況,不能是範圍匹配)

1.8=、in自動優化順序

不須要考慮=、in等的順序,mysql會自動優化這些條件的順序,以匹配儘量多的索引列。

例子:

  • 若有索引(a, b, c, d),查詢條件c > 3 and b = 2 and a = 1 and d < 4a = 1 and c > 3 and b = 2 and d < 4等順序都是能夠的,MySQL會自動優化爲a = 1 and b = 2 and c > 3 and d < 4,依次命中a、b、c。

1.9索引總結

索引在數據庫中是一個很是重要的知識點!上面談的其實就是索引最基本的東西,要建立出好的索引要顧及到不少的方面:

  • 1,最左前綴匹配原則。這是很是重要、很是重要、很是重要(重要的事情說三遍)的原則,MySQL會一直向右匹配直到遇到範圍查詢(>,<,BETWEEN,LIKE)就中止匹配。

  • 3,儘可能選擇區分度高的列做爲索引,區分度的公式是 COUNT(DISTINCT col) / COUNT(*)。表示字段不重複的比率,比率越大咱們掃描的記錄數就越少。

  • 4,索引列不能參與計算,儘可能保持列「乾淨」。好比,FROM_UNIXTIME(create_time) = '2016-06-06' 就不能使用索引,緣由很簡單,B+樹中存儲的都是數據表中的字段值,可是進行檢索時,須要把全部元素都應用函數才能比較,顯然這樣的代價太大。因此語句要寫成 : create_time = UNIX_TIMESTAMP('2016-06-06')

  • 5,儘量的擴展索引,不要新創建索引。好比表中已經有了a的索引,如今要加(a,b)的索引,那麼只須要修改原來的索引便可。

  • 6,單個多列組合索引和多個單列索引的檢索查詢效果不一樣,由於在執行SQL時,MySQL只能使用一個索引,會從多個單列索引中選擇一個限制最爲嚴格的索引。

參考資料:

2、鎖


1


在mysql中的鎖看起來是很複雜的,由於有一大堆的東西和名詞:排它鎖,共享鎖,表鎖,頁鎖,間隙鎖,意向排它鎖,意向共享鎖,行鎖,讀鎖,寫鎖,樂觀鎖,悲觀鎖,死鎖。這些名詞有的博客又直接寫鎖的英文的簡寫--->X鎖,S鎖,IS鎖,IX鎖,MMVC...

鎖的相關知識又跟存儲引擎,索引,事務的隔離級別都是關聯的....

這就給初學數據庫鎖的人帶來很多的麻煩~~~因而我下面就簡單整理一下數據庫鎖的知識點,但願你們看完會有所幫助。

2.1爲何須要學習數據庫鎖知識

很多人在開發的時候,應該不多會注意到這些鎖的問題,也不多會給程序加鎖(除了庫存這些對數量準確性要求極高的狀況下)

通常也就聽過常說的樂觀鎖和悲觀鎖,瞭解過基本的含義以後就沒了~~~

定心丸:即便咱們不會這些鎖知識,咱們的程序在通常狀況下仍是能夠跑得好好的。由於這些鎖數據庫隱式幫咱們加了

  • 對於UPDATE、DELETE、INSERT語句,InnoDB自動給涉及數據集加排他鎖(X)

  • MyISAM在執行查詢語句SELECT前,會自動給涉及的全部表加讀鎖,在執行更新操做(UPDATE、DELETE、INSERT等)前,會自動給涉及的表加寫鎖,這個過程並不須要用戶干預

只會在某些特定的場景下才須要手動加鎖,學習數據庫鎖知識就是爲了:

  • 能讓咱們在特定的場景下派得上用場

  • 更好把控本身寫的程序

  • 在跟別人聊數據庫技術的時候能夠搭上幾句話

  • 構建本身的知識庫體系!在面試的時候不虛

2.2表鎖簡單介紹

首先,從鎖的粒度,咱們能夠分紅兩大類:

  • 表鎖

    • 開銷小,加鎖快;不會出現死鎖;鎖定力度大,發生鎖衝突機率高,併發度最低

  • 行鎖

    • 開銷大,加鎖慢;會出現死鎖;鎖定粒度小,發生鎖衝突的機率低,併發度高

不一樣的存儲引擎支持的鎖粒度是不同的:

  • InnoDB行鎖和表鎖都支持

  • MyISAM只支持表鎖

InnoDB只有經過索引條件檢索數據才使用行級鎖,不然,InnoDB將使用表鎖

  • 也就是說,InnoDB的行鎖是基於索引的

表鎖下又分爲兩種模式

  • 表讀鎖(Table Read Lock)

  • 表寫鎖(Table Write Lock)

  • 從下圖能夠清晰看到,在表讀鎖和表寫鎖的環境下:讀讀不阻塞,讀寫阻塞,寫寫阻塞

    • 讀讀不阻塞:當前用戶在讀數據,其餘的用戶也在讀數據,不會加鎖

    • 讀寫阻塞:當前用戶在讀數據,其餘的用戶不能修改當前用戶讀的數據,會加鎖!

    • 寫寫阻塞:當前用戶在修改數據,其餘的用戶不能修改當前用戶正在修改的數據,會加鎖!


1


從上面已經看到了:讀鎖和寫鎖是互斥的,讀寫操做是串行

  • 若是某個進程想要獲取讀鎖,同時另一個進程想要獲取寫鎖。在mysql裏邊,寫鎖是優先於讀鎖的

  • 寫鎖和讀鎖優先級的問題是能夠經過參數調節的:max_write_lock_countlow-priority-updates

值得注意的是:

The LOCAL modifier enables nonconflicting INSERT statements (concurrent inserts) by other sessions to execute while the lock is held. (See Section 8.11.3, 「Concurrent Inserts」.) However, READ LOCAL cannot be used if you are going to manipulate the database using processes external to the server while you hold the lock. For InnoDB tables, READ LOCAL is the same as READ

  • MyISAM能夠支持查詢和插入操做的併發進行。能夠經過系統變量concurrent_insert來指定哪一種模式,在MyISAM中它默認是:若是MyISAM表中沒有空洞(即表的中間沒有被刪除的行),MyISAM容許在一個進程讀表的同時,另外一個進程從表尾插入記錄。

  • 可是InnoDB存儲引擎是不支持的

參考資料:

2.2行鎖細講

上邊簡單講解了表鎖的相關知識,咱們使用Mysql通常是使用InnoDB存儲引擎的。InnoDB和MyISAM有兩個本質的區別:

  • InnoDB支持行鎖

  • InnoDB支持事務

從上面也說了:咱們是不多手動加表鎖的。表鎖對咱們程序員來講幾乎是透明的,即便InnoDB不走索引,加的表鎖也是自動的!

咱們應該更加關注行鎖的內容,由於InnoDB一大特性就是支持行鎖!

InnoDB實現瞭如下兩種類型的行鎖。

  • 共享鎖(S鎖):容許一個事務去讀一行,阻止其餘事務得到相同數據集的排他鎖。

    • 也叫作讀鎖:讀鎖是共享的,多個客戶能夠同時讀取同一個資源,但不容許其餘客戶修改

  • 排他鎖(X鎖):容許得到排他鎖的事務更新數據,阻止其餘事務取得相同數據集的共享讀鎖和排他寫鎖。

    • 也叫作寫鎖:寫鎖是排他的,寫鎖會阻塞其餘的寫鎖和讀鎖

看完上面的有沒有發現,在一開始所說的:X鎖,S鎖,讀鎖,寫鎖,共享鎖,排它鎖其實總共就兩個鎖,只不過它們有多個名字罷了~~~

Intention locks do not block anything except full table requests (for example, LOCK TABLES ... WRITE). The main purpose of intention locks is to show that someone is locking a row, or going to lock a row in the table.

另外,爲了容許行鎖和表鎖共存,實現多粒度鎖機制,InnoDB還有兩種內部使用的意向鎖(Intention Locks),這兩種意向鎖都是表鎖

  • 意向共享鎖(IS):事務打算給數據行加行共享鎖,事務在給一個數據行加共享鎖前必須先取得該表的IS鎖。

  • 意向排他鎖(IX):事務打算給數據行加行排他鎖,事務在給一個數據行加排他鎖前必須先取得該表的IX鎖。

  • 意向鎖也是數據庫隱式幫咱們作了,不須要程序員操心

參考資料:

2.2.1MVCC和事務的隔離級別

數據庫事務有不一樣的隔離級別,不一樣的隔離級別對鎖的使用是不一樣的,鎖的應用最終致使不一樣事務的隔離級別

MVCC(Multi-Version Concurrency Control)多版本併發控制,能夠簡單地認爲:MVCC就是行級鎖的一個變種(升級版)

  • 事務的隔離級別就是經過鎖的機制來實現,只不過隱藏了加鎖細節

表鎖中咱們讀寫是阻塞的,基於提高併發性能的考慮,MVCC通常讀寫是不阻塞的(因此說MVCC不少狀況下避免了加鎖的操做)

  • MVCC實現的讀寫不阻塞正如其名:多版本併發控制--->經過必定機制生成一個數據請求時間點的一致性數據快照(Snapshot),並用這個快照來提供必定級別(語句級或事務級)的一致性讀取。從用戶的角度來看,好像是數據庫能夠提供同一數據的多個版本

快照有兩個級別

  • 語句級

    • 針對於Read committed隔離級別

  • 事務級別

    • 針對於Repeatable read隔離級別

咱們在初學的時候已經知道,事務的隔離級別有4種

  • Read uncommitted

    • 會出現髒讀,不可重複讀,幻讀

  • Read committed

    • 會出現不可重複讀,幻讀

  • Repeatable read

    • 會出現幻讀(但在Mysql實現的Repeatable read配合gap鎖不會出現幻讀!)

  • Serializable

    • 串行,避免以上的狀況!


Read uncommitted會出現的現象--->髒讀:一個事務讀取到另一個事務未提交的數據

  • 例子:A向B轉帳,A執行了轉帳語句,但A尚未提交事務,B讀取數據,發現本身帳戶錢變多了!B跟A說,我已經收到錢了。A回滾事務【rollback】,等B再查看帳戶的錢時,發現錢並無多。

  • 出現髒讀的本質就是由於操做(修改)完該數據就立馬釋放掉鎖,致使讀的數據就變成了無用的或者是錯誤的數據


Read committed避免髒讀的作法其實很簡單:

  • 就是把釋放鎖的位置調整到事務提交以後,此時在事務提交前,其餘進程是沒法對該行數據進行讀取的,包括任何操做

Read committed出現的現象--->不可重複讀:一個事務讀取到另一個事務已經提交的數據,也就是說一個事務能夠看到其餘事務所作的修改

  • 注:A查詢數據庫獲得數據,B去修改數據庫的數據,致使A屢次查詢數據庫的結果都不同【危害:A每次查詢的結果都是受B的影響的,那麼A查詢出來的信息就沒有意思了】


上面也說了,Read committed語句級別的快照!每次讀取的都是當前最新的版本

Repeatable read避免不可重複讀是事務級別的快照!每次讀取的都是當前事務的版本,即便被修改了,也只會讀取當前事務版本的數據。

呃...若是仍是不太清楚,咱們來看看InnoDB的MVCC是怎麼樣的吧(摘抄《高性能MySQL》)


1



1


至於虛讀(幻讀):是指在一個事務內讀取到了別的事務插入的數據,致使先後讀取不一致。

  • 注:和不可重複讀相似,但虛讀(幻讀)會讀到其餘事務的插入的數據,致使先後讀取不一致

  • MySQL的Repeatable read隔離級別加上GAP間隙鎖已經處理了幻讀了

參考資料:

擴展閱讀:

2.3樂觀鎖和悲觀鎖

不管是Read committed仍是Repeatable read隔離級別,都是爲了解決讀寫衝突的問題。

單純在Repeatable read隔離級別下咱們來考慮一個問題:



此時,用戶李四的操做就丟失掉了:

  • 丟失更新:一個事務的更新覆蓋了其它事務的更新結果

(ps:暫時沒有想到比較好的例子來講明更新丟失的問題,雖然上面的例子也是更新丟失,但必定程度上是可接受的..不知道有沒有人能想到不可接受的更新丟失例子呢...)

解決的方法:

  • 使用Serializable隔離級別,事務是串行執行的!

  • 樂觀鎖

  • 悲觀鎖

  1. 樂觀鎖是一種思想,具體實現是,表中有一個版本字段,第一次讀的時候,獲取到這個字段。處理完業務邏輯開始更新的時候,須要再次查看該字段的值是否和第一次的同樣。若是同樣更新,反之拒絕。之因此叫樂觀,由於這個模式沒有從數據庫加鎖,等到更新的時候再判斷是否能夠更新。

  2. 悲觀鎖是數據庫層面加鎖,都會阻塞去等待鎖。

2.3.1悲觀鎖

因此,按照上面的例子。咱們使用悲觀鎖的話其實很簡單(手動加行鎖就好了):

  • select * from xxxx for update

在select 語句後邊加了 for update至關於加了排它鎖(寫鎖),加了寫鎖之後,其餘的事務就不能對它修改了!須要等待當前事務修改完以後才能夠修改.

  • 也就是說,若是張三使用select ... for update,李四就沒法對該條記錄修改了~

2.3.2樂觀鎖

樂觀鎖不是數據庫層面上的鎖,是須要本身手動去加的鎖。通常咱們添加一個版本字段來實現:

具體過程是這樣的:

張三select * from table  --->會查詢出記錄出來,同時會有一個version字段



李四select * from table  --->會查詢出記錄出來,同時會有一個version字段



李四對這條記錄作修改:update A set Name=lisi,version=version+1 where ID=#{id} and version=#{version},判斷以前查詢到的version與如今的數據的version進行比較,同時會更新version字段

此時數據庫記錄以下:



張三也對這條記錄修改:update A set Name=lisi,version=version+1 where ID=#{id} and version=#{version},但失敗了!由於當前數據庫中的版本跟查詢出來的版本不一致


1


參考資料:

2.4間隙鎖GAP

當咱們用範圍條件檢索數據而不是相等條件檢索數據,並請求共享或排他鎖時,InnoDB會給符合範圍條件的已有數據記錄的索引項加鎖;對於鍵值在條件範圍內但並不存在的記錄,叫作「間隙(GAP)」。InnoDB也會對這個「間隙」加鎖,這種鎖機制就是所謂的間隙鎖。

值得注意的是:間隙鎖只會在Repeatable read隔離級別下使用~

例子:假如emp表中只有101條記錄,其empid的值分別是1,2,...,100,101

 Select * from  emp where empid > 100 for update; 複製代碼

上面是一個範圍查詢,InnoDB不只會對符合條件的empid值爲101的記錄加鎖,也會對empid大於101(這些記錄並不存在)的「間隙」加鎖

InnoDB使用間隙鎖的目的有兩個:

  • 爲了防止幻讀(上面也說了,Repeatable read隔離級別下再經過GAP鎖便可避免了幻讀)

  • 知足恢復和複製的須要

    • MySQL的恢復機制要求:在一個事務未提交前,其餘併發事務不能插入知足其鎖定條件的任何記錄,也就是不容許出現幻讀

2.5死鎖

併發的問題就少不了死鎖,在MySQL中一樣會存在死鎖的問題。

但通常來講MySQL經過回滾幫咱們解決了很多死鎖的問題了,但死鎖是沒法徹底避免的,能夠經過如下的經驗參考,來儘量少遇到死鎖:

  • 1)以固定的順序訪問表和行。好比對兩個job批量更新的情形,簡單方法是對id列表先排序,後執行,這樣就避免了交叉等待鎖的情形;將兩個事務的sql順序調整爲一致,也能避免死鎖。

  • 2)大事務拆小。大事務更傾向於死鎖,若是業務容許,將大事務拆小。

  • 3)在同一個事務中,儘量作到一次鎖定所須要的全部資源,減小死鎖機率。

  • 4)下降隔離級別。若是業務容許,將隔離級別調低也是較好的選擇,好比將隔離級別從RR調整爲RC,能夠避免掉不少由於gap鎖形成的死鎖。

  • 5)爲表添加合理的索引。能夠看到若是不走索引將會爲表的每一行記錄添加上鎖,死鎖的機率大大增大。

參考資料:

2.6鎖總結

上面說了一大堆關於MySQL數據庫鎖的東西,如今來簡單總結一下。

表鎖其實咱們程序員是不多關心它的:

  • 在MyISAM存儲引擎中,當執行SQL語句的時候是自動加的。

  • 在InnoDB存儲引擎中,若是沒有使用索引,表鎖也是自動加的。

如今咱們大多數使用MySQL都是使用InnoDB,InnoDB支持行鎖:

  • 共享鎖--讀鎖--S鎖

  • 排它鎖--寫鎖--X鎖

在默認的狀況下,select是不加任何行鎖的~事務能夠經過如下語句顯示給記錄集加共享鎖或排他鎖。

  • 共享鎖(S):SELECT * FROM table_name WHERE ... LOCK IN SHARE MODE

  • 排他鎖(X):SELECT * FROM table_name WHERE ... FOR UPDATE

InnoDB基於行鎖還實現了MVCC多版本併發控制,MVCC在隔離級別下的Read committedRepeatable read下工做。MVCC可以實現讀寫不阻塞

InnoDB實現的Repeatable read隔離級別配合GAP間隙鎖已經避免了幻讀!

  • 樂觀鎖實際上是一種思想,正如其名:認爲不會鎖定的狀況下去更新數據,若是發現不對勁,纔不更新(回滾)。在數據庫中每每添加一個version字段來實現。

  • 悲觀鎖用的就是數據庫的行鎖,認爲數據庫會發生併發衝突,直接上來就把數據鎖住,其餘事務不能修改,直至提交了當前事務

參考資料:

做者:Java3y連接:https://juejin.im/post/5b55b842f265da0f9e589e79來源:掘金著做權歸做者全部。商業轉載請聯繫做者得到受權,非商業轉載請註明出處。

相關文章
相關標籤/搜索