redis集羣方案

設計集羣方案時,至少要考慮如下因素:
(1)高可用要求:根據故障轉移的原理,至少須要3個主節點才能完成故障轉移,且3個主節點不該在同一臺物理機上;每一個主節點至少須要1個從節點,且主從節點不該在一臺物理機上;所以高可用集羣至少包含6個節點。
(2)數據量和訪問量:估算應用須要的數據量和總訪問量(考慮業務發展,留有冗餘),結合每一個主節點的容量和能承受的訪問量(能夠經過benchmark獲得較準確估計),計算須要的主節點數量。
(3)節點數量限制:Redis官方給出的節點數量限制爲1000,主要是考慮節點間通訊帶來的消耗。在實際應用中應儘可能避免大集羣;若是節點數量不足以知足應用對Redis數據量和訪問量的要求,能夠考慮:node

  a.業務分割,大集羣分爲多個小集羣;redis

  b.減小沒必要要的數據;算法

  c.調整數據過時策略等。
(4)適度冗餘:Redis能夠在不影響集羣服務的狀況下增長節點,所以節點數量適當冗餘便可,不用太大。數據庫

集羣的原理:緩存

集羣最核心的功能是數據分區,所以首先介紹數據的分區規則;而後介紹集羣實現的細節:通訊機制和數據結構;最後以cluster meet(節點握手)、cluster addslots(槽分配)爲例,說明節點是如何利用上述數據結構和通訊機制實現集羣命令的。ruby

數據分區方案:
  數據分區有順序分區、哈希分區等,其中哈希分區因爲其自然的隨機性,使用普遍;集羣的分區方案即是哈希分區的一種。
  哈希分區的基本思路是:對數據的特徵值(如key)進行哈希,而後根據哈希值決定數據落在哪一個節點。常見的哈希分區包括:哈希取餘分區、一致性哈希分區、帶虛擬節點的一致性哈希分區等。
  (1)哈希取餘分區
    哈希取餘分區思路很是簡單:計算key的hash值,而後對節點數量進行取餘,從而決定數據映射到哪一個節點上。該方案最大的問題是,當新增或刪減節點時,節點數量發生變化,系統中全部的數據都須要從新計算映射關係,引起大規模數據遷移。
  (2)一致性哈希分區
    一致性哈希算法將整個哈希值空間組織成一個虛擬的圓環,範圍爲0-2^32-1;對於每一個數據,根據key計算hash值,肯定數據在環上的位置,而後今後位置沿環順時針行走,找到的第一臺服務器就是其應該映射到的服務器。服務器

 

   與哈希取餘分區相比,一致性哈希分區將增減節點的影響限制在相鄰節點。若是在node1和node2之間增長node5,則只有node2中的一部分數據會遷移到node5;若是去掉node2,則原node2中的數據只會遷移到node4中,只有node4會受影響。
   一致性哈希分區的主要問題在於,當節點數量較少時,增長或刪減節點,對單個節點的影響可能很大,形成數據的嚴重不平衡。仍是以上圖爲例,若是去掉node2,node4中的數據由總數據的1/4左右變爲1/2左右,與其餘節點相比負載太高。
  (3)帶虛擬節點的一致性哈希分區
    該方案在一致性哈希分區的基礎上,引入了虛擬節點的概念。Redis集羣使用的即是該方案,其中的虛擬節點稱爲槽(slot)。槽是介於數據和實際節點之間的虛擬概念;每一個實際節點包含必定數量的槽,每一個槽包含哈希值在必定範圍內的數據。引入槽之後,網絡

   數據的映射關係由數據hash->實際節點,變成了數據hash->槽->實際節點。數據結構

    在使用了槽的一致性哈希分區中,槽是數據管理和遷移的基本單位。槽解耦了數據和實際節點之間的關係,增長或刪除節點對系統的影響很小。仍以上圖爲例,系統中有4個實際節點,假設爲其分配16個槽(0-15); 槽0-3位於node1,4-7位於node2,併發

     以此類推。若是此時刪除node2,只須要將槽4-7從新分配便可,例如槽4-5分配給node1,槽6分配給node3,槽7分配給node4;能夠看出刪除node2後,數據在其餘節點的分佈仍然較爲均衡。槽的數量通常遠小於2^32,遠大於實際節點的數量;

    在Redis集羣中,槽的數量爲16384

  下面這張圖很好的總結了Redis集羣將數據映射到實際節點的過程:

        

  (1)Redis對數據的特徵值(通常是key)計算哈希值,使用的算法是CRC16。 Crc16(key) = hash
  (2)根據哈希值,計算數據屬於哪一個槽。 Hash % 16384
  (3)根據槽與節點的映射關係,計算數據屬於哪一個節點。

 

Redis集羣搭建:

1、主/從(master/slave)(缺點: 數據冗餘,浪費內存 (ping - pong)

  1.主節點讀寫,從節點只能讀,不能寫
  2.當主節點讀寫數據變化時,會直接同步到從節點
  3.一個master能夠擁有多個slave,可是一個slave只能對應一個master

主從複製工做機制:
  當slave啓動後,主動向master發送SYNC命令。master接收到SYNC命令後在後臺保存快照(RDB持久化)和緩存保存快照這段時間的命令,而後將保存的快照文件和緩存的命令發送給slave。slave接收到快照文件和命令後加載快照文件和緩存的執行命令。複製初始化後,master每次接收到的寫命令都會同步發送給slave,保證主從數據一致性。

主從配置:
  redis默認是主數據,因此master無需配置,咱們只須要修改slave的配置便可。
  a.設置須要鏈接的master的ip端口:
    slaveof 192.168.0.107 6379
  b.若是master設置了密碼。須要配置:
    masterauth
  c.鏈接成功進入命令行後,能夠經過如下命令行查看鏈接該數據庫的其餘庫信息:
    info replication

 

2、哨兵模式(sentinel)

  哨兵的命令,哨兵是一個獨立的進程,做爲進程,它會獨立運行。其原理是哨兵經過發送命令,等待Redis服務器響應,從而監控運行的多個Redis實例。(ping - pong)

  

  

這裏的哨兵有兩個做用:
  • 經過發送命令,讓Redis服務器返回監控其運行狀態,包括主服務器和從服務器。
  • 當哨兵監測到master宕機,會自動將slave切換成master,而後經過發佈訂閱模式通知其餘的從服務器,修改配置文件,讓它們切換主機。
然而一個哨兵進程對Redis服務器進行監控,可能會出現問題,爲此,咱們可使用多個哨兵進行監控。各個哨兵之間還會進行監控,這樣就造成了多哨兵模式。
故障切換(failover)的過程:
  假設主服務器宕機,哨兵1先檢測到這個結果,系統並不會立刻進行failover過程,僅僅是哨兵1主觀的認爲主服務器不可用,這個現象成爲主觀下線。當後面的哨兵也檢測到主服務器不可用,而且數量達到必定值時,那麼哨兵之間就會進行一次投票,投票的結果由一個哨兵發起,進行failover操做。切換成功後,就會經過發佈訂閱模式,讓各個哨兵把本身監控的從服務器實現切換主機,這個過程稱爲客觀下線。這樣對於客戶端而言,一切都是透明的

配置:

  配置Redis的主從服務器,修改redis.conf文件以下
  # 使得Redis服務器能夠跨網絡訪問
  bind 0.0.0.0
  # 設置密碼
  requirepass "123456"
  # 指定主服務器,注意:有關slaveof的配置只是配置從服務器,主服務器不須要配置
  slaveof 192.168.11.128 6379
  # 主服務器密碼,注意:有關slaveof的配置只是配置從服務器,主服務器不須要配置
  masterauth 123456

  配置哨兵,在Redis安裝目錄下有一個sentinel.conf文件,copy一份進行修改
  # 禁止保護模式
  protected-mode no
  #配置監聽的主服務器,這裏sentinel monitor表明監控,mymaster表明服務器的名稱,能夠自定義,192.168.11.128表明監控的主服務器,6379表明端口,2表明只有兩個或兩個以上的哨兵認爲主服務器不可用的時候,纔會進行failover操做。
  sentinel monitor mymaster 192.168.11.128 6379 2
  # sentinel author-pass定義服務的密碼,mymaster是服務名稱,123456是Redis服務器密碼
  # sentinel auth-pass <master-name> <password>
  sentinel auth-pass mymaster 123456

啓動:
  # 啓動Redis服務器進程
  ./redis-server ../redis.conf
  # 啓動哨兵進程
  ./redis-sentinel ../sentinel.conf

須要特別注意的是:

  客觀下線是主節點纔有的概念;若是從節點和哨兵節點發生故障,被哨兵主觀下線後,不會再有後續的客觀下線和故障轉移操做。選舉領導者哨兵節點:當主節點被判斷客觀下線之後,各個哨兵節點會進行協商,選舉出一個領導者哨兵節點,並由該領導者節點對其進行故障轉移操做。監視該主節點的全部哨兵都有可能被選爲領導者,選舉使用的算法是Raft算法;Raft算法的基本思路是先到先得:即在一輪選舉中,哨兵A向B發送成爲領導者的申請,若是B沒有贊成過其餘哨兵,則會贊成A成爲領導者。選舉的具體過程這裏不作詳細描述,通常來講,哨兵選擇的過程很快,誰先完成客觀下線,通常就能成爲領導者。

  故障轉移:選舉出的領導者哨兵,開始進行故障轉移操做,該操做大致能夠分爲3個步驟:

    • 在從節點中選擇新的主節點:選擇的原則是,首先過濾掉不健康的從節點;而後選擇優先級最高的從節點(由slave-priority指定);若是優先級沒法區分,則選擇複製偏移量最大的從節點;若是仍沒法區分,則選擇runid最小的從節點。

    • 更新主從狀態:經過slaveof no one命令,讓選出來的從節點成爲主節點;並經過slaveof命令讓其餘節點成爲其從節點。

    • 將已經下線的主節點(即6379)設置爲新的主節點的從節點,當6379從新上線後,它會成爲新的主節點的從節點。

3、集羣

集羣,即Redis Cluster,是Redis 3.0開始引入的分佈式存儲方案。

集羣由多個節點(Node)組成,Redis的數據分佈在這些節點中。集羣中的節點分爲主節點和從節點:只有主節點負責讀寫請求和集羣信息的維護;從節點只進行主節點數據和狀態信息的複製。

集羣的做用,能夠概括爲兩點:

  一、數據分區:數據分區(或稱數據分片)是集羣最核心的功能。

集羣將數據分散到多個節點,一方面突破了Redis單機內存大小的限制,存儲容量大大增長;另外一方面每一個主節點均可以對外提供讀服務和寫服務,大大提升了集羣的響應能力。

Redis單機內存大小受限問題,在介紹持久化和主從複製時都有說起;例如,若是單機內存太大,bgsave和bgrewriteaof的fork操做可能致使主進程阻塞,主從環境下主機切換時可能致使從節點長時間沒法提供服務,全量複製階段主節點的複製緩衝區可能溢出……。

  二、高可用:集羣支持主從複製和主節點的自動故障轉移(與哨兵相似);當任一節點發生故障時,集羣仍然能夠對外提供服務。

集羣的搭建:
  這一部分咱們將搭建一個簡單的集羣:共6個節點,3主3從。方便起見:全部節點在同一臺服務器上,以端口號進行區分;配置從簡。3個主節點端口號:7000/7001/7002,對應的從節點端口號:8000/8001/8002。


集羣的搭建有兩種方式:

  (1)手動執行Redis命令,一步步完成搭建;

  (2)使用Ruby腳本搭建。兩者搭建的原理是同樣的,只是Ruby腳本將Redis命令進行了打包封裝;在實際應用中推薦使用腳本方式,簡單快捷不容易出錯。下面分別介紹這兩種方式。
集羣的搭建能夠分爲四步:(1)啓動節點:將節點以集羣模式啓動,此時節點是獨立的,並無創建聯繫;(2)節點握手:讓獨立的節點連成一個網絡;(3)分配槽:將16384個槽分配給主節點;(4)指定主從關係:爲從節點指定主節點。

第一種搭建: 手動
(1)啓動節點
  集羣節點的啓動仍然是使用redis-server命令,但須要使用集羣模式啓動。下面是7000節點的配置文件(只列出了節點正常工做關鍵配置,其餘配置(如開啓AOF)能夠參照單機節點進行):

  #redis-7000.conf
  port 7000
  cluster-enabled yes
  cluster-config-file "node-7000.conf"
  logfile "log-7000.log"
  dbfilename "dump-7000.rdb"
  daemonize yes
  其中的cluster-enabled和cluster-config-file是與集羣相關的配置。

  cluster-enabled yes:Redis實例能夠分爲單機模式(standalone)和集羣模式(cluster);cluster-enabled yes能夠啓動集羣模式。在單機模式下啓動的Redis實例,若是執行info server命令,能夠發現redis_mode一項爲standalone
  cluster-config-file:該參數指定了集羣配置文件的位置。每一個節點在運行過程當中,會維護一份集羣配置文件;每當集羣信息發生變化時(如增減節點),集羣內全部節點會將最新信息更新到該配置文件;當節點重啓後,會從新讀取該配置文件,獲取集羣信息,能夠方便的從新加入到集羣中。也就是說,當Redis節點以集羣模式啓動時,會首先尋找是否有集羣配置文件,若是有則使用文件中的配置啓動,若是沒有,則初始化配置並將配置保存到文件中。集羣配置文件由Redis節點維護,不須要人工修改。
編輯好配置文件後

使用redis-server命令啓動該節點:
  redis-server redis-7000.conf
(2)節點握手
  節點啓動之後是相互獨立的,並不知道其餘節點存在;須要進行節點握手,將獨立的節點組成一個網絡。
  節點握手使用cluster meet {ip} {port}命令實現
  例如在7000節點中執行cluster meet 192.168.72.128 7001,能夠完成7000節點和7001節點的握手;注意ip使用的是局域網ip而不是localhost或127.0.0.1,
是爲了其餘機器上的節點或客戶端也能夠訪問
同理,在7000節點中使用cluster meet命令,能夠將全部節點加入到集羣,完成節點握手:
  1 cluster meet 192.168.72.128 7002
  2 cluster meet 192.168.72.128 8000
  3 cluster meet 192.168.72.128 8001
  4 cluster meet 192.168.72.128 8002
(3)分配槽
  在Redis集羣中,藉助槽實現數據分區,具體原理後文會介紹。集羣有16384個槽,槽是數據管理和遷移的基本單位。當數據庫中的16384個槽都分配了節點時,集羣處於上線狀態(ok);若是有任意一個槽沒有分配節點,則集羣處於下線狀態(fail)。
分配槽使用cluster addslots命令,執行下面的命令將槽(編號0-16383)所有分配完畢:
  1 redis-cli -p 7000 cluster addslots {0..5461}
  2 redis-cli -p 7001 cluster addslots {5462..10922}
  3 redis-cli -p 7002 cluster addslots {10923..16383}
(4)指定主從關係
集羣中指定主從關係再也不使用slaveof命令,而是使用cluster replicate命令;參數使用節點id。
經過cluster nodes得到幾個主節點的節點id後,執行下面的命令爲每一個從節點指定主節點:
  1 redis-cli -p 8000 cluster replicate be816eba968bc16c884b963d768c945e86ac51ae
  2 redis-cli -p 8001 cluster replicate 788b361563acb175ce8232569347812a12f1fdb4
  3 redis-cli -p 8002 cluster replicate a26f1624a3da3e5197dde267de683d61bb2dcbf1

第二種搭建: 腳本
  使用Ruby腳本搭建集羣
  在{REDIS_HOME}/src目錄下能夠看到redis-trib.rb文件,這是一個Ruby腳本,能夠實現自動化的集羣搭建。
  (1)安裝Ruby環境
  以Ubuntu爲例,以下操做便可安裝Ruby環境:
  1 apt-get install ruby #安裝ruby環境
  2 gem install redis #gem是ruby的包管理工具,該命令能夠安裝ruby-redis依賴
  (2)啓動節點
  與第一種方法中的「啓動節點」徹底相同。
  (3)搭建集羣
  redis-trib.rb腳本提供了衆多命令,其中create用於搭建集羣,使用方法以下

  ./redis-trib.rb create --replicas 1 192.168.72.128:7000 192.168.72.128:7001 192.168.72.128:7002 192.168.72.128:8000 192.168.72.128:8001 192.168.72.128:8002

 

  其中:--replicas=1表示每一個主節點有1個從節點;後面的多個{ip:port}表示節點地址,前面的作主節點,後面的作從節點。使用redis-trib.rb搭建集羣時,要求節點不能包含任何槽和數據。

  執行建立命令後,腳本會給出建立集羣的計劃;計劃包括哪些是主節點,哪些是從節點,以及如何分配槽

 

集羣擴展:

1. 集羣伸縮
  實踐中經常須要對集羣進行伸縮,如訪問量增大時的擴容操做。Redis集羣能夠在不影響對外服務的狀況下實現伸縮;伸縮的核心是槽遷移:修改槽與節點的對應關係,實現槽(即數據)在節點之間的移動。例如,若是槽均勻分佈在集羣的3個節點中,此時增長一個節點,則須要從3個節點中分別拿出一部分槽給新節點,從而實現槽在4個節點中的均勻分佈。
  增長節點、
  假設要增長7003和8003節點,其中8003是7003的從節點;步驟以下:
  (1)啓動節點:方法參見集羣搭建
  (2)節點握手:可使用cluster meet命令,但在生產環境中建議使用redis-trib.rb的add-node工具,其原理也是cluster meet,但它會先檢查新節點是否已加入其它集羣或者存在數據,避免加入到集羣后帶來混亂。
  1 redis-trib.rb add-node 192.168.72.128:7003 192.168.72.128 7000
  2 redis-trib.rb add-node 192.168.72.128:8003 192.168.72.128 7000
  (3)遷移槽:推薦使用redis-trib.rb的reshard工具實現。reshard自動化程度很高,只須要輸入redis-trib.rb reshard ip:port (ip和port能夠是集羣中的任一節點),而後按照提示輸入如下信息,槽遷移會自動完成:
    ○ 待遷移的槽數量:16384個槽均分給4個節點,每一個節點4096個槽,所以待遷移槽數量爲4096
    ○ 目標節點id:7003節點的id
    ○ 源節點的id:7000/7001/7002節點的id
  (4)指定主從關係:方法參見集羣搭建
  減小節點、
  假設要下線7000/8000節點,能夠分爲兩步:
  (1)遷移槽:使用reshard將7000節點中的槽均勻遷移到7001/7002/7003節點
  (2)下線節點:使用redis-trib.rb del-node工具;應先下線從節點再下線主節點,由於若主節點先下線,從節點會被指向其餘主節點,形成沒必要要的全量複製。
  1 redis-trib.rb del-node 192.168.72.128:7001 {節點8000的id}
  2 redis-trib.rb del-node 192.168.72.128:7001 {節點7000的id}


2. 故障轉移
  集羣只實現了主節點的故障轉移;從節點故障時只會被下線,不會進行故障轉移。所以,使用集羣時,應謹慎使用讀寫分離技術,由於從節點故障會致使讀服務不可用,可用性變差。
這裏再也不詳細介紹故障轉移的細節,只對重要事項進行說明:
節點數量:在故障轉移階段,須要由主節點投票選出哪一個從節點成爲新的主節點;從節點選舉勝出須要的票數爲N/2+1;其中N爲主節點數量(包括故障主節點),但故障主節點實際上不能投票。所以爲了可以在故障發生時順利選出從節點,集羣中至少須要3個主節點(且部署在不一樣的物理機上)。
  故障轉移時間:從主節點故障發生到完成轉移,所須要的時間主要消耗在主觀下線識別、主觀下線傳播、選舉延遲等幾個環節;具體時間與參數cluster-node-timeout有關,通常來講:
故障轉移時間(毫秒) ≤ 1.5 * cluster-node-timeout + 1000
  cluster-node-timeout的默認值爲15000ms(15s),所以故障轉移時間會在20s量級


3. 集羣的限制及應對方法
  因爲集羣中的數據分佈在不一樣節點中,致使一些功能受限,包括:
  (1)key批量操做受限:例如mget、mset操做,只有當操做的key都位於一個槽時,才能進行。針對該問題,一種思路是在客戶端記錄槽與key的信息,每次針對特定槽執行mget/mset;另一種思路是使用Hash Tag,將在下一小節介紹。
  (2)keys/flushall等操做:keys/flushall等操做能夠在任一節點執行,可是結果只針對當前節點,例如keys操做只返回當前節點的全部鍵。針對該問題,能夠在客戶端使用cluster nodes獲取全部節點信息,並對其中的全部主節點執行keys/flushall等操做。
  (3)事務/Lua腳本:集羣支持事務及Lua腳本,但前提條件是所涉及的key必須在同一個節點。Hash Tag能夠解決該問題。
  (4)數據庫:單機Redis節點能夠支持16個數據庫,集羣模式下只支持一個,即db0。
  (5)複製結構:只支持一層複製結構,不支持嵌套。


4. Hash Tag
Hash Tag原理是:當一個key包含 {} 的時候,不對整個key作hash,而僅對 {} 包括的字符串作hash。
Hash Tag可讓不一樣的key擁有相同的hash值,從而分配在同一個槽裏;這樣針對不一樣key的批量操做(mget/mset等),以及事務、Lua腳本等均可以支持。不過Hash Tag可能會帶來數據分配不均的問題,這時須要:(1)調整不一樣節點中槽的數量,使數據分佈儘可能均勻;(2)避免對熱點數據使用Hash Tag,致使請求分佈不均。

下面是使用Hash Tag的一個例子;經過對product加Hash Tag,能夠將全部產品信息放到同一個槽中,便於操做。

 

5. 參數優化
  cluster_node_timeout
  cluster_node_timeout參數在前面已經初步介紹;它的默認值是15s,影響包括:
  (1)影響PING消息接收節點的選擇:值越大對延遲容忍度越高,選擇的接收節點越少,能夠下降帶寬,但會下降收斂速度;應根據帶寬狀況和應用要求進行調整。
  (2)影響故障轉移的斷定和時間:值越大,越不容易誤判,但完成轉移消耗時間越長;應根據網絡情況和應用要求進行調整。
  cluster-require-full-coverage
  前面提到,只有當16384個槽所有分配完畢時,集羣才能上線。這樣作是爲了保證集羣的完整性,但同時也帶來了新的問題:當主節點發生故障而故障轉移還沒有完成,原主節點中的槽不在任何節點中,此時會集羣處於下線狀態,沒法響應客戶端的請求。
  cluster-require-full-coverage參數能夠改變這一設定:若是設置爲no,則當槽沒有徹底分配時,集羣仍能夠上線。參數默認值爲yes,若是應用對可用性要求較高,能夠修改成no,但須要本身保證槽所有分配。


6. redis-trib.rb
  redis-trib.rb提供了衆多實用工具:建立集羣、增減節點、槽遷移、檢查完整性、數據從新平衡等;經過help命令能夠查看詳細信息。在實踐中若是能使用redis-trib.rb工具則儘可能使用,不但方便快捷,還能夠大大下降出錯機率。

 

緩存穿透
  緩存穿透,是指查詢一個數據庫必定不存在的數據。正常的使用緩存流程大體是,數據查詢先進行緩存查詢,若是key不存在或者key已通過期,再對數據庫進行查詢,並把查詢到的對象,放進緩存。若是數據庫查詢對象爲空,則不放進緩存。
  解決方案:若是從數據庫查詢的對象爲空,也放入緩存,只是設定的緩存過時時間較短,好比設置爲60秒, 最大不超過5min

緩存雪崩
  緩存雪崩,是指在某一個時間段,緩存集中過時失效。
  產生雪崩的緣由之一,好比在寫本文的時候,立刻就要到雙十二零點,很快就會迎來一波搶購,這波商品時間比較集中的放入了緩存,假設緩存一個小時。那麼到了凌晨一點鐘的時候,這批商品的緩存就都過時了。而對這批商品的訪問查詢,都落到了數據庫上,對於數據庫而言,就會產生週期性的壓力波峯。
  解決方案:通常是採起不一樣分類商品,緩存不一樣週期。在同一分類中的商品,加上一個隨機因子。這樣能儘量分散緩存過時時間,並且,熱門類目的商品緩存時間長一些,冷門類目的商品緩存時間短一些,也能節省緩存服務的資源

緩存擊穿  緩存擊穿,是指一個key很是熱點,在不停的扛着大併發,大併發集中對這一個點進行訪問,當這個key在失效的瞬間,持續的大併發就穿破緩存,直接請求數據庫,就像在一個屏障上鑿開了一個洞。小編在作電商項目的時候,把這貨就成爲「爆款」。  其實,大多數狀況下這種爆款很難對數據庫服務器形成壓垮性的壓力。達到這個級別的公司沒有幾家的。因此,務實主義的小編,對主打商品都是早早的作好了準備,讓緩存永不過時。即使某些商品本身發酵成了爆款,也是直接設爲永不過時就行了。mutex key互斥鎖

相關文章
相關標籤/搜索