3万字聊聊什么是Redis(五)

共 9923字,需浏览 20分钟

 ·

2021-12-12 16:12

大家好,我是Leo

继上篇Redis技术总结四,我们继续聊聊Redis的相关技术!

上一篇我们介绍了

  1. 外在原因,内在原因对Redis的影响,外内在原因的调优方案。
  2. Redis的变慢之后的排查思路,变慢的解决方案。
  3. 面试高频:内存占用率怎么那么高的一系列分析

这篇主要是介绍一下 Redis 缓冲区,

推荐阅读

3万字聊聊什么是MySQL

3万字聊聊什么是Redis(一)

3万字聊聊什么是Redis(二)

3万字聊聊什么是Redis(三)

3万字聊聊什么是Redis(四)

缓冲区

缓冲区主要就是用一块内存空间来暂时存放命令数据,以免出现数据或者命令处理过慢导致数据丢失和性能问题。缓冲区也不是无限大的,它有一个上限的阈值。一旦超过这个阈值就会出现缓冲区溢出的情况。

Redis是client-server架构,所有的操作命令都需要通过客户端发送给服务器端。所以缓冲区在Redis中的一个主要应用场景就是在客户端和服务器之间进行通信,用来暂存客户端发送的命令数据以及服务端返回的结果数据。

下面我们来聊聊如何使用Redis缓冲区吧。

客户端与服务端之间的缓冲区

为了避免客户端和服务端的请求处理速度不匹配的问题,服务端会给每个连接的用户都设置一个输入缓冲区和输出缓冲区。

  1. 输入缓冲区会先把客户端发送过来的命令暂存起来
  2. Redis主线程读取客户端的输入缓冲区,拿到用户的请求命令去执行
  3. 拿到数据之后会把结果写到客户端的输出缓冲区
  4. 再通过输出缓冲区返回给客户端

可以通过下图加以理解一下!

输入缓冲区溢出

输入缓冲区的溢出主要有下列情况。

  1. 客户端给服务端发送了一个bigkey,等待写入!
  2. 服务器端处理请求的速度过慢,例如,Redis 主线程出现了间歇性阻塞,无法及时处理正常发送的请求,导致客户端发送的请求在缓冲区越积越多。

我们可以通过使用 CLIENT LIST 命令查看输入缓冲区的内存使用情况

CLIENT LIST
id=5
addr=127.0.0.1:50487
fd=9
name=
age=4
idle=0
flags=N
db=0
sub=0
psub=0
multi=-1
qbuf=26
qbuf-free=32742
obl=0
oll=0
omem=0
events=r
cmd=client

上列信息中,我们无需全部关注。只需要关注

  1. 与服务器端连接的客户端的信息。如果有多个客户端输出结果中的addr会显示不同的客户端的IP和端口号。
  2. 与输入缓冲区相关的三个参数

cmd,表示客户端最新执行的命令。这个例子中执行的是 CLIENT 命令。

qbuf,表示输入缓冲区已经使用的大小。这个例子中的 CLIENT 命令已使用了 26 字节大小的缓冲区。

qbuf-free,表示输入缓冲区尚未使用的大小。这个例子中的 CLIENT 命令还可以使用 32742 字节的缓冲区。qbuf 和 qbuf-free 的总和就是,Redis 服务器端当前为已连接的这个客户端分配的缓冲区总大小。这个例子中总共分配了 26 + 32742 = 32768 字节,也就是 32KB 的缓冲区。

拿到上述数据之后我们就可以分析了

  • 如果qbuf很大而qbuf-free很小。就要注意了因为输入缓冲区已经占用了很多内存,而且没有多余的空闲空间了。如果客户端再写入命令有可能就要溢出了,最终导致Redis把客户端连接关闭,无法正常提供服务了。

通常情况下,Redis服务端不止服务一个客户端,所以会存在多个客户端的输入缓冲区。当每个输入缓冲区占用的内存总量超过了Redis的 maxmemory 配置项时,会有两种结果。

  1. 触发Redis进行数据淘汰。触发数据淘汰虽然也可以提供服务但是再访问这部分数据时,就会去后端数据库读取,这就极大的降低了业务的访问性能。
  2. 内存溢出(out-of-memory)问题,进而会引起Redis崩溃,给业务应用造成严重影响。

避免输入缓冲区溢出

  • 缓冲区调大
  • 从数据命令的发送和处理速度入手

通过源码我们可以得知,客户端的输入缓冲区无法调大,因为Redis在代码中就设定了1GB。也就是说Redis只允许客户端的输入缓冲区最大为1GB。

既然第一种方案实现不了,我们可以看看第二种方案。第二种方案就是避免 bigkey,和Redis主线程阻塞的问题了。

输出缓冲区溢出

Redis的输出缓冲区主要存放的是服务端查询数据后的数据。

输出缓冲区主要包括两部分

  • 大小为16KB的固定缓存空间,用来暂存OK响应和出错信息(例如,执行SET命令)
  • 可以动态增加的缓冲空间,用来暂存大小可变的响应结果(客户要的数据)

什么时候会触发输出缓冲区溢出呢?

  • 服务器端返回 bigkey 的大量结果;
  • 执行了 MONITOR 命令;
  • 缓冲区大小设置得不合理。

第一种没啥好说的,从第二种说起吧。

执行MONITOR: 它是用来监测Redis执行的,执行这个命令之后就会持续输出监测到的各个命令操作。测试环境可以用用,生产环境慎用!

菜鸟教程:Redis Monitor 命令用于 实时 打印出 Redis 服务器接收到的命令,调试用。

缓冲区大小设置: 和输入缓冲区不同,输出缓冲区可以通过 client-output-buffer-limit 来设置缓冲区的大小

设置缓冲区大小的上限阈值

设置缓冲区持续写入数据的数量上限阈值和持续写入数据的时间的上限阈值

通过 client-output-buffer-limit 设置缓冲区大小,我们需要先区分下客户端的类型。主要有两类客户端,常规的是客户端与服务端的交互服务,另一种就是订阅Redis频道的订阅客户端。

常规客户端

我们可以这样配置 client-output-buffer-limit normal 0 0 0

normal 表示当前设置的是普通的客户端

pubsub 表示当前设置的是订阅频道客户端

第二个参数 0设置的是缓冲区大小限制

第三个参数和第四个参数分别表示缓冲区持续写入量限制和持续写入时间限制。

  • 对于普通客户端来说,每发送一个请求会等待结果返回之后再发送下一个。这种我们称为阻塞式发送,只要不是遇到bigkey一般不会出现阻塞情况。
  • 对于订阅客户端来说,一旦订阅了有消息了,服务端都会通过缓冲区输出给客户端。所以不是阻塞式发送。一旦服务端发送的频率与速度过大就会占用过高的空间。

所以我们一般都会设置订阅客户端的缓冲区大小,缓冲区持续写入量限制以及持续写入时间限制。

主从集群间缓冲区

主从集群间的数据复制包括

  • 全量复制:全量复制是同步所有数据
  • 增量复制:把主从库网络断连期间主库收到的命令,同步给从库

为了保证主从节点的数据一致,都会用到缓冲区。但是,这两种复制场景下的缓冲区,在溢出影响和大小设置方面并不一样。

全量复制

在全量复制过程中,主节点在向从节点传输 RDB 文件的同时,会继续接收客户端发送的写命令请求。这些写命令就会先保存在复制缓冲区中,等 RDB 文件传输完成后,再发送给从节点去执行。主节点上会为每个从节点都维护一个复制缓冲区,来保证主从节点间的数据同步。

所以,如果在全量复制时,从节点接收和加载 RDB 较慢,同时主节点接收到了大量的写命令,写命令在复制缓冲区中就会越积越多,最终导致溢出。

为了解决溢出问题我们可以从两方面入手

  • 控制主节点的数据量,控制在2-4GB,这样全量同步的时候就会执行更快些,避免复制缓冲区累计过多命令
  • 我们可以使用 client-output-buffer-limit 配置项,来设置合理的复制缓冲区大小。设置的依据,就是主节点的数据量大小、主节点的写负载压力和主节点本身的内存大小。

我们可以这样配置config set client-output-buffer-limit slave 512mb 128mb 60

slave 参数表明该配置项是针对复制缓冲区的

第二个参数代表将缓冲区大小的上限设置为 512MB

第三个参数和第四个参数代表的是如果连续 60 秒内的写入量超过 128MB 的话,也会触发缓冲区溢出。

复制缓冲区,我们还会遇到一个问题。主节点上复制缓冲区的内存开销,会是每个从节点客户端输出缓冲区占用内存的总和。如果集群中的从节点数非常多的话,主节点的内存开销就会非常大。所以,我们还必须得控制和主节点连接的从节点个数,不要使用大规模的主从集群。

增量复制

增量复制时使用的缓冲区也称为复制积压缓冲区。

主节点在把接收到的写命令同步给从节点时,同时会把这些写命令写入复制积压缓冲区。一旦从节点发生网络闪断,再次和主节点恢复连接后,从节点就会从复制积压缓冲区中,读取断连期间主节点接收到的写命令,进而进行增量同步

首先,复制积压缓冲区是一个大小有限的环形缓冲区。当主节点把复制积压缓冲区写满后,会覆盖缓冲区中的旧命令数据。如果从节点还没有同步这些旧命令数据,就会造成主从节点间重新开始执行全量复制。

其次,为了应对复制积压缓冲区的溢出问题,我们可以调整复制积压缓冲区的大小,也就是设置 repl_backlog_size 这个参数的值。具体的调整依据,你可以再看下 3万字聊聊什么是Redis(二) repl_backlog_size 大小的计算依据。

Redis缓存如何工作

在学习MySQL时也是一个思路。掌握一门技术的工作原理才能在一定程度上提升对他的认知。也只有掌握工作原理才能真正在一些棘手的地方游刃有余。

众所周知,Redis提供了高性能的数据存取功能,所以广泛应用于缓存场景中。这里列举了比较常见的实战场景

  • Redis缓存是如何工作的
  • Redis缓存满了该怎么办
  • 缓存一致性,缓存穿透,缓存雪崩,缓存击穿该如何应对
  • Redis缓存可以使用快速固态硬盘吗

要想弄明白Redis为什么适合缓存,我们就要先了解清楚缓存都有什么特征

  1. 在一个层次化的系统中,缓存一定是一个快速的子系统,数据存在缓存中时,能避免每次从慢速子系统中存取数据。
  2. 缓存系统的容量大小总是小于后端慢速系统的,我们不可能把所有数据都放在缓存系统中。

快速子系统:缓存,内存

慢速子系统:磁盘

我们在使用Redis时,通常会有两种结果,

  1. 要不命中缓存,直接把缓存返回给客户端
  2. 要不没命中缓存,请求打到后端,后端查询数据库返回,把数据写入缓存。(这个过程也称为缓存更新)

不管是什么结果,Redis是如何实现的呢? 这也是这个技术点主要介绍的东西

Redis是一个独立的系统软件,和我们的业务应用程序是两个软件。它是一个独立的缓存中间件。我们也可以说它是旁路缓存。读取缓存、读取数据库和更新缓存的操作都需要在应用程序中来完成。

Redis缓存有两种类型:只读缓存读写缓存

只读缓存

当 Redis 用作只读缓存时,应用要读取数据的话,会先调用 Redis GET 接口,查询数据是否存在。而所有的数据写请求,会直接发往后端的数据库,在数据库中增删改。对于删改的数据来说,如果 Redis 已经缓存了相应的数据,应用需要把这些缓存的数据删除,Redis 中就没有这些数据了。

如果再有请求打过来,那么就需要通过后端查询数据,写入缓存。

  1. 从请求过来缓存未命中
  2. 后端查询数据写入缓存 (与磁盘交互!)
  3. 返回给客户端

这1,2,3步操作。对比如果缓存命令的话直接就可以返回给客户端了来说,还是后者有加速访问的效果。

只读缓存直接在数据库中更新数据的好处是,所有最新的数据都在数据库中,而数据库是提供数据可靠性保障的,这些数据不会有丢失的风险。当我们需要缓存图片、短视频这些用户只读的数据时,就可以使用只读缓存这个类型了。

读写缓存

知道了只读缓存,读写缓存也就容易理解了。

对于读写缓存来说,除了读请求会发送到缓存进行处理(直接在缓存中查询数据是否存在),所有的写请求也会发送到缓存,在缓存中直接对数据进行增删改操作。此时,得益于 Redis 的高性能访问特性,数据的增删改操作可以在缓存中快速完成,处理结果也会快速返回给业务应用,这就可以提升业务应用的响应速度。

但是,和只读缓存不一样的是,在使用读写缓存时,最新的数据是在 Redis 中,而 Redis 是内存数据库,一旦出现掉电或宕机,内存中的数据就会丢失。这也就是说,应用的最新数据可能会丢失,给应用业务带来风险。

所以,根据业务应用对数据可靠性和缓存性能的不同要求,我们会有 同步直写 和 异步写回 两种策略。其中,

  • 同步直写策略优先保证数据可靠性
  • 异步写回策略优先提供快速响应。

学习了解这两种策略,可以帮助我们根据业务需求,做出正确的设计选择。

同步直写

同步直写是指,写请求发给缓存的同时,也会发给后端数据库进行处理,等到缓存和数据库都写完数据,才给客户端返回。这样,即使缓存宕机或发生故障,最新的数据仍然保存在数据库中,这就提供了数据可靠性保证。

不过,同步直写会降低缓存的访问性能。这是因为缓存中处理写请求的速度是很快的,而数据库处理写请求的速度较慢。即使缓存很快地处理了写请求,也需要等待数据库处理完所有的写请求,才能给应用返回结果,这就增加了缓存的响应延迟。

异步写回

异步写回策略,则是优先考虑了响应延迟。此时,所有写请求都先在缓存中处理。等到这些增改的数据要被从缓存中淘汰出来时,缓存将它们写回后端数据库。这样一来,处理这些数据的操作是在缓存中进行的,很快就能完成。只不过,如果发生了掉电,而它们还没有被写回数据库,就会有丢失的风险了。

策略抉择

根据特定的业务场景选择相应的策略。

  • 如果需要对写请求进行加速,我们选择读写缓存;
  • 如果写请求很少,或者是只需要提升读请求的响应速度的话,我们选择只读缓存。

缓存淘汰策略

七种策略

为了保证硬件较高的性价比,我们在使用Redis当作我们缓存系统时,往往会考虑 "八二原理",

八二原理:80%的请求实际只访问了20%的数据

所以我们一般会考虑把热卖访问的数据写入缓存中,减少后端访问数据库,从而减少磁盘IO带来的拧影响。

缓存也是有一定限度的,随着缓存数据量越来越大,有限的缓存空间不可避免地会被写满。解决这个问题就涉及到缓存系统的一个重要机制:缓存数据的淘汰机制

缓存淘汰机制运行流程主要分两步

  • 根据一定的策略,筛选出对应于访问来说不重要的数据
  • 将这些数据从缓存中删除,为新来的数据腾出空间

缓存淘汰机制可以分成两类

  • 不进行数据淘汰策略,只有noeviction这一种
  • 会进行淘汰的7种其他策略

我们可以从会进行淘汰的7种策略再细分成两位

  • 一种是在过期时间的数据进行淘汰
  • 在所有数据范围内进行淘汰
img

noeviction策略

Redis默认情况下,使用的内存空间超过了maxmemory 值时,并不会淘汰数据,也就是设定了 noeviction 策略。

一旦缓存被写满了,再有写请求时,Redis将不再提供服务,而是直接返回错误。Redis用于缓存时,实际的数据集通常都是大于缓存容量的,总会有新的数据要写入缓存,这个策略本身不淘汰数据,也就不会腾出新的缓存空间,我们不把它用在 Redis 缓存中。

volatile-ttl策略

它筛选的时候会被限制在已经设置了过期时间的键值对上。所以即使缓存没有写满,这些数据如果过期了,也会被删除。根据过期时间的先后进行淘汰数据。越早过期的数据越先被删除。

volatile-random策略

它筛选的时候会被限制在已经设置了过期时间的键值对上。就像它的名字一样,进行随机删除

volatile-lru策略

它筛选的时候会被限制在已经设置了过期时间的键值对上。就像它的名字一样,它会采用LRU算法进行对数据的删除

volatile-lfu策略

它筛选的时候会被限制在已经设置了过期时间的键值对上。就像它的名字一样,它会采用LFU算法进行对数据的删除

allkeys-random 策略

从所有键值对中随机选择并删除数据;

allkeys-lru 策略

使用 LRU 算法在所有数据中进行筛选。

allkeys-lfu 策略

使用 LFU 算法在所有数据中进行筛选。

如果一个键值对被删除策略选中了,即使它的过期时间还没到,也需要被删除。当然,如果它的过期时间到了但未被策略选中,同样也会被删除。

上述大概介绍了几种规则。唯一不熟悉的地方就是LRU算法和LFU算法了。下面也会展开总结一下。

LRU算法

LRU 算法的全称是 Least Recently Used,从名字上就可以看出,这是按照最近最少使用的原则来筛选数据,最不常用的数据会被筛选出来,而最近频繁使用的数据会留在缓存中。

LRU 会把所有的数据组织成一个链表,链表的头和尾分别表示 MRU 端和 LRU 端,分别代表最近最常使用的数据和最近最不常用的数据,如下图所示

img

我们现在有数据 6、3、9、20、5。如果数据 20 和 3 被先后访问,它们都会从现有的链表位置移到 MRU 端,而链表中在它们之前的数据则相应地往后移一位。因为,LRU 算法选择删除数据时,都是从 LRU 端开始,所以把刚刚被访问的数据移到 MRU 端,就可以让它们尽可能地留在缓存中。

这个时候链表满了,再来一新数据15时,Redis会做两件事

  1. 15是最先被访问的,所以会放到MRU端,
  2. 会把LRU端的第一个数据删除,也就是5

因为LRU是链表实现的,所以需要用链表管理所有的缓存数据,这会带来额外的空间开销。而且当有大量数据被访问时,就会带来很多链表移动操作,会很耗时,进而降低Redis的性能。

LRU做了简化,在 3万字聊聊什么是Redis(二) 这篇文章中介绍的RedisObject。Redis会默认记录最近一次访问的时间戳,就保存在RedisObject中。

淘汰时,第一次会随机选出N个数据,把它作为一个候选集合。接下来Redis会比较这N个数据的lru字段,把lru字段最小的数据从缓存中淘汰出去。

N:上面我们提到了N个数据,这个参数其实不是随机的,它是由配置参数 maxmemory-samples 决定的。这个参数就是Redis选出的数据个数N

当需要再次淘汰数据时,Redis 需要挑选数据进入第一次淘汰时创建的候选集合。这儿的挑选标准是:能进入候选集合的数据的 lru 字段值必须小于候选集合中最小的 lru 值。当有新数据进入候选数据集后,如果候选数据集中的数据个数达到了 maxmemory-samples,Redis 就把候选数据集中 lru 字段值最小的数据淘汰出去。

这样一来,Redis 缓存不用为所有的数据维护一个大链表,也不用在每次数据访问时都移动链表项,提升了缓存的性能。

蒋德均老师的三点建议

优先使用 allkeys-lru 策略。这样,可以充分利用 LRU 这一经典缓存算法的优势,把最近最常访问的数据留在缓存中,提升应用的访问性能。如果你的业务数据中有明显的冷热数据区分,我建议你使用 allkeys-lru 策略。

如果业务应用中的数据访问频率相差不大,没有明显的冷热数据区分,建议使用 allkeys-random 策略,随机选择淘汰的数据就行。

如果你的业务中有置顶的需求,比如置顶新闻、置顶视频,那么,可以使用 volatile-lru 策略,同时不给这些置顶数据设置过期时间。这样一来,这些需要置顶的数据一直不会被删除,而其他数据会在过期时根据 LRU 规则进行筛选。

LFU算法

LFU算法是LRU算法的升级版。为什么这么说呢?我可以列表一个例子

~~~~~A~~~~~A~~~~~A~~~~A~~~~~A~~~~~A~~|
~~B~~B~~B~~B~~B~~B~~B~~B~~B~~B~~B~~B~|
~~~~~~~~~~C~~~~~~~~~C~~~~~~~~~C~~~~~~|
~~~~~D~~~~~~~~~~D~~~~~~~~~D~~~~~~~~~D|

按照LRU算法,它会认为D是经常被访问到的。但是我们用肉眼来观察的话,明明B才是最容易被访问的。也是访问次数最多的一个。

Redis作者曾想改进LRU算法,但发现Redis的LRU算法受制于随机采样数maxmemory_samples,在maxmemory_samples等于10的情况下已经很接近于理想的LRU算法性能,也就是说,LRU算法本身已经很难再进一步了。

于是LFU出来了!

LFU全称是 Least Frequently Used,最不经常使用策略,在一段时间内,数据被使用频次最少的,优先被淘汰。最少使用(LFU)是一种用于管理计算机内存的缓存算法。主要是记录和追踪内存块的使用次数,当缓存已满并且需要更多空间时,系统将以最低内存块使用频率清除内存.采用LFU算法的最简单方法是为每个加载到缓存的块分配一个计数器。每次引用该块时,计数器将增加一。当缓存达到容量并有一个新的内存块等待插入时,系统将搜索计数器最低的块并将其从缓存中删除

LFU的思路就是通过访问频繁情况,可以确定保留优先级。按照LFU算法上述的例子是B>A>C=D。如下图所示

LFU算法中,可以为每个key维护一个计数器。每次key被访问的时候,计数器增大。计数器越大,可以约等于访问越频繁。

LFU参数配置

  • lfu-log-factor 10
  • lfu-decay-time 1

lfu-log-factor可以调整计数器counter的增长速度,lfu-log-factor越大,counter增长的越慢。

lfu-decay-time是一个以分钟为单位的数值,可以调整counter的减少速度

LFU与LRU优缺点

LRU和LFU侧重点不同,LRU主要体现在对元素的使用时间上,而LFU主要体现在对元素的使用频次上。LFU的缺陷是:在短期的时间内,对某些缓存的访问频次很高,这些缓存会立刻晋升为热点数据,而保证不会淘汰,这样会驻留在系统内存里面。而实际上,这部分数据只是短暂的高频率访问,之后将会长期不访问,瞬时的高频访问将会造成这部分数据的引用频率加快,而一些新加入的缓存很容易被快速删除,因为它们的引用频率很低。

如何处理

了解完七种策略以及七种策略中的LRU算法和LFU算法,我们再来看一下被Redis锁定待淘汰的数据是如何处理的。

如果这个数据是干净数据,那么我们就直接删除;如果这个数据是脏数据,我们需要把它写回数据库

如何判断是脏的还是干净的呢?

干净数据和脏数据的区别就在于,和最初从后端数据库里读取时的值相比,有没有被修改过。干净数据一直没有被修改,所以后端数据库里的数据也是最新值。在替换时,它可以被直接删除。

而脏数据就是曾经被修改过的,已经和后端数据库中保存的数据不一致了。此时,如果不把脏数据写回到数据库中,这个数据的最新值就丢失了,就会影响应用的正常使用。

这么一来,缓存替换既腾出了缓存空间,用来缓存新的数据,同时,将脏数据写回数据库,也保证了最新数据不会丢失。

不过,对于 Redis 来说,它决定了被淘汰的数据后,会把它们删除。即使淘汰的数据是脏数据,Redis 也不会把它们写回数据库。所以,我们在使用 Redis 缓存时,如果数据被修改了,需要在数据修改时就将它写回数据库。否则,这个脏数据被淘汰时,会被 Redis 删除,而数据库里也没有最新的数据了。

结尾

这篇文章我们分析了Redis多种类型的缓冲区溢出的风险,措施以及调优方案(比如普通客户端,订阅客户端,主从集群)

通过Redis多种类型溢出的原因逐渐分析(比如bigkey,主库数据量过大生成的RDB快照过大,命令发送速度过大过快大于处理速度,)

主要学习了Redis缓存的特征,平时应用Redis的流程工作。我们从Redis缓存入手,把缓存一分为二。分别介绍了只读缓存和读写读写。

再从缓存类型入手介绍了同步直写策略,异步写回策略。也提出了根据实际的应用场景选择对应的Redis策略。

个人看法: Redis的精髓在于快,如果我们在配置Redis时,不把握每一个Redis性能的细节点,那我们用的Redis应该是削弱版的。

用了个削弱版的Redis,对于Redis来说就失去了它高性能的价值!

也了解了缓存的七种淘汰策略,七种淘汰策略中使用的LRU和LFU算法以及选定之后Redis后续处理判断脏缓存干净缓存的流程。

每个知识点都是自己整理浓缩表达出来的,部分有些不容易懂的地方请及时指出,我们一起共同进步!

非常欢迎大家加我个人微信有关后端方面的问题我们在群内一起讨论! 我们下期再见!

长按上方扫码二维码,加我微信,拉你进群


浏览 45
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报