Redis 在我们平时的开发或者练习的时候, 往往很容易忽略一个问题, 那就是我们的 Redis 内存占满的问题. 但是在真是的商业开发中, Redis 的实际占满是真正会存在这样的问题的. 那么如果 Redis 在某一刻占满内存, 我们又没有对它进行相应的设置它会出现什么情况呢? 会不会导致我们整个因为使用 Redis 而整个业务垮掉? 这就是我们本篇文章所要讲述的问题.
什么是 Redis 淘汰机制
Redis 内存淘汰机制其实简单讲就是将过期的数据或者很久没有访问, 或者在一段时间内很少有访问的数据进行删除. 它分为很多中, 有主动的数据淘汰, 如: 用户设定过期时间. 有被动的淘汰, 比如: Redis 数据占满了内存, 这个时候就会将过期的数据或者很久没有访问的数据删除掉.
Redis 的淘汰有哪些类型
定时过期: 每个设置过期时间的 key 都需要创建一个定时器, 到过期时间就会立即清除. 该策略可以立即清除过期的数据, 对内存很友好; 但是会占用大量的 CPU 资源去处理过期的数据, 从而影响缓存的响应时间和吞吐量.
惰性过期: 只有当访问一个 key 时, 才会判断该 key 是否已过期, 过期则清除. 该策略可以最大化地节省 CPU 资源, 却对内存非常不友好. 极端情况可能出现大量的过期 key 没有再次被访问, 从而不会被清除, 占用大量内存.
定期过期: 每隔一定的时间, 会扫描一定数量的数据库的 expires 字典中一定数量的 key, 并清除其中已过期的 key. 该策略是前两者的一个折中方案. 通过调整定时扫描的时间间隔和每次扫描的限定耗时, 可以在不同情况下使得 CPU 和内存资源达到最优的平衡效果.
(expires 字典会保存所有设置了过期时间的 key 的过期时间数据, 其中, key 是指向键空间中的某个键的指针, value 是该键的毫秒精度的 UNIX 时间戳表示的过期时间. 键空间是指该 Redis 集群中保存的所有键.)
定时过期的问题: 缓存雪崩
很多人可能对这个词很熟悉, 因为有很多的商业案例展示了血淋淋的教训, 但是也有一部分人估计没有接触过. 缓存雪崩其实也不是什么新词, 它主要的引起原因就是指缓存中数据大批量的到过期时间. 定时过期它本身就有一个缺点, 那就是会占用大量的 CPU 资源, 如果我们主动设置过期时间的键过多, 在同一时间过期, 很有可能就会造就我们 Redis 挂掉. 但是这并不是最可怕的, 雪崩不仅仅影响自己, 还在我们的业务中影响数据库. 因为我们很多业务设计都是在我们 Redis 的数据过期之后, 从新查询数据库, 但我们 Redis 主动批量过期的时候, 会有大量的请求发送到我们的数据库, 很有可能导致我们的数据库也挂掉. 这才是最大的问题所在.
解决方案:
缓存数据的过期时间设置随机, 防止同一时间大量数据过期现象发生.
如果缓存数据库是分布式部署, 将热点数据均匀分布在不同搞得缓存数据库中.
设置热点数据永远不过期.
从几种淘汰策略中其实我们可以看到基本的一些问题所在, 所以我们在使用缓存的时候最好有一个全面的了解和全面的考虑应对. 在实际开发中, 我们更应该多去关注的和了解的是定期过期, 因为它涉及真实开发中的一些问题. 所以我们应该提前设置好.
怎么设置定期过期最大使用内存
定期过期的最大内存设置在我们的 Redis.conf 文件中, 我们可以在该文件中看到这个配置: maxmemory <bytes>, 但是这个配置一般都是注释掉的, 也就是说安装之后如果我们没有主动对他进行配置, 那么他就不会有默认大小值. 对应的它不设置的情况下, 那么它可以使用多少的内存空间呢? 这个跟系统有关. 如果说我们将 Redis 安装在 32 位的系统上, 它的最大使用内存空间应该是在 3G 左右, 如果是 64 位的系统, 那么可以将我们的内存占满. 当然如果真正占满内存, 这是一件比较恶劣的事情, 不仅仅访问 Redis 的时候, 我们不能在进行写的操作, 而且我们系统本身的其他操作也会受到限制. 所以我们可以采用命令来对它进行一个初始化的设置
config set maxmemory 268435456
使用命令进行设置之后我们需要重启 Redis 才能生效. 当然我们也可以直接找到 Redis 的安装目录, 然后使用 vi 命令, 直接更改配置文件中的对应的该内容, 更改完之后, 重启即可.
定期过期的淘汰策略
volatile-lru: 根据 LRU 算法生成的过期时间来删除.
allkeys-lru: 根据 LRU 算法删除任何 key.
volatile-lfu: 从所有配置了过期时间的键中驱逐使用频率最少的键
allkeys-lfu: 从所有键中驱逐使用频率最少的键
volatile-random: 根据过期设置来随机删除 key.
allkeys-random: 无差别随机删.
volatile-ttl: 根据最近过期时间来删除(辅以 TTL)
noeviction: 谁也不删, 直接在写操作时返回错误.
随机淘汰策略
随机找 hash 桶再次 hash 指定位置的 dictEntry 即可. 就是在场景 REDIS_MAXMEMORY_VOLATILE_RANDOM 和 REDIS_MAXMEMORY_ALLKEYS_LRU 情况下的待淘汰的 key. 我们可以一观它的源码:
- dictEntry *dictGetRandomKey(dict *d)
- {
- dictEntry *he, *orighe;
- unsigned int h;
- int listlen, listele;
- if (dictSize(d) == 0) return NULL;
- if (dictIsRehashing(d)) _dictRehashStep(d);
- if (dictIsRehashing(d)) {
- // T = O(N)
- do {
- h = random() % (d->ht[0].size+d->ht[1].size);
- he = (h>= d->ht[0].size) ? d->ht[1].table[h - d->ht[0].size] :
- d->ht[0].table[h];
- } while(he == NULL);
- } else {
- // T = O(N)
- do {
- h = random() & d->ht[0].sizemask;
- he = d->ht[0].table[h];
- } while(he == NULL);
- }
- /* Now we found a non empty bucket, but it is a linked
- * list and we need to get a random element from the list.
- * The only sane way to do so is counting the elements and
- * select a random index. */
- listlen = 0;
- orighe = he;
- while(he) {
- he = he->next;
- listlen++;
- }
- listele = random() % listlen;
- he = orighe;
- // T = O(1)
- while(listele--) he = he->next;
- return he;
- }
TTL 时间淘汰
- for (k = 0; k < server.maxmemory_samples; k++) {
- sds thiskey;
- long thisval;
- de = dictGetRandomKey(dict);
- thiskey = dictGetKey(de);
- thisval = (long) dictGetVal(de);
- /* Expire sooner (minor expire unix timestamp) is better
- * candidate for deletion */
- if (bestkey == NULL || thisval < bestval) {
- bestkey = thiskey;
- bestval = thisval;
- }
- }
更多源码, 请参考 Redis 官网. 这里只是简单的展示两种. 我们可以根据这样的代码来对我们的 Redis 的定期过期做一个合理的配置
思考:
基于一个数据结构做缓存, 怎么实现一个 LRU 算法?
做一个有底线的博客主
来源: https://www.cnblogs.com/xlecho/p/11832128.html