redis-py简介
安装
在之前的学习笔记(一)中已经安装过redis-py,我的Python版本是3.5.2
$ pip3 install redis
快速开始
>>> import redis
>>> r = redis.StrictRedis(host='localhost', port=6379, db=0)
>>> r.set('key', 'value')
True
>>> r.get('key')
b'value'
API参考
Redis的官方命令文档很好地解释了每个命令的详细信息。 redis-py公开了实现这些命令的两个客户端类。
第一,StrictRedis类试图遵守官方命令语法, 但是有些一些例外:
- SELECT: 没有实现,考虑到线程安全的原因。
- DEL: 由于del是python语法关键字,所用delete来代替。
- CONFIG GET|SET: 分开用 config_get or config_set来代替
- MULTI/EXEC: 事务作为Pipeline类的其中一部分的实现。Pipeline默认保证了MULTI,EXEC声明。但是你可以指定transaction=False来禁用这一行为。
- SUBSCRIBE/LISTEN:PubSub作为一个独立的类来实现发布订阅机制。
- SCAN/SSCAN/HSCAN/ZSCAN:每个命令都对应一个等价的迭代器方法scan_iter/sscan_iter/hscan_iter/zscan_iter methods for this behavior。
第二,Redis类是StrictRedis的子类,提供redis-py版本向后的兼容性。
关于StrictRedis与Redis的区别:(官方推荐使用StrictRedis.)
以下几个方法在StrictRedis和Redis类中的参数顺序不同。
- LREM: 在Redis类中是这样的:
lrem(self, name, value, num=0)
在StrictRedis类中是这样的:
lrem(self, name, count, value) - ZADD: 在Redis类中是这样的:
zadd(‘my-key’, ‘name1’, 1.1, ‘name2’, 2.2, name3=3.3, name4=4.4)
在StrictRedis中是这样的:
zadd(‘my-key’, 1.1, ‘name1’, 2.2, ‘name2’, name3=3.3, name4=4.4) - SETEX: 在Redis类中是这样的:
setex(self, name, value, time)
而在StrictRedis中是这样的:
setex(self, name, time, value)
连接池
redis-py使用connection pool来管理对一个redis server的所有连接,避免每次建立、释放连接的开销。默认情况下,每个Redis实例都会依次创建并维护一个自己的连接池。我们可以直接建立一个连接池,然后传递给Redis或StrictRedis连接命令作为参数,这样就可以实现多个Redis实例共享一个连接池,以实现客户端分片,或者对连接的管理方式进行更高精度的控制。
>>> pool = redis.ConnectionPool(host='localhost', port=6379, db=0)
>>> r = redis.StrictRedis(connection_pool=pool)
我们也可以创建自己的Connection子类,用于控制异步框架中的套接字行为,要使用自己的连接实例化客户端类,需要创建一个连接池,将类传递给connection_class参数。
>>> pool = redis.ConnectionPool(connection_class=YourConnectionClass,your_arg='...', ...)
释放连接回到连接池:可以使用Redis类的reset()方法,或者使用with上下文管理语法。
解析器:解析器控制如何解析Redis-server的响应内容,redis-py提供两种方式的解析器类支持PythonParser和HiredisParser(需要单独安装)。它优先选用HiredisParser,如果不存在,则选用PythonParser. Hiredis是redis核心团队开发的一个高性能c库,能够提高10x的解析速度。
响应回调:The client class使用一系列的callbacks来完成响应到对应python类型的映射。这些响应回调,定义在 Redis client class中的RESPONSE_CALLBACKS字典中。你可以使用set_response_callback 方法来添加自定义回调类。这个方法接受两个参数:一个命令名字,一个回调类。回调类接受至少一个参数:响应内容,关键字参数作为命令调用时的参数。
线程安全性
Redis客户端实例可以安全地在线程之间共享。 在内部,连接实例只在命令执行期间从连接池检索,并在执行后直接返回到池中。 命令执行过程从不修改客户端实例上的状态。
但是,有一个警告:Redis SELECT命令。 SELECT命令允许您切换连接正在使用的数据库。 该数据库保持选中,直到选择另一个或连接关闭为止。 这会创建一个问题,因为可以将连接返回到连接到不同数据库的池。
因此,redis-py不会在客户端实例上实现SELECT命令。 如果在同一应用程序中使用多个Redis数据库,则应为每个数据库创建一个单独的客户机实例(也可能是单独的连接池)。
在线程之间传递PubSub或Pipeline对象是不安全的。
Redis命令及其对应redis-py API
由于Redis官方命令文档很好地解释了每个命令的详细信息,所以我这里只对最常用的Redis命令进行整理,并给出其redis-py API。
字符串
下表展示了对Redis字符串执行自增和自减操作的命令及其redis-py API。
| 命令 | 用例 | 描述 | redis-py API |
|---|---|---|---|
| INCR | INCR key-name | 将键存储的值加1 | incr(name, amount=1) |
| DECR | DECR key-name | 将键存储的值减1 | decr(name, amount=1) |
| INCRBY | INCRBY key-name amount | 将键存储的值加整数amount | incr(name, amount=1) |
| DECRBY | DECRBY key-name amount | 将键存储的值减整数amount | decr(name, amount=1) |
| INCRBYFLOAT | INCRBYFLOAT key-name amount | 将键存储的值加浮点数amount | incrbyfloat(name, amount=1.0) |
在redis-py内部,使用了INCRBY和DECRBY命令来实现incr()和decr()方法,并且第二个参数amount是可选的,默认为1。
下面这个交互示例展示了Redis的INCR和DECR操作
>>> r.get('key')
>>> r.incr('key')
1
>>> r.incr('key', 15)
16
>>> r.get('key')
b'16'
>>> r.decr('key', 5)
11
>>> r.set('key', 13)
True
>>> r.incr('key')
14
当用户将一个值存储到Redis字符串中时,如果这个值可以被解释(interpet)为十进制整数或者浮点数,那么Redis会允许用户对这个字符串执行各种INCR和DECR操作。如果用户对一个不存在的键或者一个保存了空串的键执行自增或自减操作,Redis会自动将这个键的值当作是0来处理。若非上述情况,则Redis将会返回一个错误。
除了自增和自减操作,Redis还可以对字节串进行读取和写入的操作。
下表展示了Redis用来处理字符串子串和二进制位的命令及其redis-py API。
| 命令 | 用例 | 描述 | redis-py API |
|---|---|---|---|
| APPEND | APPEND key-name value | 将值value追加到给定键key-name当前存储的值的末尾 | append(key, value) |
| GETRANGE | GETRANGE key-name start end | 获取一个偏移量从start到end的子串,包含start和end | getrange(key, start, end) |
| SETRANGE | SETRANGE key-name offset value | 将从start开始的子串设置为给定值 | setrange(name, offset, value) |
| GETBIT | GETBIT key-name offset | 将字节串看作是二进制位串,并返回位串中偏移量为offset的二进制位的值 | getbit(name, offset) |
| SETBIT | SETBIT key-name offset value | 将字节串看作是二进制位串,并将位串中偏移量为offset的二进制位的值设为value | setbit(name, offset, value) |
| BITCOUNT | BITCOUNT key-name [start end] | 统计字符串被设置为1的bit数,通过指定额外的 start 或 end 参数,可以让计数只在特定的位上进行 | bitcount(key, start=None, end=None) |
| BITOP | BITOP operation dest-key key-name [key-name …] | 对一个或多个保存二进制位的字符串 key 进行位元操作,并将结果保存到 destkey 上。 | bitop(operation, dest, *keys) |
在执行SETRANGE或者SETBIT命令时,如果offset比当前key对应string还要长,那这个string后面就补空字节(null)以达到offset。使用GETRANGE时超出字符串末尾的数据会被认为是空字符串,而使用GETBIT时超出字符串末尾的二进制位会被视为是0。
下面这个交互示例展示了Redis的子串操作和二进制位操作
>>> r.append('new-string-key', 'hello ')
6
>>> r.append('new-string-key', 'world!')
12
>>> r.substr('new-string-key', 3, 7)
b'lo wo'
>>> r.getrange('new-string-key', 3, 7)
b'lo wo'
>>> r.setrange('new-string-key', 0, 'H')
12
>>> r.get('new-string-key')
b'Hello world!'
>>> r.setrange('new-string-key', 11, ', how are you?')
25
>>> r.get('new-string-key')
b'Hello world, how are you?'
>>> r.setbit('another-key', 2, 1)
0
>>> r.setbit('another-key', 7, 1)
0
>>> r.getbit('another-key', 1)
0
>>> r.get('another-key')
b'!'
Redis现在的GETRANGE命令是由以前的SUBSTR命令改名而来,所以现在redis-py中两者仍然都可以使用,但是最好还是使用getrange()方法来获取子串。
列表
下表展示了一些之前介绍过的常用列表命令
| 命令 | 用例 | 描述 | redis-py API |
|---|---|---|---|
| RPUSH | RPUSH key value [value …] | 向存于key的列表的尾部插入所有指定的值 | rpush(name, *values) |
| LPUSH | LPUSH key value [value …] | 将所有指定的值插入到存于key的列表的头部 | lpush(name, *values) |
| RPOP | RPOP key | 移除并返回key对应的list的最后一个元素 | rpop(name) |
| LPOP | LPOP key | 移除并返回key对应的list的第一个元素 | lpop(name) |
| LINDEX | LINDEX key index | 返回列表索引位置的元素 | lindex(name, index) |
| LRANGE | LRANGE key start stop | 返回存储在key的列表里指定范围内的元素 | lrange(name, start, end) |
| LTRIM | LTRIM key start stop | 修剪(trim)一个已存在的list,这样list就会只包含指定范围的指定元素 | ltrim(name, start, end) |
下面这个交互示例展示了Redis列表的推入和弹出操作
>>> r.rpush('list-key', 'last')
1
>>> r.lpush('list-key', 'first')
2
>>> r.rpush('list-key', 'new last')
3
>>> r.lrange('list-key', 0, -1)
[b'first', b'last', b'new last']
>>> r.lpop('list-key')
b'first'
>>> r.lpop('list-key')
b'last'
>>> r.lrange('list-key', 0, -1)
[b'new last']
>>> r.rpush('list-key', 'a', 'b', 'c')
4
>>> r.lrange('list-key', 0, -1)
[b'new last', b'a', b'b', b'c']
>>> r.ltrim('list-key', 2, -1)
True
>>> r.lrange('list-key', 0, -1)
[b'b', b'c']
还有几个列表命令能将元素从一个列表移动到另一个列表,或者阻塞(block)执行命令的客户端直到有其他客户端给列表添加元素为止。
下表列出了这些阻塞弹出命令以及列表之间移动元素的命令
| 命令 | 用例 | 描述 | redis-py API |
|---|---|---|---|
| BLPOP | BLPOP key [key …] timeout | 弹出第一个非空列表的头元素,或在timeout秒内阻塞并等待可弹出的元素出现 | blpop(keys, timeout=0) |
| BRPOP | BRPOP key [key …] timeout | 弹出第一个非空列表的末尾元素,或在timeout秒内阻塞并等待可弹出的元素出现 | brpop(keys, timeout=0) |
| RPOPLPUSH | RPOPLPUSH source destination | 原子性地返回并移除存储在source的列表的最后一个元素(列表尾部元素), 并把该元素放入存储在destination的列表的第一个元素位置(列表头部) | rpoplpush(src, dst) |
| BRPOPLPUSH | BRPOPLPUSH source destination timeout | BRPOPLPUSH 是 RPOPLPUSH 的阻塞版本。 当 source 包含元素的时候,这个命令表现得跟 RPOPLPUSH 一模一样。 当 source 是空的时候,Redis将会阻塞这个连接,直到另一个客户端 push 元素进入或者达到 timeout 时限。 | brpoplpush(src, dst, timeout=0) |
注:原子性是指命令正在都区或者修改数据的时候,其他客户端不能读取或修改相同的数据。
下面这个交互示例展示了Redis列表的阻塞弹出命令以及元素移动命令
>>> r.rpush('list', 'item1')
1
>>> r.rpush('list', 'item2')
2
>>> r.rpush('list2', 'item3')
1
>>> r.brpoplpush('list2', 'list', 1)
b'item3'
>>> r.brpoplpush('list2', 'list', 1)
>>> r.lrange('list', 0, -1)
[b'item3', b'item1', b'item2']
>>> r.brpoplpush('list', 'list2', 1)
b'item2'
>>> r.blpop(['list', 'list2'], 1)
(b'list', b'item3')
>>> r.blpop(['list', 'list2'], 1)
(b'list', b'item1')
>>> r.blpop(['list', 'list2'], 1)
(b'list2', b'item2')
>>> r.blpop(['list', 'list2'], 1)
对于阻塞弹出命令和弹出并推入命令,最常见的用例就是消息传递(messaging)和任务队列(task queue)。
集合
下表展示了一部分最常用的集合命令
| 命令 | 用例 | 描述 | redis-py API |
|---|---|---|---|
| SADD | SADD key member [member …] | 添加一个或多个指定的member元素到key集合中 | sadd(name, *values) |
| SREM | SREM key member [member …] | 在key集合中移除指定的元素 | srem(name, *values) |
| SISMEMBER | SISMEMBER key member | 返回成员member是否是存储的集合key的成员 | sismember(name, value) |
| SCARD | SCARD key | 返回集合包含元素的数量 | scard(name) |
| SMEMBERS | SMEMBERS key | 返回key集合所有的元素 | smembers(name) |
| SRANDMEMBER | SRANDMEMBER key [count] | 仅提供key参数,那么随机返回key集合中的一个元素,返回含有 count 个不同的元素的数组,对count分情况处理 | srandmember(name, number=None) |
| SPOP | SPOP key [count] | 从key对应集合中返回并删除一个或多个元素 | spop(name) |
| SMOVE | SMOVE source destination member | 将member从source集合移动到destination集合中 | smove(src, dst, value) |
下面这个交互示例展示了这些常用的集合命令
>>> r.sadd('set-key', 'a', 'b', 'c')
3
>>> r.srem('set-key', 'c', 'd')
1
>>> r.srem('set-key', 'c', 'd')
0
>>> r.scard('set-key')
2
>>> r.smembers('set-key')
{b'b', b'a'}
>>> r.smove('set-key', 'set-key2', 'a')
True
>>> r.smove('set-key', 'set-key2', 'c')
False
>>> r.smembers('set-key2')
{b'a'}
但是集合真正厉害的地方在于组合和关联多个集合,下表展示了相关的Redis命令
| 命令 | 用例 | 描述 | redis-py API |
|---|---|---|---|
| SDIFF | SDIFF key [key …] | 返回一个集合与给定集合的差集的元素 | sdiff(keys, *args) |
| SDIFFSTORE | SDIFFSTORE destination key [key …] | 类似于 SDIFF,不同之处在于该命令不返回结果集,而是将结果存放在destination集合中,如果destination已经存在, 则将其覆盖重写 | sdiffstore(dest, keys, *args) |
| SINTER | SINTER key [key …] | 返回指定所有的集合的成员的交集 | sinter(keys, *args) |
| SINTERSTORE | SINTERSTORE destination key [key …] | 与SINTER命令类似,但是它并不是直接返回结果集,而是将结果保存在 destination集合中,如果destination集合存在, 则会被重写 | sinterstore(dest, keys, *args) |
| SUNION | SUNION key [key …] | 返回给定的多个集合的并集中的所有成员 | sunion(keys, *args) |
| SUNIONSTORE | SUNIONSTORE destination key [key …] | 类似于SUNION命令,不同的是它并不返回结果集,而是将结果存储在destination集合中,如果destination已经存在,则将其覆盖. | sunionstore(dest, keys, *args) |
这些命令分别是并集运算、交集运算和差集运算这三个基本集合操作的“返回结果”版本和“存储结果”版本,下面这个交互示例展示了这些命令的基本使用
>>> r.sadd('skey1', 'a', 'b', 'c', 'd')
4
>>> r.sadd('skey2', 'c', 'd', 'e', 'f')
4
>>> r.sdiff('skey1', 'skey2')
{b'b', b'a'}
>>> r.sinter('skey1', 'skey2')
{b'c', b'd'}
>>> r.sunion('skey1', 'skey2')
{b'd', b'a', b'f', b'e', b'c', b'b'}
和Python的集合相比,Redis的集合除了可以被多个客户端远程地进行访问之外,其他的语义和功能基本都是相同的。
散列
首先介绍一些常用的添加和删除键值对的Redis散列命令
| 命令 | 用例 | 描述 | redis-py API |
|---|---|---|---|
| HMGET | HMGET key field [field …] | 返回key指定的散列中指定字段的值 | hmget(name, keys, *args) |
| HMSET | HMSET key field value [field value …] | 设置key指定的散列中指定字段的值,该命令将重写所有在散列中存在的字段,如果key指定的散列不存在,会创建一个新的散列并与key关联 | hmset(name, mapping) |
| HDEL | HDEL key field [field …] | 从key指定的散列中移除指定的域,在散列中不存在的域将被忽略,如果key指定的散列不存在,它将被认为是一个空的散列,该命令将返回0 | hdel(name, *keys) |
| HLEN | HLEN key | 返回key指定的散列包含的字段的数量 | hlen(name) |
其中,HDEL命令已经介绍过了,而HLEN以及用于一次读取或设置多个键的HMGET和HMSET则是新出现的命令。它们既可以给用户带来方便,又可以通过减少命令的调用次数以及客户端与Redis之间的通信往返次数来提升Redis的性能。
下面这个交互示例展示了这些命令的使用方法
>>> r.hmset('hash-key', {'k1':'v1','k2':'v2','k3':'v3'})
True
>>> r.hmget('hash-key', ['k2', 'k3'])
[b'v2', b'v3']
>>> r.hlen('hash-key')
3
>>> r.hdel('hash-key', 'k1', 'k3')
2
之前的学习笔记(一)介绍的HGET命令和HSET命令分别是HMGET和HMSET命令的单参数版本。因为HDEL已经可以同时删除多个键值对了,所以Redis没有实现HMDEL命令。
下表列出了散列的其他几个批量操作命令,以及一些和字符串操作类似的散列命令。
| 命令 | 用例 | 描述 | redis-py API |
|---|---|---|---|
| HEXISTS | HEXISTS key field | 检查给定键是否存在于散列中 | hexists(name, key) |
| HKEYS | HKEYS key | 返回散列包含的所有键 | hkeys(name) |
| HVALS | HVALS key | 返回散列包含的所有值 | hvals(name) |
| HGETALL | HGETALL key | 返回散列包含的所有键值对 | hgetall(name) |
| HINCRBY | HINCRBY key field increment | 将键存储的值加上整数increment | hincrby(name, key, amount=1) |
| HINCRBYFLOAT | HINCRBYFLOAT key field increment | 将键存储的值加上浮点数increment | hincrbyfloat(name, key, amount=1.0) |
下面这个交互示例展示了这些命令的使用方法
>>> r.hmset('hash-key2', {'short':'hello', 'long':1000*1})
True
>>> r.hkeys('hash-key2')
[b'short', b'long']
>>> r.hexists('hash-key2', 'num')
False
>>> r.hincrby('hash-key2', 'num')
1
>>> r.hexists('hash-key2', 'num')
True
在对散列进行处理时,如果键值对的值的体积非常大,那么用户可以先用HKEYS获取散列的所有键,然后只获取必要的值,这样可以有效地减少需要传输的数据量,避免服务器阻塞。
有序集合
下表展示了一些常用的有序集合命令,大部分在第一章都有介绍
| 命令 | 用例 | 描述 | redis-py API |
|---|---|---|---|
| ZADD | ZADD key score member [score member …] | 将带有给定分值的成员添加到有序集合中 | zadd(name, args, *kwargs) |
| ZREM | ZREM key member [member …] | 移除给定的成员,并返回被移除成员的数量 | zrem(name, *values) |
| ZCARD | ZCARD key | 返回有序集合包含的成员数量 | zcard(name) |
| ZINCRBY | ZINCRBY key increment member | 将member成员的分值加上increment | zincrby(name, value, amount=1) |
| ZCOUNT | ZCOUNT key min max | 返回分值介于min和max之间的成员数量 | zcount(name, min, max) |
| ZRANK | ZRANK key member | 返回成员member在有序集合中的排名 | zrank(name, value) |
| ZSCORE | ZSCORE key member | 返回成员member的分值 | zscore(name, value) |
| ZRANGE | ZRANGE key start stop [WITHSCORES] | 返回排名介于start和stop之间的成员,如果给定了可选的WITHSCORES选项,那么命令会将成员的分值也一并返回 | zrange(name, start, end, desc=False, withscores=False, score_cast_func= ) |
下面这个交互示例展示了Redis中的一些常用的有序集合命令
>>> r.zadd('zset-key', 3, 'a', 2, 'b', 1, 'c')
3
>>> r.zcard('zset-key')
3
>>> r.zincrby('zset-key', 'c', 3)
4.0
>>> r.zscore('zset-key', 'b')
2.0
>>> r.zrank('zset-key', 'c')
2
>>> r.zcount('zset-key', 0, 3)
2
>>> r.zrem('zset-key', 'b')
1
>>> r.zrange('zset-key', 0, -1, withscores=True)
[(b'a', 3.0), (b'c', 4.0)]
其中在Python客户端用StrictRedis客户端类执行ZADD命令需要先输入分值,再输入成员,这也是Redis的标准,而Redis客户端类则截然相反。
下表展示了另外一下非常有用的有序集合命令
| 命令 | 用例 | 描述 | redis-py API | ||
|---|---|---|---|---|---|
| ZREVRANK | ZREVRANK key member | 返回有序集合里成员member的排名,成员按照分值从大到小排列 | zrevrank(name, value) | ||
| ZREVRANGE | ZREVRANGE key start stop [WITHSCORES] | 返回有序集合给定排名范围内的成员,成员按照分值从大到小排列 | zrevrange(name, start, end, withscores=False, score_cast_func= ) | ||
| ZRANGEBYSCORE | ZRANGEBYSCORE key min max [WITHSCORES] [LIMIT offset count] | 返回有序集合中指定分数区间内的成员 | zrangebyscore(name, min, max, start=None, num=None, withscores=False, score_cast_func= ) | ||
| ZREVRANGEBYSCORE | ZREVRANGEBYSCORE key max min [WITHSCORES][LIMIT offset count] | 返回有序集合中指定分数区间内的成员,分数由高到低排序。 | zrevrangebyscore(name, max, min, start=None, num=None, withscores=False, score_cast_func= ) | ||
| ZREMRANGEBYRANK | ZREMRANGEBYRANK key start stop | 移除有序集key中,指定排名(rank)区间内的所有成员 | zremrangebyrank(name, min, max) | ||
| ZREMRANGEBYSCORE | ZREMRANGEBYSCORE key min max | 移除有序集key中,所有score值介于min和max之间(包括等于min或max)的成员 | zremrangebyscore(name, min, max) | ||
| ZINTERSTORE | ZINTERSTORE destination numkeys key [key …] [WEIGHTS weight] [SUM | MIN | MAX] | 计算给定的numkeys个有序集合的交集,并且把结果放到destination中 | zinterstore(dest, keys, aggregate=None) |
| ZUNIONSTORE | ZUNIONSTORE destination numkeys key [key …] [WEIGHTS weight] [SUM | MIN | MAX] | 计算给定的numkeys个有序集合的并集,并且把结果放到destination中。 | zunionstore(dest, keys, aggregate=None) |
其中有几个是没有介绍过的新命令,除了使用逆序来处理有序集合之外,ZREV*命令的工作方式和相对应的非逆序命令的工作方式完全一样(逆序就是指元素按照分值从大到小地排列)。
下面这个交互示例展示了ZINTERSTORE和ZUNIONSTORE命令的用法
>>> r.zadd('zset-1', 1, 'a', 2, 'b', 3, 'c')
3
>>> r.zadd('zset-2', 4, 'b', 1, 'c', 0, 'd')
3
>>> r.zinterstore('zset-i', ['zset-1', 'zset-2'])
2
>>> r.zrange('zset-i', 0, -1, withscores=True)
[(b'c', 4.0), (b'b', 6.0)]
>>> r.zunionstore('zset-u', ['zset-1', 'zset-2'], aggregate='min')
4
>>> r.zrange('zset-u', 0, -1, withscores=True)
[(b'd', 0.0), (b'a', 1.0), (b'c', 1.0), (b'b', 2.0)]
>>> r.sadd('set-1', 'a', 'd')
2
>>> r.zunionstore('zset-u2', ['zset-1', 'zset-2', 'set-1'])
4
>>> r.zrange('zset-u2', 0, -1, withscores=True)
[(b'd', 1.0), (b'a', 2.0), (b'c', 4.0), (b'b', 6.0)]
用户可以在执行交并运算时传入不同的聚合函数,共有sum、min、max三种可选;用户还可以把集合作为输入传给ZINTERSTORE和ZUNIONSTORE,命令会将集合看作是成员分值全为1的有序集合来处理。
# Redis, Python 🐶 怕是要给老板下跪了哦~ 🐶 赞赏微信打赏
支付宝打赏