面试时 Redis 内存淘汰总被问,但是总答不好,怎么解决?(1)

先自我介绍一下,小编浙江大学毕业,去过华为、字节跳动等大厂,目前阿里P7

深知大多数程序员,想要提升技能,往往是自己摸索成长,但自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年最新Java开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友。
img
img
img
img
img
img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上Java开发知识点,真正体系化!

由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新

如果你需要这些资料,可以添加V获取:vip1024b (备注Java)
img

正文

Don’t use more memory than the specified amount of bytes.

When the memory limit is reached Redis will try to remove keys

according to the eviction policy selected (see maxmemory-policy).

If Redis can’t remove keys according to the policy, or if the policy is

set to ‘noeviction’, Redis will start to reply with errors to commands

that would use more memory, like SET, LPUSH, and so on, and will continue

to reply to read-only commands like GET.

This option is usually useful when using Redis as an LRU cache, or to set

a hard memory limit for an instance (using the ‘noeviction’ policy).

WARNING: If you have slaves attached to an instance with maxmemory on,

the size of the output buffers needed to feed the slaves are subtracted

from the used memory count, so that network problems / resyncs will

not trigger a loop where keys are evicted, and in turn the output

buffer of slaves is full with DELs of keys evicted triggering the deletion

of more keys, and so forth until the database is completely emptied.

In short… if you have slaves attached it is suggested that you set a lower

limit for maxmemory so that there is some free RAM on the system for slave

output buffers (but this is not needed if the policy is ‘noeviction’).

maxmemory

渣翻译如下:

不能使用超过指定数量bytes的内存。当该内存限制被达到时,redis会根据过期策略(eviction policy,通过参数 maxmemory-policy来指定)来驱逐key。

如果redis根据指定的策略,或者策略被设置为“noeviction”,redis会开始针对如下这种命令,回复错误。什么命令呢?会使用更多内存的那类命令,比如set、lpush;只读命令还是不受影响,可以正常响应。

该选项通常在redis使用LRU缓存时有用,或者在使用noeviction策略时,设置一个进程级别的内存limit。

内存淘汰策略

=========================================================================

所谓策略,意思是,当我们要删除部分key的时候,删哪些,不删哪些?是不是需要一个策略?比如是随机删,就像灭霸一样?还是按照lru时间来删,lru的策略意思就是,最近最少使用的key,将被优先删除。

总之,我们需要定一个规则。

redis默认支持以下策略:

MAXMEMORY POLICY: how Redis will select what to remove when maxmemory

is reached. You can select among five behaviors:

volatile-lru -> remove the key with an expire set using an LRU algorithm

allkeys-lru -> remove any key accordingly to the LRU algorithm

volatile-random -> remove a random key with an expire set

allkeys-random -> remove a random key, any key

volatile-ttl -> remove the key with the nearest expire time (minor TTL)

noeviction -> don’t expire at all, just return an error on write operations

Note: with any of the above policies, Redis will return an error on write

operations, when there are not suitable keys for eviction.

At the date of writing this commands are: set setnx setex append

incr decr rpush lpush rpushx lpushx linsert lset rpoplpush sadd

sinter sinterstore sunion sunionstore sdiff sdiffstore zadd zincrby

zunionstore zinterstore hset hsetnx hmset hincrby incrby decrby

getset mset msetnx exec sort

The default is:

maxmemory-policy noeviction

maxmemory-policy allkeys-lru

针对设置了过期时间的,使用lru算法

volatile-lru -> remove the key with an expire set using an LRU algorithm

针对全部key,使用lru算法

allkeys-lru -> remove any key accordingly to the LRU algorithm

针对设置了过期时间的,随机删

volatile-random -> remove a random key with an expire set

针对全部key,随机删

allkeys-random -> remove a random key, any key

针对设置了过期时间的,马上要过期的,删掉

volatile-ttl -> remove the key with the nearest expire time (minor TTL)

不过期,不能写了,就报错

noeviction -> don’t expire at all, just return an error on write operations

源码实现

=======================================================================

配置读取


在如下结构体中,定义了如下字段:

struct redisServer {

unsigned long long maxmemory; /* Max number of memory bytes to use */

int maxmemory_policy; /* Policy for key eviction */

int maxmemory_samples; /* Pricision of random sampling */

}

当我们在配置文件中,进入如下配置时,该结构体中几个字段的值如下:

maxmemory 3mb

maxmemory-policy allkeys-lru

maxmemory-samples 5 这个取了默认值

maxmemory_policy为3,是因为枚举值为3:

#define REDIS_MAXMEMORY_VOLATILE_LRU 0

#define REDIS_MAXMEMORY_VOLATILE_TTL 1

#define REDIS_MAXMEMORY_VOLATILE_RANDOM 2

#define REDIS_MAXMEMORY_ALLKEYS_LRU 3

#define REDIS_MAXMEMORY_ALLKEYS_RANDOM 4

#define REDIS_MAXMEMORY_NO_EVICTION 5

#define REDIS_DEFAULT_MAXMEMORY_POLICY REDIS_MAXMEMORY_NO_EVICTION

处理命令时,判断是否进行内存淘汰


在处理命令的时候,会调用中的

redis.c processCommand

int processCommand(redisClient *c) {

/* The QUIT command is handled separately. Normal command procs will

  • go through checking for replication and QUIT will cause trouble

  • when FORCE_REPLICATION is enabled and would be implemented in

  • a regular command proc. */

// 特别处理 quit 命令

void *commandName = c->argv[0]->ptr;

redisLog(REDIS_NOTICE, “The server is now processing %s”, commandName);

if (!strcasecmp(c->argv[0]->ptr, “quit”)) {

addReply(c, shared.ok);

c->flags |= REDIS_CLOSE_AFTER_REPLY;

return REDIS_ERR;

}

/* Now lookup the command and check ASAP about trivial error conditions

  • such as wrong arity, bad command name and so forth. */

// 1 查找命令,并进行命令合法性检查,以及命令参数个数检查

c->cmd = c->lastcmd = lookupCommand(c->argv[0]->ptr);

if (!c->cmd) {

// 没找到指定的命令

flagTransaction©;

addReplyErrorFormat(c, “unknown command ‘%s’”,

(char *) c->argv[0]->ptr);

return REDIS_OK;

}

/* Check if the user is authenticated */

//2 检查认证信息

if (server.requirepass && !c->authenticated && c->cmd->proc != authCommand) {

flagTransaction©;

addReply(c, shared.noautherr);

return REDIS_OK;

}

/* If cluster is enabled perform the cluster redirection here.

  • 3 如果开启了集群模式,那么在这里进行转向操作。

  • However we don’t perform the redirection if:

  • 不过,如果有以下情况出现,那么节点不进行转向:

    1. The sender of this command is our master.
  • 命令的发送者是本节点的主节点

    1. The command has no key arguments.
  • 命令没有 key 参数

*/

if (server.cluster_enabled &&

!(c->flags & REDIS_MASTER) &&

!(c->cmd->getkeys_proc == NULL && c->cmd->firstkey == 0)) {

int hashslot;

// 集群已下线

if (server.cluster->state != REDIS_CLUSTER_OK) {

flagTransaction©;

addReplySds(c, sdsnew(“-CLUSTERDOWN The cluster is down. Use CLUSTER INFO for more information\r\n”));

return REDIS_OK;

// 集群运作正常

} else {

int error_code;

clusterNode *n = getNodeByQuery(c, c->cmd, c->argv, c->argc, &hashslot, &error_code);

// 不能执行多键处理命令

if (n == NULL) {

flagTransaction©;

if (error_code == REDIS_CLUSTER_REDIR_CROSS_SLOT) {

addReplySds(c, sdsnew(“-CROSSSLOT Keys in request don’t hash to the same slot\r\n”));

} else if (error_code == REDIS_CLUSTER_REDIR_UNSTABLE) {

/* The request spawns mutliple keys in the same slot,

  • but the slot is not “stable” currently as there is

  • a migration or import in progress. */

addReplySds(c, sdsnew(“-TRYAGAIN Multiple keys request during rehashing of slot\r\n”));

} else {

redisPanic(“getNodeByQuery() unknown error.”);

}

return REDIS_OK;

//3.1 命令针对的槽和键不是本节点处理的,进行转向

} else if (n != server.cluster->myself) {

flagTransaction©;

// - :

// 例如 -ASK 10086 127.0.0.1:12345

addReplySds(c, sdscatprintf(sdsempty(),

“-%s %d %s:%d\r\n”,

(error_code == REDIS_CLUSTER_REDIR_ASK) ? “ASK” : “MOVED”,

hashslot, n->ip, n->port));

return REDIS_OK;

}

// 如果执行到这里,说明键 key 所在的槽由本节点处理

// 或者客户端执行的是无参数命令

}

}

/* Handle the maxmemory directive.

  • First we try to free some memory if possible (if there are volatile

  • keys in the dataset). If there are not the only thing we can do

  • is returning an error. */

//4 如果设置了最大内存,那么检查内存是否超过限制,并做相应的操作

if (server.maxmemory) {

//4.1 如果内存已超过限制,那么尝试通过删除过期键来释放内存

int retval = freeMemoryIfNeeded();

// 如果即将要执行的命令可能占用大量内存(REDIS_CMD_DENYOOM)

// 并且前面的内存释放失败的话

// 那么向客户端返回内存错误

if ((c->cmd->flags & REDIS_CMD_DENYOOM) && retval == REDIS_ERR) {

flagTransaction©;

addReply(c, shared.oomerr);

return REDIS_OK;

}

}

  • 1处,查找命令,对应的函数指针(类似于java里的策略模式,根据命令,找对应的策略)

  • 2处,检查,是否密码正确

  • 3处,集群相关操作;

  • 3.1处,不是本节点处理,直接返回ask,指示客户端转向

  • 4处,判断是否设置了maxMemory,这里就是本文重点:设置了maxMemory时,内存淘汰策略

  • 4.1处,调用了下方的 freeMemoryIfNeeded

接下来,深入4.1处:

int freeMemoryIfNeeded(void) {

size_t mem_used, mem_tofree, mem_freed;

int slaves = listLength(server.slaves);

/* Remove the size of slaves output buffers and AOF buffer from the

  • count of used memory. */

// 计算出 Redis 目前占用的内存总数,但有两个方面的内存不会计算在内:

// 1)从服务器的输出缓冲区的内存

// 2)AOF 缓冲区的内存

mem_used = zmalloc_used_memory();

if (slaves) {

}

if (server.aof_state != REDIS_AOF_OFF) {

mem_used -= sdslen(server.aof_buf);

mem_used -= aofRewriteBufferSize();

}

/* Check if we are over the memory limit. */

//1 如果目前使用的内存大小比设置的 maxmemory 要小,那么无须执行进一步操作

if (mem_used <= server.maxmemory) return REDIS_OK;

//2 如果占用内存比 maxmemory 要大,但是 maxmemory 策略为不淘汰,那么直接返回

if (server.maxmemory_policy == REDIS_MAXMEMORY_NO_EVICTION)

return REDIS_ERR; /* We need to free memory, but policy forbids. */

/* Compute how much memory we need to free. */

// 3 计算需要释放多少字节的内存

mem_tofree = mem_used - server.maxmemory;

// 初始化已释放内存的字节数为 0

mem_freed = 0;

// 根据 maxmemory 策略,

//4 遍历字典,释放内存并记录被释放内存的字节数

while (mem_freed < mem_tofree) {

int j, k, keys_freed = 0;

// 遍历所有字典

for (j = 0; j < server.dbnum; j++) {

long bestval = 0; /* just to prevent warning */

sds bestkey = NULL;

dictEntry *de;

redisDb *db = server.db + j;

dict *dict;

if (server.maxmemory_policy == REDIS_MAXMEMORY_ALLKEYS_LRU ||

server.maxmemory_policy == REDIS_MAXMEMORY_ALLKEYS_RANDOM) {

// 如果策略是 allkeys-lru 或者 allkeys-random

//5 那么淘汰的目标为所有数据库键

dict = server.db[j].dict;

} else {

// 如果策略是 volatile-lru 、 volatile-random 或者 volatile-ttl

//6 那么淘汰的目标为带过期时间的数据库键

dict = server.db[j].expires;

}

/* volatile-random and allkeys-random policy */

// 如果使用的是随机策略,那么从目标字典中随机选出键

if (server.maxmemory_policy == REDIS_MAXMEMORY_ALLKEYS_RANDOM ||

server.maxmemory_policy == REDIS_MAXMEMORY_VOLATILE_RANDOM) {

de = dictGetRandomKey(dict);

bestkey = dictGetKey(de);

}

/* volatile-lru and allkeys-lru policy */

//7

else if (server.maxmemory_policy == REDIS_MAXMEMORY_ALLKEYS_LRU ||

server.maxmemory_policy == REDIS_MAXMEMORY_VOLATILE_LRU) {

struct evictionPoolEntry *pool = db->eviction_pool;

while (bestkey == NULL) {

// 8

evictionPoolPopulate(dict, db->dict, db->eviction_pool);

/* Go backward from best to worst element to evict. */

for (k = REDIS_EVICTION_POOL_SIZE - 1; k >= 0; k–) {

if (pool[k].key == NULL) continue;

// 8.1

de = dictFind(dict, pool[k].key);

/* 8.2 Remove the entry from the pool. */

sdsfree(pool[k].key);

/* Shift all elements on its right to left. */

memmove(pool + k, pool + k + 1,

sizeof(pool[0]) * (REDIS_EVICTION_POOL_SIZE - k - 1));

/* Clear the element on the right which is empty

  • since we shifted one position to the left. */

pool[REDIS_EVICTION_POOL_SIZE - 1].key = NULL;

pool[REDIS_EVICTION_POOL_SIZE - 1].idle = 0;

/* If the key exists, is our pick. Otherwise it is

  • a ghost and we need to try the next element. */

// 8.3

if (de) {

bestkey = dictGetKey(de);

break;

} else {

/* Ghost… */

continue;

}

}

}

}

/* volatile-ttl */

// 策略为 volatile-ttl ,从一集 sample 键中选出过期时间距离当前时间最接近的键

else if (server.maxmemory_policy == REDIS_MAXMEMORY_VOLATILE_TTL) {

}

/* Finally remove the selected key. */

// 8.4 删除被选中的键

if (bestkey) {

long long delta;

robj *keyobj = createStringObject(bestkey, sdslen(bestkey));

propagateExpire(db, keyobj);

/* We compute the amount of memory freed by dbDelete() alone.

  • It is possible that actually the memory needed to propagate

  • the DEL in AOF and replication link is greater than the one

  • we are freeing removing the key, but we can’t account for

  • that otherwise we would never exit the loop.

  • AOF and Output buffer memory will be freed eventually so

  • we only care about memory used by the key space. */

// 计算删除键所释放的内存数量

delta = (long long) zmalloc_used_memory();

dbDelete(db, keyobj);

delta -= (long long) zmalloc_used_memory();

mem_freed += delta;

// 对淘汰键的计数器增一

server.stat_evictedkeys++;

notifyKeyspaceEvent(REDIS_NOTIFY_EVICTED, “evicted”,

keyobj, db->id);

decrRefCount(keyobj);

keys_freed++;

}

}

if (!keys_freed) return REDIS_ERR; /* nothing to free… */

}

return REDIS_OK;

}

  • 1处,如果目前使用的内存大小比设置的 maxmemory 要小,那么无须执行进一步操作

  • 2处,如果占用内存比 maxmemory 要大,但是 maxmemory 策略为不淘汰,那么直接返回

  • 3处,计算需要释放多少字节的内存

  • 4处,遍历字典,释放内存并记录被释放内存的字节数

  • 5处,如果策略是 allkeys-lru 或者 allkeys-random 那么淘汰的目标为所有数据库键

  • 6处,如果策略是 volatile-lru 、 volatile-random 或者 volatile-ttl ,那么淘汰的目标为带过期时间的数据库键

  • 7处,如果使用的是 LRU 策略, 那么从 sample 键中选出 IDLE 时间最长的那个键

  • 8处,调用evictionPoolPopulate,该函数在下面讲解,该函数的功能是,传入一个链表,即这里的db->eviction_pool,然后在函数内部,随机找出n个key,放入传入的链表中,并按照空闲时间排序,空闲最久的,放到最后。

当该函数,返回后,db->eviction_pool这个链表里就存放了我们要淘汰的key。

  • 8.1处,找到这个key,这个key,在后边会被删除

  • 8.2处,下面这一段,从db->eviction_pool将这个已经处理了的key删掉

  • 8.3处,如果这个key,是存在的,则跳出循环,在后面8.4处,会被删除

  • 8.4处,删除这个key

选择哪些key作为被淘汰的key


前面我们看到,在7处,如果为lru策略,则会进入8处的函数:

evictionPoolPopulate。

该函数的名称为:填充(populate)驱逐(eviction)对象池(pool)。驱逐的意思,就是现在达到了maxmemory,没办法,只能开始删除掉一部分元素,来腾空间了,不然新的put类型的命令,根本没办法执行。

该方法的大概思路是,使用lru的时候,随机找n个key,类似于抽样,然后放到一个链表,根据空闲时间排序。

具体看看该方法的实现:

void evictionPoolPopulate(dict *sampledict, dict *keydict, struct evictionPoolEntry *pool) {

其中,传入的第三个参数,是要被填充的对象,在c语言中,习惯传入一个入参,然后在函数内部填充或者修改入参对象的属性。

该属性,就是前面说的那个链表,用来存放收集的随机的元素,该链表中节点的结构如下:

struct evictionPoolEntry {

unsigned long long idle; /* Object idle time. */

sds key; /* Key name. */

总结

在这里,由于面试中MySQL问的比较多,因此也就在此以MySQL为例为大家总结分享。但是你要学习的往往不止这一点,还有一些主流框架的使用,Spring源码的学习,Mybatis源码的学习等等都是需要掌握的,我也把这些知识点都整理起来了

面试真题

Spring源码笔记

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化的资料的朋友,可以添加V获取:vip1024b (备注Java)
img

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!
意思,就是现在达到了maxmemory,没办法,只能开始删除掉一部分元素,来腾空间了,不然新的put类型的命令,根本没办法执行。

该方法的大概思路是,使用lru的时候,随机找n个key,类似于抽样,然后放到一个链表,根据空闲时间排序。

具体看看该方法的实现:

void evictionPoolPopulate(dict *sampledict, dict *keydict, struct evictionPoolEntry *pool) {

其中,传入的第三个参数,是要被填充的对象,在c语言中,习惯传入一个入参,然后在函数内部填充或者修改入参对象的属性。

该属性,就是前面说的那个链表,用来存放收集的随机的元素,该链表中节点的结构如下:

struct evictionPoolEntry {

unsigned long long idle; /* Object idle time. */

sds key; /* Key name. */

总结

在这里,由于面试中MySQL问的比较多,因此也就在此以MySQL为例为大家总结分享。但是你要学习的往往不止这一点,还有一些主流框架的使用,Spring源码的学习,Mybatis源码的学习等等都是需要掌握的,我也把这些知识点都整理起来了

[外链图片转存中…(img-R3kS5I8m-1713562093398)]

[外链图片转存中…(img-ncQh3VqM-1713562093398)]

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化的资料的朋友,可以添加V获取:vip1024b (备注Java)
[外链图片转存中…(img-sx46y3Ym-1713562093399)]

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

  • 30
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值