Redis LRU caches解析
LRU简介
LRU(least recently used)最近最少使用算法,页面置换算法。根据数据的历史访问记录来进行淘汰数据,其核心思想是:如果数据最近被访问过,那么将来被访问的几率也更高。
redis配置文件中的参数项
# 限定redis最大内存使用量
# maxmemory <bytes>
# 置换策略
# 当redis内存使用达到maxmemory时,需要选择设置好的maxmemory-policy对旧数据进行置换
# 1) noeviction:不进行置换,表示即使内存达到上限也不进行置换,所有能引起内存增加的命令都返回error
# 2) allkeys-lru:优先删除最近最不经常使用的key,用以保存新数据
# 3) allkeys-random:只从设置失效的key中选择最近最不经常使用的key进行删除,用以保存新数据
# 4) volatile-random:只从设置失效的key中,选择一些key进行删除,用以保存新数据
# 5) volatile-ttl:只从设置失效的key中,选出存活时间最短的key进行删除,用以保存新数据
# maxmemory-policy volatile-lru
# redis的LRU不是严格意义上的LRU算法实现,是一种近似的LRU现实,主要是为了节约内存占用提升性能
# redis的LRU取出maxmemory-samples数量的key,然后从中选择一个最近最不经常使用的key进行置换
# maxmemory-samples 3
置换策略选择
一般的经验规则:
- 使用allkeys-lru策略:当你希望你的请求符合一个幂定律分布,也就是说,你希望部分的子集元素将比其它其它元素被访问的更多。如果你不确定选择什么,这是个很好的选择。
- 使用allkeys-random:如果你是循环访问,所有的键被连续的扫描,或者你希望请求分布正常(所有元素被访问的概率都差不多)。
- 使用volatile-ttl:如果你想要通过创建缓存对象时设置TTL值,来决定哪些对象应该被过期。
- allkeys-lru 和 volatile-random策略对于当你想要单一的实例实现缓存及持久化一些键时很有用。不过一般运行两个实例是解决这个问题的更好方法。
为了键设置过期时间也是需要消耗内存的,所以使用allkeys-lru这种策略更加高效,因为没有必要为键取设置过期时间当内存有压力时。
LRU test
# <keys>表示key的分布范围
--lru-test <keys>
用于测试redis置换策略的性能以及分析设置的maxmemory项是否合理。
测试
置换策略是allkeys-lru
设置配置文件maxmemory项为100MB
maxmemory 104857600
$ ./redis-cli --lru-test 10000000
156000 Gets/sec | Hits: 4552 (2.92%) | Misses: 151448 (97.08%)
153750 Gets/sec | Hits: 12906 (8.39%) | Misses: 140844 (91.61%)
159250 Gets/sec | Hits: 21811 (13.70%) | Misses: 137439 (86.30%)
151000 Gets/sec | Hits: 27615 (18.29%) | Misses: 123385 (81.71%)
145000 Gets/sec | Hits: 32791 (22.61%) | Misses: 112209 (77.39%)
157750 Gets/sec | Hits: 42178 (26.74%) | Misses: 115572 (73.26%)
154500 Gets/sec | Hits: 47418 (30.69%) | Misses: 107082 (69.31%)
151250 Gets/sec | Hits: 51636 (34.14%) | Misses: 99614 (65.86%)
N Gets/sec 表示每秒执行了多少次get
Hits: M(m%) 表示M次命中,命中率m%
Misses: P(p%) 表示P次没有命中,命中率p%
N = M + P
随着时间的增加,命中率会增加,但是当内存使用量超过设置的最大内存时,命中率就会在一个范围内波动。
内部实现
实现位于redis-cli.c
#define LRU_CYCLE_PERIOD 1000 /* 1000 milliseconds. */
#define LRU_CYCLE_PIPELINE_SIZE 250
static void LRUTestMode(void) {
redisReply *reply;
char key[128];
PORT_LONGLONG start_cycle;
int j;
srand((unsigned int)(time(NULL)^getpid())); WIN_PORT_FIX /* cast (unsigned int) */
while(1) {
/* Perform cycles of 1 second with 50% writes and 50% reads.
* We use pipelining batching writes / reads N times per cycle in order
* to fill the target instance easily. */
start_cycle = mstime();
PORT_LONGLONG hits = 0, misses = 0;
while(mstime() - start_cycle < 1000) {
/* Write cycle. */
for (j = 0; j < LRU_CYCLE_PIPELINE_SIZE; j++) {
LRUTestGenKey(key,sizeof(key));
redisAppendCommand(context, "SET %s val",key);
}
for (j = 0; j < LRU_CYCLE_PIPELINE_SIZE; j++)
redisGetReply(context, (void**)&reply);
/* Read cycle. */
for (j = 0; j < LRU_CYCLE_PIPELINE_SIZE; j++) {
LRUTestGenKey(key,sizeof(key));
redisAppendCommand(context, "GET %s",key);
}
/* 统计命中次数 */
for (j = 0; j < LRU_CYCLE_PIPELINE_SIZE; j++) {
if (redisGetReply(context, (void**)&reply) == REDIS_OK) {
switch(reply->type) {
case REDIS_REPLY_ERROR:
printf("%s\n", reply->str);
break;
case REDIS_REPLY_NIL:
misses++;
break;
default:
hits++;
break;
}
}
}
if (context->err) {
fprintf(stderr,"I/O error during LRU test\n");
exit(1);
}
}
/* 计算命中率 */
printf(
"%lld Gets/sec | Hits: %lld (%.2f%%) | Misses: %lld (%.2f%%)\n",
hits+misses,
hits, (double)hits/(hits+misses)*100,
misses, (double)misses/(hits+misses)*100);
}
exit(0);
}
可以看出命中率的计算分为以下5步:
1. 首先通过pipeline机制 SET 250次,SET的内容:key->”lru:<%d>” value->”val”,<%d>根据传入参数项keys按照2-8分布计算得到,具体可查看powerLowRand函数
2. 然后通过pipeline机制 GET 250次,GET的key使用上述相同的方法生成
3. 接着统计这250次GET中的命中次数
4. 重复1,2,3步,统计不超过1秒间隔内GET的命中次数,并计算命中率
5. 重复前四步,不断计算GET的命中率
可以看出,刚开始测试时,由于数据库中插入的key-value较少,命中率会很低,随着时间的增加,插入的key-value越来越多,GET的命中率会提高,但是当内存使用量超过最大内存设置量时,会出现页面交换的情况,此时命令率会在一个范围内波动。
影响因素
- key-value大小,m
- key-value数量,n
- maxmemory设置大小,p
如果m*n>p,就发生页面置换,会影响Redis性能。