3w字深度好文|Redis面试全攻略,读完这个就可以和面试官大战几个回合了
Posted 后端研究所
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了3w字深度好文|Redis面试全攻略,读完这个就可以和面试官大战几个回合了相关的知识,希望对你有一定的参考价值。
(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;
跳表结点的平均层数
如果某件事的发生频率和它的某个属性成幂关系,那么这个频率就可以称之为符合幂次定律。幂次定律的表现是少数几个事件的发生频率占了整个发生频率的大部分, 而其余的大多数事件只占整个发生频率的一个小部分。
节点层数至少为1,大于1的节点层数满足一个概率分布。
节点层数恰好等于1的概率为p^0(1-p)。
节点层数恰好等于2的概率为p^1(1-p)。
节点层数恰好等于3的概率为p^2(1-p)。
节点层数恰好等于4的概率为p^3(1-p)。
依次递推节点层数恰好等于K的概率为p^(k-1)(1-p)
单实例Redis架构
注:图片来自网络
集群与分片
客户端分片:这种情况主要是类似于哈希取模的做法,当客户端对服务端的数量完全掌握和控制时,可以简单使用。
中间层分片:这种情况是在客户端和服务器端之间增加中间层,充当管理者和调度者,客户端的请求打向中间层,由中间层实现请求的转发和回收,当然中间层最重要的作用是对多台服务器的动态管理。
服务端分片:不使用中间层实现去中心化的管理模式,客户端直接向服务器中任意结点请求,如果被请求的Node没有所需数据,则像客户端回复MOVED,并告诉客户端所需数据的存储位置,这个过程实际上是客户端和服务端共同配合,进行请求重定向来完成的。
中间层分片的集群版Redis
服务端分片的官方集群版本
注:图片来自网络
A.2 Redis Cluster的基本运行原理结点状态信息结构
当前集群状态
集群中各节点所负责的slots信息,及其migrate状态
集群中各节点的master-slave状态
集群中各节点的存活状态及不可达投票
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算法又被称为反熵(Anti-Entropy),熵是物理学上的一个概念,代表杂乱无章,而反熵就是在杂乱无章中寻求一致,这充分说明了Gossip的特点:在一个有界网络中,每个节点都随机地与其他节点通信,经过一番杂乱无章的通信,最终所有节点的状态都会达成一致。每个节点可能知道所有其他节点,也可能仅知道几个邻居节点,只要这些节可以通过网络连通,最终他们的状态都是一致的,当然这也是疫情传播的特点。
https://www.backendcloud.cn/2017/11/12/raft-gossip/
Gossip协议的使用
Meet 通过「cluster meet ip port」命令,已有集群的节点会向新的节点发送邀请,加入现有集群。
Ping 节点每秒会向集群中其他节点发送 ping 消息,消息中带有自己已知的两个节点的地址、槽、状态信息、最后一次通信时间等。
Pong 节点收到 ping 消息后会回复 pong 消息,消息中同样带有自己已知的两个节点信息。
Fail 节点 ping 不通某节点后,会向集群所有节点广播该节点挂掉的消息。其他节点收到消息后标记已下线。
注:图片来自网络
基于Gossip协议的故障检测
有半数以上的主节点将 node 标记为 PFAIL 状态。
当前节点也将 node 标记为 PFAIL 状态。
Redis作为内存型数据库,如果单纯的只进不出早晚就撑爆了,事实上很多把Redis当做主存储DB用的家伙们早晚会尝到这个苦果,当然除非你家厂子确实不差钱,数T级别的内存都毛毛雨,或者数据增长一定程度之后不再增长的场景,就另当别论了。
为了让Redis服务安全稳定的运行,让使用内存保持在一定的阈值内是非常有必要的,因此我们就需要删除该删除的,清理该清理的,把内存留给需要的键值对,试想一条大河需要设置几个警戒水位来确保不决堤不枯竭,Redis也是一样的,只不过Redis只关心决堤即可,来一张图:
图中设定机器内存为128GB,占用64GB算是比较安全的水平,如果内存接近80%也就是100GB左右,那么认为Redis目前承载能力已经比较大了,具体的比例可以根据公司和个人的业务经验来确定。
笔者只是想表达出于安全和稳定的考虑,不要觉得128GB的内存就意味着存储128GB的数据,都是要打折的。
B.1 回收的内存从哪里来Redis占用的内存是分为两部分:存储键值对消耗和本身运行消耗。显然后者我们无法回收,因此只能从键值对下手了,键值对可以分为几种:带过期的、不带过期的、热点数据、冷数据。对于带过期的键值是需要删除的,如果删除了所有的过期键值对之后内存仍然不足怎么办?那只能把部分数据给踢掉了。
要实施对键值对的删除我们需要明白如下几点:
带过期超时的键值对存储在哪里?
如何判断带超时的键值对是否可以被删除了?
删除机制有哪些以及如何选择?
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保存的是所有带过期的键值对,称之为过期字典吧,它才是我们研究的重点。
对于键,我们可以设置绝对和相对过期时间、以及查看剩余时间:
使用EXPIRE和PEXPIRE来实现键值对的秒级和毫秒级生存时间设定,这是相对时长的过期设置
使用EXPIREAT和EXPIREAT来实现键值对在某个秒级和毫秒级时间戳时进行过期删除,属于绝对过期设置
通过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、通过反射运行配置文件内容
- 加载配置文件,并解析配置文件得到相应信息
- 根据解析的字符串利用反射机制获取某个类的Class实例动态配置属性
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 的子类,分为 Error 和 Exception。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)]
数据类型 | 内存大小 | 默认值 | 取值范围 |
---|---|---|---|
byte | 1 B | (byte)0 | -128 ~ 127 |
short | 2 B | (short)0 | -215 ~ 215-1 |
int | 4 B | 0 | -231 ~ 231-1 |
long | 8 B | 0L | -263 ~ 263-1 |
float | 4 B | 0.0F | ±3.4E+38(有效位数 6~7 位) |
double | 8 B | 0.0D | ±1.7E+308(有效位数 15 位) |
char | 英文 1B,中文 UTF-8 占 3B,GBK 占 2B。 | ‘\\u0000’ | ‘\\u0000’ ~ ‘\\uFFFF’ |
boolean | 单个变量 4B / 数组 1B | false | true、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”);
创建了几个字符串对象?
- 使用
String a = “aaa” ;
,程序运行时会在常量池中查找”aaa”字符串,若没有,会将”aaa”字符串放进常量池,再将其地址赋给a;若有,将找到的”aaa”字符串的地址赋给a。 - 使用String b = new String(“aaa”);`,程序会在堆内存中开辟一片新空间存放新对象,同时会将”aaa”字符串放入常量池,相当于创建了两个对象,无论常量池中有没有”aaa”字符串,程序都会在堆内存中开辟一片新空间存放新对象。
2、String有哪些特性?
- 不变性:String 是只读字符串,是一个典型的 immutable 对象,对它进行任何操作,其实都是创建一个新的对象,再把引用指向该对象。不变模式的主要作用在于当一个对象需要被多线程共享并频繁访问时,可以保证数据的一致性;
- 常量池优化:String 对象创建之后,会在字符串常量池中进行缓存,如果下次创建同样的对象时,会直接返回缓存的引用;
- final:使用 final 来定义 String 类,表示 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 的区别是什么?
可变和不可变:
- string对象是不可变的
- StringBuilder与StringBuffer这两种对象都是可变的
是否多线程安全:
- String中的对象是不可变的,也就可以理解为常量,显然线程安全。
- StringBuilder是非线程安全的。
- StringBuffer对方法加了同步锁或者对调用的方法加了同步锁,所以是线程安全的。
性能:
- 每次对String 类型进行改变的时候,都会生成一个新的String对象,然后将指针指向新的String 对象,效率低。
- StringBuffer每次都会对StringBuffer对象本身进行操作,而不是生成新的对象并改变对象引用。
5、String为什么要设计成不可变的?
1、便于实现字符串池
-
在堆中开辟一块存储空间String pool,当初始化一个String变量时,如果该字符串已经存在了,就不会去创建一个新的字符串变量,而是会返回已经存在了的字符串的引用。
-
如果字符串是可变的,某一个字符串变量改变了其值,那么其指向的变量的值也会改变,String pool将不能够实现!
2、使多线程安全
- 在并发场景下,多个线程同时读一个资源,是安全的,不会引发竞争,但对资源进行写操作时是不安全的,不可变对象不能被写,所以保证了多线程的安全。
3、避免安全问题
- 在网络连接和数据库连接中字符串常常作为参数,例如,网络连接地址URL,文件路径path,反射机制所需要的String参数。其不可变性可以保证连接的安全性。如果字符串是可变的,黑客就有可能改变字符串指向对象的值,那么会引起很严重的安全问题。
4、加快字符串处理速度
- 由于String是不可变的,保证了hashcode的唯一性,于是在创建对象时其hashcode就可以放心的缓存了,不需要重新计算。这也就是Map喜欢将String作为Key的原因,处理速度要快过其它的键对象。所以HashMap中的键往往都使用String。
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 请求时才启动一个线程处理。适用连接数目多且连接时间短的场景。
同步是指线程还是要不断接收客户端连接并处理数据,非阻塞是指如果一个管道没有数据,不需要等待,可以轮询下一个管道。
核心组件:
-
Selector: 多路复用器,轮询检查多个 Channel 的状态,判断注册事件是否发生,即判断 Channel 是否处于可读或可写状态。使用前需要将 Channel 注册到 Selector,注册后会得到一个 SelectionKey,通过 SelectionKey 获取 Channel 和 Selector 相关信息。
-
Channel: 双向通道,替换了 BIO 中的 Stream 流,不能直接访问数据,要通过 Buffer 来读写数据,也可以和其他 Channel 交互。
-
Buffer: 缓冲区,本质是一块可读写数据的内存,用来简化数据读写。Buffer 三个重要属性:position 下次读写数据的位置,limit 本次读写的极限位置,capacity 最大容量。
flip
将写转为读,底层实现原理把 position 置 0,并把 limit 设为当前的 position 值。clear
将读转为写模式(用于读完全部数据的情况,把 position 置 0,limit 设为 capacity)。compact
将读转为写模式(用于存在未读数据的情况,让 position 指向未读数据的下一个)。- 通道方向和 Buffer 方向相反,读数据相当于向 Buffer 写,写数据相当于从 Buffer 读。
使用步骤:向 Buffer 写数据,调用 flip 方法转为读模式,从 Buffer 中读数据,调用 clear 或 compact 方法清空缓冲区。
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 退出时会全部销毁,如果需要将对象及状态持久化,就要通过序列化实现,将内存中的对象保存在二进制流中,需要时再将二进制流反序列化为对象。对象序列化保存的是对象的状态,因此属于类属性的静态变量不会被序列化。
常见的序列化有三种:
-
Java 原生序列化
实现
Serializabale
标记接口,Java 序列化保留了对象类的元数据(如类、成员变量、继承类信息)以及对象数据,兼容性最好,但不支持跨语言,性能一般。序列化和反序列化必须保持序列化 ID 的一致,一般使用private static final long serialVersionUID
定义序列化 ID,如果不设置编译器会根据类的内部实现自动生成该值。如果是兼容升级不应该修改序列化 ID,防止出错,如果是不兼容升级则需要修改。 -
Hessian 序列化
Hessian 序列化是一种支持动态类型、跨语言、基于对象传输的网络协议。Java 对象序列化的二进制流可以被其它语言反序列化。Hessian 协议的特性:① 自描述序列化类型,不依赖外部描述文件,用一个字节表示常用基础类型,极大缩短二进制流。② 语言无关,支持脚本语言。③ 协议简单,比 Java 原生序列化高效。Hessian 会把复杂对象所有属性存储在一个 Map 中序列化,当父类和子类存在同名成员变量时会先序列化子类再序列化父类,因此子类值会被父类覆盖。
-
JSON 序列化
JSON 序列化就是将数据对象转换为 JSON 字符串,在序列化过程中抛弃了类型信息,所以反序列化时只有提供类型信息才能准确进行。相比前两种方式可读性更好,方便调试。
序列化通常会使用网络传输对象,而对象中往往有敏感数据,容易遭受攻击,Jackson 和 fastjson 等都出现过反序列化漏洞,因此不需要进行序列化的敏感属性传输时应加上 transient 关键字。transient 的作用就是把变量生命周期仅限于内存而不会写到磁盘里持久化,变量会被设为对应数据类型的零值。
以上是关于3w字深度好文|Redis面试全攻略,读完这个就可以和面试官大战几个回合了的主要内容,如果未能解决你的问题,请参考以下文章