Redis面试题整理(有的乱后续会分模块整理)

过期策略: 定时删除、惰性删除和定期删除。
1、Redis过期键删除策略删除策略
        使用的是惰性删除和定期删除两种策略相结合。
        定期删除:指的是 redis 默认每秒10次,就随机抽取一些设置了过期时间的 key,检查其是否过期,如果过期就删除。定时任务中删除过期键逻辑采用了自适应算法, 根据键的 过期比例、使用快慢两种速率模式回收键 ,默认采用慢模式,随时检查20个键,如果超过25%的键过期,循环回收知道不足25%或运行超时,慢模式下超时时间为25毫秒。如果超时则使用快模式,快模式下超时为1秒且2秒内只能运行1次。    
         惰性删除:获取 key 的时候,如果此时 key 已经过期,就删除,不会返回任何东西。
2、内存使用达到maxmemory上限时触发内存溢出控制策略
    1 noeviction :默认策略,不会删除任何数据,拒绝所有写入操作并返 回客户端错误信息。
    2 volatile-lru:根据LRU算法删除设置了超时属性(expire)的键,直到腾出足够空间为止。
    3 allkeys-lru:根据LRU算法删除键,不管数据有没有设置超时属性,直到腾出足够空间为止。
    4 allkeys-random:随机删除所有键,直到腾出足够空间为止。
    5 volatile-random:随机删除过期键,直到腾出足够空间为止。
    6 volatile-ttl:根据键值对象的ttl属性,删除最近将要过期数据。
3、 Redis 单线程模型
  因为 Redis 是单线程来处理命令的,所以一条命令从客户端达到服务端不会立刻被执行,所有命令都会进入一个队列中,然后逐个被执行。
4、单线程还能这么快
     1)纯内存访问
     2)非阻塞 I/O Redis 使用 epoll 作为 I/O 多路复用技术的实现,再加上Redis自身的事件处理模型将epoll中的连接、读写、关闭都转换为事件,不在网络I/O上浪费过多的时间。
     3) 简化数据结构和算法的实现, 避免了线程切换和竞态产生的消耗。   
5、I/O多路复用实现与Redis如何结合
    IO多路复用程序负责监听多个套接字,并向文件事件分派器传送那些产生了事件的套接字。
    转存失败 重新上传 取消 uploading.4e448015.gif正在上传… 重新上传 取消 uploading.4e448015.gif正在上传… 重新上传 取消 uploading.4e448015.gif转存失败 重新上传 取消
        I/O多路复用程序总会将所有产生事件的套接字都放在一个队列里,以有序、同步、每次一个套接字的方式向文件事件派发器。当上一个套接字产生的事件被处理完毕之后(该套接字为事件所关联的事件处理器执行完毕),I/O多路复用程序才会继续向文件事件分派器传送下一个套接字。
uploading.4e448015.gif转存失败 重新上传 取消 uploading.4e448015.gif正在上传… 重新上传 取消 uploading.4e448015.gif正在上传… 重新上传 取消 uploading.4e448015.gif转存失败 重新上传 取消
6、RDB持久化原理以及优缺点
        指定的时间间隔内将内存中的数据集快照写入磁盘, 实际操作过程是fork一个子进程,先将数据集写入临时文件,写入成功后,再替换之前的文件,用二进制压缩存储。
        RDB优点:
          1)、适用于备份,全量复制等场景,主从复制集群之间数据进行复制。
          2)、对于数据量大的,恢复数据的速度快于AOF持久化方式
          3)、性能比AOF更好。
         RDB缺点:
          1)、系统一旦在定时持久化之前出现宕机现象,此前没有来得及写入磁盘的数据都将丢失。
          2)、由于RDB是通过fork子进程来协助完成数据持久化工作的,因此,如果当数据集较大时,重量级操作,频繁执行成本过高。
7、AOF持久化原理以及优缺点
        以日志的形式记录服务器所处理的每一个写、删除操作,查询操作不会记录,以文本的方式记录,可以打开文件看到详细的操作记录。
         AOf优点:
           1)、该机制可以带来更高的数据安全性,即数据持久性。 Redis中提供了3中同步策略,即每秒同步、每修改同步和不同步三种策略来保证消息不丢失。
           2)、由于该机制对日志文件的写入操作采用的是append模式,因此在写入过程中即使出现宕机现象,也不会破坏日志文件中已经存在的内容。然而如果我们本次操作只是写入了一半数据就出现了系统崩溃问题,不用担心,在Redis下一次启动之前,我们可以通过redis-check-aof工具来帮助我们解决数据一致性的问题。
          3)、 如果日志过大,Redis可以自动启用rewrite机制。即Redis以append模式不断的将修改数据写入到老的磁盘文件中,同时Redis还会创建一个新的文件用于记录此期间有哪些修改命令被执行。因此在进行rewrite切换时可以更好的保证数据安全性。
          4)、AOF包含一个格式清晰、易于理解的日志文件用于记录所有的修改操作。事实上,我们也可以通过该文件完成数据的重建。
         AOF的缺点:
          1)、对于相同数量的数据集而言,AOF文件通常要大于RDB文件。RDB 在恢复大数据集时的速度比 AOF 的恢复速度要快。
          2)、根据同步策略的不同,AOF在运行效率上往往会慢于RDB。总之,每秒同步策略的效率是比较高的,同步禁用策略的效率和RDB一样高效。
          3)、当文件变大时需要fork子线程进行重写,耗费性能。
8、AOF文件重写时如何保证新来的请求消息不丢失?
      重写机制的实现原理:AOF重写不需要读取现在的AOF文件,这个功能时通过读取 目前数据库状态来实现的。
      重写条件:AOF文件的大小与 当前AOF文件大小和最后一次重写后的大小之间的比率等于或者等于指定的增长百分比。
      Redis服务器设置了一个AOF重写缓冲区,当服务器创建重写子进程的时候开始使用,当服务器执行命令的时候,会同时向AOF缓冲区和AOF重写缓冲区发送命令。当AOF重写完成之后会给服务器发送一个信号,父进程会调用一个函数,将重写缓冲区中的数据写入到新的AOF文件当中, 对新文件改名原子性的覆盖原来的文件,当处理函数结束之后就可以正常接收客户端的请求了。
9、 Redis主从复制实现过程
      Redis的复制功能分为同步和命令传播两个操作。
        同步:将从服务器的状态更新至主服务器的状态。
        命令传播:用于在主服务器的数据库状态修改之后,让从服务器也进行修改,保证状态一致。
       同步过程
            当执行 slaveof命令之后,将从服务器的状态更新至主服务器的状态。
            1)、从服务器向主服务器发送psync命令。
            2)、收到命令的主服务器执行bgsave命令,在后台生成 一个RDB文件,并使用一个 缓冲区记录从 现在开始执行的写命令(写命令代表增删改命令)
            3)、当主服务器的bgsave命令执行完毕之后,将RDB文件发送给从服务器,从服务器载入这个文件。
            4)、主服务器将缓冲区的里面的所有命令发送给从服务器,从服务器执行这些命令,最终保证主从数据库状态一致。
      命令传播:        
            在同步完成之后,每当主服务器执行客户端发送的写命令时,主服务的数据库状态就有可能被修过,这时主从数据库状态就不一致了,想要恢复到一致的状态,就需要采用命令传播,将不一致的命令也传给从服务器。 写命令的发 送过程是异步完成,也就是说主节点自身处理完写命令后直接返回给客户 端,并不等待从节点复制完成。
10、Redis 主从复制中的 同步 过程
      同步:分为全量复制和部分复制。
      全量复制:用于初次复制的情况,会将整个RDB文件发送给从服务器。
      部分复制:用于处理断线之后重复制的情况。
      部分复制
          部分复制 命令运行需要以下组成:
                1)、主从服务器各自复制偏移量。
                2)、主节点复制积压缓冲区。
                3)、服务器运行ID
          部分复制流程
                 1)、当主从复制之间网络出现中断时,如果超过 repl-timeout(默认60秒) 时间,主节点会认为从节点故障并中断复制链接。
                 2)、 主从连接中断期间主节点依然响应命令,但因复制连接中断命令无 法发送给从节点,不过主节点内部存在的 复制积压缓冲区(保存了写命令) ,依然可以保存最近一段时间的写命令数据,默认最大缓存1MB。
                 3)、 当主从节点网络恢复后,从节点会再次连上主节点, 当主从连接恢复后,由于从节点之前保存了自身已复制的偏移量和主节点的运行ID。
                4)、 主节点接到 psync 命令后首先核对参数 runId 是否与自身一致,如果一致,说明之前复制的是当前主节点;之后根据参数offset在自身复制积压缓冲区查找,如果偏移量之后的数据存在缓冲区中,则对从节点发送+CONTINUE响应,表示可以进行部分复制。
                5)、 主节点根据偏移量把复制积压缓冲区里的数据发送给从节点,保证主从复制进入正常状态。
11、Redis复制中主从之间的心跳
    1)主节点默认每隔 10 秒对从节点发送 ping 命令,判断从节点的存活性 和连接状态。可通过参数repl-ping-slave-period控制发送频率。
    2)从节点在主线程中每隔 1 秒发送 replconf ack{offset} 命令,给主节点上报自身当前的复制偏移量。
         replconf 命令主要作用如下
             1、检测主从服务器的网络连接状态。如果主服务器超过real-timeout没有收到心跳则认为网络出现问题。
             2、辅助实现min- slaves配置选项。min-slaves-to-write=3和 min-slaves-max-lag=10。 那么在从服务器的数量少于3个,或者 三个从服务器的延迟(lag)值都大于或等于10秒时,主服务器将拒绝执行写命令,这里的延迟值就是上面提到的INFO replication命令的lag 值。
             3、检测命令丢失。主服务器接收到命令之后,发现从服务器的偏移量和自己的不一致,会将缺少的命令发送给从服务器,从而保证数据的一致性
12、Redis主从复制中注意的问题    
       1)、注意只对主节点进行写操作,对于从节点只进行读操作。读从节点可能会遇到数据延迟、读到过期数据和从节点故障等问题。
        2)、 主从配置不一致是一个容易忽视的问题。 对于有些配置主从之间是可以不一致,比如:主节点关闭AOF在从节点开启。但对于内存相关的配置必须要一致,比如maxmemory,hash-max-ziplist-entries等参数。
       3)、 规避全量复制
              节点运行 ID 不匹配 :当主从复制关系建立后,从节点会保存主节点的运行ID,如果此时主节点因故障重启,那么它的运行ID会改变,从节点发现主节点运行ID不匹配时,会认为自己复制的是一个新的主节点从而进行全量复制。
              复制积压缓冲区不足 :当主从节点网络中断后,从节点再次连上主节点时会发送psync{offset}{runId}命令请求部分复制,如果请求的偏移量不在主节点的积压缓冲区内,则无法提供给从节点数据,因此部分复制会退化为全量复制。
               规避复制风暴:复制风暴是指大量从节点对同一主节点或者对同一台机器的多个主节点短时间内发起全量复制的过程。
13、Redis哨兵模式介绍
     Redis Sentinel 一个分布式架构,其中包含若干个Sentinel节点和Redis数据节点,每个Sentinel节点会对数据节点和其余Sentinel节点进行监控,当它发现节点不可达时,会对节点做下线标识。如果被标识的是主节点,它还会和其他Sentinel节点进行“协商”,当大多数Sentinel节点都认为主节点不可达时,它们会选举出一个Sentinel节点来完成自动故障转移的工作,同时会将这个变化实时通知给Redis应用方。整个过程完全是自动的,不需要人工来介入,所以这套方案很有效地解决了Redis的高可用问题。
缺点:只能实现单个主节点的高可用、 主节点的写能力受到单机的限制和 主节点的存储能力受到单机的限制。
14、Redis哨兵模式实现原理  
    基本实现原理:Redis Sentinel 的三个定时任务(心跳)、主观下线和客观下线、 Sentinel 领导者选举、故障转移。
       (1) 三个定时任务(心跳)
             1)、 每隔 10 秒,每个 Sentinel 节点会向主节点和从节点发送 info 命令获取最新的拓扑结构。
                主要作用:
                    1、通过向主节点执行info命令,获取从节点的信息 。
                    2、 当有新的从节点加入时都可以立刻感知出来。
                    3、节点不可达或者故障转移后,可以通过info命令实时更新节点拓扑信息    。               
              2)、 每隔 2 秒,每个 Sentinel 节点会向 Redis 数据节点的 __sentinel__ hello频道上发送该Sentinel节点对于主节点的判断以及当前Sentinel节点的信息,同时每个Sentinel节点也会订阅该频道,来了解其他Sentinel节点以及它们对主节点的判断,所以这个定时任务可以完成以下两个工作:
                    1、 发现新的 Sentinel 节点:通过订阅主节点的 __sentinel__ hello 了解其他的Sentinel节点信息,如果是新加入的Sentinel节点,将该Sentinel节点信息保存起来,并与该Sentinel节点创建连接。
                    2、 Sentinel 节点之间交换主节点的状态,作为后面客观下线以及领导者选举的依据Sentinel节点之间交换主节点的状态, 作为后面客观下线以及领导者选举的依据
              3)、 每隔 1 秒,每个 Sentinel 节点会向主节点、从节点、其余 Sentinel 节点发送一条ping命令做一次心跳检测,来确认这些节点当前是否可达。
      (2) 主观下线和客观下线:
             1).主观下线:
                         每个 Sentinel 节点会每隔 1 秒对主节点、从节点、其他Sentinel节点发送ping命令做心跳检测,当这些节点超过down-
             after-milliseconds没有进行有效回复,Sentinel节点就会对该节点做失败判定,这个行为叫做主观下线。
             2).客观下线:
                         Sentinel 主观下线的节点是主节点时,该 Sentinel 节点会通过 sentinel is master-down-by-addr命令向其他     Sentinel节点询问对主节点的               判断,当超过<quorum>个数,Sentinel节点认为主节点确实有问题,这时该Sentinel节点会做出客观下线的决定。
      (3) 领导者Sentinel节点选举:
                         一个redis服务被判断为客观下线时,多个监视该服务的sentinel协商,只能有一个sentinel节点去完成故障转移。
                 sentinel is-master-down-by-addr这个命令有两个作用,一是确认下线判定,二是进行领导者选举。
            选举过程:
                1)每个做主观下线的sentinel节点向其他sentinel节点发送上面那条命令,要求将它设置为领导者。
                2)收到命令的sentinel节点如果还没有同意过其他的sentinel发送的命令(还未投过票),那么就会同意,否则拒绝。
                3)如果该sentinel节点发现自己的票数已经过半且达到了quorum的值,就会成为领导者
                4)如果这个过程出现多个sentinel成为领导者,则会等待一段时间重新选举。
      (4) 故障转移:
           故障转移步 骤:
                  1 )在从节点列表中选出一个节点作为新的主节点 。
                  如何选出从节点:
                      a )过滤:"不健康"(主观下线、断线)、5秒内没有回复过Sentinel节点ping响应、与主节点失联超过down-after-milliseconds*10秒。
                     b )选择 slave-priority(从节点优先级)最高的从节点列表,如果存在则返回,不存在则继续。
                     c )选择复制偏移量最大的从节点(复制的最完整),如果存在则返回,不存在则继续。
                     d )选择 runid 最小的从节点。
                  2 Sentinel 领导者节点会对第一步选出来的从节点执行slaveof no one命令让其成为主节点。
                 3 Sentinel 领导者节点会向剩余的从节点发送命令,让它们成为新主节点的从节点,复制规则和parallel-syncs参数有关。
                 4 Sentinel 节点集合会将原来的主节点更新为从节点,并保持着对其关注,当其恢复后命令它去复制新的主节点。
15、Redis集群
    集群的实现原理(《Redis设计与实现》):
    Redis集群数据分布:
             分布方式: 哈希分区(离散度好、无法顺序访问和数据分布业务无关)和 顺序分区(离散度好、可顺序访问和            数据分布业务无关)
     1)集群里的数据结构:
        集群中的每一个节点都会使用一个clusterNode结构来记录自己的状态,并为集群中所有的其他节点都创建一个相应的clusterNode结构,以此来记录其他节点的状态。
        最后每一个节点都保存着一个clusterState结构,这个结构记录着集群所处的状态,例如集群是在线还是下线,集群中包含多少个节点等信息。
    2)槽指派
        Redis 集群通过分片的方式来保存数据库中的键值对:集群的整个数据库被分为16384个槽(slot),数据库中的每个键都属于16384个槽的其中一个,集群中的每个节可以处理0个或最多16384槽。
         (1)、记录节点的槽指派信息。 clusterNode 结构的slots属性和numslot属性记录了节点负责处理哪些槽
         (2)、传播节点的槽指派信息。一个节点除了会将自己负责处理的槽记录在clusterNode结构的slots属性合numslots属性外,她还会将自己的slots数组通过消息发送给集群中的其他节点。         
         (3)、记录集群所有槽的指派信息。 clusterState结构中的slots数组记录了集群中所有16384个槽的指派信息。slots数组包含16384个项,每个数组项都是一个指向clusterNode结构的指针,如果指向null,表示槽尚未指派给任何节点。
            总结:cluster.slots数组记录了集群中所有槽的指派信息,但是使用clusterNode结构的slots数组来记录单个节点的槽指派信息也是有必要的:
                  * 当程序需要将某个节点的槽指派信息通过消息发送给其他节点时,只需要将相应节点的clusterNode.slots数组发送出去就可以了。
                  * 如果不使用clusterNode结构的slots数组每次需要将槽指派信息发送给其它节点的时候,都需要整个遍历clusterState.slots。
    3)在集群中执行命令
        在对数据库中的16384个槽都进行了指派之后,集群就会进入上线状态,客户端就可以向集群发送命令了。
        执行步骤:
            1)节点计算键属于哪个槽
                   CRC16(key)& 16384 计算出一个在0~16384之间的值。
            2)判断槽是否由当前节点负责处理
                    计算出槽的位置之后,节点就会检查clusterState.slots数组的项i,判断所在的槽是否由自己负责。
            3)由其他节点处理则返回从MOVED错误
                    当前节点发现不是由自己处理,则返回一个MOVED错误,并返回负责的IP和端口来指引客户端转向正确的节点。
   4)重新分片
        重新分片是实现集群伸缩和扩容的关键。
        重新分片操作可以将已经分配给某个节点的槽更改为另一个节点,并且相关槽的键值对也会相应的转移到另一个节点。
      重新分片实现原理:
          Redis重新分片是由集群管理软件redis-trib负责执行的,Redis提供了分片的所有命令,redis-trib通过向源节点和目标节点发送命令来进行重新分片操作。
    单个槽分片步骤:
        (1) 对目标节点发送 cluster setslot{slot}importing{sourceNodeId} 命令,让目标节点准备导入槽的数据。
        (2) 对源节点发送 cluster setslot{slot}migrating{targetNodeId} 命令,让源节点准备迁出槽的数据。
        (3) 源节点循环执行 cluster getkeysinslot{slot}{count} 命令,获取 count 属于槽{slot}的键。
        (4) 在源节点上执行 migrate{targetIp}{targetPort}""0{timeout}keys{keys...}命令,把获取的键通过流水线(pipeline)机制批量迁移到目标节点,批量迁移版本的        migrate命令在Redis3.0.6以上版本提供,之前的migrate命令只能单个键迁移。对于大量key的场景,批量键迁移将极大降低节点之间网络IO次数。
        (5) 重复执行步骤 3 )和步骤 4 )直到槽下所有的键值数据迁移到目标节点。
        (6) 向集群内所有主节点发送 cluster setslot{slot}node{targetNodeId} 令,通知槽分配给目标节点。为了保证槽节点映射变更及时传播,需要遍历发送给所有主        节点更新被迁移的槽指向新节点。
    批量处理槽分片:
         际操作时肯定涉及大量槽并且每个槽对应非常多的键。因此redis-trib提供了槽重分片功能.reshard命令简化了数据迁移的工作量,其内部针对每个槽的数据迁移同
    样使用之前的流程,只需输入迁移槽的数量,以及目标地址和源地址的信息就可以由 redis-trib自动完成。
    5)ASK重定向
    1. 客户端 ASK 重定向流程
         Redis 集群支持在线迁移槽( slot )和数据来完成水平伸缩,当 slot 对应的数据从源节点到目标节点迁移过程中,客户端需要做到智能识别,保证键
    命令可正常执行。例如当一个slot数据从源节点迁移到目标节点时,期间可能出现一部分数据在源节点,而另一部分在目标节点。
      当出现上述情况时,客户端键命令执行流程将发生变化,如下所示
        1 )客户端根据本地 slots 缓存发送命令到源节点,如果存在键对象则直接执行并返回结果给客户端。
        2 )如果键对象不存在,则可能存在于目标节点,这时源节点会回复ASK重定向异常。格式如下:(error)ASK{slot}{targetIP}:{targetPort}。
        3 )客户端从 ASK 重定向异常提取出目标节点信息,发送 asking 命令到目标节点打开客户端连接标识,再执行键命令。如果存在则执行,不存在则返回不存在信息。
    2. 节点内部处理
        为了支持 ASK 重定向,源节点和目标节点在内部的 clusterState 结构中维护当前正在迁移的槽信息,用于识别槽迁移情况。节点每次接收到键命令时,都会根据        clusterState内的迁移属性进行命令处理。
  • 如果键所在的槽由当前节点负责,但键不存在则查找migrating_slots_to数组查看槽是否正在迁出,如果是返回ASK重定向。
  • 如果客户端发送asking命令打开了CLIENT_ASKING标识,则该客户端下次发送键命令时查找importing_slots_from数组获取clusterNode,如果指向自身则执行命令。
  • 需要注意的是,asking命令是一次性命令,每次执行完后客户端标识都会修改回原状态,因此每次客户端接收到ASK重定向后都需要发送asking命令。
  • 批量操作。ASK重定向对单键命令支持得很完善,但是,在开发中我们经常使用批量操作,如mget或pipeline。当槽处于迁移状态时,批量操作会受到影响。
    6)ASK MOVED区别
    ASK MOVED 虽然都是对客户端的重定向控制,但是有着本质区别。ASK重定向说明集群正在进行slot数据迁移,客户端无法知道什么时候迁移完成,因此只能是临时性的重定向,客户端不会更新slots缓存。但是MOVED重定向说明键对应的槽已经明确指定到新的节点,因此需要更新slots缓存。
    7)故障迁移
    (1) 故障发现 :Redis 集群内节点通过 ping/pong 消息实现节点通信,消息不但可以传播节点槽信息,还可以传播其他状态如:主从状态、节点故障等。因此故障发现也是通过消息传播机制实现的,主要环节包括:疑似下线(pfail)和下线下线(fail)。 半数以上负责处理槽的主节点都认为某个主节点疑似下线,那么这个主节点将被标记为下线,并将下线的消息向集群广播。所有收到这条消息的节点都会立即将主节点x标记为已下线。
       1. 主观下线
        集群中每个节点都会定期向其他节点发送 ping 消息,接收节点回复 pong消息作为响应。如果在cluster-node-timeout时间内通信一直失败,则发送节点会认为接收节        点存在故障,把接收节点标记为主观下线(pfail)状态。  
      2. 客观下线
         当某个节点判断另一个节点主观下线后,相应的节点状态会跟随消息在集群内传播。ping/pong消息的消息体会携带集群1/10的其他节点状态数据,当接受节点发现         消息体中含有主观下线的节点状态时,会在本地找到故障节点的ClusterNode结构,保存到下线报告链表中。
    例子:
        一共ABCD四个节点。如何节点A向B发送了一条PING消息,B没有在规定的时间回复,那么A就会在自己的clusterState.nodes字典中找到节点B所对应的clusterNode结构,打开疑似下线的标识。
        当C通过消息得知A节点认为B节点进入疑似下线状态,那么C就会在自己的clusterState.nodes字典中找到节点B所对应的clusterNode结构,打开疑似下线的标识。
        如果D节点收到A和C发送的消息得知,都认为B进入疑似下线状态,那么D将为B创建下线报告,并向集群广播B节点下线的消息。
    (2)故障恢复
    步骤:
  • 在所有的从节点中选择一个
  • 被选中的从节点会执行SLAVEOF no one命令,成为的新的主节点。
  • 新的主节点会撤销所有对已下线主节点的槽指派,并将这些槽全部指派给自己。
  • 新的主节点会向集群广播一条PONG消息,这条消息可以让集群中的其他节点立即知道这个主节点接管了原本由已下线节点负责的槽。
  • 新的主节点开始接收和处理槽有关的命令请求。
如何选举从节点:
  • 资格检查。 每个从节点都要检查最后与主节点断线时间 如果从节点与主节点断线时间,如果超过 cluster-node-time*cluster-slavevalidity-factor 则当前从节点不具备故障转移资格。
  • 准备选举时间。 从节点符合故障转移资格后,更新触发故障选举的时间,只有到达该时间后才能执行后续流程,这里之所以采用延迟触发机制,主要是通过对多个从节点使用不同的延迟选举时间来支持优先级问题。复制偏移量越大说明从节点延迟越低,那么它应该具有更高的优先级来替换故障主节点。
  • 发起选举。
  • 选举投票。
  • 替换主节点。
 16)缓存穿透   
     缓存穿透是指查询一个根本不存在的数据,缓存层和存储层都不会命中,通常出于容错的考虑,如果从存储层查不到数据则不写入缓存层, 缓存穿透将导致不存在的数据每次请求都要到存储层去查询,失去了缓存保护后端存储的意义。
    解决方案:
     (1)、 缓存空对象
            仍然将空对象保留到缓存层中,之后再访问这个数据将会从缓存中获取,这样就保护了后端数据源。
       适用场景:数据命中不高,数据频繁变化实时性高。
     存在的问题:
       第一,空值做了缓存,意味着缓存层中存了更多的键,需要更多的内存空间(如果是攻击,问题更严重),比较有效的方法是针对这类数据设置一个 较短的过期时间,让其自动 剔除。
       第二,缓存层和存储层的数据会有一段时间 数据不 ,可能会对业务有一定影响。
     解决方案:
  • 在持久层数据做更改成功之后删除缓存中的数据,之后再从数据库中读取。(高并发下也会出现不一致的情况)
  • 先删除缓存中的数据,更新数据库并且新增一条缓存 更新的记录,缓存更新完成之后删除这条记录。(可以保证)               
      (2)、 布隆过滤器拦截
         在访问缓存层和存储层之前,将存在的key用布隆过滤器提前保存起来,做第一层拦截。
      做法: 布隆过滤器可以使用google已经实现的jar
      适用场景 :数据命中不高、数据相对固定、实时性低(通常是数据集较大)的应用场景,代码维护较为复杂,但是缓存空间占用少。
17)无底洞优化
        为了满足业务增加了许多新的节点,但是性能没有增加反而下降了。原因:因为像mget命令可能需要跨越好几个节点来进行获取数据,需要次网络请求,多次命令耗时。
    解决方案:
  • 串行命令: 它的操作时间=n次网络时间+n次命令时间,网络次数是n。很显然这种方案不是最优的,但是实现起来比较简单
  • 串行 IO: Smart 客户端会保存 slot 和节点的对应关系, 得到每个节点的key子列表,之后对每个节点执行mget或者Pipeline操作,它的操作时间=node次网络时间+n次命令时间,网络次数是node的个数
  • 并行 IO: 方案 2 中的最后一步改为多线程执行,同时请求多个子节点。
  • hash_tag 实现: 它可以将多个 key 强制分配到一个节点上,它的操作时间=1次网络时间+n次命令时间
  18)缓存雪崩      
     指的是缓存层宕掉后,流量会全部打向后端存储,将后端也挂掉。
     预防和解决缓存雪崩问题,可以从以下三个方面进行着手:
  • 保证缓存层服务高可用性(redis高可用,主从+哨兵,redis cluster,避免全盘崩溃
  • 依赖隔离组件为后端限流并降级(本地ehcache缓存 + hystrix限流&降级
  • 做好redis的持久化,用于灾难恢复
  • 做好演练
19) 如何保证缓存与数据库的双写一致性?( 待思考验证
    一般来说,就是如果你的系统不是严格要求缓存+数据库必须一致性的话,缓存可以稍微的跟数据库偶尔有不一致的情况,最好不要做这个方案,读请求和写请求 串行化,串到一个内存队列里去,这样就可以保证一定不会出现不一致的情况
    串行化之后,就会导致系统的吞吐量会大幅度的降低,用比正常情况下多几倍的机器去支撑线上的一个请求。
    解决方案如下:
    更新数据的时候,根据数据的唯一标识,将操作路由之后,发送到一个 jvm 内部队列中。读取数据的时候,如果发现数据不在缓存中,那么将重新读取数据+更新缓存的 操作,根据唯一标识路由之后,也发送同一个 jvm 内部队列中。
    一个队列对应一个工作线程,每个工作线程串行拿到对应的操作,然后一条一条的执行。这样的话,一个数据变更的操作,先删除缓存,然后再去更新数据库,但是还没完成更新。此时如果一个读请求过来,读到了空的缓存,那么可以先将缓存更新的请求发送到队列中,此时会在队列中积压,然后同步等待缓存更新完成。
这里有一个优化点,一个队列中,其实多个更新缓存请求串在一起是没意义的,因此可以做过滤,如果发现队列中已经有一个更新缓存的请求了,那么就不用再放个更新请求操作进去了,直接等待前面的更新操作请求完成即可。
    待那个队列对应的工作线程完成了上一个操作的数据库的修改之后,才会去执行下一个操作,也就是缓存更新的操作,此时会从数据库中读取最新的值,然后写入缓存中。
如果请求还在等待时间范围内,不断轮询发现可以取到值了,那么就直接返回;如果请求等待的时间超过一定时长,那么这一次直接从数据库中读取当前的旧值。
高并发的场景下,该解决方案要注意的问题:
  • 读请求长时阻塞
    由于读请求进行了非常轻度的异步化,所以一定要注意读超时的问题,每个读请求必须在超时时间范围内返回。
该解决方案,最大的风险点在于说,可能数据更新很频繁,导致队列中积压了大量更新操作在里面,然后读请求会发生大量的超时,最后导致大量的请求直接走数据库。务必通过一些模拟真实的测试,看看更新数据的频率是怎样的。
    另外一点,因为一个队列中,可能会积压针对多个数据项的更新操作,因此需要根据自己的业务情况进行测试,可能需要部署多个服务,每个服务分摊一些数据的更新操作。如果一个内存队列里居然会挤压 100 个商品的库存修改操作,每隔库存修改操作要耗费 10ms 去完成,那么最后一个商品的读请求,可能等待 10 * 100 = 1000ms = 1s 后,才能得到数据,这个时候就导致读请求的长时阻塞。
    一定要做根据实际业务系统的运行情况,去进行一些压力测试,和模拟线上环境,去看看最繁忙的时候,内存队列可能会挤压多少更新操作,可能会导致最后一个更新操作对应的读请求,会 hang 多少时间,如果读请求在 200ms 返回,如果你计算过后,哪怕是最繁忙的时候,积压 10 个更新操作,最多等待 200ms,那还可以的。
如果一个内存队列中可能积压的更新操作特别多,那么你就要加机器,让每个机器上部署的服务实例处理更少的数据,那么每个内存队列中积压的更新操作就会越少。
其实根据之前的项目经验,一般来说,数据的写频率是很低的,因此实际上正常来说,在队列中积压的更新操作应该是很少的。像这种针对读高并发、读缓存架构的项目,一般来说写请求是非常少的,每秒的 QPS 能到几百就不错了。
     我们来实际粗略测算一下。
    如果一秒有 500 的写操作,如果分成 5 个时间片,每 200ms 就 100 个写操作,放到 20 个内存队列中,每个内存队列,可能就积压 5 个写操作。每个写操作性能测试后,一般是在 20ms 左右就完成,那么针对每个内存队列的数据的读请求,也就最多 hang 一会儿,200ms 以内肯定能返回了。
经过刚才简单的测算,我们知道,单机支撑的写 QPS 在几百是没问题的,如果写 QPS 扩大了 10 倍,那么就扩容机器,扩容 10 倍的机器,每个机器 20 个队列。
  • 读请求并发量过高
    这里还必须做好压力测试,确保恰巧碰上上述情况的时候,还有一个风险,就是突然间大量读请求会在几十毫秒的延时 hang 在服务上,看服务能不能扛的住,需要多少机器才能扛住最大的极限情况的峰值。
    但是因为并不是所有的数据都在同一时间更新,缓存也不会同一时间失效,所以每次可能也就是少数数据的缓存失效了,然后那些数据对应的读请求过来,并发量应该也不会特别大。
  • 多服务实例部署的请求路由
可能这个服务部署了多个实例,那么必须保证说,执行数据更新操作,以及执行缓存更新操作的请求,都通过 Nginx 服务器路由到相同的服务实例上。
比如说,对同一个商品的读写请求,全部路由到同一台机器上。可以自己去做服务间的按照某个请求参数的 hash 路由,也可以用 Nginx 的 hash 路由功能等等。
  • 热点商品的路由问题,导致请求的倾斜
万一某个商品的读写请求特别高,全部打到相同的机器的相同的队列里面去了,可能会造成某台机器的压力过大。就是说,因为只有在商品数据更新的时候才会清空缓存,然后才会导致读写并发,所以其实要根据业务系统去看,如果更新频率不是太高的话,这个问题的影响并不是特别大,但是的确可能某些机器的负载会高一些。
 
 20)Redis数据结构? 
    Redis使用对象来表示数据库的键和值,每次当我们在Redis的数据库中新建一个对象的时候,我们至少会创建两个对象,一个对象用作键值对的键,另一个对象用作键值对的值对象。
uploading.4e448015.gif转存失败 重新上传 取消 uploading.4e448015.gif正在上传… 重新上传 取消 uploading.4e448015.gif正在上传… 重新上传 取消 uploading.4e448015.gif转存失败 重新上传 取消   
  简单字符串(SDS):
 数据结构:            uploading.4e448015.gif转存失败 重新上传 取消 uploading.4e448015.gif正在上传… 重新上传 取消 uploading.4e448015.gif正在上传… 重新上传 取消 uploading.4e448015.gif转存失败 重新上传 取消
    优缺点:
  •  可以直接获取字符串的长度
  •  SDS可以动态的控制字符串的大小
  •  减少修改字符串带来的内存重分配次数            
            (1)空间预分配:当字符串需要增长的时候,不仅会为SDS分配足够的空间, 还会分配额外的未使用的空间。
            (2)惰性空间释放:当程序需要缩短SDS保存的字符串时,不会理解回收多出来的字节而是使用free属性记录下来,等待将来使用。
  • 可以保存任意格式二进制数据。
  • 兼容部分C字符串的函数:strcasecmp 对比函数
链表
是Redis中list的一种实现方式   。 
 数据结构:
uploading.4e448015.gif转存失败 重新上传 取消 uploading.4e448015.gif正在上传… 重新上传 取消 uploading.4e448015.gif正在上传… 重新上传 取消 uploading.4e448015.gif转存失败 重新上传 取消    uploading.4e448015.gif转存失败 重新上传 取消 uploading.4e448015.gif正在上传… 重新上传 取消 uploading.4e448015.gif正在上传… 重新上传 取消 uploading.4e448015.gif转存失败 重新上传 取消
                                                                                             上图为链表和链表节点的数据结构
uploading.4e448015.gif转存失败 重新上传 取消 uploading.4e448015.gif正在上传… 重新上传 取消 uploading.4e448015.gif正在上传… 重新上传 取消 uploading.4e448015.gif转存失败 重新上传 取消
字典
    是Redis中 set和hash的底层实现方式之一。
 Redis中的字典使用哈希表作为底层实现,一个哈希表中可以有多个哈希节点,每一个哈希节点保存一个键值对
    数据结构:
uploading.4e448015.gif转存失败 重新上传 取消 uploading.4e448015.gif正在上传… 重新上传 取消 uploading.4e448015.gif正在上传… 重新上传 取消 uploading.4e448015.gif转存失败 重新上传 取消       
uploading.4e448015.gif转存失败 重新上传 取消 uploading.4e448015.gif正在上传… 重新上传 取消 uploading.4e448015.gif正在上传… 重新上传 取消 uploading.4e448015.gif转存失败 重新上传 取消 uploading.4e448015.gif转存失败 重新上传 取消 uploading.4e448015.gif正在上传… 重新上传 取消 uploading.4e448015.gif正在上传… 重新上传 取消 uploading.4e448015.gif转存失败 重新上传 取消 uploading.4e448015.gif转存失败 重新上传 取消 uploading.4e448015.gif正在上传… 重新上传 取消 uploading.4e448015.gif正在上传… 重新上传 取消 uploading.4e448015.gif转存失败 重新上传 取消
uploading.4e448015.gif转存失败 重新上传 取消 uploading.4e448015.gif正在上传… 重新上传 取消 uploading.4e448015.gif正在上传… 重新上传 取消 uploading.4e448015.gif转存失败 重新上传 取消
uploading.4e448015.gif转存失败 重新上传 取消 uploading.4e448015.gif正在上传… 重新上传 取消 uploading.4e448015.gif正在上传… 重新上传 取消 uploading.4e448015.gif转存失败 重新上传 取消
跳跃表
是redis中zset的底层实现方式之一
跳跃表的效率可以和平衡树做媲美
uploading.4e448015.gif转存失败 重新上传 取消 uploading.4e448015.gif正在上传… 重新上传 取消 uploading.4e448015.gif正在上传… 重新上传 取消 uploading.4e448015.gif转存失败 重新上传 取消
uploading.4e448015.gif转存失败 重新上传 取消 uploading.4e448015.gif正在上传… 重新上传 取消 uploading.4e448015.gif正在上传… 重新上传 取消 uploading.4e448015.gif转存失败 重新上传 取消
uploading.4e448015.gif转存失败 重新上传 取消 uploading.4e448015.gif正在上传… 重新上传 取消 uploading.4e448015.gif正在上传… 重新上传 取消 uploading.4e448015.gif转存失败 重新上传 取消
uploading.4e448015.gif转存失败 重新上传 取消 uploading.4e448015.gif正在上传… 重新上传 取消 uploading.4e448015.gif正在上传… 重新上传 取消 uploading.4e448015.gif转存失败 重新上传 取消
uploading.4e448015.gif转存失败 重新上传 取消 uploading.4e448015.gif正在上传… 重新上传 取消 uploading.4e448015.gif正在上传… 重新上传 取消 uploading.4e448015.gif转存失败 重新上传 取消
uploading.4e448015.gif转存失败 重新上传 取消 uploading.4e448015.gif正在上传… 重新上传 取消 uploading.4e448015.gif正在上传… 重新上传 取消 uploading.4e448015.gif转存失败 重新上传 取消
uploading.4e448015.gif转存失败 重新上传 取消 uploading.4e448015.gif正在上传… 重新上传 取消 uploading.4e448015.gif正在上传… 重新上传 取消 uploading.4e448015.gif转存失败 重新上传 取消
整数集合:
    是Redis set的底层实现方式之一,当一个集合中只包含整数元素,并且这个整数元素数量不多的时候,Redis 就会采用整数集合作为set的实现方式.
uploading.4e448015.gif转存失败 重新上传 取消 uploading.4e448015.gif正在上传… 重新上传 取消 uploading.4e448015.gif正在上传… 重新上传 取消 uploading.4e448015.gif转存失败 重新上传 取消
uploading.4e448015.gif转存失败 重新上传 取消 uploading.4e448015.gif正在上传… 重新上传 取消 uploading.4e448015.gif正在上传… 重新上传 取消 uploading.4e448015.gif转存失败 重新上传 取消
uploading.4e448015.gif转存失败 重新上传 取消 uploading.4e448015.gif正在上传… 重新上传 取消 uploading.4e448015.gif正在上传… 重新上传 取消 uploading.4e448015.gif转存失败 重新上传 取消
uploading.4e448015.gif转存失败 重新上传 取消 uploading.4e448015.gif正在上传… 重新上传 取消 uploading.4e448015.gif正在上传… 重新上传 取消 uploading.4e448015.gif转存失败 重新上传 取消
编码只支持升级,不支持降级。
压缩列表:
压缩列表是Redis中hash、list和 zset 的实现方式之一。
    当hash、list和zset的的值为小整数值或者字符串的时候,就会用压缩列表来进行保存。
    压缩列表是为了 节约内存而设计的,由一些特殊编码的连续内存块组成的顺序型数据结构,一个压缩列表可以包含任意个节点,每个节点保存一个字节数组或者一个整数值。
uploading.4e448015.gif转存失败 重新上传 取消 uploading.4e448015.gif正在上传… 重新上传 取消 uploading.4e448015.gif正在上传… 重新上传 取消 uploading.4e448015.gif转存失败 重新上传 取消
uploading.4e448015.gif转存失败 重新上传 取消 uploading.4e448015.gif正在上传… 重新上传 取消 uploading.4e448015.gif正在上传… 重新上传 取消 uploading.4e448015.gif转存失败 重新上传 取消
uploading.4e448015.gif转存失败 重新上传 取消 uploading.4e448015.gif正在上传… 重新上传 取消 uploading.4e448015.gif正在上传… 重新上传 取消 uploading.4e448015.gif转存失败 重新上传 取消
uploading.4e448015.gif转存失败 重新上传 取消 uploading.4e448015.gif正在上传… 重新上传 取消 uploading.4e448015.gif正在上传… 重新上传 取消 uploading.4e448015.gif转存失败 重新上传 取消
uploading.4e448015.gif正在上传… 重新上传 取消 uploading.4e448015.gif正在上传… 重新上传 取消 uploading.4e448015.gif正在上传… 重新上传 取消 uploading.4e448015.gif转存失败 重新上传 取消
uploading.4e448015.gif正在上传… 重新上传 取消 uploading.4e448015.gif正在上传… 重新上传 取消 uploading.4e448015.gif正在上传… 重新上传 取消 uploading.4e448015.gif转存失败 重新上传 取消
uploading.4e448015.gif正在上传… 重新上传 取消 uploading.4e448015.gif正在上传… 重新上传 取消 uploading.4e448015.gif正在上传… 重新上传 取消 uploading.4e448015.gif转存失败 重新上传 取消
uploading.4e448015.gif正在上传… 重新上传 取消 uploading.4e448015.gif正在上传… 重新上传 取消 uploading.4e448015.gif正在上传… 重新上传 取消 uploading.4e448015.gif转存失败 重新上传 取消
21)Redis编码?
 Redis 对外提供了 string、list、hash、set、zet等类型,但是Redis内部针对不同类型存在编码的概念,所谓编码就是具体使用哪种底层数据结构来实现。 编码不同将直接影响数据的内存占用和读写效率 Redis 作者想通过不同编码实现效率和空间的平衡。 Redis 之所以不支持编码回退,主要是数据增删频繁时,数据向压缩编码转换非常消耗CPU,得不偿失。
uploading.4e448015.gif正在上传… 重新上传 取消 uploading.4e448015.gif正在上传… 重新上传 取消 uploading.4e448015.gif正在上传… 重新上传 取消 uploading.4e448015.gif转存失败 重新上传 取消
22)Redis的数据类型以及应用场景
        String:
           常用命令:get、set、incr、decr、mget等。
             使用场景:常规key-value缓存应用。常规计数: 微博数, 粉丝数(自增)。
        hash: 
             常用命令:hget,hset,hgetall 等。
             使用场景:存储部分变更数据,如用户信息等。
             比如我们要存储一个用户信息对象数据, Redis的Hash实际是内部存储的Value为一个HashMap ,Key是用户ID, value是一个Map,这个Map的key是成员的属性名,            value是属性值,这样对数据的修改和存取都可以直接通过其内部Map的Key(Redis里称内部Map的key为field), 也就是通过 key(用户ID) + field(属性标签) 就可以操作对应属性数据了,既不需要重复存储数据,也不会带来序列化和并发修改控制的问题,很好的解决了问题。
         list: 
            列表是简单的字符串列表,按照插入顺序排序。你可以添加一个元素到列表的头部(左边)或者尾部(右边)。   
         常用命令:lpush(添加左边元素),rpush,lpop(移除左边第一个元素),rpop,lrange(获取列表片段,LRANGE key start stop)等。
      应用场景:Redis list的应用场景非常多,也是Redis最重要的数据结构之一,比如微博的关注列表,粉丝列表等都可以用Redis的list结构来实现。List的另一个应用就是消息队列,可以利用List的PUSH操作,将任务存在List中,然后工作线程再用POP操作将任务取出进行执行。Redis还提供了操作List中某一段的api,你可以直接查询,删除List中某一段的元素。
         实现方式:Redis list的实现为一个双向链表,即可以支持反向查找和遍历,更方便操作,不过带来了部分额外的内存开销,Redis内部的很多实现,包括发送缓冲队列等也都是用的这个数据结构。
  Redis的list是每个子元素都是String类型的双向链表,可以通过push和pop操作从列表的头部或者尾部添加或者删除元素,这样List即可以作为栈,也可以作为队列。 获取越接近两端的元素速度越快,但通过索引访问时会比较慢。
使用场景:
  消息队列系统:使用list可以构建队列系统,使用sorted set甚至可以构建有优先级的队列系统。比如:将Redis用作日志收集器,实际上还是一个队列,多个端点将日志信息写入Redis,然后一个worker统一将所有日志写到磁盘。
  取最新N个数据的操作:记录前N个最新登陆的用户Id列表,超出的范围可以从数据库中获得。列表最多可存储 232 - 1 元素
        set
          是string类型的无序集合。集合是通过hashtable实现的,概念和数学中个的集合基本类似,可以交集,并集,差集等等,set中的元素是没有顺序的。所以添加,删除,查找的复杂度都是O(1)。
   sadd 命令: 添加一个 string 元素到 key 对应的 set 集合中,成功返回1,如果元素已经在集合中返回 0,如果 key 对应的 set 不存在则返回错误。
  常用命令:sadd,spop,smembers,sunion 等。
  应用场景:Redis set对外提供的功能与list类似是一个列表的功能,特殊之处在于set是可以自动排重的,当你需要存储一个列表数据,又不希望出现重复数据时,set是一个很好的选择,并且set提供了判断某个成员是否在一个set集合内的重要接口,这个也是list所不能提供的。
  Set 就是一个集合,集合的概念就是一堆不重复值的组合。利用Redis提供的Set数据结构,可以存储一些集合性的数据。
  案例:在微博中,可以将一个用户所有的关注人存在一个集合中,将其所有粉丝存在一个集合。Redis还为集合提供了求交集、并集、差集等操作,可以非常方便的实现如共同关注、共同喜好、二度好友等功能,对上面的所有集合操作,你还可以使用不同的命令选择将结果返回给客户端还是存集到一个新的集合中。
  实现方式: set 的内部实现是一个 value永远为null的HashMap,实际就是通过计算hash的方式来快速排重的,这也是set能提供判断一个成员是否在集合内的原因。
  使用场景:
           ①交集,并集,差集:(Set)     
             ②获取某段时间所有数据去重值
  这个使用Redis的set数据结构最合适了,只需要不断地将数据往set中扔就行了,set意为集合,所以会自动排重。 
       zset 
        和 set 一样也是string类型元素的集合,且不允许重复的成员。
  zadd 命令: 添加元素到集合,元素在集合中存在则更新对应score。
  常用命令:zadd,zrange,zrem,zcard等
  使用场景:Redis sorted set的使用场景与set类似,区别是set不是自动有序的,而sorted set可以通过用户额外提供一个优先级(score)的参数来为成员排序,并且是插入有序的,即自动排序。当你需要一个有序的并且不重复的集合列表,那么可以选择sorted set数据结构,比如twitter 的public timeline可以以发表时间作为score来存储,这样获取时就是自动按时间排好序的。和Set相比, Sorted Set关联了一个double类型权重参数score ,使得集合中的元素能够按score进行有序排列,redis正是通过分数来为集合中的成员进行从小到大的排序。zset的成员是唯一的,但分数(score)却可以重复。比如一个存储全班同学成绩的Sorted Set,其集合value可以是同学的学号,而score就可以是其考试得分,这样在数据插入集合的时候,就已经进行了天然的排序。另外还可以用Sorted Set来做带权重的队列,比如普通消息的score为1,重要消息的score为2,然后工作线程可以选择按score的倒序来获取工作任务。让重要的任务优先执行。
  实现方式:Redis sorted set的内部使用HashMap和跳跃表(SkipList)来保证数据的存储和有序,HashMap里放的是成员到score的映射,而跳跃表里存放的是所有的成员,排序依据是HashMap里存的score,使用跳跃表的结构可以获得比较高的查找效率,并且在实现上比较简单。
uploading.4e448015.gif正在上传…重新上传取消uploading.4e448015.gif正在上传…重新上传取消uploading.4e448015.gif正在上传…重新上传取消uploading.4e448015.gif转存失败重新上传取消
22)Redis数据丢失的场景?
(1)异步复制导致的数据丢失
    因为master -> slave的复制是异步的,所以可能有部分数据还没复制到slave,master就宕机了,此时这些部分数据就丢失了,因为保存数据的时候,master保存成功了则返回客户端保存成功。
(2)脑裂导致的数据丢失
    脑裂,也就是说,某个master所在机器突然脱离了正常的网络,跟其他slave机器不能连接,但是实际上master还运行着,此时哨兵可能就会认为master宕机了,然后开启选举,将其他slave切换成了master这个时候,集群里就会有两个master,也就是所谓的脑裂此时虽然某个slave被切换成了master,但是可能client还没来得及切换到新的master,还继续写向旧master的数据可能也丢失了。
 
 
 
 
  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值