1、简介
Redis是基于内存存储的key-value数据库,我们知道内存虽然快但空间小,当物理内存达到上限时,系统就会跑的很慢,这是因为swap机制会将部分内存的数据转移到swap分区中,通过与swap的交换保证系统继续运行;但是swap属于硬盘存储,速度远远比不上内存,尤其是对于Redis这种QPS非常高的服务,发生这种情况是无法接收的。(注意如果swap分区内存也满了,系统就会发生错误!)
Linux操作系统可以通过free -m
查看swap大小:
因此如何防止Redis发生这种情况非常重要(面试官问到Redis几乎没有不问这个知识点的)。
2、maxmemory配置
Redis针对上述问题提供了maxmemory配置,这个配置可以指定Redis存储器的最大数据集,通常情况都是在redis.conf文件中进行配置,也可以运行时使用CONFIG SET命令进行一次性配置。
redis.conf文件中的配置项示意图:
默认情况maxmemory配置项并未启用,Redis官方介绍64位操作系统默认无内存限制,32位操作系统默认3GB隐式内存配置,如果maxmemory 为0,代表内存不受限。
因此我们在做缓存架构时,要根据硬件资源+业务需求做合适的maxmemory配置。
3、内存达到maxmemory怎么办
如果Redis中数据非常多,将服务器中的内存都耗尽,这样就会出现内存溢出的情况,Redis开发组考虑到了这种问题,使用数据淘汰策略可以解决这个问题。
可以设置内存最大使用量,当内存使用量超出时,会施行数据淘汰策略。
Redis 具体有 6 种淘汰策略:
策略 | 描述 | 应用场景 |
---|---|---|
volatile-lru | 从已设置过期时间的数据集中挑选最近最少使用的数据淘汰 | 如果设置了过期时间,且分热数据与冷数据,推荐使用 volatile-lru 策略。 |
volatile-ttl | 从已设置过期时间的数据集中挑选将要过期的数据淘汰 | 如果让 Redis 根据 TTL 来筛选需要删除的key,请使用 volatile-ttl 策略。 |
volatile-random | 从已设置过期时间的数据集中任意选择数据淘汰 | 从已设置过期时间的数据集中任意选择数据淘汰 |
allkeys-lru | 从所有数据集中挑选最近最少使用的数据淘汰 | 使用 Redis 缓存数据时,为了提高缓存命中率,需要保证缓存数据都是热点数据。可以将内存最大使用量设置为热点数据占用的内存量,然后启用 allkeys-lru 淘汰策略,将最近最少使用的数据淘汰。值得一提的是,设置 expire 会消耗额外的内存,所以使用 allkeys-lru 策略,可以更高效地利用内存,因为这样就可以不再设置过期时间了。 |
allkeys-random | 从所有数据集中任意选择数据进行淘汰 | 如果需要循环读写所有的key,或者各个key的访问频率差不多,可以使用 allkeys-random 策略 |
noeviction | 不删除策略,达到最大内存限制时,如果需要更多内存,直接返回错误信息。大多数写命令都会导致占用更多的内存 | 不删除策略,达到最大内存限制时,如果需要更多内存,直接返回错误信息。大多数写命令都会导致占用更多的内存 |
作为内存数据库,出于对性能和内存消耗的考虑,Redis 的淘汰算法实际实现上并非针对所有 key,而是抽样一小部分并且从中选出被淘汰的 key。
Redis 4.0 引入了
volatile-lfu
和allkeys-lfu
淘汰策略,LFU 策略通过统计访问频率,将访问频率最少的键值对淘汰。
您需要根据系统的特征,来选择合适的淘汰策略。 当然,在运行过程中也可以通过命令动态设置淘汰策略,并通过 INFO 命令监控缓存的 miss 和 hit,来进行调优。
4. 淘汰策略的内部实现
- 客户端执行一个命令,导致 Redis 中的数据增加,占用更多内存
- Redis 检查内存使用量,如果超出 maxmemory 限制,根据策略清除部分 key
- 继续执行下一条命令,以此类推
在这个过程中,内存使用量会不断地达到 limit 值,然后超过,然后删除部分 key,使用量又下降到 limit 值之下。
如果某个命令导致大量内存占用(比如通过新key保存一个很大的set),在一段时间内,可能内存的使用量会明显超过 maxmemory 限制。
5、LRU算法实现
我们先用Java的容器实现一个简单的LRU算法,我们使用ConcurrentHashMap做key-value结果存储元素的映射关系,使用ConcurrentLinkedDeque来维持key的访问顺序。
LRU实现代码:
package com.lizba.redis.lru;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentLinkedDeque;
/**
* <p>
* LRU简单实现
* </p>
*
* @Author: Liziba
* @Date: 2021/9/17 23:47
*/
public class SimpleLru {
/** 数据缓存 */
private ConcurrentHashMap<String, Object> cacheData;
/** 访问顺序记录 */
private ConcurrentLinkedDeque<String> sequence;
/** 缓存容量 */
private int capacity;
public SimpleLru(int capacity) {
this.capacity = capacity;
cacheData = new ConcurrentHashMap(capacity);
sequence = new ConcurrentLinkedDeque();
}
/**
* 设置值
*
* @param key
* @param value
* @return
*/
public Object setValue(String key, Object value) {
// 判断是否需要进行LRU淘汰
this.maxMemoryHandle();
// 包含则移除元素,新访问的元素一直保存在队列最前面
if (sequence.contains(key)) {
sequence.remove();
}
sequence.addFirst(key);
cacheData.put(key, value);
return value;
}
/**
* 达到最大内存,淘汰最近最少使用的key
*/
private void maxMemoryHandle() {
while (sequence.size() >= capacity) {
String lruKey = sequence.removeLast();
cacheData.remove(lruKey);
System.out.println("key: " + lruKey + "被淘汰!");
}
}
/**
* 获取访问LRU顺序
*
* @return
*/
public List<String> getAll() {
return Arrays.asList(sequence.toArray(new String[] {}));
}
}
测试代码:
package com.lizba.redis.lru;
/**
* <p>
* 测试最近最少使用
* </p>
*
* @Author: Liziba
* @Date: 2021/9/18 0:00
*/
public class TestSimpleLru {
public static void main(String[] args) {
SimpleLru lru = new SimpleLru(8);
for (int i = 0; i < 10; i++) {
lru.setValue(i+"", i);
}
System.out.println(lru.getAll());
}
}
测试结果:
从上数的测试结果可以看出,先加入的key0,key1被淘汰了,最后加入的key也是最新的key保存在sequence的队头。
通过这种方案,可以很简单的实现LRU算法;但缺点也十分明显,方案需要使用额外的数据结构来保存key的访问顺序,这样会使Redis内存消耗增加,本身用来优化内存的方案,却要消耗不少内存,显然是不行的。
6、Redis的近似LRU
针对这种情况,Redis使用了近似LRU算法,并不是完完全全准确的淘汰掉最近最不经常使用的key,但是总体的准确度也可以得到保证。近似LRU算法非常简单,在Redis的key对象中,增加24bit用于存储最近一次访问的系统时间戳,当客户端对Redis服务端发送key的写入相关请求时,发现内存达到maxmemory,此时触发惰性删除;Redis服务通过随机采样,选择5个满足条件的key(注意这个随机采样allkeys-lru
是从所有的key中随机采样,volatile-lru
是从设置了过期时间的所有key中随机采样),通过key对象中记录的最近访问时间戳进行比较,淘汰掉这5个key中最旧的key;如果内存仍然不够,就继续重复这个步骤。
注意,5是Redis默认的随机采样数值大小,它可以通过redis.conf中的maxmemory_samples进行配置:
针对上述的随机LRU算法,Redis官方给出了一张测试准确性的数据图:
- 最上层浅灰色表示被淘汰的key,图一是标准的LRU算法淘汰的示意图
- 中间深灰色层表示未被淘汰的旧key
- 最下层浅绿色表示最近被访问的key
- 在Redis 3.0 maxmemory_samples设置为10的时候,Redis的近似LRU算法已经非常的接近真实LRU算法了,但是显然maxmemory_samples设置为10比maxmemory_samples 设置为5要更加消耗CPU计算时间,因为每次采样的样本数据增大,计算时间也会增加。
- Redis3.0的LRU比Redis2.8的LRU算法更加准确,是因为Redis3.0增加了一个与maxmemory_samples相同大小的淘汰池,每次淘汰key的时候,先与淘汰池中等待被淘汰的key进行比较,最后淘汰掉最老旧的key,其实就是被选中淘汰的key放到一起再比较一下,淘汰其中最旧的。
7、存在问题
LRU算法看似比较好用,但是也存在不合理的地方,比如A和B两个key,在发生淘汰时的前一个小时前同一时刻添加到Redis,A在前49分钟被访问了1000次,但是后11分钟没有被访问;B在这一个小时内仅仅第59分钟被访问了1次;此时如果使用LRU算法,如果A、B均被Redis采样选中,A将会被淘汰很显然这个是不合理的。
针对这种情况Redis 4.0添加了LFU算法,(Least frequently used) 最不经常使用,这种算法比LRU更加合理,下文将会一起学习中淘汰算法,如有需要请关注我的专栏。