Redis的数据类型之有序集合 · zset

书接上回

上一篇我们学习的 set集合这一数据类型。其内部是由insethashtable这种两种数据结构编码的。
如果不记得了,那就来坐穿梭机回去看看吧。 开始穿梭

接下来,我们继续学习一个新的数据类型, 有序集合. zset.

zset简介

zset ,中文名字叫 有序集合. 序这个字,在Redis的实现是 score 字段。我们先不急这个字段,后面会介绍。

Redis中有序的数据类型,还有一个就是我们前面学习的 list 了。 它们还都可以获得某一定范围内的元素。

zset 的优点是: list 通过链表实现,在两端操作数据都很方便. 但是操作中间的数据就比较慢了。 zset 是用 hashtableskiplist 来实现的。即使是操作中间数据,速度也很快. 时间复杂度为: O(logN)

zset 的缺点就是: 就是比较耗费内存。

zset类型的应用场景

  • 存储学生成绩快速做成绩排名功能。
  • 排行榜,比如:列出某用户当前的全球排名, 比赛中胜场数排名。
  • 带权重的消息队列功能

zset的基本命令

zadd

  • 语法

ZADD key [NX|XX] [CH] [INCR] score member [score member ...]

  • 解释

member添加有序集合中.

如果member存在,会更新memberscore值。

  • 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的成员memberscore值加上 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值在minmax 之间的元素的个数。包括等于 minmax

  • 演示
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 值介于 maxmin 之间(默认包括等于 maxmin )的所有的成员。有序集成员按 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)区间内的所有成员。

区间分别以下标参数 startstop 指出,包含 startstop 在内。

  • 演示
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 值介于 minmax 之间(包括等于 minmax )的成员。

  • 演示
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 中, 值介于 minmax 之间的成员。

注意:

合法的 minmax 参数必须包含 ( 或者 [ , 其中 ( 表示开区间(指定的值不会被包含在范围之内), 而 [ 则表示闭区间(指定的值会被包含在范围之内)。

特殊值 +-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 来说, 这个命令会返回该集合中, 成员介于 minmax 范围内的元素数量。

  • 演示
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 来说, 这个命令会移除该集合中, 成员介于 minmax 范围内的所有元素。

  • 演示
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入手,找到zsetadd通用方法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 有序集合。底层的数据结构是使用的是 skipListhashtable . 关于Skiplist的初步了解文章穿梭机Redisskiplist的实现源码解读(请期待,已经安排上了)
  • 然后简单介绍了 Redis ZSET数据类型的基础使用场景. 关键字有 有序排名权重等.
  • ZSET20 个常有命令。 后面我会针对这 20 个命令的实现进行简单的分享.
  • 然后简单的看了一下Redis中的数据结构的实现,还是那句话,Redis的数据结构是动态编码的, ZSET是有hashtableskiplist 实现的。 skiplist 是一个非常高效的数据结构,增删查的效率都是 O(logN). 实现原理可以参考这篇文章直通车,里面有几种流行的语言的实现,可以针对自己擅长的语言进行查看。

最后

希望和你成为朋友!我们一起学习~
最新文章尽在公众号【方家小白】,期待和你相逢在【方家小白】

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

方_小_白

谢谢金主子,记得关注方家小白哦

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值