首頁 > 資料庫 > Redis > 主體

Redis中必須掌握的20個問題,快來收藏吧! !

青灯夜游
發布: 2021-10-19 10:32:29
轉載
1605 人瀏覽過

這篇文章跟大家分享20個必知必會、必須掌握的Redis問題,希望對大家有幫助,快來收藏吧!

Redis中必須掌握的20個問題,快來收藏吧! !

Redis是什麼?

Redis(Remote Dictionary Server)是一個使用 C 語言編寫的,高效能非關係型的鍵值對資料庫。與傳統資料庫不同的是,Redis 的資料是存在記憶體中的,所以讀寫速度非常快,被廣泛應用於快取方向。 Redis可以將資料寫入磁碟中,確保了資料的安全不會遺失,而且Redis的操作是原子性的。 【相關推薦:Redis影片教學

Redis的優點?

  • 基於記憶體運算,記憶體讀寫速度快。

  • Redis是單執行緒的,避免執行緒切換開銷及多執行緒的競爭問題。單線程是指網路請求使用一個線程來處理,即一個線程處理所有網路請求,Redis 運行時不只一個線程,例如資料持久化的過程會另起線程。

  • 支援多種資料型別,包括String、Hash、List、Set、ZSet等。

  • 支援持久化。 Redis支援RDB和AOF兩種持久化機制,持久化功能可以有效避免資料遺失問題。

  • 支援交易。 Redis的所有操作都是原子性的,同時Redis也支援對幾個操作合併後的原子性執行。

  • 支援主從複製。主節點會自動將資料同步到從節點,可以進行讀寫分離。

Redis為什麼這麼快?

  • 基於記憶體:Redis是使用記憶體存儲,沒有磁碟IO上的開銷。資料存在記憶體中,讀寫速度快。
  • 單執行緒實作( Redis 6.0以前):Redis使用單一執行緒處理請求,避免了多個執行緒之間執行緒切換和鎖定資源爭用的開銷。
  • IO多工模型:Redis 採用 IO 多路復用技術。 Redis 使用單線程來輪詢描述符,將資料庫的操作都轉換成了事件,不在網路I/O上浪費過多的時間。
  • 高效率的資料結構:Redis 每種資料型別底層都做了最佳化,目的就是為了追求更快的速度。

Redis為何選擇單執行緒?

  • 避免過多的上下文切換開銷。程式始終運行在進程中單一執行緒內,沒有多執行緒切換的場景。
  • 避免同步機制的開銷:如果Redis選擇多執行緒模型,需要考慮資料同步的問題,則必然會引入某些同步機制,會導致在操作資料過程中帶來更多的開銷,增加程式複雜度的同時也會降低效能。
  • 實作簡單,方便維護:如果Redis使用多執行緒模式,那麼所有的底層資料結構的設計都必須考慮執行緒安全性問題,那麼Redis 的實作將會變得更加複雜。

Redis應用程式場景有哪些?

  • 快取熱點資料,緩解資料庫的壓力。

  • 利用 Redis 原子性的自增操作,可以實現計數器的功能,例如統計使用者按讚數、使用者存取數等。

  • 簡單的訊息佇列,可以使用Redis本身的發布/訂閱模式或List來實作簡單的訊息佇列,實作非同步操作。

  • 限速器,可用來限制某個使用者存取某個介面的頻率,例如秒殺場景用於防止使用者快速點擊帶來不必要的壓力。

  • 好友關係,利用集合的一些指令,如交集、並集、差集等,實現共同好友、共同嗜好之類的功能。

Memcached和Redis的差別?

  • Redis 只使用單核心,而 Memcached 可以使用多核心。

  • MemCached 資料結構單一,僅用來快取數據,而 Redis 支援多種資料類型

  • MemCached 不支援資料持久化,重新啟動後資料會消失。 Redis 支援資料持久化

  • Redis 提供主從同步機制和 cluster 叢集部署能力,能夠提供高可用服務。 Memcached 沒有提供原生的叢集模式,需要依賴客戶端實作往叢集中分片寫入資料。

  • Redis 的速度比 Memcached 快很多。

  • Redis 使用單執行緒的多路 IO 復用模型,Memcached使用多執行緒的非阻塞 IO 模型。

Redis 資料型別有哪些?

基本資料類型

1、String:最常用的一種資料類型,String類型的值可以是字串、數字或二進制,但值最大不能超過512MB。

2、Hash:Hash 是一個鍵值對集合。

3、Set:無序去重的集合。 Set 提供了交集、並集等方法,對於實現共同好友、共同關注等功能特別方便。

4、List:有序可重複的集合,底層是依賴雙向鍊錶實現的。

5、SortedSet(ZSet):有序S​​et。內部維護了一個score的參數來實現。適用於排行榜和帶權重的消息隊列等場景。

特殊的資料型別

1、Bitmap:位圖,可以認為是一個以位元為單位數組,數組中的每個單元只能存0或1,陣列的下標在Bitmap 中叫做偏移量。 Bitmap的長度與集合中元素個數無關,而是與基數的上限有關。

2、Hyperloglog。 HyperLogLog 是用來做基數統計的演算法,其優點是,在輸入元素的數量或體積非常非常大時,計算基數所需的空間總是固定的、且是很小的。典型的使用場景是統計獨立訪客。

3、Geospatial :主要用於儲存地理位置信息,並對儲存的資訊進行操作,適用場景如定位、附近的人等。

Redis交易

交易的原理是將一個交易範圍內的若干指令傳送給 Redis,然後再讓 Redis 依序執行這些指令。

交易的生命週期:

  • 使用MULTI開啟一個交易;

  • 在開啟交易的時候,每次操作的指令將會被插入到一個佇列中,同時這個指令並不會被真正執行;

  • EXEC指令進行提交事務。

Redis中必須掌握的20個問題,快來收藏吧! !

一個交易範圍內某個指令出錯不會影響其他指令的執行,不保證原子性:

first:0>MULTI
"OK"
first:0>set a 1
"QUEUED"
first:0>set b 2 3 4
"QUEUED"
first:0>set c 6
"QUEUED"
first:0>EXEC
1) "OK"
2) "OK"
3) "OK"
4) "ERR syntax error"
5) "OK"
6) "OK"
7) "OK"
登入後複製

WATCH指令

WATCH指令可以監控一個或多個鍵,一旦其中有一個鍵被修改,之後的交易就不會執行(類似樂觀鎖) 。執行EXEC指令之後,就會自動取消監控。

first:0>watch name
"OK"
first:0>set name 1
"OK"
first:0>MULTI
"OK"
first:0>set name 2
"QUEUED"
first:0>set gender 1
"QUEUED"
first:0>EXEC
(nil)
first:0>get gender
(nil)
登入後複製

例如上面的程式碼中:

  1. watch name開啟了對name這個key的監控
  2. 修改name的值
  3. 開啟交易a
  4. 在交易a中設定了namegender的值
  5. 使用EXEC指令進提交交易
  6. 使用指令get gender發現不存在,即交易a沒有執行

使用UNWATCH可以取消WATCH指令對key的監控,所有監控鎖定將會被取消。

持久化機制

持久化就是把記憶體的資料寫到磁碟中,防止服務宕機導致記憶體資料遺失。

Redis支持兩種方式的持久化,一種是RDB的方式,一種是AOF的方式。 前者會根據指定的規則定時將記憶體中的資料儲存在硬碟上,而後者在每次執行完指令後將指令記錄下來。一般將兩者結合使用。

RDB方式

RDB是 Redis 預設的持久化方案。 RDB持久化時會將記憶體中的資料寫入磁碟中,並在指定目錄下產生一個dump.rdb檔案。 Redis 重啟會載入dump.rdb檔案恢復資料。

bgsave是主流的觸發RDB 持久化的方式,執行過程如下:

Redis中必須掌握的20個問題,快來收藏吧! !

    ##執行
  • BGSAVE 指令
  • Redis 父行程判斷目前
  • 是否存在正在執行的子程序,如果存在,BGSAVE指令直接回傳。
  • 父行程執行
  • fork操作建立子程序,fork操作過程中父程序會阻塞。
  • 父進程
  • fork完成後,父進程繼續接收並處理客戶端的請求,而子進程開始將記憶體中的資料寫入硬碟的暫存檔案;
  • 當子行程寫完所有資料後會
  • 用該暫存檔案取代舊的RDB 檔案
Redis啟動時會讀取RDB快照文件,將資料從硬碟載入記憶體。透過 RDB 方式的持久化,一旦Redis異常退出,就會遺失最近一次持久化以後更改的資料。

觸發 RDB 持久化的方式:

  1. 手动触发:用户执行SAVEBGSAVE命令。SAVE命令执行快照的过程会阻塞所有客户端的请求,应避免在生产环境使用此命令。BGSAVE命令可以在后台异步进行快照操作,快照的同时服务器还可以继续响应客户端的请求,因此需要手动执行快照时推荐使用BGSAVE命令。

  2. 被动触发

    • 根据配置规则进行自动快照,如SAVE 100 10,100秒内至少有10个键被修改则进行快照。
    • 如果从节点执行全量复制操作,主节点会自动执行BGSAVE生成 RDB 文件并发送给从节点。
    • 默认情况下执行shutdown命令时,如果没有开启 AOF 持久化功能则自动执行·BGSAVE·。

优点

  • Redis 加载 RDB 恢复数据远远快于 AOF 的方式

  • 使用单独子进程来进行持久化,主进程不会进行任何 IO 操作,保证了 Redis 的高性能

缺点

  • RDB方式数据无法做到实时持久化。因为BGSAVE每次运行都要执行fork操作创建子进程,属于重量级操作,频繁执行成本比较高。

  • RDB 文件使用特定二进制格式保存,Redis 版本升级过程中有多个格式的 RDB 版本,存在老版本 Redis 无法兼容新版 RDB 格式的问题

AOF方式

AOF(append only file)持久化:以独立日志的方式记录每次写命令,Redis重启时会重新执行AOF文件中的命令达到恢复数据的目的。AOF的主要作用是解决了数据持久化的实时性,AOF 是Redis持久化的主流方式。

默认情况下Redis没有开启AOF方式的持久化,可以通过appendonly参数启用:appendonly yes。开启AOF方式持久化后每执行一条写命令,Redis就会将该命令写进aof_buf缓冲区,AOF缓冲区根据对应的策略向硬盘做同步操作。

默认情况下系统每30秒会执行一次同步操作。为了防止缓冲区数据丢失,可以在Redis写入AOF文件后主动要求系统将缓冲区数据同步到硬盘上。可以通过appendfsync参数设置同步的时机。

appendfsync always //每次写入aof文件都会执行同步,最安全最慢,不建议配置
appendfsync everysec  //既保证性能也保证安全,建议配置
appendfsync no //由操作系统决定何时进行同步操作
登入後複製

接下来看一下 AOF 持久化执行流程:

Redis中必須掌握的20個問題,快來收藏吧! !

  • 所有的写入命令会追加到 AOP 缓冲区中。

  • AOF 缓冲区根据对应的策略向硬盘同步。

  • 随着 AOF 文件越来越大,需要定期对 AOF 文件进行重写,达到压缩文件体积的目的。AOF文件重写是把Redis进程内的数据转化为写命令同步到新AOF文件的过程。

  • 当 Redis 服务器重启时,可以加载 AOF 文件进行数据恢复。

优点

  • AOF可以更好的保护数据不丢失,可以配置 AOF 每秒执行一次fsync操作,如果Redis进程挂掉,最多丢失1秒的数据。

  • AOF以append-only的模式写入,所以没有磁盘寻址的开销,写入性能非常高。

缺点

  • 对于同一份文件AOF文件比RDB数据快照要大。

  • 数据恢复比较慢。

主从复制

Redis的复制功能是支持多个数据库之间的数据同步。主数据库可以进行读写操作,当主数据库的数据发生变化时会自动将数据同步到从数据库。从数据库一般是只读的,它会接收主数据库同步过来的数据。一个主数据库可以有多个从数据库,而一个从数据库只能有一个主数据库。

//启动Redis实例作为主数据库
redis-server  
//启动另一个实例作为从数据库
redis-server --port 6380 --slaveof  127.0.0.1 6379   
slaveof 127.0.0.1 6379
//停止接收其他数据库的同步并转化为主数据库
SLAVEOF NO ONE
登入後複製

主从复制的原理?

  • 当启动一个从节点时,它会发送一个 PSYNC 命令给主节点;

  • 如果是从节点初次连接到主节点,那么会触发一次全量复制。此时主节点会启动一个后台线程,开始生成一份 RDB 快照文件;

  • 同时还会将从客户端 client 新收到的所有写命令缓存在内存中。RDB 文件生成完毕后, 主节点会将RDB文件发送给从节点,从节点会先将RDB文件写入本地磁盘,然后再从本地磁盘加载到内存中

  • 接著主節點會將記憶體中快取的寫入指令傳送到從節點,從節點同步這些資料;

  • ##如果從節點跟主節點之間網路故障,連線中斷了,會自動重連,連線之後主節點僅會將部分缺少的資料同步給從節點。

哨兵Sentinel

主從複製存在無法自動故障轉移、無法達到高可用的問題。哨兵模式解決了這些問題。透過哨兵機制可以自動切換主從節點。

客戶端連接Redis的時候,先連接哨兵,哨兵會告訴客戶端Redis主節點的位址,然後客戶端連接上Redis並進行後續的操作。當主節點宕機的時候,哨兵監測到主節點宕機,會重新推選出某個表現良好的從節點成為新的主節點,然後透過發布訂閱模式通知其他的從伺服器,讓它們切換主機。

Redis中必須掌握的20個問題,快來收藏吧! !

工作原理

    #每個
  • Sentinel以每秒鐘一次的頻率向它所知道的MasterSlave以及其他Sentinel實例發送一個PING指令。
  • 如果一個實例距離最後一次有效回覆
  • PING 指令的時間超過指定值, 則這個實例會被 Sentine 標記為主觀下線。
  • 如果一個
  • Master被標記為主觀下線,則正在監視這個Master的所有Sentinel要以每秒一次的頻率確認Master是否真正進入主觀下線狀態。
  • 當有足夠數量的
  • Sentinel(大於等於設定檔指定值)在指定的時間範圍內確認 Master的確進入了主觀下線狀態,則Master會被標記為客觀下線。若沒有足夠數量的 Sentinel同意 Master 已經下線, Master 的客觀下線狀態就會解除。若 Master重新向 SentinelPING 指令返回有效回复, Master 的主觀下線狀態就會移除。
  • 哨兵節點會選出哨兵 leader,負責故障轉移的工作。
  • 哨兵 leader 會推導出某個表現良好的從節點成為新的主節點,然後通知其他從節點更新主節點資訊。
Redis cluster

哨兵模式解決了主從複製無法自動故障轉移、無法達到高可用的問題,但還是存在主節點的寫入能力、容量受限於單機配置的問題。而cluster模式實現了Redis的分散式存儲,每個節點存儲不同的內容,解決主節點的寫入能力、容量受限於單機配置的問題。

Redis cluster叢集節點最小配置6個節點以上(3主3從),其中主節點提供讀寫操作,從節點作為備用節點,不提供請求,只作為故障轉移使用。

Redis cluster採用

虛擬槽分區,所有的鍵根據雜湊函數映射到0~16383個整數槽內,每個節點負責維護一部分槽以及槽所映射的鍵值數據。

Redis中必須掌握的20個問題,快來收藏吧! !

哈希槽是如何對應到 Redis 實例上的?

  • 對鍵值對的

    key使用crc16 演算法計算一個結果

  • 將結果對16384 取餘,得到的值表示

    key 對應的雜湊槽

  • 根據該槽資訊定位到對應的實例

優點:

    無中心架構,
  • 支援動態擴音容;
  • 資料依照
  • slot儲存分佈在多個節點,節點間資料共享,可動態調整資料分佈
  • ##高可用性
  • 。部分節點不可用時,叢集仍可用。叢集模式能夠實現自動故障轉移(failover),節點之間透過gossip協定交換狀態訊息,用投票機製完成SlaveMaster的角色轉換。
缺點:

  • 不支援批次操作(pipeline)。
  • 資料透過非同步複製,不保證資料的強一致性
  • 事務操作支援有限,只支援多重key在同一節點上的事務操作,當多個key分佈在不同的節點上時無法使用交易功能。
  • key作為資料分區的最小粒度,不能將一個很大的鍵值物件如hashlist等對應到不同的節點。
  • 不支援多資料庫空間,單機下的Redis可以支援到16個資料庫,在叢集模式下只能使用1個資料庫空間。

過期鍵的刪除策略?

1、被動刪除(惰性)。在訪問key時,如果發現key已經過期,那麼會將key刪除。

2、主動刪除(定期)。定時清理key,每次清理會依序遍歷所有DB,從db隨機取出20個key,如果過期就刪除,如果其中有5個key過期,那麼就繼續對這個db進行清理,否則開始清理下一個db。

3、記憶體不夠時清理。 Redis有最大內存的限制,透過maxmemory參數可以設定最大內存,當使用的內存超過了設定的最大內存,就要進行內存釋放, 在進行內存釋放的時候,會按照配置的淘汰策略清理內存。

記憶體淘汰策略有哪些?

當Redis的記憶體超過最大允許的記憶體之後,Redis 會觸發記憶體淘汰策略,刪除一些不常用的數據,以確保Redis伺服器正常運作。

Redisv4.0前提供6 種資料淘汰策略

  • volatile-lru:LRU(Least Recently Used ),最近使用。利用LRU演算法移除設定了過期時間的key
  • allkeys-lru:當記憶體不足以容納新寫入資料時,從資料集中移除最近最少使用的key
  • volatile-ttl:從已設定過期時間的資料集中挑選將要過期的資料淘汰
  • volatile-random:從已設定過期時間的資料集中任意選擇資料淘汰
  • allkeys-random:從資料集中任意選擇資料淘汰
  • no-eviction:禁止刪除數據,當當記憶體不足以容納新寫入資料時,新寫入操作會報錯

Redisv4.0後增加以下兩種

  • volatile-lfu:LFU,Least Frequently Used,最少使用,從已設定過期時間的資料集中挑選最不常使用的資料淘汰。
  • allkeys-lfu:當記憶體不足以容納新寫入資料時,從資料集中移除最不常使用的key。

記憶體淘汰策略可以透過設定檔來修改,對應的設定項是maxmemory-policy,預設組態是noeviction

如何保證快取與資料庫雙寫時的資料一致性?

1、先刪除快取再更新資料庫

進行更新操作時,先刪除緩存,然後更新資料庫,後續的請求再次讀取時,會從資料庫讀取後再將新資料更新到快取。

存在的問題:刪除快取資料之後,更新資料庫完成之前,這個時間段內如果有新的讀取請求過來,就會從資料庫讀取舊資料重新寫到快取中,再次造成不一致,且後續讀的都是舊數據。

2、先更新資料庫再刪除快取

更新作業時,先更新MySQL,成功之後,刪除緩存,後續讀取請求時再將新數據回寫快取。

存在的問題:更新MySQL、刪除快取這段時間內,請求讀取的還是快取的舊數據,不過等資料庫更新完成,就會恢復一致,影響相對比較小。

3、非同步更新緩存

資料庫的更新操作完成後不直接操作緩存,而是把這個操作命令封裝成訊息丟到訊息佇列中,然後由Redis自己去消費更新數據,訊息佇列可以確保資料操作順序一致性,確保快取系統的資料正常。

快取穿透、快取雪崩、快取擊穿【詳解】Redis快取擊穿、穿透、雪崩概念及解決方案

快取穿透

快取穿透是指查詢一個不存在的資料,由於快取是不命中時被動寫的,如果從DB查不到資料則不寫入緩存,這將導致這個不存在的資料每次請求都要到DB去查詢,失去了快取的意義。在流量大時,可能DB就掛掉了。

  • 快取空值,不會查資料庫。

  • 采用布隆过滤器,将所有可能存在的数据哈希到一个足够大的bitmap中,查询不存在的数据会被这个bitmap拦截掉,从而避免了对DB的查询压力。

布隆过滤器的原理:当一个元素被加入集合时,通过K个散列函数将这个元素映射成一个位数组中的K个点,把它们置为1。查询时,将元素通过散列函数映射之后会得到k个点,如果这些点有任何一个0,则被检元素一定不在,直接返回;如果都是1,则查询元素很可能存在,就会去查询Redis和数据库。

缓存雪崩

缓存雪崩是指在我们设置缓存时采用了相同的过期时间,导致缓存在某一时刻同时失效,请求全部转发到DB,DB瞬时压力过重挂掉。

解决方法:在原有的失效时间基础上增加一个随机值,使得过期时间分散一些。

缓存击穿

缓存击穿:大量的请求同时查询一个 key 时,此时这个 key 正好失效了,就会导致大量的请求都落到数据库。缓存击穿是查询缓存中失效的 key,而缓存穿透是查询不存在的 key。

解决方法:加分布式锁,第一个请求的线程可以拿到锁,拿到锁的线程查询到了数据之后设置缓存,其他的线程获取锁失败会等待50ms然后重新到缓存取数据,这样便可以避免大量的请求落到数据库。

public String get(String key) {
    String value = redis.get(key);
    if (value == null) { 
        //缓存值过期
        String unique_key = systemId + ":" + key;
        //设置30s的超时
        if (redis.set(unique_key, 1, 'NX', 'PX', 30000) == 1) {  //设置成功
            value = db.get(key);
            redis.set(key, value, expire_secs);
            redis.del(unique_key);
        } else {  
            //其他线程已经到数据库取值并回写到缓存了,可以重试获取缓存值
            sleep(50);
            get(key);  //重试
        }
    } else {
        return value;
    }
}
登入後複製

pipeline的作用?

redis客户端执行一条命令分4个过程:发送命令、命令排队、命令执行、返回结果。使用pipeline可以批量请求,批量返回结果,执行速度比逐条执行要快。

使用pipeline组装的命令个数不能太多,不然数据量过大,增加客户端的等待时间,还可能造成网络阻塞,可以将大量命令的拆分多个小的pipeline命令完成。

原生批命令(mset和mget)与pipeline对比:

  • 原生批命令是原子性,pipeline非原子性。pipeline命令中途异常退出,之前执行成功的命令不会回滚

  • 原生批命令只有一个命令,但pipeline支持多命令

LUA脚本

Redis 通过 LUA 脚本创建具有原子性的命令:当lua脚本命令正在运行的时候,不会有其他脚本或 Redis 命令被执行,实现组合命令的原子操作。

在Redis中执行Lua脚本有两种方法:evalevalshaeval命令使用内置的 Lua 解释器,对 Lua 脚本进行求值。

//第一个参数是lua脚本,第二个参数是键名参数个数,剩下的是键名参数和附加参数
> eval "return {KEYS[1],KEYS[2],ARGV[1],ARGV[2]}" 2 key1 key2 first second
1) "key1"
2) "key2"
3) "first"
4) "second"
登入後複製

lua脚本作用

1、Lua脚本在Redis中是原子执行的,执行过程中间不会插入其他命令。

2、Lua脚本可以将多条命令一次性打包,有效地减少网络开销。

应用场景

举例:限制接口访问频率。

在Redis维护一个接口访问次数的键值对,key是接口名称,value是访问次数。每次访问接口时,会执行以下操作:

  • 通过aop拦截接口的请求,对接口请求进行计数,每次进来一个请求,相应的接口访问次数count加1,存入redis。
  • 如果是第一次请求,则会设置count=1,并设置过期时间。因为这里set()expire()组合操作不是原子操作,所以引入lua脚本,实现原子操作,避免并发访问问题。
  • 如果给定时间范围内超过最大访问次数,则会抛出异常。
private String buildLuaScript() {
    return "local c" +
        "\nc = redis.call('get',KEYS[1])" +
        "\nif c and tonumber(c) > tonumber(ARGV[1]) then" +
        "\nreturn c;" +
        "\nend" +
        "\nc = redis.call('incr',KEYS[1])" +
        "\nif tonumber(c) == 1 then" +
        "\nredis.call('expire',KEYS[1],ARGV[2])" +
        "\nend" +
        "\nreturn c;";
}

String luaScript = buildLuaScript();
RedisScript<Number> redisScript = new DefaultRedisScript<>(luaScript, Number.class);
Number count = redisTemplate.execute(redisScript, keys, limit.count(), limit.period());
登入後複製

PS:这种接口限流的实现方式比较简单,问题也比较多,一般不会使用,接口限流用的比较多的是令牌桶算法和漏桶算法。

更多编程相关知识,请访问:编程入门!!

以上是Redis中必須掌握的20個問題,快來收藏吧! !的詳細內容。更多資訊請關注PHP中文網其他相關文章!

相關標籤:
來源:juejin.cn
本網站聲明
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn
最新問題
熱門教學
更多>
最新下載
更多>
網站特效
網站源碼
網站素材
前端模板
關於我們 免責聲明 Sitemap
PHP中文網:公益線上PHP培訓,幫助PHP學習者快速成長!