当前位置:   article > 正文

Redis(4):Redis中常见的面试问题汇总_redis每秒的吞吐量

redis每秒的吞吐量

1.redis的优点

  • 读写速度快,数据存放在内存中。
  • 丰富的数据类型:字符串,哈希,链表、集合、有序集合
  • 支持事务watch
  • 可以用作缓存和消息队列,按key设置过期时间,超时自动删除
  • 数据持久化,支持AOF日志和RDB快照两种持久化方式,防止数据丢失
  • 支持主从复制来实现数据备份,主机数据会同步到从机

2.redis和memcache的比较

  • redis可以用来做存储(storge), 而memccached是用来做缓存(cache) 这个特点主要因为其有”持久化”的功能.
  • 存储的数据有”结构”,对于memcached来说,存储的数据,只有1种类型–”字符串”, 而redis则可以存储字符串,链表,哈希结构,集合,有序集合.

3.Redis目录下的重要文件的意义

redis-benchmark 性能测试工具

redis-check-aof 日志文件检测工(比如断电造成日志损坏,可以检测并修复)

redis-check-dump 快照文件检测工具,效果类上

redis-cli 客户端

redis-server 服务端

redis.windows.conf redis配置文件,在启动redis服务器的时候,必须要制定配置文件,那么相当于于一个配置文件就是一个redis数据库服务器。
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

4.Redis 属于单线程还是多线程?

Redis 在 4.0 之前一直采用单线程的模式,原因如下:

  • 使用单线程模型是 Redis 的开发和维护更简单,因为单线程模型方便开发和调试;
  • 即使使用单线程模型也并发的处理多客户端的请求,主要使用的是多路复用;
  • Redis 是基于内存操作的,主要的性能瓶颈是内存或者网络带宽而并非 CPU,自然采用简单的单线程。

Redis 4.0 中的多线程:惰性删除(异步删除)

我们使用 del 指令可以很快的删除数据,但是当被删除的 key 是一个非常大的对象数据时,那么 del 指令就会造成 Redis 主线程卡顿,因此使用惰性删除可以有效的避免 Redis 卡顿的问题。Redis 在 4.0 中引入了惰性删除(异步删除),意思是说我们可以使用异步的方式对 Redis 中的数据进行删除操作了,例如 unlink key / flushdb async / flushall async 等命令,他们的执行示例如下:

> unlink key # 后台删除某个 key
> OK # 执行成功
> flushall async # 清空所有数据
> OK # 执行成功
  • 1
  • 2
  • 3
  • 4

这样处理的好处是不会导致 Redis 主线程卡顿,会把这些删除操作交给后台线程来执行。

redis6.0多线程

  • Redis 6.0 中新增了多线程的功能来提高 I/O 的读写性能

  • Redis 4.0 版本中虽然引入了多线程,但此版本中的多线程只能用于大数据量的异步删除,然而对于非删除操作的意义并不是

  • Redis 6.0 默认是禁用多线程的,可以通过修改 Redis 的配置文件 开启 。官方的建议是如果为 4 核的 CPU,建议线程数设置为 2 或 3,如果为 8 核 CPU 建议线程数设置为 6,线程数一定要小于机器核数,线程数并不是越大越好。

5.Redis4.0是单线程,速度比较快的原因?

  • 基于内存操作:Redis 的所有数据都存在内存中,因此所有的运算都是内存级别的,所以他的性能比较高;
  • 数据结构简单:Redis 的数据结构比较简单,是为 Redis 专门设计的,而这些简单的数据结构的查找和操作的时间复杂度都是 O(1),因此性能比较高;
  • 多路复用和非阻塞 I/O:Redis 使用 I/O 多路复用功能来监听多个 socket 连接客户端,这样就可以使用一个线程连接来处理多个请求,减少线程切换带来的开销,同时也避免了 I/O 阻塞操作,从而大大提高了 Redis 的性能;
  • 避免上下文切换:因为是单线程模型,因此就避免了不必要的上下文切换和多线程竞争,这就省去了多线程切换带来的时间和性能上的消耗,而且单线程不会导致死锁问题的发生。
  • 官方使用基准测试的结果是,单线程的 Redis 吞吐量可以达到 10W/每秒

6.Redis的数据类型

常见的数据类型

类型特点使用场景
string字符串key=value的形式,可存数字定时持久化,常规计数
hash映射表string类型的key=value的映射表,适合存储对象存储部分变更数据,比如用户信息表
list链表有序可重复的链表消息队列
set集合无序不可重复的序列常用交并差集,比如可以求共同关注的博主
zset有序集合带score排名的无序不可重复序列排行榜

zset有序集合是由 ziplist (压缩列表) 或 skiplist (跳跃表) 组成的。

当数据比较少时,有序集合使用的是 ziplist 存储的,如下代码所示:

127.0.0.1:6379> zadd myzset 1 db 2 redis 3 mysql
(integer) 3
127.0.0.1:6379> object encoding myzset
"ziplist"
  • 1
  • 2
  • 3
  • 4

从结果可以看出,有序集合把 myset 键值对存储在 ziplist 结构中了。 有序集合使用 ziplist 格式存储必须满足以下两个条件:

  • 有序集合保存的元素个数要小于 128 个;
  • 有序集合保存的所有元素成员的长度都必须小于 64 字节。

当有序集合保存的所有元素成员的长度大于 64 字节时,有序集合就会从 ziplist 转换成为 skiplist。

高级数据类型

类型使用场景
GEO(地理位置类型)Redis 3.2 版本中新增的数据类型,用于存储和查询地理位置的,使用它我们可以实现查询附近的人或查询附近的商家等功能
Stream(流类型)是 Redis 5.0 版本中新增的数据类型,因为使用 Stream 可以实现消息消费确认的功能,使用“xack key group-key ID”命令,所以此类型的出现给 Redis 更好的实现消息队列提供了很大的帮助。
HyperLogLog(统计类型)Redis 2.8.9 版本添加的数据结构,它用于高性能的基数 (去重) 统计功能,它的缺点就是存在极低的误差率。

7.Redis实现限流

问题: 假设我们的系统只能为 10 万人同时提供购物服务,但是某一天因为老罗带货突然就涌进了 100 万用户,那么导致的直接后果就是服务器瘫痪,谁也甭想买东西了,所以这个时候我们需要“限流”的功能保证先让一部分用户享受购物的服务,而其他用户进行排队等待购物,这样就可以让整个系统正常的运转了。

  • 滑动时间限流算法: 我们可以使用 Redis 中的 ZSet加上滑动时间算法来实现简单的限流。所谓的滑动时间算法指的是以当前时间为截止时间,往前取一定的时间,比如往前取 60s 的时间,在这 60s 之内运行最大的访问数为 100,此时算法的执行逻辑为,先清除 60s 之前的所有请求记录,再计算当前集合内请求数量是否大于设定的最大请求数 100,如果大于则执行限流拒绝策略,否则插入本次请求记录并返回可以正常执行的标识给客户端。
  • 漏桶算法:滑动时间算法有一个问题就是在一定范围内,比如 60s 内只能有 10 个请求,当第一秒时就到达了 10 个请求,那么剩下的 59s 只能把所有的请求都给拒绝掉,而漏桶算法可以解决这个问题。漏桶算法类似于生活中的漏斗,无论上面的水流倒入漏斗有多大,也就是无论请求有多少,它都是以均匀的速度慢慢流出的。当上面的水流速度大于下面的流出速度时,漏斗会慢慢变满,当漏斗满了之后就会丢弃新来的请求;当上面的水流速度小于下面流出的速度的话,漏斗永远不会被装满,并且可以一直流出。漏桶算法的实现步骤是,先声明一个队列用来保存请求,这个队列相当于漏斗,当队列容量满了之后就放弃新来的请求,然后重新声明一个线程定期从任务队列中获取一个或多个任务进行执行,这样就实现了漏桶算法。
  • **令牌算法:**在令牌桶算法中有一个程序以某种恒定的速度生成令牌,并存入令牌桶中,而每个请求需要先获取令牌才能执行,如果没有获取到令牌的请求可以选择等待或者放弃执行

滑动时间算法缺点是占用内存空间大,并且是非原子操作;令牌算法为程序级别的单机限流实现方案

8.Redis 内存用完会怎样?

Redis 的内存用完指的是 Redis 的运行内存超过了 Redis 设置的最大内存,此值可以通过 Redis 的配置文件 redis.conf 进行设置,设置项为 maxmemory。

127.0.0.1:6379> config get maxmemory
1) "maxmemory"
2) "0"
  • 1
  • 2
  • 3

当此值为 0 时,表示没有内存大小限制,直到耗尽机器中所有的内存为止,这是 Redis 服务器端在 64 位操作系统下的默认值。(32 位操作系统,默认最大内存值为 3GB。)

Redis 内存淘汰策略可以使用 config get maxmemory-policy 命令来查看(4.0 版本之后一共有 8 种)

  1. noeviction:不淘汰任何数据,当内存不足时,新增操作会报错,Redis 默认内存淘汰策略;
  2. allkeys-lru:淘汰整个键值中最久未使用的键值;
  3. allkeys-random:随机淘汰任意键值;
  4. volatile-lru:淘汰所有设置了过期时间的键值中最久未使用的键值;
  5. volatile-random:随机淘汰设置了过期时间的任意键值;
  6. volatile-ttl:优先淘汰更早过期的键值;
  7. volatile-lfu:淘汰所有设置了过期时间的键值中,最少使用的键值;
  8. allkeys-lfu:淘汰整个键值中最少使用的键值。

9.Redis 如何处理已经过期的数据?

常见的过期策略,有以下三种:

  • 定时删除

    • 原理:在设置键值过期时间时,创建一个定时事件,当过期时间到达时,由事件处理器自动执行键的删除操作。
    • 优点:保证内存可以被尽快的释放
    • 缺点:在 Redis 高负载的情况下或有大量过期键需要同时处理时,会造成 Redis 服务器卡顿,影响主业务执行。
  • 惰性删除

    • 原理:不主动删除过期键,每次从数据库获取键值时判断是否过期,如果过期则删除键值,并返回 null
    • 优点:因为每次访问时,才会判断过期键,所以此策略只会使用很少的系统资源。
    • 缺点:系统占用空间删除不及时,导致空间利用率降低,造成了一定的空间浪费。
  • 定期删除

    • 原理:每隔一段时间检查一次数据库,随机删除一些过期键。 Redis 默认每秒进行 10 次过期扫描,此配置可通过 Redis 的配置文件 redis.conf 进行配置,配置键为 hz 它的默认值是 hz 10 。 需要注意的是:Redis 每次扫描并不是遍历过期字典中的所有键,而是采用随机抽取判断并删除过期键的形式执行的。

    • 流程:

      - 从过期字典中随机取出 20 个键;
      - 删除这 20 个键中过期的键;
      - 如果过期 key 的比例超过 25% ,重复步骤 1。
      
      同时为了保证过期扫描不会出现循环过度,导致线程卡死现象,算法还增加了扫描时间的上限,默认不会超过 25ms。
      
      • 1
      • 2
      • 3
      • 4
      • 5
    • 优点:通过限制删除操作的时长和频率,来减少删除操作对 Redis 主业务的影响,同时也能删除一部分过期的数据减少了过期键对空间的无效占用。

    • 缺点:内存清理方面没有定时删除效果好,同时没有惰性删除使用的系统资源少。

10.分布式锁

7.1 Redis 实现分布式锁可以通过以下两种手段来实现:

  • incr 方式:例如设置了incr值后,每次取这个值,有数量则为正在使用中,其他的进行等待。即只要使用锁中值就不为0;
  • setnx 方式:同理setnx只要有锁,那么就设置一个定时值,其他时候值为null

7.2 分布式死锁

在系统中当一个程序在创建了分布式锁之后,因为某些特殊的原因导致程序意外退出了,那么这个锁将永远不会被释放,就造成了死锁的问题

  • 因此为了解决死锁问题,我们最简单的方式就是设置锁的过期时间,这样即使出现了程序意外退出的情况,那么等待此锁超过了设置的过期时间之后就会释放此锁,这样其他程序就可以继续使用了。
  • 另外也可以在每次创建锁的时候先查询,再创建,通过lua脚本保证原子性操作

7.3 锁被误删

假设锁的最大超时时间是 30s,应用 1 执行了 35s,然而应用 2 在 30s,锁被自动释放之后,用重新获取并设置了锁,然后在 35s 时,应用 1 执行完之后,就会把应用 2 创建的锁给删除掉。

锁被误删的解决方案是在使用 set 命令创建锁时,给 value 值设置一个归属人标识,例如给应用关联一个 UUID,每次在删除之前先要判断 UUID 是不是属于当前的线程,如果属于在删除,这样就避免了锁被误删的问题。 注意:如果是在代码中执行删除,不能使用先判断再删除的方法,伪代码如下:

if(xxx.equals(xxx)){ // 判断是否是自己的锁
    del(luck); // 删除锁
}
  • 1
  • 2
  • 3

因为判断代码和删除代码不具备原子性,因此也不能这样使用,这个时候可以使用 Lua 脚本来执行判断和删除的操作,因为多条 Lua 命令可以保证原子性

11.如何在海量数据中查询一个值是否存在?

统计一个值是否在海量数据中可以使用布隆过滤器,布隆过滤器(Bloom Filter)是 1970 年由布隆提出的。它实际上是一个很长的二进制向量和一系列随机映射函数。布隆过滤器可以用于检索一个元素是否在一个集合中。它的优点是空间效率和查询时间都比一般的算法要好的多,缺点是有一定的误识别率和删除困难。也就是说布隆过滤器的优点就是计算和查询速度很快,但是缺点也很明显就是存在一定的误差。

在 Redis 中布隆过滤器的用法如下:

  1. bf.add 添加元素;
  2. bf.exists 判断某个元素是否存在;
  3. bf.madd 添加多个元素;
  4. bf.mexists 判断多个元素是否存在;
  5. bf.reserve 设置布隆过滤器的准确率

使用示例如下:

127.0.0.1:6379> bf.add user xiaoming
(integer) 1
127.0.0.1:6379> bf.add user xiaohong
(integer) 1
127.0.0.1:6379> bf.add user laowang
(integer) 1
127.0.0.1:6379> bf.exists user laowang
(integer) 1
127.0.0.1:6379> bf.exists user lao
(integer) 0
127.0.0.1:6379> bf.madd user huahua feifei
1) (integer) 1
2) (integer) 1
127.0.0.1:6379> bf.mexists user feifei laomiao
1) (integer) 1
2) (integer) 0
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

可以看出以上结果没有任何误差,我们再来看一下准确率 bf.reserve 的使用:

127.0.0.1:6379> bf.reserve user 0.01 200
(error) ERR item exists #已经存的 key 设置会报错
127.0.0.1:6379> bf.reserve userlist 0.9 10
OK
  • 1
  • 2
  • 3
  • 4

可以看出此命令必须在元素刚开始执行,否则会报错,它有三个参数:key、errorrate 和 initialsize。 其中:

  • error_rate:允许布隆过滤器的错误率,这个值越低过滤器占用空间也就越大,以为此值决定了位数组的大小,位数组是用来存储结果的,它的空间占用的越大 (存储的信息越多),错误率就越低,它的默认值是 0.01;
  • initial_size:布隆过滤器存储的元素大小,实际存储的值大于此值,准确率就会降低,它的默认值是 100。

布隆过滤器常见使用场景有:

  • 垃圾邮件过滤;
  • 爬虫里的 URL 去重;
  • 判断一个值在亿级数据中是否存在。

12.常用的 Redis 优化手段有哪些?

  • 缩短键值对的存储长度和不使用耗时长的 Redis 命令。redis 绝大多数读写命令的时间复杂度都在 O(1) 到 O(N) 之间 ,其中 O(1) 表示可以安全使用的,而 O(N) 就应该当心了,N 表示不确定,数据越大查询的速度可能会越慢。因为 Redis 只用一个线程来做数据查询,如果这些指令耗时很长,就会阻塞 Redis,造成大量延时。要避免 O(N) 命令对 Redis 造成的影响,可以从以下几个方面入手改造:
    • 决定禁止使用 keys 命令;
    • 避免一次查询所有的成员,要使用 scan 命令进行分批的,游标式的遍历;
    • 通过机制严格控制 Hash、Set、Sorted Set 等结构的数据大小;
    • 将排序、并集、交集等操作放在客户端执行,以减少 Redis 服务器运行压力;
    • 删除 (del) 一个大数据的时候,可能会需要很长时间,所以建议用异步删除的方式 unlink,它会启动一个新的线程来删除目标数据,而不阻塞 Redis 的主线程。
  • 批量操作命令Redis 管道技术——Pipeline
  • 使用 lazy free(延迟删除)特性;
  • 设置键值的过期时间;
  • 避免大量数据同时失效;
  • 客户端使用优化;
  • 限制 Redis 内存大小;
  • 使用物理机而非虚拟机安装 Redis 服务;
  • 最有效的提高 Redis 性能的方案就是在没有必要开启持久化的情况下,关闭 Redis 的持久化功能,这样每次对 Redis 的操作就无需进行 IO 磁盘写入了,因此性能会提升很多。
  • 使用分布式架构来增加读写速度。

13.Redis 持久化

Redis 持久化总共有以下三种方式:

  • 快照方式(RDB, Redis DataBase)将某一个时刻的内存数据,以二进制的方式写入磁盘;
  • 文件追加方式(AOF, Append Only File),记录所有的操作命令,并以文本的形式追加到文件中;
  • 混合持久化方式,Redis 4.0 之后新增的方式,混合持久化是结合了 RDB 和 AOF 的优点,在写入的时候,先把当前的数据以 RDB 的形式写入文件的开头,再将后续的操作命令以 AOF 的格式存入文件,这样既能保证 Redis 重启时的速度,又能减低数据丢失的风险。

1.RDB 持久化

RDB(Redis DataBase)是将某一个时刻的内存快照(Snapshot),以二进制的方式写入磁盘的过程。

RDB 优点:

  • RDB 的内容为二进制的数据,占用内存更小,更紧凑,更适合做为备份文件;
  • RDB 对灾难恢复非常有用,它是一个紧凑的文件,可以更快的传输到远程服务器进行 Redis 服务恢复;
  • RDB 可以更大程度的提高 Redis 的运行速度,因为每次持久化时 Redis 主进程都会 fork() 一个子进程,进行数据持久化到磁盘,Redis 主进程并不会执行磁盘 I/O 等操作;
  • 与 AOF 格式的文件相比,RDB 文件可以更快的重启。

RDB 缺点:

  • 因为 RDB 只能保存某个时间间隔的数据,如果中途 Redis 服务被意外终止了,则会丢失一段时间内的 Redis 数据;
  • RDB 需要经常 fork() 才能使用子进程将其持久化在磁盘上。如果数据集很大,fork() 可能很耗时,并且如果数据集很大且 CPU 性能不佳,则可能导致 Redis 停止为客户端服务几毫秒甚至一秒钟。

2.AOF 持久化

AOF(Append Only File)中文是附加到文件,顾名思义 AOF 可以把 Redis 每个键值对操作都记录到文件(appendonly.aof)中。Redis 默认是关闭 AOF 持久化的。

AOF 优点:

  • AOF 持久化保存的数据更加完整,AOF 提供了三种保存策略:每次操作保存、每秒钟保存一次、跟随系统的持久化策略保存,其中每秒保存一次,从数据的安全性和性能两方面考虑是一个不错的选择,也是 AOF 默认的策略,即使发生了意外情况,最多只会丢失 1s 钟的数据;
  • AOF 采用的是命令追加的写入方式,所以不会出现文件损坏的问题,即使由于某些意外原因,导致了最后操作的持久化数据写入了一半,也可以通过 redis-check-aof 工具轻松的修复;
  • AOF 持久化文件,非常容易理解和解析,它是把所有 Redis 键值操作命令,以文件的方式存入了磁盘。即使不小心使用 flushall 命令删除了所有键值信息,只要使用 AOF 文件,删除最后的 flushall 命令,重启 Redis 即可恢复之前误删的数据。

AOF 缺点:

  • 对于相同的数据集来说,AOF 文件要大于 RDB 文件;
  • 在 Redis 负载比较高的情况下,RDB 比 AOF 性能更好;
  • RDB 使用快照的形式来持久化整个 Redis 数据,而 AOF 只是将每次执行的命令追加到 AOF 文件中,因此从理论上说,RDB 比 AOF 更健壮。

3.混合持久化

  • RDB 和 AOF 持久化各有利弊,RDB 可能会导致一定时间内的数据丢失,而 AOF 由于文件较大则会影响 Redis 的启动速度,为了能同时使用 RDB 和 AOF 各种的优点,Redis 4.0 之后新增了混合持久化的方式。
  • 在开启混合持久化的情况下,AOF 重写时会把 Redis 的持久化数据,以 RDB 的格式写入到 AOF 文件的开头,之后的数据再以 AOF 的格式化追加的文件的末尾。

混合持久化优点:

  • 混合持久化结合了 RDB 和 AOF 持久化的优点,开头为 RDB 的格式,使得 Redis 可以更快的启动,同时结合 AOF 的优点,有减低了大量数据丢失的风险。

混合持久化缺点:

  • AOF 文件中添加了 RDB 格式的内容,使得 AOF 文件的可读性变得很差;
  • 兼容性差,如果开启混合持久化,那么此混合持久化 AOF 文件,就不能用在 Redis 4.0 之前版本了。

14.缓存雪崩

缓存雪崩是指在短时间内,有大量缓存同时过期,导致大量的请求直接查询数据库,从而对数据库造成了巨大的压力,严重情况下可能会导致数据库宕机的情况叫做缓存雪崩。

1.解决方案1.加锁排队

加锁排队可以起到缓冲的作用,防止大量的请求同时操作数据库,但它的缺点是增加了系统的响应时间,降低了系统的吞吐量,牺牲了一部分用户体验。

解决方案2.随机化过期时间

为了避免缓存同时过期,可在设置缓存时添加随机时间,这样就可以极大的避免大量的缓存同时失效。 示例代码如下:

解决方案3.设置二级缓存

二级缓存指的是除了 Redis 本身的缓存,再设置一层缓存,当 Redis 失效之后,先去查询二级缓存。 例如可以设置一个本地缓存,在 Redis 缓存失效的时候先去查询本地缓存而非查询数据库。 加入二级缓存之后程序执行流程,如下图所示:

15.缓存穿透

缓存穿透是指查询数据库和缓存都无数据,因为数据库查询无数据,出于容错考虑,不会将结果保存到缓存中,因此每次请求都会去查询数据库,这种情况就叫做缓存穿透。

解决方案1.使用过滤器

我们可以使用过滤器来减少对数据库的请求,例如使用我们前面章节所学的布隆过滤器,我们这里简单复习一下布隆过滤器,它的原理是将数据库的数据哈希到 bitmap 中,每次查询之前,先使用布隆过滤器过滤掉一定不存在的无效请求,从而避免了无效请求给数据库带来的查询压力。

解决方案2.缓存空结果

另一种方式是我们可以把每次从数据库查询的数据都保存到缓存中,为了提高前台用户的使用体验 (解决长时间内查询不到任何信息的情况),我们可以将空结果的缓存时间设置的短一些,例如 3-5 分钟。

16.缓存击穿

缓存击穿指的是某个热点缓存,在某一时刻恰好失效了,然后此时刚好有大量的并发请求,此时这些请求将会给数据库造成巨大的压力,这种情况就叫做缓存击穿。

解决方案1.加锁排队

此处理方式和缓存雪崩加锁排队的方法类似,都是在查询数据库时加锁排队,缓冲操作请求以此来减少服务器的运行压力。

解决方案2.设置永不过期

对于某些热点缓存,我们可以设置永不过期,这样就能保证缓存的稳定性,但需要注意在数据更改之后,要及时更新此热点缓存,不然就会造成查询结果的误差。

17.缓存预热

首先来说,缓存预热并不是一个问题,而是使用缓存时的一个优化方案,它可以提高前台用户的使用体验。 缓存预热指的是在系统启动的时候,先把查询结果预存到缓存中,以便用户后面查询时可以直接从缓存中读取,以节约用户的等待时间。

缓存预热的实现思路有以下三种:

  1. 把需要缓存的方法写在系统初始化的方法中,这样系统在启动的时候就会自动的加载数据并缓存数据;
  2. 把需要缓存的方法挂载到某个页面或后端接口上,手动触发缓存预热;
  3. 设置定时任务,定时自动进行缓存预热。
声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/小蓝xlanll/article/detail/484322
推荐阅读
相关标签
  

闽ICP备14008679号