本文已参与掘金创作者训练营第三期「话题写作」赛道,详情查看:掘力计划|创作者训练营第三期正在进行,「写」出个人影响力。
前言
我们知道Redis数据主要是存放在内存里面, 内存的数据是有限, 不可能一直存储,那么就需要缓存的淘汰策略,去保证我们访问的数据高效.
缓存淘汰策略
1. 为什么需要淘汰策略?
众所周知,Redis作为知名内存型NOSQL,极大提升了程序访问数据的性能,高性能互联网应用里,几乎都能看到Redis的身影。为了提升系统性能,Redis也从单机版、主从版发展到集群版、读写分离集群版等等,业界也有诸多著名三方扩展库(如Codis、Twemproxy).
阿里云的企业版Redis(Tair)的性能增强型集群版更是“豪无人性”,内存容量高达4096 GB 内存,支持约61440000 QPS。
对然内存容量越来越大, 但是4096G 相对于互联大数据, 这个也是不够的, 另外内存的成本还是很高的, 所以当数据量到达一定程度,肯定需要进行内存数据淘汰了.
2. 淘汰策略设置
线上环境Redis不设置淘汰策略, 是十分危险的, 当流量太大会把Redis服务内存耗尽导致不可挽回的生产事故.
设置了maxmemory的选项,redis内存使用达到上限。可以通过设置LRU算法来删除部分key,释放空间。默认是按照过期时间的,如果set时候没有加上过期时间就会导致数据写满maxmemory。
如果不设置maxmemory或者设置为0,64位系统不限制内存,32位系统最多使用3GB内存。
# 动态修改maxmemory
config set maxmemory 10GB
# 查看 maxmemory
config get maxmemory info memory | grep maxmemory
redis-cli -h 127.0.01 -p 6379 config get maxmemory
默认的淘汰策略是noeviction, 建议修改
3. 实现原理
3.1 优胜劣汰LRU
LRU(Least Recently Used)最近最少使用。优先淘汰最近未被使用的数据,其核心思想是“如果数据最近被访问过,那么将来被访问的几率也更高”。
LRU底层结构是 hash 表 + 双向链表。hash 表用于保证查询操作的时间复杂度是O(1),双向链表用于保证节点插入、节点删除的时间复杂度是O(1)。
近似LRU算法原理
Redis为什么不使用原生LRU算法?
- 原生LRU算法需要 双向链表 来管理数据,需要额外内存;
- 数据访问时涉及数据移动,有性能损耗;
- Redis现有数据结构需要改造;
Redis近似LRU算法的优势?
在Redis中,Redis的key的底层结构是 redisObject,redisObject 中 lru:LRU_BITS 字段用于记录该key最近一次被访问时的Redis时钟 server.lruclock(Redis在处理数据时,都会调用lookupKey方法用于更新该key的时钟)。server.lruclock 实际是一个 24bit 的整数,默认是 Unix 时间戳对 2^24 取模的结果,其精度是毫秒。
我们看下它的源码:
typedef struct redisObject {
unsigned type:4; // 类型
unsigned encoding:4; // 编码
unsigned lru:LRU_BITS; /* LRU time (relative to global lru_clock) or
* LFU data (least significant 8 bits frequency
* and most significant 16 bits access time). */
int refcount;
void *ptr; // 指向存储实际值的数据结构的指针,数据结构由 type、encoding 决定
} robj;
server.lruclock 的值是如何更新的呢?
Redis启动时,initServer 方法中通过 aeCreateTimeEvent 将 serverCron 注册为时间事件(serverCron 是Redis中最核心的定时处理函数), serverCron 中 则会 触发 更新Redis时钟的方法 server.lruclock = getLRUClock() 。
int serverCron(struct aeEventLoop *eventLoop, long long id, void *clientData) {
// ...
```
/* We have just LRU_BITS bits per object for LRU information.
* So we use an (eventually wrapping) LRU clock.
*
* Note that even if the counter wraps it's not a big problem,
* everything will still work but some object will appear younger
* to Redis. However for this to happen a given object should never be
* touched for all the time needed to the counter to wrap, which is
* not likely.
*
* Note that you can change the resolution altering the
* LRU_CLOCK_RESOLUTION define. */
unsigned int lruclock = getLRUClock();
atomicSet(server.lruclock,lruclock);
cronUpdateMemoryStats();
// ```
}
当 mem_used > maxmemory 时,Redis通过 freeMemoryIfNeeded 方法完成数据淘汰。LRU策略淘汰核心逻辑在 evictionPoolPopulate(淘汰数据集合填充) 方法。
3.2 精益求精-LFU VS LRU
LFU Least Frequently Used,使用频率最少的(最不经常使用的) Antirez 在Redis 4.0 里面引入了一个新的淘汰策略LFU模式. 这个后面在从源码角度详细介绍吧.
最后
Reids作为一个非常优秀的开源代码, 代码量只有2w行左右的代码, 感觉每个每个原理都是经典, 学C的同学推荐可以通读一下Redis的源码了解它的设计思想原理.
参考
《Redis深度历险 核心原理与应用实践》
Redis-8种数据淘汰策略及近似LRU、LFU原理