Redis(八)—— LRU Cache

在计算机中缓存可谓无所不在,无论还是应用还是操作系统中,为了性能都需要做缓存。然缓存必然与缓存算法息息相关,LRU就是其中之一。笔者在最先接触LRU是大学学习操作系统时的了解到的,至今已经非常模糊。在学习Redis时,又再次与其相遇,这里将这块内容好好梳理总结。

  LRU(Least Recently Used)是缓存算法家族的一员——最近最少使用算法,类似的算法还有FIFO(先进先出)、LIFO(后进先出)等。因为缓存的选择一般都是用内存(RAM)或者计算机中的L1甚至L2高速缓存,无疑这些存储器都是有大小限制,使用的代价都非常高昂,不可能将所有需要缓存的数据都缓存起来,需要使用特定的算法,比如LRU将不常用的数据驱逐出缓存,以腾出更多的空间存储更有价值的数据。

  上图引用wiki上描述LRU算法的图。其中A是最近最少被使用,所以当缓存E时,根据LRU的特征,驱逐A,存储E。

  在LRU中最核心的部分是"最近最少",所以必然需要对缓存数据的访问(读和写)做跟踪记录——即什么时候使用了什么数据!

  本文先介绍Redis中的LRU特点,再介绍如何自实现一个LRU。

  • Redis中的LRU
  • 自实现LRU Cache
  • Guava Cache

Redis中的LRU

1.内存限制

  前文中介绍Redis是基于内存的K-V数据结构服务器。Redis在存储方面有内存容量限制。在Redis中通过maxmemory配置设置Redis可以使用的最大内存。如:

maxmemory 100mb

以上配置则限制Redis可以使用的最大内存为100M。当设置为0时,表示没有进行内存限制。

  上述说到Redis的内存限制问题,如果Redis使用的内存达到限制,再进行写入操作,Redis该如何处理?

  Redis中有很多驱逐策略——Eviction Policies,当内存达到限制时,Redis会使用其配置的策略对内存中的数据进行驱逐回收,释放内存空间,以便存储待写入的数据。

2.驱逐策略

Redis中有以下的内存策略可供选择

  • noeviction:没有任何驱逐策略,当内存达到限制,如果执行的命令需要使用更多的内存,则直接返回错误;
  • allkeys-lru:使用lru算法,即驱逐最近最少被使用的键,回收空间以便写入新的数据;
  • volatile-lru:使用lru算法,即驱逐最近最少被使用的被设置过期的键,回收空间以便写入新的数据;
  • allkeys-random:使用随机算法,即随机驱逐任意的键,回收空间以便写入新的数据
  • volatile-random:使用随机算法,即随机驱逐被设置的过期键,回收空间以便写入新的数据;
  • volatile-ttl:驱逐只有更短的有效期的被设置的过期键,回收空间以便写入新的数据;

其中volatile-lru、volatile-random和volatile-ttl策略驱逐,如果没有可驱逐的键,则结果和noeviction策略一样,都是返回错误。

Notes:

在Redis中的LRU算法并不是完全精确的LRU实现,只是近似的LRU算法——即通过采样一些键,然后从采样中按照LRU驱逐。如果要实现完全精确的LRU算法,势必需要跨越整个Redis内存进行统计,这样对性能就有折扣。在性能和LRU之间的trade off。

关于更多详细内容,请参考:Approximated LRU algorithm

自实现LRU Cache

上述介绍了Redis中对于内存限制实现其LRU策略,下面笔者综合平时所学,简单实现一个LRU Cache,加深对其理解。

实现LRU Cache的关键点在于:

  1. Cache目的为了提高查找的性能,所以如何设计Cache的数据结构保证查找的算法复杂度比较低是关键;
  2. LRU算法决定了必须要对Cache中的所有数据进行年龄追踪,即LRU中的数据的访问(读和写)都需要实时记录;

对于第一点,能够实现快速查找的数据结构Map是必选,映射表结构具有天然的快速查找的特点。

对于第二点,要么对每个元素维护一份访问信息——每个元素都有一个访问时间字段,要么以特定的顺序表示其访问信息——列表前后顺序表示访问时间排序。

基于以上分析,JDK中提供了Map和访问顺序的数据结构——LinkedHashMap。这里为了简单,基于LinkedHashMap实现。当然感兴趣还可以基于Map接口自实现,不过都是大同小异。

当然还可以使用第二点中的第一种方式,但是这样需要跨越整个缓存空间,遍历比较每个元素的访问时间,代价高昂。

先定义LRUCache的接口:

public interface LRUCache<K, V> {

    V put(K key, V value);

    V get(K key);

    int getCapacity();

    int getCurrentSize();
}

然后继承实现LinkedHashMap,在LinkedHashMap中有布尔accessOrder属性控制其顺序:true表示按照访问顺序,false表示按照插入顺序。 在构造LinkedHashMap时需要设置true,表示按照访问顺序进行迭代。

重写removeEldestEntry方法:如果当前缓存中的元素个数大于缓存的容量,则返回true,表示需要移除元素。

/**
* 基于{@link LinkedHashMap}实现的LRU Cache,该缓存是非线程安全,需要caller保证同步。
* 原理:
* 1.LinkedHashMap中使用双向循环链表的顺序有两种,其中访问顺序表示最近最少未被访问的顺序
* 2.基于HashMap,所以get的算法复杂度O(1)
*
* @author huaijin
*/
public final class LRUCacheBaseLinkedHashMap<K, V>
extends LinkedHashMap<K, V> implements LRUCache<K, V>{ private static final int DEFAULT_CAPACITY = 16;
private static final float DEFAULT_LOAD_FACTOR = 0.75F; /**
* 缓存大小
*/
private int capacity; public LRUCacheBaseLinkedHashMap() {
this(DEFAULT_CAPACITY);
} public LRUCacheBaseLinkedHashMap(int capacity) {
this(capacity, DEFAULT_LOAD_FACTOR);
} public LRUCacheBaseLinkedHashMap(int capacity, float loadFactor) {
super(capacity, loadFactor, true);
this.capacity = capacity;
} @Override
protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
return size() > capacity;
} @Override
public V put(K key, V value) {
Objects.requireNonNull(key, "key must not be null.");
Objects.requireNonNull(value, "value must not be null.");
return super.put(key, value);
} @Override
public String toString() {
List<String> sb = new ArrayList<>();
Set<Map.Entry<K, V>> entries = entrySet();
for (Map.Entry<K, V> entry : entries) {
sb.add(entry.getKey().toString() + ":" + entry.getValue().toString());
}
return String.join(" ", sb);
} @Override
public int getCapacity() {
return capacity;
} @Override
public int getCurrentSize() {
return size();
}
}

如果对LinkedHashMap不是很熟悉,请移步至Map 综述(二):彻头彻尾理解 LinkedHashMap

Guava Cache

对于使用Cache而言,Guava工具库中的Guava Cache是一个非常不错的选择。其优势在于:

  • 适用性:缓存在很多场景中都适用,无论是大到操作系统、数据库、浏览器和大型网站等等,小到平时开发的小型应用、移动app等等;
  • 多样性:Guava Cache提供多种方式载入数据至缓存;
  • 可驱逐:内存资源是宝贵的,这点无可否认!所以缓存数据需要置换策略,Guava Cache从多种维度提供了不同的策略;

详细情况可以参考:Caches。在Guava Cache中的驱逐策略有基于大小的策略,该策略就是LRU的实现:

Cache<String, String> guavaCache = CacheBuilder.newBuilder()
.maximumSize(5)
.build();

当Cache的容量达到5个时,如果再往缓存中写入数据,Cache将淘汰最近最少被使用的数据。

测试如下案例如下:

@Test
public void testLRUGuavaCacheBaseSize() throws ExecutionException {
Cache<String, String> guavaCache = CacheBuilder.newBuilder()
.maximumSize(5)
.build();
guavaCache.put("1", "1");
guavaCache.put("2", "2");
guavaCache.put("3", "3");
guavaCache.put("4", "4");
guavaCache.put("5", "5");
printGuavaCache("原cache:", guavaCache);
guavaCache.getIfPresent("1");
guavaCache.put("6", "6");
printGuavaCache("put一次后cache:", guavaCache);
}

执行结果:

原cache:2:2  3:3  1:1  5:5  4:4
put一次后cache:6:6 3:3 1:1 5:5 4:4

因为数据1被get一次,导致2是最近最少被使用,当put 6时,将2淘汰驱逐。

总结

  缓存目的就是为了提高访问速度以带来性能上质的提升。但是缓存的容量和命中率却是从反比。

  基于内存存储数据,无论应用本地缓存,还是分布式缓存服务器甚至操作系统,都需要考虑存储容量的限制对命中率的影响。采用合适缓存算法,对提高缓存命中率至为关键。

Redis(八) LRU Cache的更多相关文章

  1. 【Redis】LRU算法和Redis的LRU实现

    LRU原理 在一般标准的操作系统教材里,会用下面的方式来演示 LRU 原理,假设内存只能容纳3个页大小,按照 7 0 1 2 0 3 0 4 的次序访问页.假设内存按照栈的方式来描述访问时间,在上面的 ...

  2. redis的LRU算法(二)

    前文再续,书接上一回.上次讲到redis的LRU算法,文章实在精妙,最近可能有机会用到其中的技巧,顺便将下半部翻译出来,实现的时候参考下. 搏击俱乐部的第一法则:用裸眼观测你的算法 Redis2.8的 ...

  3. redis的LRU算法(一)

    最近加班比较累,完全不想写作了.. 刚看到一篇有趣的文章,是redis的作者antirez对redis的LRU算法的回顾.LRU算法是Least Recently Used的意思,将最近最少使用的资源 ...

  4. Redis的LRU机制(转)

    原文:Redis的LRU机制 在Redis中,如果设置的maxmemory,那就要配置key的回收机制参数maxmemory-policy,默认volatile-lru,参阅Redis作者的原博客:a ...

  5. Redis的LRU算法

    Redis的LRU算法 LRU算法背后的的思想在计算机科学中无处不在,它与程序的"局部性原理"很相似.在生产环境中,虽然有Redis内存使用告警,但是了解一下Redis的缓存使用策 ...

  6. 【Redis 设置Redis使用LRU算法】

    转自:http://ifeve.com/redis-lru/ 本文将介绍Redis在生产环境中使用的Redis的LRU策略,以及自己动手实现的LRU算法(php) 1.设置Redis使用LRU算法 L ...

  7. 从 LRU Cache 带你看面试的本质

    前言 大家好,这里是<齐姐聊算法>系列之 LRU 问题. 在讲这道题之前,我想先聊聊「技术面试究竟是在考什么」这个问题. 技术面试究竟在考什么 在人人都知道刷题的今天,面试官也都知道大家会 ...

  8. [LeetCode] LRU Cache 最近最少使用页面置换缓存器

    Design and implement a data structure for Least Recently Used (LRU) cache. It should support the fol ...

  9. 【leetcode】LRU Cache

    题目简述: Design and implement a data structure for Least Recently Used (LRU) cache. It should support t ...

随机推荐

  1. 如何使 highchart图表标题文字可选择复制

    highchart图表的一个常见问题是不能复制文字 比如官网的某个图表例子,文字不能选择,也无法复制,有时产品会抓狂... 本文给出一个简单的方案,包括一些解决的思路,希望能帮助到有需要的人 初期想了 ...

  2. 汇编push,pop

    版权声明:本文为博主原创文章,转载请附上原文出处链接和本声明.2019-08-24,00:40:12作者By-----溺心与沉浮----博客园 1.BASE,TOP是2个32位的通用寄存器,里面存储的 ...

  3. Django框架(二十三)-- Django rest_framework-视图组件

    一.基本视图 class PublishView(APIView): def get(self, request): publish_list = Publish.objects.all() bs = ...

  4. R 基于朴素贝叶斯模型实现手机垃圾短信过滤

    # 读取数数据, 查看数据结构 df_raw <- read.csv("sms_spam.csv", stringsAsFactors=F) str(df_raw) leng ...

  5. 防止xss攻击的前端的方法

    项目当中在进行安全测试的时候,遇到了xss的攻击,要求前端来做个防御,针对于遇到的xss攻击,做个总结 1.xss---存储型xss的攻击 前端只要在接收到后台数据的时候做个特殊字符的过滤,即可抵制攻 ...

  6. C语言之整除

    除法运算符:/ 当除数和被除数都整形时,就是整除. 当浮点数和整数放到一起运算时,C语言会将整数转换成浮点数,然后进行浮点数的运算. #include<stdio.h> int main( ...

  7. 201871010124-王生涛《面向对象程序设计(java)》第四周学习总结

    项目 内容 这个作业属于哪个课程 <任课教师博客主页链接>https://www.cnblogs.com/nwnu-daizh/ 这个作业的要求在哪里 <作业链接地址>http ...

  8. 【Spring Data JPA篇】项目环境搭建(一)

    项目环境: spring4.1.6 hibernate4.3.11 spring-data-jpa1.9.0 1. 创建一个Java Project,将jar导入到lib目录下 #spring spr ...

  9. USACO Apple Delivery

    洛谷 P3003 [USACO10DEC]苹果交货Apple Delivery 洛谷传送门 JDOJ 2717: USACO 2010 Dec Silver 1.Apple Delivery JDOJ ...

  10. AWS云部署项目——数据库与服务器

    1.连接数据库 选择服务RDS,进入后点击数据库实例,在之前建好的数据库内进行操作 首先是安全组,类似于aws云上的防火墙一样的东西,先设置好公开性,安全组置为default(就是尽量避免测试时访问阻 ...