关键字:maxmemory、LRU、LFU、Eviction policies、如何选择最合适的淘汰策略?
把Redis当做一个缓存来用的时候(Redis也可以作为持久化的数据库来使用),在内存不足时或者超过配置的上限时,它会删除一些旧的数据让新的数据能够被添加到缓存中,这是作为缓存部件最基本的功能。
Redis可以使用的内存大小是这么规定的:32位版本默认是3GB,而64位默认不限制。但是,在实际生产中,建议对它进行最大值配置,可通过在redis.conf中增加配置:
maxmemory 500mb |
或者在Redis运行时,通过一下命令来:
config set maxmemory 500mb |
当然如果配置为0,则是不限制内存大小(不建议如此)。
一旦内存使用到达上限,那么我们就得采用不同的内存淘汰策略:
noeviction:默认。不淘汰旧数据,新的请求在无法分派到空间时,会返回异常。注意,读请求是不受影响的。
allkeys-lru:least recently used,在所有的键值中,使用【近似LRU算法】进行淘汰
allkeys-random:在所有的键值中,随机淘汰。
volatile-lru:在设置了expire的键值中,使用【近似LRU算法】进行淘汰。
volatile-random:在设置了expire的键值中,随机淘汰。
volatile-ttl:shorter time to live有效期最小的数据
其中,volatile-lru, volatile-lfu, volatile-ttl在没有键值对设置了expire的情况下,会跟noeviction一样,空间不足时返回异常。
根据应用程序的访问模式来决定淘汰策略是很重要的,当然,淘汰策略可以在运行时随时修改。我们随时可以借助INFO命令来查看命中率,然后调整我们的策略。一般来说:
- allkeys-lru适合那些符合幂律分布(简单地理解为二八法则)的数据,即少量一部分数据的访问频次远远比其他数据要高。
- allkeys-random适用于所有的数据的访问频次相差不大。
- volatile-ttl,当你希望通过expire来控制淘汰策略的时候,expire越小,越被早淘汰。
- volatile-lru, volatile-random,当你的一个Redis实例即作为缓存也作为数据持久库的时候,可以使用这两种策略。当然,老叟更建议用两个Redis实例来分别做缓存和数据持久化【注1】。
另,设置expire是要占空间的,所以使用allkeys-lru的场景下,不需要设置expire,这使得空间使用率更高。
淘汰的过程到底是怎么样的呢?
- 客户端发来一个写命令,将数据保存。
- Redis检查空间使用情况,如果当前使用空间大于限制,那么会根据淘汰策略去淘汰键值。
- 执行下一条命令。
从以上过程可以看出,内存的使用是会超过我们所配置的限制的,一旦超过,则会去执行淘汰,让内存的使用在限制值之内。如果该命令会占用很大的内存,那么Redis会在很短的时间之内(在淘汰之前)会超过限制很多。
近似LRU算法
为了节省内存,Redis中的LRU算法并不是严格的LRU算法,意味着,Redis并不具备找到最合适的淘汰的对象的能力,即被访问过最久远的对象。Redis实现的是近似LRU算法,通过采样的方式,在样本中选取最合适淘汰对象(with the olded access time)。
在3.0版本中,改善了这个算法,使得这个算法更加接近于真正的LRU算法,还可以更改maxmemory-samples来调整这个算法的精确性。
下图是官方给出的算法对比图(https://redis.io/topics/lru-cache)
其中:
- 每个点(淡灰色的,灰色的,蓝色的)代表者一个键值对
- 淡灰色和灰色是沾满了内存的键值对
- 绿色是新加入的键值对,会触发allkeys-lru策略去淘汰键值对
- 淡灰色是被淘汰的键值对
测试过程是这样子的,往Redis中存入满员的键值对(假设有10M),键值对按照插入先后顺序依次被访问过,对LRU算法来说,相当于第一个键值对是最符合的淘汰对象。接着,我们再插入5M的键值对,然后观察哪5M的键值对会被淘汰。
- 左上图是理论上的LRU算法的效果图,严格按照Least Recenty Used来淘汰对象。
- 左下图是Redis早期版本中的近似LRU算法,你会发现,有部分绿色的键值对也会被淘汰。
- 右下图是Redis3.0版本之后改进的近似LRU算法,其中样本数是5的效果,更加接近LRU。
- 右上图是样本数是10的效果,相对上述两个来说最接近真是的LRU。
所以,如果缓存中的数据符合幂律分布,那么LRU算法可以很好的处理键值对的淘汰问题。
样本数默认是5(config get maxmemory-samples),当然你可以调整该样本数的大小来观察命中率,值越大,效果越接近真实的LRU,但是代价是性能会有所消耗。大量的测试表明(官方说法),样本数为5,而且数据符合幂律分布的情况下,Redis的近似LRU几乎已经等价于真是的LRU。
LRU算法的实现,可以去看看Mybatis中的org.apache.ibatis.cache.decorators.LruCache,它使用了LinkedHashMap(accessOrder=true,并且重写了removeEldestEntry方法)来实现。
近似LFU算法
LRU算法有个缺点,比如在准备执行LRU之前,有个很少用到的键值对被访问了一次,那么在LRU算法下,它被淘汰的概率很小,比热点数据的淘汰概率还要小。所以在Redis4.0版本新增了近似LFU(Lest Frequently Used eviction mode)策略。
- volatile-lfu:在设置了expire的键值对中使用近似LFU算法进行淘汰。
- allkeys-lfu:在所有的键值对中使用近似LFU算法进行淘汰。
LFU算法是淘汰一段时间内最少被访问到的键值对,它使用了Approximate counting algorithm,使用很少的内存就可以估算一个键值对被访问到的概率。
注:
- Redis作为持久化数据库,为了保证数据不丢失,会配置AOF(always)+ RDB。而当作为缓存的时候,为了更高的性能,一般不配置AOF,而是看情况来配置RDB。