文章目錄
書接上回
上一篇我們學習的 set
集合這一數據類型。其內部是由inset
和hashtable
這種兩種數據結構編碼的。
如果不記得了,那就來坐穿梭機回去看看吧。 開始穿梭
接下來,我們繼續學習一個新的數據類型, 有序集合. zset
.
zset
簡介
zset
,中文名字叫 有序集合. 序這個字,在Redis
的實現是 score
字段。我們先不急這個字段,後面會介紹。
在Redis
中有序的數據類型,還有一個就是我們前面學習的 list
了。 它們還都可以獲得某一定範圍內的元素。
而 zset
的優點是: list
通過鏈表實現,在兩端操作數據都很方便. 但是操作中間的數據就比較慢了。 zset
是用 hashtable
和 skiplist
來實現的。即使是操作中間數據,速度也很快. 時間複雜度爲: O(logN)
zset
的缺點就是: 就是比較耗費內存。
zset
類型的應用場景
- 存儲學生成績快速做成績排名功能。
- 排行榜,比如:列出某用戶當前的全球排名, 比賽中勝場數排名。
- 帶權重的消息隊列功能
zset
的基本命令
zadd
- 語法
ZADD key [NX|XX] [CH] [INCR] score member [score member ...]
- 解釋
將member
添加有序集合中.
如果member
存在,會更新member
的score
值。
-
NX
表示存在相同的member
就會設置失敗,NX的作用就是 新增member
,不會修改Member
-
XX
表示不存在相同的member
就會設置失敗。所以:XX
總是更新元素。不會新增元素 -
CH
(change
): 返回修改的元素個數。更改的元素是添加的新元素以及已爲其更新分數的現有元素。因此,命令行中指定的具有與過去相同分數的元素將不計算在內。注意:通常,ZADD
的返回值僅計算添加的新元素的數量。 -
INCR
: 指定此選項後,ZADD
的行爲類似於ZINCRBY
。在此模式下只能指定一對得分元素。 -
演示
## 設置一個元素
127.0.0.1:6379> ZADD k67 1 m1
(integer) 1
## 設置多個元素
127.0.0.1:6379> ZADD k67 2 m2 3 m3 4 m4 5 m5
(integer) 4
## 演示 NX 語義,只能新增.
127.0.0.1:6379> ZADD k67 NX 5 m5
(integer) 0
127.0.0.1:6379> ZADD k67 NX 6 m6
(integer) 1
127.0.0.1:6379> ZADD k67 NX 6 m6
(integer) 0
## 演示 XX 語言,只能修改
127.0.0.1:6379> ZADD k67 XX 7 m7
(integer) 0
127.0.0.1:6379> ZADD k67 7 m7
(integer) 1
## 進行修改,注意返回值. 如果要返回個數,則加 CH
127.0.0.1:6379> ZADD k67 XX 7 m7
(integer) 0
127.0.0.1:6379> ZADD k67 XX 77 m7
(integer) 0
## 演示CH, 返回修改的個數
127.0.0.1:6379> ZADD k67 CH 8 m8 9 m9 10 m10
(integer) 3
127.0.0.1:6379> ZADD k67 CH 8 m8 999 m9 10 m10
(integer) 1
## 演示INCR, 增長
127.0.0.1:6379> ZADD k67 11 m11
(integer) 1
## 此時 score 表示的是步長
127.0.0.1:6379> ZADD k67 INCR 10 m11
"21"
## 查看設置的值。
127.0.0.1:6379> ZRANGE k67 0 -1 WITHSCORES
1) "m1"
2) "1"
3) "m2"
4) "2"
5) "m3"
6) "3"
7) "m4"
8) "4"
9) "m5"
10) "5"
11) "m6"
12) "6"
13) "m8"
14) "8"
15) "m10"
16) "10"
17) "m11"
18) "21"
19) "m7"
20) "77"
21) "m9"
22) "999"
zscore
- 語法
ZSCORE key member
- 解釋
zset score
查看對應元素的score
值
* 當`key`不存在或者`member`不存在的時候,返回`(nil)`
返回score
的值。
- 演示
# 驗證k68不存在的時候,返回nil
127.0.0.1:6379> ZSCORE k68 m1
(nil)
127.0.0.1:6379> ZADD k68 1 m1
(integer) 1
# 返回元素的score值
127.0.0.1:6379> ZSCORE k68 m1
"1"
# 驗證 member 不存在的時候,返回
127.0.0.1:6379> ZSCORE k68 m2
(nil)
zincrby
- 語法
ZINCRBY key increment member
- 解釋
zset increment by
* increment
: 步長。
* member
: 指定的成員
爲有序集合key
的成員member
的score
值加上 increment
。
如果key
或者member
不存在, 則新增一個元素. 相當於 zadd
.
- 演示
# 插入一個不存在的key。
127.0.0.1:6379> ZINCRBY k69 10 m1
"10"
127.0.0.1:6379> ZRANGE k69 0 -1 WITHSCORES
1) "m1"
2) "10"
#累加
127.0.0.1:6379> ZINCRBY k69 10 m1
"20"
127.0.0.1:6379> ZRANGE k69 0 -1 WITHSCORES
1) "m1"
2) "20"
# 累加一個負數
127.0.0.1:6379> ZINCRBY k69 -30 m1
"-10"
127.0.0.1:6379> ZRANGE k69 0 -1 WITHSCORES
1) "m1"
2) "-10"
zcard
- 語法
ZCARD key
- 解釋
返回 有序集合的key
中的元素個數.即member
的個數。
不存在的時候,返回0
.
- 演示
# 返回member的個數
127.0.0.1:6379> ZADD k70 1 m1 2 m2 3 m3
(integer) 3
127.0.0.1:6379> ZCARD k70
(integer) 3
127.0.0.1:6379> ZADD k70 4 m4 5 m5
(integer) 2
127.0.0.1:6379> ZCARD k70
(integer) 5
# 不存在的時候,返回0
127.0.0.1:6379> EXISTS k70_1
(integer) 0
127.0.0.1:6379> ZCARD k70_1
(integer) 0
zcount
- 語法
ZCOUNT key min max
- 解釋
返回 score
值在min
和 max
之間的元素的個數。包括等於 min
和 max
- 演示
127.0.0.1:6379> ZADD k71 1 m1 2 m2 3 m3 4 m4 5 m5 6 m6 7 m7 8 m8 9 m9 10 m10
(integer) 10
127.0.0.1:6379> ZCOUNT k71 2 5
(integer) 4
#不存在的key或者不在區間內時,返回0
127.0.0.1:6379> zcount k71_1 0 10
(integer) 0
127.0.0.1:6379> zcount k71 11 12
(integer) 0
127.0.0.1:6379> zcount k71 12 11
(integer) 0
127.0.0.1:6379>
zrange
- 語法
ZRANGE key start stop [WITHSCORES]
- 解釋
這個命令我們已經用過,就是返回指定開始結束位置上的元素. 從 0
開始。
- 演示
127.0.0.1:6379> ZRANGE k72 1 3
1) "m2"
2) "m3"
3) "m4"
127.0.0.1:6379> ZRANGE k72 1 3 WITHSCORES
1) "m2"
2) "2"
3) "m3"
4) "3"
5) "m4"
6) "4"
zrevrange
- 語法
ZREVRANGE key start stop [WITHSCORES]
- 解釋
返回有序集合中指定區間的成員。
其中成員的位置按 score
值遞減(從大到小)來排列。 具有相同 score
值的成員按字典序的逆序(reverse lexicographical order
)排列。
- 演示
127.0.0.1:6379> ZADD k73 1 m1 2 m2 3 m3 4 m4 5 m5 6 m6 7 m7 8 m8 9 m9 10 m10
(integer) 10
# 使用 zrange 正序返回數據
127.0.0.1:6379> ZRANGE k73 0 3 WITHSCORES
1) "m1"
2) "1"
3) "m2"
4) "2"
5) "m3"
6) "3"
7) "m4"
8) "4"
# 使用 zrevrange 倒序返回數據
127.0.0.1:6379> ZREVRANGE k73 0 3 WITHSCORES
1) "m10\x11"
2) "10"
3) "m9"
4) "9"
5) "m8"
6) "8"
7) "m7"
8) "7"
zrangebyscore
- 語法
ZRANGEBYSCORE key min max [WITHSCORES] [LIMIT offset count]
- 解釋
zset range by score
類似 zrange
, 不過是按照 score
的值進行排序的。
[LIMIT offset count]
, 是從offset開始,返回count
個。
- 演示
# 返回 score值在 [9,10]之間的member。
127.0.0.1:6379> ZADD k74 1 m1 2 m2 3 m3 4 m4 5 m5 6 m6 7 m7 8 m8 9 m9 10 m10
(integer) 10
127.0.0.1:6379> ZRANGEBYSCORE k74 9 10 WITHSCORES
1) "m9"
2) "9"
3) "m10\x11"
4) "10"
# 從第2個(區間內索引爲1)開始,返回1個元素
127.0.0.1:6379> ZRANGEBYSCORE k74 9 10 WITHSCORES LIMIT 1 1
1) "m10\x11"
2) "10"
zrevrangebyscore
- 語法
ZREVRANGEBYSCORE key max min [WITHSCORES] [LIMIT offset count]
- 解釋
返回有序集 key
中, score
值介於 max
和 min
之間(默認包括等於 max
或 min
)的所有的成員。有序集成員按 score
值遞減(從大到小)的次序排列。
注意各個參數的位置哦。這裏和 zrevrange
的參數不一樣。
- 演示
127.0.0.1:6379> ZADD k75 1 m1 2 m2 3 m3 4 m4 5 m5 6 m6 7 m7 8 m8 9 m9 10 m10
(integer) 10
127.0.0.1:6379> ZREVRANGEBYSCORE k75 8 6 WITHSCORES
1) "m8"
2) "8"
3) "m7"
4) "7"
5) "m6"
6) "6"
127.0.0.1:6379> ZREVRANGEBYSCORE k75 8 6 WITHSCORES LIMIT 1 1
1) "m7"
2) "7"
zrank
- 語法
ZRANK key member
- 解釋
返回有序集 key
中成員 member
的排名。其中有序集成員按 score
值遞增(從小到大)順序排列
- 演示
127.0.0.1:6379> ZADD k76 1 m1 2 m2 3 m3 4 m4 5 m5 6 m6 7 m7 8 m8 9 m9 10 m10
(integer) 10
127.0.0.1:6379> zrank k76 m4
(integer) 3
127.0.0.1:6379> zrank k76 m10
(integer) 9
zrevrank
- 語法
ZREVRANK key member
- 解釋
返回有序集 key
中成員 member
的排名。其中有序集成員按 score
值遞減(從大到小)排序。
排名以 0
爲底,也就是說, score
值最大的成員排名爲 0
。
- 演示
127.0.0.1:6379> ZADD k77 1 m1 2 m2 3 m3 4 m4 5 m5 6 m6 7 m7 8 m8 9 m9 10 m10
(integer) 10
127.0.0.1:6379> ZREVRANK k77 m10
(integer) 0
127.0.0.1:6379> ZREVRANK k77 m4
(integer) 6
zrem
- 語法
ZREM key member [member ...]
- 解釋
zset remove
移除有序集 key
中的一個或多個成員,不存在的成員將被忽略。
當 key
存在但不是有序集類型時,返回一個錯誤。
- 演示
127.0.0.1:6379> ZADD k78 1 m1 2 m2 3 m3 4 m4 5 m5 6 m6 7 m7 8 m8 9 m9 10 m10
(integer) 10
# 刪除m2,m3,m4
127.0.0.1:6379> ZREM k78 m2 m3 m4
(integer) 3
127.0.0.1:6379> ZRANGE k78 0 -1 WITHSCORES
1) "m1"
2) "1"
3) "m5"
4) "5"
5) "m6"
6) "6"
7) "m7"
8) "7"
9) "m8"
10) "8"
11) "m9"
12) "9"
13) "m10"
14) "10"
zremrangebyrank
- 語法
ZREMRANGEBYRANK key start stop
- 解釋
移除有序集 key
中,指定排名(rank
)區間內的所有成員。
區間分別以下標參數 start
和 stop
指出,包含 start
和 stop
在內。
- 演示
127.0.0.1:6379> ZADD k79 1 m1 2 m2 3 m3 4 m4 5 m5 6 m6 7 m7 8 m8 9 m9 10 m10
(integer) 10
# 刪除排名第2到排名第4的member
127.0.0.1:6379> ZREMRANGEBYRANK k79 1 3
(integer) 3
127.0.0.1:6379> ZRANGE k79 0 -1 WITHSCORES
1) "m1"
2) "1"
3) "m5"
4) "5"
5) "m6"
6) "6"
7) "m7"
8) "7"
9) "m8"
10) "8"
11) "m9"
12) "9"
13) "m10"
14) "10"
zremrangebyscore
- 語法
ZREMRANGEBYSCORE key min max
- 解釋
移除有序集 key
中,所有 score
值介於 min
和 max
之間(包括等於 min
或 max
)的成員。
- 演示
127.0.0.1:6379> ZADD k80 1 m1 2 m2 3 m3 4 m4 5 m5 6 m6 7 m7 8 m8 9 m9 10 m10
(integer) 10
# 刪除 score>=1 and score <=9 的元素
127.0.0.1:6379> ZREMRANGEBYSCORE k80 1 9
(integer) 9
127.0.0.1:6379> zrange k80 0 -1 WITHSCORES
1) "m10"
2) "10"
zrangebylex
- 語法
ZRANGEBYLEX key min max [LIMIT offset count]
- 解釋
當有序集合的所有成員都具有相同的分值時, 有序集合的元素會根據成員的字典序(lexicographical ordering
)來進行排序, 而這個命令則可以返回給定的有序集合鍵 key
中, 值介於 min
和 max
之間的成員。
注意:
合法的 min
和 max
參數必須包含 ( 或者 [
, 其中 (
表示開區間(指定的值不會被包含在範圍之內), 而 [
則表示閉區間(指定的值會被包含在範圍之內)。
特殊值 +
和 -
在 min
參數以及 max
參數中具有特殊的意義, 其中 +
表示正無限, 而 -
表示負無限。 因此, 向一個所有成員的分值都相同的有序集合發送命令 ZRANGEBYLEX <zset> - +
, 命令將返回有序集合中的所有元素
lex
:
表示如果score
相等,則按照member
的字典順序排序。
此外這個命令, 比如ZRANGBYSCORE
稍微強大一點兒. 可以指定區間範圍, 當只知道member
,不知道score的時候,可以是使用帶有lex
的命令。
- 演示
127.0.0.1:6379> zadd k81 1 a 2 b 3 c 4 d 5 f 6 g
(integer) 6
# 返回 score值在a的score值和c的score值之間的member
# 即: score> Score(a) && score <= Score(c)
127.0.0.1:6379> ZRANGEBYLEX k81 (a [c
1) "b"
2) "c"
# 返回 小於等於c的Score值的元素
127.0.0.1:6379> ZRANGEBYLEX k81 - [c
1) "a"
2) "b"
3) "c"
# 返回所有元素
127.0.0.1:6379> ZRANGEBYLEX k81 - +
1) "a"
2) "b"
3) "c"
4) "d"
5) "f"
6) "g"
zlexcount
- 語法
ZLEXCOUNT key min max
- 解釋
對於一個所有成員的分值都相同的有序集合鍵 key
來說, 這個命令會返回該集合中, 成員介於 min
和 max
範圍內的元素數量。
- 演示
127.0.0.1:6379> zadd k82 1 a 2 b 3 c 4 d 5 f 6 g
(integer) 6
127.0.0.1:6379> ZLEXCOUNT k82 2 5
(error) ERR min or max not valid string range item
127.0.0.1:6379> ZLEXCOUNT k82 a b
# 大於Score(a),小於等於Score(b)的member,只有b.
127.0.0.1:6379> ZLEXCOUNT k82 (a [b
(integer) 1
# 大於Score(a),小於等於Score(d)的member,有b.c.d,三個
127.0.0.1:6379> ZLEXCOUNT k82 (a [d
(integer) 3
zremrangebylex
- 語法
ZREMRANGEBYLEX key min max
- 解釋
對於一個所有成員的分值都相同的有序集合鍵 key
來說, 這個命令會移除該集合中, 成員介於 min
和 max
範圍內的所有元素。
- 演示
127.0.0.1:6379> zadd k83 1 a 2 b 3 c 4 d 5 f 6 g
(integer) 6
# 刪除 score值在 (Score(a),Score(c)] 之間的member
127.0.0.1:6379> ZREMRANGEBYLEX k83 (a [c
(integer) 2
# 刪除了,b,c
127.0.0.1:6379> zrange k83 0 -1
1) "a"
2) "d"
3) "f"
4) "g"
zscan
- 語法
ZSCAN key cursor [MATCH pattern] [COUNT count]
- 解釋
這是一個查詢命令。 同 SCAN
命令. 可以參考這篇文章 010-其他命令
SCAN
命令是一個基於遊標的迭代器(cursor based iterator
): SCAN
命令每次被調用之後, 都會向用戶返回一個新的遊標, 用戶在下次迭代時需要使用這個新遊標作爲 SCAN
命令的遊標參數, 以此來延續之前的迭代過程。
- 演示
127.0.0.1:6379> ZADD k84 1 m1 2 m2 3 m3 4 m4 5 m5 6 m6 7 m7 8 m8 9 m9 10 m10
(integer) 10
127.0.0.1:6379> zscan k84 0 MATCH m* COUNT 3
1) "0"
2) 1) "m1"
2) "1"
3) "m2"
4) "2"
5) "m3"
6) "3"
7) "m4"
8) "4"
9) "m5"
10) "5"
11) "m6"
12) "6"
13) "m7"
14) "7"
15) "m8"
16) "8"
17) "m9"
18) "9"
19) "m10"
20) "10"
zunionstore
- 語法
ZUNIONSTORE destination numkeys key [key ...] [WEIGHTS weight] [AGGREGATE SUM|MIN|MAX]
-
解釋
計算給定的一個或多個有序集合的並集,其中給定 key 的數量必須以 numkeys 參數指定,並將該並集(結果集)儲存到 destination 。
如果key相同的時候,對應的score值會相加。WEIGHTS
: 使用WEIGHTS
選項,你可以爲 每個 給定有序集 分別 指定一個乘法因子(multiplication factor
),每個給定有序集的所有成員的score
值在傳遞給聚合函數(aggregation function
)之前都要先乘以該有序集的因子。AGGREGATE
: 使用AGGREGATE
選項,你可以指定並集的結果集的聚合方式。
默認使用的參數SUM
,可以將所有集合中某個成員的score
值之 和 作爲結果集中該成員的score
值;使用參數MIN
,可以將所有集合中某個成員的 最小score
值作爲結果集中該成員的score
值;而參數MAX
則是將所有集合中某個成員的 最大score
值作爲結果集中該成員的score
值。
-
演示
127.0.0.1:6379> ZADD k85_1 1 m1 2 m2 3 m3
(integer) 3
127.0.0.1:6379> ZADD k85_2 1 m1 4 m4 5 m5
(integer) 3
127.0.0.1:6379> ZUNIONSTORE k85 2 k85_1 k85_2
(integer) 5
127.0.0.1:6379> zrange k85 0 -1 WITHSCORES
1) "m1"
2) "2"
3) "m2"
4) "2"
5) "m3"
6) "3"
7) "m4"
8) "4"
9) "m5"
10) "5"
# 演示 Weights參數: WEIGHTS 2 3
# 指: 第一個zset的所有元素 *2 ,第二個有序集合中的元素 *3
127.0.0.1:6379> ZUNIONSTORE k85 2 k85_1 k85_2 WEIGHTS 2 3
(integer) 5
127.0.0.1:6379> ZRANGE k85 0 -1 WITHSCORES
1) "m2"
2) "4"
3) "m1"
4) "5"
5) "m3"
6) "6"
7) "m4"
8) "12"
9) "m5"
10) "15"
# 演示 Weights參數: WEIGHTS 2 4
# 指: 第一個zset的所有元素 *2 ,第二個有序集合中的元素 *3
127.0.0.1:6379> ZUNIONSTORE k85 2 k85_1 k85_2 WEIGHTS 2 4
(integer) 5
127.0.0.1:6379> ZRANGE k85 0 -1 WITHSCORES
1) "m2"
2) "4"
3) "m1"
4) "6"
5) "m3"
6) "6"
7) "m4"
8) "16"
9) "m5"
10) "20"
zinterstore
-
語法
-
解釋
計算給定的一個或多個有序集的交集,其中給定 key
的數量必須以 numkeys
參數指定,並將該交集(結果集)儲存到 destination
。
默認情況下,結果集中某個成員的 score
值是所有給定集下該成員 score
值之和.
- 演示
127.0.0.1:6379> zadd k86_1 1 m1 2 m2 3 m3 4 m4
(integer) 4
127.0.0.1:6379> ZADD k86_2 20 m2 30 m3 50 m5
(integer) 3
127.0.0.1:6379> ZINTERSTORE k86 2 k86_1 k86_2
(integer) 2
# 取交集(默認相加)
127.0.0.1:6379> zrange k86 0 -1 WITHSCORES
1) "m2"
2) "22"
3) "m3"
4) "33"
# WEIGTHS 參數和上面的 ZUNIONSTORE命令一樣.
# 這裏演示一下, AGGREGATE參數
# 默認使用的是SUM. 就是本命令中上面的例子了.
# 下面演示MIN 和 MAX
127.0.0.1:6379> ZINTERSTORE k86 2 k86_1 k86_2 AGGREGATE MIN
(integer) 2
127.0.0.1:6379> ZRANGE k86 0 -1 WITHSCORES
1) "m2"
2) "2"
3) "m3"
4) "3"
127.0.0.1:6379> ZINTERSTORE k86 2 k86_1 k86_2 AGGREGATE MAX
(integer) 2
127.0.0.1:6379> ZRANGE k86 0 -1 WITHSCORES
1) "m2"
2) "20"
3) "m3"
4) "30"
zset
的內部結構
這裏我們主要看skiplist
,如果忘記了hashtable
,就看着這篇文章
我們從命令zadd
入手,找到zset
的add
通用方法zaddGenericCommand(c,ZADD_NONE);
來看一下。省略了部分代碼。
...
if (server.zset_max_ziplist_entries == 0 ||
server.zset_max_ziplist_value < sdslen(c->argv[scoreidx+1]->ptr))
{
/// 創建 OBJ_ENCODING_SKIPLIST 編碼的結構
zobj = createZsetObject();
} else {
/// 創建 OBJ_ZSET 編碼的結構
zobj = createZsetZiplistObject();
}
....
來看一下 createZsetObject()
方法的實現,就再清晰不過了。
/// 創建 Zset 對象。
robj *createZsetObject(void) {
zset *zs = zmalloc(sizeof(*zs));
robj *o;
zs->dict = dictCreate(&zsetDictType, NULL);
zs->zsl = zslCreate();
o = createObject(OBJ_ZSET, zs);
o->encoding = OBJ_ENCODING_SKIPLIST;
return o;
}
到這裏,就是我們和 zset
這種數據類型的初次深入見面了。 我們先看下 zset
這種結構體的定義。
/// 有序集合的結構定義
typedef struct zset {
/// 字典,鍵爲成員,值爲score
/// 用於支持 O(1) 複雜度的按成員分值操作。
dict *dict;
/// 跳躍表,按分值排序成員
/// 用於支持平均複雜度爲 O(logN)的按分值定位成員以及範圍的操作。
zskiplist *zsl;
} zset;
dict
前面已經看過了,這裏來看下zskiplist
。
skiplist
/// 跳躍表
typedef struct zskiplist {
/// 表頭,表尾
struct zskiplistNode *header, *tail;
/// 表中節點的數量
unsigned long length;
/// 表中層數最大的節點的層數
int level;
} zskiplist;
typedef struct zskiplistNode {
sds ele;
/// 分數
double score;
/// 後退的指針
struct zskiplistNode *backward;
/// 層
struct zskiplistLevel {
/// 前進指針
struct zskiplistNode *forward;
/// 跨度
unsigned long span;
} level[];
} zskiplistNode;
總結
- 本章是一個新的常用數據類型,
ZSET
有序集合。底層的數據結構是使用的是skipList
和hashtable
. 關於Skiplist
的初步瞭解文章穿梭機 和Redis
中skiplist
的實現源碼解讀(請期待,已經安排上了) - 然後簡單介紹了
Redis ZSET
數據類型的基礎使用場景. 關鍵字有有序
,排名
,權重
等. ZSET
的20
個常有命令。 後面我會針對這20
個命令的實現進行簡單的分享.- 然後簡單的看了一下
Redis
中的數據結構的實現,還是那句話,Redis
的數據結構是動態編碼的,ZSET
是有hashtable
和skiplist
實現的。 skiplist 是一個非常高效的數據結構,增刪查的效率都是O(logN)
. 實現原理可以參考這篇文章直通車,裏面有幾種流行的語言的實現,可以針對自己擅長的語言進行查看。
最後
希望和你成爲朋友!我們一起學習~
最新文章盡在公衆號【方家小白】,期待和你相逢在【方家小白】