3w字深度好文|Redis面试全攻略,读完这个就可以和面试官大战几个回合了

Posted 后端研究所

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了3w字深度好文|Redis面试全攻略,读完这个就可以和面试官大战几个回合了相关的知识,希望对你有一定的参考价值。

int processed = (!(flags & AE_TIME_EVENTS) && !(flags & AE_FILE_EVENTS)) (eventLoop->maxfd != || ((flags & AE_TIME_EVENTS) && !(flags & AE_DONT_WAIT))) int j; aeTimeEvent *shortest = struct timeval tv, *tvp; (flags & AE_TIME_EVENTS && !(flags & AE_DONT_WAIT)) shortest = aeSearchNearestTimer(eventLoop); (shortest) aeGetTime(&now_sec, &now_ms); long long ms = (shortest->when_sec - now_sec)*+ shortest->when_ms - now_ms; (ms > tvp->tv_sec = ms/ tvp->tv_usec = (ms % tvp->tv_sec = tvp->tv_usec = (flags & AE_DONT_WAIT) tv.tv_sec = tv.tv_usec = tvp = &tv; tvp = numevents = aeApiPoll(eventLoop, tvp); (eventLoop->aftersleep != && flags & AE_CALL_AFTER_SLEEP) eventLoop->aftersleep(eventLoop); (j = aeFileEvent *fe = &eventLoop->events[eventLoop->fired[j].fd]; int mask = eventLoop->fired[j].mask; int fd = eventLoop->fired[j].fd; (!invert && fe->mask & mask & AE_READABLE) fe->rfileProc(eventLoop,fd,fe->clientData,mask); fired++; (fe->mask & mask & AE_WRITABLE) (!fired || fe->wfileProc != fe->rfileProc) fe->wfileProc(eventLoop,fd,fe->clientData,mask); fired++; (invert && fe->mask & mask & AE_READABLE) (!fired || fe->wfileProc != fe->rfileProc) fe->rfileProc(eventLoop,fd,fe->clientData,mask); fired++; processed++; (flags & AE_TIME_EVENTS) processed; #获取当前最近的待执行的时间事件 #计算最近执行事件与当前时间的差值 #判断时间事件是否已经到期 则重置 马上执行 #阻塞等待文件事件 具体的阻塞等待时间由remain_gap_time决定 #如果remain_gap_time为0 那么不阻塞立刻返回 #处理所有文件事件 #处理所有时间事件 HAVE_EVPORT HAVE_EPOLL HAVE_KQUEUE CONFIG GET save CONFIG SET save zskiplist *zsl;* The return value of this function is between 1 and ZSKIPLIST_MAXLEVEL* (both inclusive), with a powerlaw-alike distribution where higher* levels are less likely to be returned. */ int level = 1; while ((random()&0xFFFF) < (ZSKIPLIST_P * 0xFFFF)) level += 1; return (level<ZSKIPLIST_MAXLEVEL) ? level : ZSKIPLIST_MAXLEVEL;<Key, Value>SkipList<Key, Value>::randomLevel() kBranching = height = (height < kMaxLevel && ((::Next(rnd_) % kBranching) == height++; assert(height > assert(height <= kMaxLevel); height;Next( seed = seed & (seed == || seed == seed = M = A = product = seed * A; seed = (seed > M) seed -= M; seed;
可以看到leveldb使用随机数与kBranching取模,如果值为0就增加一层,这样虽然没有使用浮点数,但是也实现了概率平衡。

  • 跳表结点的平均层数

  • 我们很容易看出,产生越高的节点层数出现概率越低,无论如何层数总是满足幂次定律越大的数出现的概率越小。

    如果某件事的发生频率和它的某个属性成幂关系,那么这个频率就可以称之为符合幂次定律。幂次定律的表现是少数几个事件的发生频率占了整个发生频率的大部分, 而其余的大多数事件只占整个发生频率的一个小部分。

    幂次定律应用到跳表的随机层数来说就是大部分的节点层数都是黄色部分,只有少数是绿色部分,并且概率很低。

    定量的分析如下:
    1. 节点层数至少为1,大于1的节点层数满足一个概率分布。

    2. 节点层数恰好等于1的概率为p^0(1-p)。

    3. 节点层数恰好等于2的概率为p^1(1-p)。

    4. 节点层数恰好等于3的概率为p^2(1-p)。

    5. 节点层数恰好等于4的概率为p^3(1-p)。

    6. 依次递推节点层数恰好等于K的概率为p^(k-1)(1-p)


    如果要求节点的平均层数,那么也就转换成了求概率分布的期望问题了,灵魂画手大白再次上线:

    表中P为概率,V为对应取值,给出了所有取值和概率的可能,因此就可以求这个概率分布的期望了。

    方括号里面的式子其实就是高一年级学的等比数列,常用技巧错位相减求和,从中可以看到结点层数的期望值与1-p成反比。对于Redis而言,当p=0.25时结点层数的期望是1.33。

    在Redis源码中有详尽的关于插入和删除调整跳表的过程,本文就不展开了,代码并不算难懂,都是纯C写的没有那么多炫技特效,大胆读起来。
    0x0A.谈谈集群版Redis和Gossip协议
    集群版的Redis听起来很高大上,确实相比单实例一主一从或者一主多从模式来说复杂了许多,互联网的架构总是随着业务的发展不断演进的。
    A.1 关于集群的一些基础
  • 单实例Redis架构

  • 最开始的一主N从加上读写分离,Redis作为缓存单实例貌似也还不错,并且有Sentinel哨兵机制,可以实现主从故障迁移。

    单实例一主两从+读写分离结构:

    注:图片来自网络


    单实例的由于本质上只有一台Master作为存储,就算机器为128GB的内存,一般建议使用率也不要超过70%-80%,所以最多使用100GB数据就已经很多了,实际中50%就不错了,以为数据量太大也会降低服务的稳定性,因为数据量太大意味着持久化成本高,可能严重阻塞服务,甚至最终切主。

    如果单实例只作为缓存使用,那么除了在服务故障或者阻塞时会出现缓存击穿问题,可能会有很多请求一起搞死mysql

    如果单实例作为主存,那么问题就比较大了,因为涉及到持久化问题,无论是bgsave还是aof都会造成刷盘阻塞,此时造成服务请求成功率下降,这个并不是单实例可以解决的,因为由于作为主存储,持久化是必须的。

    所以我们期待一个多主多从的Redis系统,这样无论作为主存还是作为缓存,压力和稳定性都会提升,尽管如此,笔者还是建议:Redis尽量不要做主存储!

  • 集群与分片

  • 要支持集群首先要克服的就是分片问题,也就是一致性哈希问题,常见的方案有三种:

    客户端分片这种情况主要是类似于哈希取模的做法,当客户端对服务端的数量完全掌握和控制时,可以简单使用。


    中间层分片这种情况是在客户端和服务器端之间增加中间层,充当管理者和调度者,客户端的请求打向中间层,由中间层实现请求的转发和回收,当然中间层最重要的作用是对多台服务器的动态管理。


    服务端分片不使用中间层实现去中心化的管理模式,客户端直接向服务器中任意结点请求,如果被请求的Node没有所需数据,则像客户端回复MOVED,并告诉客户端所需数据的存储位置,这个过程实际上是客户端和服务端共同配合,进行请求重定向来完成的。


  • 中间层分片的集群版Redis

  • 前面提到了变为N主N从可以有效提高处理能力和稳定性,但是这样就面临一致性哈希的问题,也就是动态扩缩容时的数据问题。

    在Redis官方发布集群版本之前,业内有一些方案迫不及待要用起自研版本的Redis集群,其中包括国内豌豆荚的Codis、国外Twiter的twemproxy。

    核心思想都是在多个Redis服务器和客户端Client中间增加分片层,由分片层来完成数据的一致性哈希和分片问题,每一家的做法有一定的区别,但是要解决的核心问题都是多台Redis场景下的扩缩容、故障转移、数据完整性、数据一致性、请求处理延时等问题。

    业内Codis配合LVS等多种做法实现Redis集群的方案有很多都应用到生成环境中,表现都还不错,主要是官方集群版本在Redis3.0才出现,对其稳定性如何,很多公司都不愿做小白鼠,不过事实上经过迭代目前已经到了Redis5.x版本,官方集群版本还是很不错的,至少笔者这么认为。

  • 服务端分片的官方集群版本

  • 官方版本区别于上面的Codis和Twemproxy,实现了服务器层的Sharding分片技术,换句话说官方没有中间层,而是多个服务结点本身实现了分片,当然也可以认为实现sharding的这部分功能被融合到了Redis服务本身中,并没有单独的Sharding模块。

    之前的文章也提到了官方集群引入slot的概念进行数据分片,之后将数据slot分配到多个Master结点,Master结点再配置N个从结点,从而组成了多实例sharding版本的官方集群架构。

    Redis Cluster 是一个可以在多个 Redis 节点之间进行数据共享的分布式集群,在服务端,通过节点之间的特殊协议进行通讯,这个特殊协议就充当了中间层的管理部分的通信协议,这个协议称作Gossip流言协议。

    分布式系统一致性协议的目的就是为了解决集群中多结点状态通知的问题,是管理集群的基础,如图展示了基于Gossip协议的官方集群架构图:

    注:图片来自网络

    A.2 Redis Cluster的基本运行原理
  • 结点状态信息结构

  • Cluster中的每个节点都维护一份在自己看来当前整个集群的状态,主要包括:
    1. 当前集群状态

    2. 集群中各节点所负责的slots信息,及其migrate状态

    3. 集群中各节点的master-slave状态

    4. 集群中各节点的存活状态及不可达投票


    也就是说上面的信息,就是集群中Node相互八卦传播流言蜚语的内容主题,而且比较全面,既有自己的更有别人的,这么一来大家都相互传,最终信息就全面而且准确了,区别于拜占庭帝国问题,信息的可信度很高。

    基于Gossip协议当集群状态变化时,如新节点加入、slot迁移、节点宕机、slave提升为新Master,我们希望这些变化尽快的被发现,传播到整个集群的所有节点并达成一致。节点之间相互的心跳(PING,PONG,MEET)及其携带的数据是集群状态传播最主要的途径。

  • Gossip协议的概念

  • gossip 协议(gossip protocol)又称 epidemic 协议(epidemic protocol),是基于流行病传播方式的节点或者进程之间信息交换的协议。

    在分布式系统中被广泛使用,比如我们可以使用 gossip 协议来确保网络中所有节点的数据一样。

    gossip protocol 最初是由施乐公司帕洛阿尔托研究中心(Palo Alto Research Center)的研究员艾伦·德默斯(Alan Demers)于1987年创造的。

    https://www.iteblog.com/archives/2505.html
    Gossip协议已经是P2P网络中比较成熟的协议了。Gossip协议的最大的好处是,即使集群节点的数量增加,每个节点的负载也不会增加很多,几乎是恒定的。这就允许Consul管理的集群规模能横向扩展到数千个节点。

    Gossip算法又被称为反熵(Anti-Entropy),熵是物理学上的一个概念,代表杂乱无章,而反熵就是在杂乱无章中寻求一致,这充分说明了Gossip的特点:在一个有界网络中,每个节点都随机地与其他节点通信,经过一番杂乱无章的通信,最终所有节点的状态都会达成一致。每个节点可能知道所有其他节点,也可能仅知道几个邻居节点,只要这些节可以通过网络连通,最终他们的状态都是一致的,当然这也是疫情传播的特点。

    https://www.backendcloud.cn/2017/11/12/raft-gossip/
    上面的描述都比较学术,其实Gossip协议对于我们吃瓜群众来说一点也不陌生,Gossip协议也成为流言协议,说白了就是八卦协议,这种传播规模和传播速度都是非常快的,你可以体会一下。所以计算机中的很多算法都是源自生活,而又高于生活的。

  • Gossip协议的使用

  • Redis 集群是去中心化的,彼此之间状态同步靠 gossip 协议通信,集群的消息有以下几种类型:
    1. Meet 通过「cluster meet ip port」命令,已有集群的节点会向新的节点发送邀请,加入现有集群。

    2. Ping  节点每秒会向集群中其他节点发送 ping 消息,消息中带有自己已知的两个节点的地址、槽、状态信息、最后一次通信时间等。

    3. Pong  节点收到 ping 消息后会回复 pong 消息,消息中同样带有自己已知的两个节点信息。

    4. Fail  节点 ping 不通某节点后,会向集群所有节点广播该节点挂掉的消息。其他节点收到消息后标记已下线。

    由于去中心化和通信机制,Redis Cluster 选择了最终一致性和基本可用。例如当加入新节点时(meet),只有邀请节点和被邀请节点知道这件事,其余节点要等待 ping 消息一层一层扩散。

    除了 Fail 是立即全网通知的,其他诸如新节点、节点重上线、从节点选举成为主节点、槽变化等,都需要等待被通知到,也就是Gossip协议是最终一致性的协议。

    由于 gossip 协议对服务器时间的要求较高,否则时间戳不准确会影响节点判断消息的有效性。另外节点数量增多后的网络开销也会对服务器产生压力,同时结点数太多,意味着达到最终一致性的时间也相对变长,因此官方推荐最大节点数为1000左右。

    如图展示了新加入结点服务器时的通信交互图:

                                                   注:图片来自网络


    总起来说Redis官方集群是一个去中心化的类P2P网络,P2P早些年非常流行,像电驴、BT什么的都是P2P网络。

    在Redis集群中Gossip协议充当了去中心化的通信协议的角色,依据制定的通信规则来实现整个集群的无中心管理节点的自治行为。


  • 基于Gossip协议的故障检测

  • 集群中的每个节点都会定期地向集群中的其他节点发送PING消息,以此交换各个节点状态信息,检测各个节点状态:在线状态、疑似下线状态PFAIL、已下线状态FAIL。

    自己保存信息:当主节点A通过消息得知主节点B认为主节点D进入了疑似下线(PFAIL)状态时,主节点A会在自己的clusterState.nodes字典中找到主节点D所对应的clusterNode结构,并将主节点B的下线报告添加到clusterNode结构的fail_reports链表中,并后续关于结点D疑似下线的状态通过Gossip协议通知其他节点。

    一起裁定:如果集群里面,半数以上的主节点都将主节点D报告为疑似下线,那么主节点D将被标记为已下线(FAIL)状态,将主节点D标记为已下线的节点会向集群广播主节点D的FAIL消息,所有收到FAIL消息的节点都会立即更新nodes里面主节点D状态标记为已下线。

    最终裁定:将 node 标记为 FAIL 需要满足以下两个条件:
    1. 有半数以上的主节点将 node 标记为 PFAIL 状态。

    2. 当前节点也将 node 标记为 PFAIL 状态。


    也就是说当前节点发现其他结点疑似挂掉了,那么就写在自己的小本本上,等着通知给其他好基友,让他们自己也看看,最后又一半以上的好基友都认为那个节点挂了,并且那个节点自己也认为自己挂了,那么就是真的挂了,过程还是比较严谨的。
    0x0B.谈谈对Redis的内存回收机制的理解

    Redis作为内存型数据库,如果单纯的只进不出早晚就撑爆了,事实上很多把Redis当做主存储DB用的家伙们早晚会尝到这个苦果,当然除非你家厂子确实不差钱,数T级别的内存都毛毛雨,或者数据增长一定程度之后不再增长的场景,就另当别论了。


    为了让Redis服务安全稳定的运行,让使用内存保持在一定的阈值内是非常有必要的,因此我们就需要删除该删除的,清理该清理的,把内存留给需要的键值对,试想一条大河需要设置几个警戒水位来确保不决堤不枯竭,Redis也是一样的,只不过Redis只关心决堤即可,来一张图:

    图中设定机器内存为128GB,占用64GB算是比较安全的水平,如果内存接近80%也就是100GB左右,那么认为Redis目前承载能力已经比较大了,具体的比例可以根据公司和个人的业务经验来确定。


    笔者只是想表达出于安全和稳定的考虑,不要觉得128GB的内存就意味着存储128GB的数据,都是要打折的。

    B.1 回收的内存从哪里来

    Redis占用的内存是分为两部分:存储键值对消耗和本身运行消耗显然后者我们无法回收,因此只能从键值对下手了,键值对可以分为几种:带过期的、不带过期的、热点数据、冷数据对于带过期的键值是需要删除的,如果删除了所有的过期键值对之后内存仍然不足怎么办?那只能把部分数据给踢掉了。


    B.2 如何实施过期键值对的删除

    要实施对键值对的删除我们需要明白如下几点:

  • 带过期超时的键值对存储在哪里

  • 如何判断带超时的键值对是否可以被删除了?

  • 删除机制有哪些以及如何选择

  • 1.键值对的存储

    老规矩来到github看下源码,src/server.h中给的redisDb结构体给出了答案:

    typedef struct redisDb 
        dict *dict;                 /* The keyspace for this DB */
        dict *expires;              /* Timeout of keys with a timeout set */
        dict *blocking_keys;        /* Keys with clients waiting for data (BLPOP)*/
        dict *ready_keys;           /* Blocked keys that received a PUSH */
        dict *watched_keys;         /* WATCHED keys for MULTI/EXEC CAS */
        int id;                     /* Database ID */
        long long avg_ttl;          /* Average TTL, just for stats */
        unsigned long expires_cursor; /* Cursor of the active expire cycle. */
        list *defrag_later;         /* List of key names to attempt to defrag one by one, gradually. */
     redisDb;

    Redis本质上就是一个大的key-value,key就是字符串,value有是几种对象:字符串、列表、有序列表、集合、哈希等,这些key-value都是存储在redisDb的dict中的,来看下黄健宏画的一张非常赞的图:

    看到这里,对于删除机制又清晰了一步,我们只要把redisDb中dict中的目标key-value删掉就行,不过貌似没有这么简单,Redis对于过期键值对肯定有自己的组织规则,让我们继续研究吧!

    redisDb的expires成员的类型也是dict,和键值对是一样的,本质上expires是dict的子集,expires保存的是所有带过期的键值对,称之为过期字典吧,它才是我们研究的重点。

    对于键,我们可以设置绝对和相对过期时间、以及查看剩余时间

    1. 使用EXPIRE和PEXPIRE来实现键值对的秒级和毫秒级生存时间设定,这是相对时长的过期设置

    2. 使用EXPIREAT和EXPIREAT来实现键值对在某个秒级和毫秒级时间戳时进行过期删除,属于绝对过期设置

    3. 通过TTL和PTTL来查看带有生存时间的键值对的剩余过期时间

    上述三组命令在设计缓存时用处比较大,有心的读者可以留意。

    过期字典expires和键值对空间dict存储的内容并不完全一样,过期字典expires的key是指向Redis对应对象的指针,其value是long long型的unix时间戳,前面的EXPIRE和PEXPIRE相对时长最终也会转换为时间戳,来看下过期字典expires的结构,笔者画了个图:

    2. 键值对的过期删除判断

    判断键是否过期可删除,需要先查过期字典是否存在该值,如果存在则进一步判断过期时间戳当前时间戳相对大小,做出删除判断,简单的流程如图:


    3. 键值对的删除策略

    经过前面的几个环节,我们知道了Redis的两种存储位置:键空间和过期字典,以及过期字典expires的结构、判断是否过期的方法,那么该如何实施删除呢?

    先抛开Redis来想一下可能的几种删除策略

  • 定时删除:在设置键的过期时间的同时,创建定时器,让定时器在键过期时间到来时,即刻执行键值对的删除;

  • 定期删除:每隔特定的时间对数据库进行一次扫描,检测并删除其中的过期键值对;

  • 惰性删除:键值对过期暂时不进行删除,至于删除的时机与键值对的使用有关,当获取键时先查看其是否过期,过期就删除,否则就保留;

  • 在上述的三种策略中定时删除和定期删除属于不同时间粒度的主动删除,惰性删除属于被动删除

    三种策略都有各自的优缺点:定时删除对内存使用率有优势,但是对CPU不友好惰性删除对内存不友好,如果某些键值对一直不被使用,那么会造成一定量的内存浪费,定期删除是定时删除和惰性删除的折中。

    Reids采用的是惰性删除和定时删除的结合,一般来说可以借助最小堆来实现定时器,不过Redis的设计考虑到时间事件的有限种类和数量,使用了无序链表存储时间事件,这样如果在此基础上实现定时删除,就意味着O(N)遍历获取最近需要删除的数据。

    但是我觉得antirez如果非要使用定时删除,那么他肯定不会使用原来的无序链表机制,所以个人认为已存在的无序链表不能作为Redis不使用定时删除的根本理由冒昧猜测唯一可能的是antirez觉得没有必要使用定时删除。

    4. 定期删除的实现细节

    定期删除听着很简单,但是如何控制执行的频率和时长呢?

    试想一下如果执行频率太少就退化为惰性删除了,如果执行时间太长又和定时删除类似了,想想还确实是个难题!并且执行定期删除的时机也需要考虑,所以我们继续来看看Redis是如何实现定期删除的吧!笔者在src/expire.c文件中找到了activeExpireCycle函数,定期删除就是由此函数实现的,在代码中antirez做了比较详尽的注释,不过都是英文的,试着读了一下模模糊糊弄个大概,所以学习英文并阅读外文资料是很重要的学习途径

    先贴一下代码,核心部分算上注释大约210行,具体看下:

    #define ACTIVE_EXPIRE_CYCLE_KEYS_PER_LOOP 20 /* Keys for each DB loop. */
    #define ACTIVE_EXPIRE_CYCLE_FAST_DURATION 1000 /* Microseconds. */
    #define ACTIVE_EXPIRE_CYCLE_SLOW_TIME_PERC 25 /* Max % of CPU to use. */
    #define ACTIVE_EXPIRE_CYCLE_ACCEPTABLE_STALE 10 /* % of stale keys after which
                                                       we do extra efforts. */


    void activeExpireCycle(int type) 
        /* Adjust the running parameters according to the configured expire
         * effort. The default effort is 1, and the maximum configurable effort
         * is 10. */

        unsigned long
        effort = server.active_expire_effort-1/* Rescale from 0 to 9. */
        config_keys_per_loop = ACTIVE_EXPIRE_CYCLE_KEYS_PER_LOOP +
                               ACTIVE_EXPIRE_CYCLE_KEYS_PER_LOOP/4*effort,
        config_cycle_fast_duration = ACTIVE_EXPIRE_CYCLE_FAST_DURATION +
                                     ACTIVE_EXPIRE_CYCLE_FAST_DURATION/4*effort,
        config_cycle_slow_time_perc = ACTIVE_EXPIRE_CYCLE_SLOW_TIME_PERC +
                                      2*effort,
        config_cycle_acceptable_stale = ACTIVE_EXPIRE_CYCLE_ACCEPTABLE_STALE-
                                        effort;

        /* This function has some global state in order to continue the work
         * incrementally across calls. */

        static unsigned int current_db = 0/* Last DB tested. */
        static int timelimit_exit = 0;      /* Time limit hit in previous call? */
        static long long last_fast_cycle = 0/* When last fast cycle ran. */

        int j, iteration = 0;
        int dbs_per_call = CRON_DBS_PER_CALL;
        long long start = ustime(), timelimit, elapsed;

        /* When clients are paused the dataset should be static not just from the
         * POV of clients not being able to write, but also from the POV of
         * expires and evictions of keys not being performed. */

        if (clientsArePaused()) return;

        if (type == ACTIVE_EXPIRE_CYCLE_FAST) 
            /* Don\'t start a fast cycle if the previous cycle did not exit
             * for time limit, unless the percentage of estimated stale keys is
             * too high. Also never repeat a fast cycle for the same period
             * as the fast cycle total duration itself. */

            if (!timelimit_exit &&
                server.stat_expired_stale_perc < config_cycle_acceptable_stale)
                return;

            if (start < last_fast_cycle + (long long)config_cycle_fast_duration*2)
                return;

            last_fast_cycle = start;
        

        /* We usually should test CRON_DBS_PER_CALL per iteration, with
         * two exceptions:
         *
         * 1) Don\'t test more DBs than we have.
         * 2) If last time we hit the time limit, we want to scan all DBs
         * in this iteration, as there is work to do in some DB and we don\'t want
         * expired keys to use memory for too much time. */

        if (dbs_per_call > server.dbnum || timelimit_exit)
            dbs_per_call = server.dbnum;

        /* We can use at max \'config_cycle_slow_time_perc\' percentage of CPU
         * time per iteration. Since this function gets called with a frequency of
         * server.hz times per second, the following is the max amount of
         * microseconds we can spend in this function. */

        timelimit = config_cycle_slow_time_perc*1000000/server.hz/100;
        timelimit_exit = 0;
        if (timelimit <= 0) timelimit = 1;

        if (type == ACTIVE_EXPIRE_CYCLE_FAST)
            timelimit = config_cycle_fast_duration; /* in microseconds. */

        /* Accumulate some global stats as we expire keys, to have some idea
         * about the number of keys that are already logically expired, but still
         * existing inside the database. */

        long total_sampled = 0;
        long total_expired = 0;

        for (j = 0; j < dbs_per_call && timelimit_exit == 0; j++) 
            /* Expired and checked in a single loop. */
            unsigned long expired, sampled;

            redisDb *db = server.db+(current_db % server.dbnum);

            /* Increment the DB now so we are sure if we run out of time
             * in the current DB we\'ll restart from the next. This allows to
             * distribute the time evenly across DBs. */

            current_db++;

            /* Continue to expire if at the end of the cycle more than 25%
             * of the keys were expired. */

            do 
                unsigned long num, slots;
                long long now, ttl_sum;
                int ttl_samples;
                iteration++;

                /* If there is nothing to expire try next DB ASAP. */
                if ((num = dictSize(db->expires)) == 0
                    db->avg_ttl = 0;
                    break;
                
                slots = dictSlots(db->expires);
                now = mstime();

                /* When there are less than 1% filled slots, sampling the key
                 * space is expensive, so stop here waiting for better times...
                 * The dictionary will be resized asap. */

                if (num && slots > DICT_HT_INITIAL_SIZE &&
                    (num*100/slots < 1)) break;

                /* The main collection cycle. Sample random keys among keys
                 * with an expire set, checking for expired ones. */

                expired = 0;
                sampled = 0;
                ttl_sum = 0;
                ttl_samples = 0;

                if (num > config_keys_per_loop)
                    num = config_keys_per_loop;

                /* Here we access the low level representation of the hash table
                 * for speed concerns: this makes this code coupled with dict.c,
                 * but it hardly changed in ten years.
                 *
                 * Note that certain places of the hash table may be empty,
                 * so we want also a stop condition about the number of
                 * buckets that we scanned. However scanning for free buckets
                 * is very fast: we are in the cache line scanning a sequential
                 * array of NULL pointers, so we can scan a lot more buckets
                 * than keys in the same time. */

                long max_buckets = num*20;
                long checked_buckets = 0;

                while (sampled < num && checked_buckets < max_buckets) 
                    for (int table = 0; table < 2; table++) 
                        if (table == 1 && !dictIsRehashing(db->expires)) break;

                        unsigned long idx = db->expires_cursor;
                        idx &= db->expires->ht[table].sizemask;
                        dictEntry *de = db->expires->ht[table].table[idx];
                        long long ttl;

                        /* Scan the current bucket of the current table. */
                        checked_buckets++;
                        while(de) 
                            /* Get the next entry now since this entry may get
                             * deleted. */

                            dictEntry *e = de;
                            de = de->next;

                            ttl = dictGetSignedIntegerVal(e)-now;
                            if (activeExpireCycleTryExpire(db,e,now)) expired++;
                            if (ttl > 0
                                /* We want the average TTL of keys yet
                                 * not expired. */

                                ttl_sum += ttl;
                                ttl_samples++;
                            
                            sampled++;
                        
                    
                    db->expires_cursor++;
                
                total_expired += expired;
                total_sampled += sampled;

                /* Update the average TTL stats for this database. */
                if (ttl_samples) 
                    long long avg_ttl = ttl_sum/ttl_samples;

                    /* Do a simple running average with a few samples.
                     * We just use the current estimate with a weight of 2%
                     * and the previous estimate with a weight of 98%. */

                    if (db->avg_ttl == 0) db->avg_ttl = avg_ttl;
                    db->avg_ttl = (db->avg_ttl/50)*49 + (avg_ttl/50);
                

                /* We can\'t block forever here even if there are many keys to
                 * expire. So after a given amount of milliseconds return to the
                 * caller waiting for the other active expire cycle. */

                if ((iteration & 0xf) == 0)  /* check once every 16 iterations. */
                    elapsed = ustime()-start;
                    if (elapsed > timelimit) 
                        timelimit_exit = 1;
                        server.stat_expired_time_cap_reached_count++;
                        break;
                    
                
                /* We don\'t repeat the cycle for the current database if there are
                 * an acceptable amount of stale keys (logically expired but yet
                 * not reclained). */

             while ((expired*100/sampled) > config_cycle_acceptable_stale);
        

        elapsed = ustime()-start;
        server.stat_expire_cycle_time_used += elapsed;
        latencyAddSampleIfNeeded("expire-cycle",elapsed/1000);

        /* Update our estimate of keys existing but yet to be expired.
         * Running average with this sample accounting for 5%. */

        double current_perc;
        if (total_sampled) 
            current_perc = (double)total_expired/total_sampled;
         else
            current_perc = 0;
        server.stat_expired_stale_perc = (current_perc*0.05)+
                                         (server.stat_expired_stale_perc*0.95);

    说实话这个代码细节比较多,由于笔者对Redis源码了解不多,只能做个模糊版本的解读,所以难免有问题,还是建议有条件的读者自行前往源码区阅读,抛砖引玉看下笔者的模糊版本

  • 该算法是个自适应的过程,当过期的key比较少时那么就花费很少的cpu时间来处理,如果过期的key很多就采用激进的方式来处理,避免大量的内存消耗,可以理解为判断过期键多就多跑几次,少则少跑几次

  • 由于Redis中有很多数据库db,该算法会逐个扫描,本次结束时继续向后面的db扫描,是个闭环的过程

  • 定期删除有快速循环和慢速循环两种模式,主要采用慢速循环模式,其循环频率主要取决于server.hz,通常设置为10,

    深度干货 | 38道Java基础面试题 (1.2W字详细解析)

    目录

    基础语法

    1、Java 语言的优点?

    1、平台无关性,摆脱硬件束缚,“一次编写,到处运行”。

    2、相对安全的内存管理和访问机制,避免大部分内存泄漏和指针越界。

    3、热点代码检测和运行时编译及优化,使程序随运行时间增长获得更高性能。

    4、 完善的应用程序接口,支持第三方类库。

    5、持网络编程并且很方便。


    2、Java 如何实现平台无关?

    JVM: Java 编译器可生成与计算机体系结构无关的字节码指令,字节码文件不仅可以轻易地在任何机器上解释执行,还可以动态地转换成本地机器代码,转换是由 JVM 实现的,JVM 是平台相关的,屏蔽了不同操作系统的差异。

    语言规范: 基本数据类型大小有明确规定,例如 int 永远为 32 位,而 C/C++ 中可能是 16 位、32 位,也可能是编译器开发商指定的其他大小。Java 中数值类型有固定字节数,二进制数据以固定格式存储和传输,字符串采用标准的 Unicode 格式存储。


    3、JVM,JDK 和 JRE 的区别?

    JDK: Java Development Kit,开发工具包。提供了编译运行 Java 程序的各种工具,包括编译器、JRE 及常用类库,是 JAVA 核心。

    JRE: Java Runtime Environment,运行时环境,运行 Java 程序的必要环境,包括 JVM、核心类库、核心配置工具。

    JDK包含JRE,JRE包含JVM。


    4、Java 按值调用还是引用调用?

    按值调用指方法接收调用者提供的值,按引用调用指方法接收调用者提供的变量地址。

    Java 总是按值调用,方法得到的是所有参数值的副本,传递对象时实际上方法接收的是对象引用的副本。方法不能修改基本数据类型的参数,如果传递了一个 int 值 ,改变值不会影响实参,因为改变的是值的一个副本。

    可以改变对象参数的状态,但不能让对象参数引用一个新的对象。如果传递了一个 int 数组,改变数组的内容会影响实参,而改变这个参数的引用并不会让实参引用新的数组对象。


    5、浅拷贝和深拷贝的区别?

    浅拷贝: 只复制当前对象的基本数据类型及引用变量,没有复制引用变量指向的实际对象。修改克隆对象可能影响原对象,不安全。

    深拷贝: 完全拷贝基本数据类型和引用数据类型,安全。


    6、什么是反射?

    概念: 在运行状态中,对于任意一个类都能知道它的所有属性和方法,对于任意一个对象都能调用它的任意方法和属性,这种动态获取信息及调用对象方法的功能称为反射。

    原理: 通过将类对应的字节码文件加载到jvm内存中得到一个Class对象,通过这个Class对象可以反向获取实例的各个属性以及调用它的方法。

    使用场景:

    1、通过反射运行配置文件内容

    2、JDK动态代理

    3、jdbc通过Class.forName()加载数据的驱动程序

    4、Spring解析xml装配Bean


    7、Class 类的作用?如何获取一个 Class 对象?

    在程序运行期间,Java 运行时系统为所有对象维护一个运行时类型标识,这个信息会跟踪每个对象所属的类,虚拟机利用运行时类型信息选择要执行的正确方法,保存这些信息的类就是 Class,这是一个泛型类。

    获取 Class 对象:① 类名.class 。②对象的 getClass方法。③ Class.forName(类的全限定名)


    8、什么是注解?什么是元注解?

    注解是一种标记,使类或接口附加额外信息,帮助编译器和 JVM 完成一些特定功能,例如 @Override 标识一个方法是重写方法。

    元注解是自定义注解的注解,例如:

    @Target:约束作用位置,值是 ElementType 枚举常量,包括 METHOD 方法、VARIABLE 变量、TYPE 类/接口、PARAMETER 方法参数、CONSTRUCTORS 构造方法和 LOACL_VARIABLE 局部变量等。

    @Rentention:约束生命周期,值是 RetentionPolicy 枚举常量,包括 SOURCE 源码、CLASS 字节码和 RUNTIME 运行时。

    @Documented:表明这个注解应该被 javadoc 记录。


    9、什么是泛型,有什么作用?

    泛型就是将类型参数化,其在编译时才确定具体的参数

    泛型的好处:

    1、 类型安全,放置什么出来就是什么,不存在 ClassCastException。

    2、 提升可读性,编码阶段就显式知道泛型集合、泛型方法等处理的对象类型。

    3、 代码重用,合并了同类型的处理代码。

    4、消除强制类型转换

    10、泛型擦除是什么?

    泛型擦除:使用泛型的时候加上的类型参数,编译器在编译的时候去掉类型参数。

    泛型用于编译阶段,编译后的字节码文件不包含泛型类型信息,因为虚拟机没有泛型类型对象,所有对象都属于普通类。例如定义 List<Object>List<String>,在编译后都会变成 List

    定义一个泛型类型,会自动提供一个对应原始类型,类型变量会被擦除。如果没有限定类型就会替换为 Object,如果有限定类型就会替换为第一个限定类型,例如 <T extends A & B> 会使用 A 类型替换 T。


    11、JDK8 新特性有哪些?

    lambda 表达式: 允许把函数作为参数传递到方法,简化匿名内部类代码。

    函数式接口: 使用 @FunctionalInterface 标识,有且仅有一个抽象方法,可被隐式转换为 lambda 表达式。

    方法引用: 可以引用已有类或对象的方法和构造方法,进一步简化 lambda 表达式。

    接口: 接口可以定义 default 修饰的默认方法,降低了接口升级的复杂性,还可以定义静态方法。

    注解: 引入重复注解机制,相同注解在同地方可以声明多次。注解作用范围也进行了扩展,可作用于局部变量、泛型、方法异常等。

    类型推测: 加强了类型推测机制,使代码更加简洁。

    Optional 类: 处理空指针异常,提高代码可读性。

    Stream 类: 引入函数式编程风格,提供了很多功能,使代码更加简洁。方法包括 forEach 遍历、count 统计个数、filter 按条件过滤、limit 取前 n 个元素、skip 跳过前 n 个元素、map 映射加工、concat 合并 stream 流等。

    日期: 增强了日期和时间 API,新的 java.time 包主要包含了处理日期、时间、日期/时间、时区、时刻和时钟等操作。

    JavaScript: 提供了一个新的 JavaScript 引擎,允许在 JVM上运行特定 JavaScript 应用。


    12、异常有哪些分类?

    所有异常都是 Throwable 的子类,分为 ErrorException。Exception的子类为RuntimeException异常和RuntimeException以外的异常(例如IOException)。

    因此异常主要分为Error,RuntimeException异常和RuntimeException以外的异常。(错误、运行时异常和编译时异常)

    Error就是一些程序处理不了的错误,代表JVM出现了一些错误,应用程序无法处理。例如当JVM不再有继续执行操作所需的内存资源时,将出现 OutOfMemoryError

    常见异常: NullPointerException、ClassNotFoundException、arrayindexoutofboundsexception、ClassCastException(类型强制转换)


    数据类型

    1、Java 有哪些基本数据类型?

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-CMxcCBDr-1655393678043)(Java基础.assets/image-20210219172725756.png)]

    数据类型内存大小默认值取值范围
    byte1 B(byte)0-128 ~ 127
    short2 B(short)0-215 ~ 215-1
    int4 B0-231 ~ 231-1
    long8 B0L-263 ~ 263-1
    float4 B0.0F±3.4E+38(有效位数 6~7 位)
    double8 B0.0D±1.7E+308(有效位数 15 位)
    char英文 1B,中文 UTF-8 占 3B,GBK 占 2B。‘\\u0000’‘\\u0000’ ~ ‘\\uFFFF’
    boolean单个变量 4B / 数组 1Bfalsetrue、false

    JVM 没有 boolean 赋值的专用字节码指令,boolean f = false 就是使用 ICONST_0 即常数 0 赋值。单个 boolean 变量用 int 代替,boolean 数组会编码成 byte 数组。


    2、自动装箱/拆箱是什么?

    每个基本数据类型都对应一个包装类,除了 int 和 char 对应 Integer 和 Character 外,其余基本数据类型的包装类都是首字母大写即可。

    自动装箱: 将基本数据类型包装为一个包装类对象,例如向一个泛型为 Integer 的集合添加 int 元素。

    自动拆箱: 将一个包装类对象转换为一个基本数据类型,例如将一个包装类对象赋值给一个基本数据类型的变量。

    比较两个包装类数值要用 equals ,而不能用 ==


    3、String 是不可变类为什么值可以修改?

    String 类和其存储数据的成员变量 value 字节数组都是 final 修饰的。对一个 String 对象的任何修改实际上都是创建一个新 String 对象,再引用该对象。只是修改 String 变量引用的对象,没有修改原 String 对象的内容。


    4、字符串拼接的方式有哪些?

    ① 直接用 + ,底层用 StringBuilder 实现。只适用小数量,如果在循环中使用 + 拼接,相当于不断创建新的 StringBuilder 对象再转换成 String 对象,效率极差。

    ② 使用 String 的 concat 方法,该方法中使用 Arrays.copyOf 创建一个新的字符数组 buf 并将当前字符串 value 数组的值拷贝到 buf 中,buf 长度 = 当前字符串长度 + 拼接字符串长度。之后调用 getChars 方法使用 System.arraycopy 将拼接字符串的值也拷贝到 buf 数组,最后用 buf 作为构造参数 new 一个新的 String 对象返回。效率稍高于直接使用 +

    ③ 使用 StringBuilder 或 StringBuffer,两者的 append 方法都继承自 AbstractStringBuilder,该方法首先使用 Arrays.copyOf 确定新的字符数组容量,再调用 getChars 方法使用 System.arraycopy 将新的值追加到数组中。StringBuilder 是 JDK5 引入的,效率高但线程不安全。StringBuffer 使用 synchronized 保证线程安全。


    5、String a = “a” + new String(“b”) 创建了几个对象?

    常量和常量拼接仍是常量,结果在常量池,只要有变量参与拼接结果就是变量,存在堆。

    使用字面量时只创建一个常量池中的常量,使用 new 时如果常量池中没有该值就会在常量池中新创建,再在堆中创建一个对象引用常量池中常量。因此 String a = "a" + new String("b") 会创建四个对象,常量池中的 a 和 b,堆中的 b 和堆中的 ab。


    面向对象

    1、谈一谈你对面向对象的理解 ?

    面向过程: 一件事该怎么做,注重实现过程,以过程为中心

    面向对象: 实现对象是谁,只关心怎样使用,不关心具体实现(只关心实现对象是谁,有封装、继承、多态三大特性)

    面向对象是一种编程思想,早期的面向过程的思想就是一件事该怎么做,而面向对象就是一件事该由谁来做,它怎么做的我不管,我只需要调用就行。而这些是由面向对象的三大特性来实现的,三大特性就是封装、继承、多态。封装就是将一类属性和行为抽象成一个类,
    使其属性私有化,行为公开化,提高属性的安全性的同时,也可以使代码模块化,这样做使代码的复用性更高。继承就是将几个类共有的属性和行为抽象成一个父类,每个子类都有父类的属性和行为,也有自己的属性和行为,这样做,扩展了已存在的代码,进一步提高的代码的复用性,但是继承是耦合度很高的一种关系,父类代码修改,子类行为也会改变,如果过度使用继承会起到反效果。多态必须要有继承和重写,并且父类/接口引用指向子类/实现类对象,很多的设计模式都是基于面向对象中多态性设计的。


    2、面向对象的三大特性?

    **封装: ** 可以不用关心内部实现,具体构造,只需知道怎么操作它就是,比如电视,手机,将内部封装起来,直接使用

    多态: 同一个方法调用,由于对象不同可能会有不同的行为。比如都是休息,张三是睡觉,李四是爬山等;或则具体场景中,我不知道现在具体传进来的对象是student,还是teacher,那么可以用pepole去接收它。
    多态的存在要有3个必要条件:继承,方法重写,父类引用指向子类对象。3.父类引用指向子类对象后,用该父类引用调用子类重写的方法,此时多态就出现了

    继承: 使代码更容易扩展,比如有学生教师,他们都有一些公用方法与属性,可以将其抽取出来定义为父类,再去继承它,复用代码,减少冗余,易于扩展


    3、重载和重写的区别?

    方法的重载和重写都是实现多态的方式,区别在于前者实现的是编译时的多态性,而后者实现的是运行时的多态性。

    重载: 发生在同一个类中,方法名相同参数列表不同(参数类型不同、个数不同、顺序不同),与方法返回值和访问修饰符无关,即重载的方法不能根据返回类型进行区分

    重写: 发生在父子类中,方法名、参数列表必须相同,返回值小于等于父类,
    抛出的异常
    小于等于父类,访问修饰符大于等于父类(里氏代换原则);如果父类方法访问修饰符为private则子类中就不是重写。


    4、类之间有哪些关系?

    类关系描述权力强侧举例
    继承父子类之间的关系:is-a父类小狗继承于动物
    实现接口和实现类之间的关系:can-do接口小狗实现了狗叫接口
    组合比聚合更强的关系:contains-a整体头是身体的一部分
    聚合暂时组装的关系:has-a组装方小狗和绳子是暂时的聚合关系
    依赖一个类用到另一个:depends-a被依赖方人养小狗,人依赖于小狗
    关联平等的使用关系:links-a平等人使用卡消费,卡可以提取人的信息

    5、Object 类有哪些方法?

    1、getClass: final方法,获得运行时类型。

    2、toString: 对象的字符串表示形式(对象所属类的名称+@+转换为十六进制的对象的哈希值组成的字符串)

    3、equas方法: 检测对象是否相等,默认使用 == 比较对象引用,可以重写 equals 方法自定义比较规则。

    4、Clone方法: 保护方法,实现对象的浅复制,只有实现了Cloneable接口才可以调用该方法,否则抛出CloneNotSupportedException异常。
    5、notify方法: 唤醒在该对象上等待的某个线程

    6、notifyAll方法: 唤醒在该对象上等待的所有线程

    7、wait方法: wait()的作用是让当前线程进入等待状态,同时,wait()也会让当前线程释放它所持有的锁,直到其他线程调用此对象的notify()方法或 notifyAll()方法”,当前线程被唤醒(进入“就绪状态”)。还有一个wait(long timeout)超时时间-补充sleep不会释放锁

    8、Finalize()方法: 可以用于对象的自我拯救
    9、Hashcode方法: 该方法用于哈希查找,可以减少在查找中使用equals的次数,重写了equals方法一般都要重写hashCode方法。这个方法在一些具有哈希功能的Collection中用到。一般必须满足obj1.equals(obj2) == true。可以推出obj1.hashCode() == obj2.hashCode(),但是hashCode相等不一定就满足equals。不过为了提高效率,应该尽量使上面两个条件接近等价


    6、内部类的作用是什么,有哪些分类?

    内部类可对同一包中其他类隐藏,内部类方法可以访问定义这个内部类的作用域中的数据,包括 private 数据。

    内部类是一个编译器现象,与虚拟机无关。编译器会把内部类转换成常规的类文件,用 $ 分隔外部类名与内部类名,其中匿名内部类使用数字编号,虚拟机对此一无所知。

    静态内部类: 属于外部类,只加载一次。作用域仅在包内,可通过 外部类名.内部类名 直接访问,类内只能访问外部类所有静态属性和方法。HashMap 的 Node 节点,ReentrantLock 中的 Sync 类,ArrayList 的 SubList 都是静态内部类。内部类中还可以定义内部类,如 ThreadLoacl 静态内部类 ThreadLoaclMap 中定义了内部类 Entry。

    成员内部类: 属于外部类的每个对象,随对象一起加载。不可以定义静态成员和方法,可访问外部类的所有内容。

    局部内部类: 定义在方法内,不能声明访问修饰符,只能定义实例成员变量和实例方法,作用范围仅在声明类的代码块中。

    匿名内部类: 只用一次的没有名字的类,可以简化代码,创建的对象类型相当于 new 的类的子类类型。用于实现事件监听和其他回调。


    7、访问权限控制符有哪些?

    访问权限控制符本类包内包外子类任何地方
    public
    protected×
    default××
    private×××

    8、接口和抽象类的异同?

    相同: 接口和抽象类对实体类进行更高层次的抽象,仅定义公共行为和特征。

    区别抽象类接口
    成员变量无特殊要求,可以和普通类─样定义任意类型只能是 public static final 常量
    构造方法有构造方法,不能实例化没有构造方法,不能实例化
    方法抽象类可以没有抽象方法,但有抽象方法一定是抽象类。接口只有定义,不能有方法的实现,JDK8 支持默认/静态方法,JDK9 支持私有方法
    继承单继承多继承

    9、接口和抽象类应该怎么选择?

    抽象类体现 is-a 关系,接口体现 can-do 关系。与接口相比,抽象类通常是对同类事物相对具体的抽象。

    抽象类是模板式设计,包含一组具体特征,例如某汽车,底盘、控制电路等是抽象出来的共同特征,但内饰、显示屏、座椅材质可以根据不同级别配置存在不同实现。

    接口是契约式设计,是开放的,定义了方法名、参数、返回值、抛出的异常类型,谁都可以实现它,但必须遵守接口的约定。例如所有车辆都必须实现刹车这种强制规范。

    接口是顶级类,抽象类在接口下面的第二层,对接口进行了组合,然后实现部分接口。当纠结定义接口和抽象类时,推荐定义为接口,遵循接口隔离原则,按维度划分成多个接口,再利用抽象类去实现这些,方便后续的扩展和重构。

    例如 Plane 和 Bird 都有 fly 方法,应把 fly 定义为接口,而不是抽象类的抽象方法再继承,因为除了 fly 行为外 Plane 和 Bird 间很难再找到其他共同特征。


    10、子类初始化的顺序

    ① 父类静态代码块和静态变量。② 子类静态代码块和静态变量。③ 父类普通代码块和普通变量。④ 父类构造方法。⑤ 子类普通代码块和普通变量。⑥ 子类构造方法。

    String相关

    1、String str="aaa"与 String str=new String(“aaa”)一样吗?new String(“aaa”);创建了几个字符串对象?

    2、String有哪些特性?

    3、在使用 HashMap 的时候,用 String 做 key 有什么好处?

    HashMap 内部实现是通过 key 的 hashcode 来确定 value 的存储位置,因为字符串是不可变的,所以当创建字符串时,它的 hashcode 被缓存下来,不需要再次计算,所以相比于其他对象更快。

    对象相等判断

    1、== 和 equals 区别是什么?

    如果==比较的是基本数据类型,那么比较的是两个基本数据类型的值是否相等;

    如果==是比较的两个对象,那么比较的是两个对象的引用

    equals方法主要用于两个对象之间,检测一个对象是否等于另一个对象

    2、hashCode(),equals()两种方法是什么关系?

    如果两个对象相等,则hashcode一定也是相同的;

    两个对象相等,对两个对象分别调用equals方法都返回true;

    两个对象有相同的hashcode值,它们也不一定是相等的;

    3、为什么重写 equals 方法必须重写 hashcode 方法 ?

    判断的时候先根据hashcode进行的判断,相同的情况下再根据equals()方法进行判断。如果只重写了equals方法,而不重写hashcode的方法,会造成hashcode的值不同,而equals()方法判断出来的结果为true。

    在Java中的一些容器中,不允许有两个完全相同的对象,插入的时候,如果判断相同则会进行覆盖。这时候如果只重写了equals()的方法,而不重写hashcode的方法,Object中hashcode是根据对象的存储地址转换而形成的一个哈希值。这时候就有可能因为没有重写hashcode方法,造成相同的对象散列到不同的位置而造成对象的不能覆盖的问题。[String,StringBuffer, StringBuilder 的区别是什么?

    4、String,StringBuffer, StringBuilder 的区别是什么?

    可变和不可变:

    是否多线程安全:

    性能:

    5、String为什么要设计成不可变的?

    1、便于实现字符串池

    2、使多线程安全

    3、避免安全问题

    4、加快字符串处理速度

    IO 流

    1、同步/异步/阻塞/非阻塞 IO 的区别?

    同步和异步是通信机制,阻塞和非阻塞是调用状态。

    同步 IO 是用户线程发起 IO 请求后需要等待或轮询内核 IO 操作完成后才能继续执行。异步 IO 是用户线程发起 IO 请求后可以继续执行,当内核 IO 操作完成后会通知用户线程,或调用用户线程注册的回调函数。

    阻塞 IO 是 IO 操作需要彻底完成后才能返回用户空间 。非阻塞 IO 是 IO 操作调用后立即返回一个状态值,无需等 IO 操作彻底完成。


    2、什么是 BIO?

    BIO 是同步阻塞式 IO,JDK1.4 之前的 IO 模型。服务器实现模式为一个连接请求对应一个线程,服务器需要为每一个客户端请求创建一个线程,如果这个连接不做任何事会造成不必要的线程开销。可以通过线程池改善,这种 IO 称为伪异步 IO。适用连接数目少且服务器资源多的场景。


    3、什么是 NIO?

    NIO 是 JDK1.4 引入的同步非阻塞 IO。服务器实现模式为多个连接请求对应一个线程,客户端连接请求会注册到一个多路复用器 Selector ,Selector 轮询到连接有 IO 请求时才启动一个线程处理。适用连接数目多且连接时间短的场景。

    同步是指线程还是要不断接收客户端连接并处理数据,非阻塞是指如果一个管道没有数据,不需要等待,可以轮询下一个管道。

    核心组件:


    4、什么是 AIO?

    AIO 是 JDK7 引入的异步非阻塞 IO。服务器实现模式为一个有效请求对应一个线程,客户端的 IO 请求都是由操作系统先完成 IO 操作后再通知服务器应用来直接使用准备好的数据。适用连接数目多且连接时间长的场景。

    异步是指服务端线程接收到客户端管道后就交给底层处理IO通信,自己可以做其他事情,非阻塞是指客户端有数据才会处理,处理好再通知服务器。

    实现方式包括通过 Future 的 get 方法进行阻塞式调用以及实现 CompletionHandler 接口,重写请求成功的回调方法 completed 和请求失败回调方法 failed


    5、java.io 包下有哪些流?

    主要分为字符流和字节流,字符流一般用于文本文件,字节流一般用于图像或其他文件。

    字符流包括了字符输入流 Reader 和字符输出流 Writer,字节流包括了字节输入流 InputStream 和字节输出流 OutputStream。字符流和字节流都有对应的缓冲流,字节流也可以包装为字符流,缓冲流带有一个 8KB 的缓冲数组,可以提高流的读写效率。除了缓冲流外还有过滤流 FilterReader、字符数组流 CharArrayReader、字节数组流 ByteArrayInputStream、文件流 FileInputStream 等。


    6、序列化和反序列化是什么?

    Java 对象 JVM 退出时会全部销毁,如果需要将对象及状态持久化,就要通过序列化实现,将内存中的对象保存在二进制流中,需要时再将二进制流反序列化为对象。对象序列化保存的是对象的状态,因此属于类属性的静态变量不会被序列化。

    常见的序列化有三种:

    序列化通常会使用网络传输对象,而对象中往往有敏感数据,容易遭受攻击,Jackson 和 fastjson 等都出现过反序列化漏洞,因此不需要进行序列化的敏感属性传输时应加上 transient 关键字。transient 的作用就是把变量生命周期仅限于内存而不会写到磁盘里持久化,变量会被设为对应数据类型的零值。

    以上是关于3w字深度好文|Redis面试全攻略,读完这个就可以和面试官大战几个回合了的主要内容,如果未能解决你的问题,请参考以下文章

    Redis泛泛而谈(详细3W字)

    3W字吃透:微服务 sentinel 限流 底层原理和实操

    3W字吃透:微服务网关SpringCloud gateway底层原理和实操

    3w+深度盘点:机器学习面试知识点梳理!

    java考研学校,深度好文

    Redis面试全攻略,面试官看完也得跪!