阶段十:总结专题(第六章:缓存篇)

本文详细介绍了Redis中的数据类型,包括String、List、Hash、SortedSet等,以及Redis的缓存策略,如keys命令问题、过期key删除策略、AOF和RDB持久化。此外,还讨论了缓存击穿、雪崩、穿透问题和旁路缓存的一致性解决方案。最后,提到了基于链表的LRUCache实现和Redis的LRUCache策略。
摘要由CSDN通过智能技术生成

第六章:缓存篇

1. Redis 数据类型

要求

  • 掌握常见数据类型的底层结构

概述

数据类型实际描述的是 value 的类型,key 都是 string,常见数据类型(value)有

  1. 字符串)string(embstr、raw、int)
  2. 列表)list(quicklist,由多个 ziplist 双向链表组成)
  3. 哈希表)hash(ziplist、hashtable)
  4. 集合中的元素不能重复)set(intset、hashtable)
  5. 在set基础上有序)sorted set(ziplist、skiplist)
  6. 用的不多,做统计)bitmap
  7. 用的不多,做统计)hyperloglog

每一种类型都用 redisObject 结构体来表示,每种类型根据情况不同,有不同的编码 encoding(即底层数据结构)

String

  1. 如果字符串保存的是整数值,则底层编码为 int(4字节),实际使用 long(8字节) 来存储

    好处:①数字占用的空间小;②做数据运算方便,不用把字符装换成数字在做运算啦;

  2. 如果字符串保存的是非整数值(浮点数字或其它字符)又分两种情况

    1. 长度 <= 39 字节,使用 embstr 编码来保存,即将 redisObject 和 sdshdr 结构体保存在一起,分配内存只需一次
    2. 长度 > 39 字节,使用 raw 编码来保存,即 redisObject 结构体分配一次内存,sdshdr 结构体分配一次内存,用指针相连
  3. sdshdr 称为简单动态字符串,实现上有点类似于 java 中的 StringBuilder,有如下特性

    1. 单独存储字符长度,相比 char* 获取长度效率高(char* 是 C 语言原生字符串表示)
    2. 支持动态扩容,方便字符串拼接操作
    3. 预留空间,减少内存分配、释放次数(< 1M 时容量是字符串实际长度 2 倍,>= 1M 时容量是原有容量 + 1M)
    4. 二进制安全,例如传统 char* 以 \0 作为结束字符,这样就不能保存视频、图片等二进制数据,而 sds 以长度来进行读取

List

面试题:quicklist与ziplist的区别

  1. 3.2 开始,Redis 采用 quicklist(大链表)作为其编码方式,它是一个双向链表,节点元素是 ziplist(小链表,其中存储数据)

    1. 由于是链表,内存上不连续
    2. 操作头尾效率高,时间复杂度 O(1),操作其他部位平均时间复杂度是O(n)
    3. 链表中 ziplist 的大小和元素个数都可以设置,其中大小默认 8kb
  2. ziplist 用一块连续的内存存储数据,设计目标是让数据存储更紧凑,减少碎片开销,节约内存,它的结构(了解即可)如下

    1. zlbytes – 记录整个 ziplist 占用字节数
    2. zltail-offset – 记录尾节点偏移量
    3. zllength – 记录节点数量
    4. entry – 节点,1 ~ N 个,每个 entry 记录了前一 entry 长度(为了倒序遍历),本 entry 的编码、长度、实际数据,为了节省内存,根据实际数据长度不同,用于记录长度的字节数也不同,例如前一 entry 长度是 253 时,需要用 1 个字节,但超过了 253,需要用 5 个字节
    5. zlend – 结束标记
  3. ziplist 适合存储少量元素,否则查询效率不高,并且长度可变的设计会带来连锁更新问题

Hash

  1. 在数据量较小时,采用 ziplist 作为其编码(将键值作为两个连续entry),当键或值长度过大(64)或个数过多(512)时,转为 hashtable 编码

  2. hashtable 编码【下面都是要点,就不做标注了】

    • hash 函数,Redis 5.0 采用了 SipHash 算法

    • 采用拉链法解决 key 冲突(解决哈希冲突)

    • rehash 时机

      ① 当元素数 < 1 * 桶个数时,不扩容

      ② 当元素数 > 5 * 桶个数时,一定扩容

      ③ 当 1 * 桶个数 <= 元素数 <= 5 * 桶个数时,如果此时没有进行 AOF 或 RDB 操作时,就进行rehash

      ④ 当元素数 < 桶个数 / 10 时,缩容

    • rehash 要点

      ① 每个字典有两个哈希表,桶个数为 2 n 2^n 2n,平时使用 ht[0],ht[1] 开始为 null,扩容时新数组大小为元素个数 * 2

      渐进式 rehash(防止一次时间过长),即不是一次将所有桶都迁移过去,每次对这张表 CRUD 仅迁移一个桶

      active(主动) rehash,server 的主循环中,每 100 ms 里留出 1s 进行主动迁移

      ④ rehash 过程中,新增操作 ht[1] ,其它操作先操作 ht[0],若没有,再操作 ht[1]

      ⑤ redis 所有 CRUD 都是单线程,因此 rehash 一定是线程安全的

Sorted Set

  1. 在数据量较小时,采用 ziplist 作为其编码,按 score 有序,当键或值长度过大(64)或个数过多(128)时,转为 skiplist(跳表) + hashtable 编码,同时采用的理由是

    • 只用 hashtable,CRUD 是 O(1),但要执行有序操作,需要排序,带来额外时间空间复杂度
    • 只用 skiplist,虽然范围操作优点保留,但时间复杂度上升
    • 虽然同时采用了两种结构,但由于采用了指针,元素并不会占用双份内存
  2. skiplist 要点:多层链表、排序规则、 backward、level(span,forward)
    在这里插入图片描述

  • score 存储分数、member 存储数据、按 score 排序,如果 score 相同再按 member 排序
  • backward 存储上一个节点指针(方便反向遍历)
  • 每个节点中会存储层级信息(level),同一个节点可能会有多层,每个 level 有属性:
    • foward 同层中下一个节点指针(前向指针,方便正序遍历)
    • span 跨度,用于计算排名,不是所有跳表都实现了跨度,Redis 实现特有
  1. 多层链表可以加速查询,减少查询次数,规则为,从顶层开始

    1. 大于同层右边的,继续在同层向右找

    2. 相等找到了

    3. 小于同层右边的或右边为 NULL,下一层,重复 1、2 步骤
      在这里插入图片描述

  • 以查找【崔八】为例
    1. 从顶层(4)层向右找到【王五】节点,22 > 7 继续向右找,但右侧是 NULL,下一层
    2. 在【王五】节点的第 3 层向右找到【孙二】节点,22 < 37,下一层
    3. 在【王五】节点的第 2 层向右找到【赵六】节点,22 > 19,继续向右找到【孙二】节点,22 < 37,下一层
    4. 在【赵六】节点的第 1 层向右找到【崔八】节点,22 = 22,返回

注意

  • 数据量较小时,不能体现跳表的性能提升,跳表查询的时间复杂度是 l o g 2 ( N ) log_2(N) log2(N),与二叉树性能相当

2. keys 命令问题

面试题:redis有一亿个 key,使用 keys 命令是否会影响线上服务?

要求

  • 理解低效命令对单线程的 Redis 影响

问题描述

  • redis有一亿个 key,使用 keys 命令是否会影响线上服务?

解答

  • keys 命令时间复杂度是 O ( n ) O(n) O(n),n 即总的 key 数量,n 如果很大,性能非常低
  • redis 执行命令是单线程执行,一个命令执行太慢会阻塞其它命令,阻塞时间长甚至会让 redis 发生故障切换

改进方案【重要】

  • 可以使用 scan 命令替换 keys 命令,语法 scan 起始游标 match 匹配规则 count 提示数目,返回值代表下次的起点
    1. 虽然 scan 命令的时间复杂度仍是 O ( n ) O(n) O(n),但它是通过游标分步执行,不会导致长时间阻塞
    2. 可以用 count 参数提示返回 key 的个数(默认是10)
    3. 返回值代表下次的起点(桶下标)
    4. 弱状态,客户端仅需维护游标
    5. scan 能保证在 rehash 也正常工作
    6. 缺点是可能会重复遍历 key(缩容时)、应用程序应自己处理重复 key
      在这里插入图片描述

3. 过期 key 的删除策略

要求

  • 了解 Redis 如何记录 key 的过期时间
  • 掌握 Redis 对过期 key 的删除策略

记录 key 过期时间

  • 每个库中都包含了 expires 过期字典
    • hashtable结构,键为指针,指向真正 key,值为 long 类型的时间戳,毫秒精度
  • 当设置某个 key 有过期时间时,就会向过期字典中添加此 key 的指针和时间戳

过期 key 删除策略

  • 惰性删除

    • 在执行读写数据库的命令时,执行命令前会检查 key 是否过期,如果已过期,则删除 key
  • 定期删除

    • redis 有一个定时任务处理器 serverCron,负责周期性任务处理,默认 100 ms 执行一次(hz 参数控制)包括:① 处理过期 key、② hash 表 rehash、③ 更新统计结果、④ 持久化、⑤ 清理过期客户端

    • 对于处理过期 key 会:依次遍历库,在规定时间(默认2.5ms)内运行如下操作(每次删一点)

      ① 从每个库的 expires 过期字典中随机选择 20 个 key 检查,如果过期则删除

      ② 如果删除达到 5 个,重复 ① 步骤,没有达到,遍历至下一个库

      ③ 规定时间没有做完,等待下一轮 serverCron 运行

4. Redis 持久化

要求

  • 掌握 AOF 持久化和 AOF 重写
  • 掌握 RDB 持久化
  • 了解混合持久化

AOF 持久化

  • AOF - 将每条写命令追加至 aof 文件,当重启时会执行 aof 文件中每条命令来重建内存数据
  • AOF 日志是写后日志,即先执行命令,再记录日志
    • Redis 为了性能,向 aof 记录日志时没有对命令进行语法检查,如果要先记录日志,那么日志里就会记录语法错误的命令
  • 记录 AOF 日志时,有三种同步策略
    • Always 同步写【安全性高,性能不高】,日志写入磁盘再返回,可以做到基本不丢数据,性能不高
      • 为什么说基本不丢呢,因为 aof 是在 serverCron 事件循环中执行 aof 写入的,并且这次写入的是上一次循环暂存在 aof 缓冲中的数据,因此最多还是可能丢失一个循环的数据
    • Everysec 每秒写,日志写入 AOF 文件的内存缓冲区,每隔一秒将内存缓冲区数据刷入磁盘,最多丢一秒的数据
    • No 操作系统写【性能高,安全性不高】,日志写入AOF 文件的内存缓冲区,由操作系统决定何时将数据刷入磁盘

AOF 重写

  • 针对AOF 文件太大引起的问题
    1. 文件大小受操作系统限制
    2. 文件太大,写入效率变低
    3. 文件太大,恢复时非常慢
  • 重写就是对同一个 key 的多次操作进行瘦身
    1. 例如一个 key 我改了 100 遍,aof 里记录了100 条修改日志,但实际上只有最后一次有效
    2. 重写无需操作现有 aof 日志,只需要根据当前内存数据的状态,生成相应的命令,记入一个新的日志文件即可
    3. 重写过程是由另一个后台子进程完成的,不会阻塞主进程
  • AOF 重写过程
    1. 创建子进程时会根据主进程生成内存快照(记录当前的状态),只需要对子进程的内存进行遍历,把每个 key 对应的命令写入新的日志文件(即重写日志)
    2. 此时如果有新的命令执行,修改的是主进程内存,不会影响子进程内存,并且新命令会记录到 重写缓冲区
    3. 等子进程所有的 key 处理完毕,再将 重写缓冲区 记录的增量指令写入重写日志
    4. 在此期间旧的 AOF 日志仍然在工作,待到重写完毕,用重写日志替换掉旧的 AOF 日志

RDB 持久化

  • RDB - 是把整个内存数据以二进制方式写入磁盘
    • 对应数据文件为 dump.rdb
    • 好处是恢复速度快
  • 相关命令有两个
    • save - 在主进程执行,会阻塞其它命令
    • bgsave - 创建子进程执行,避免阻塞,是默认方式
      • 子进程不会阻塞主进程,但创建子进程的期间,仍会阻塞,内存越大,阻塞时间越长
      • bgsave 也是利用了快照机制,执行 RDB 持久化期间如果有新数据写入,新的数据修改发生在主进程,子进程向 RDB 文件中写入还是旧的数据,这样新的修改不会影响到 RDB 操作
      • 但这些新数据不会补充至 RDB 文件 【备份时期间如果有更改,若下一次备份时宕机就会丢失信息】
  • 缺点: 可以通过调整 redis.conf 中的 save 参数来控制 rdb 的执行周期,但这个周期不好把握
    在这里插入图片描述
    • 频繁执行的话,会影响性能
    • 偶尔执行的话,如果宕机又容易丢失较多数据

混合持久化

  • 从 4.0 开始,Redis 支持混合持久化,即使用 RDB 作为全量备份,两次 RDB 之间使用 AOF 作为增量备份
    • 配置项 aof-use-rdb-preamble 用来控制是否启用混合持久化,默认值 no
    • 持久化时将数据都存入 AOF 日志,日志前半部分为二进制的 RDB 格式,后半部分是 AOF 命令日志
    • 下一次 RDB 时,会覆盖之前的日志文件
  • 优缺点
    • 结合了 RDB 与 AOF 的优点,恢复速度快,增量用 AOF 表示,数据更完整(取决于同步策略)、也无需 AOF 重写
    • 与旧版本的 redis 文件格式不兼容

5. 缓存问题

要求

  • 掌握缓存击穿
  • 掌握缓存雪崩
  • 掌握缓存穿透
  • 掌握旁路缓存与缓存一致性

缓存击穿

  • 缓存击穿是指某一热点 key 在缓存和数据库中都存在,它过期时,这时由于并发用户特别多,同时读缓存没读到,又同时去数据库去读,压垮数据库;

  • 解决方法

    1. 热点数据不过期【推荐】
    2. 对【查询缓存没有,查询数据库,结果放入缓存】这三步进行加锁,这时只有一个客户端能获得锁,其它客户端会被阻塞,等锁释放开,缓存已有了数据,其它客户端就不必访问数据库了。但会影响吞吐量(有损方案)
      在这里插入图片描述

缓存雪崩

  • 情况1由于大量 key 设置了相同的过期时间(数据在缓存和数据库都存在),一旦到达过期时间点,这些 key 集体失效,造成访问这些 key 的请求全部进入数据库。
    给某个 key 加锁能解决雪崩吗?:不行

  • 解决方法:

    1. 错开过期时间:在过期时间上加上随机值(比如 1~5 分钟)
    2. 服务降级:暂停非核心数据查询缓存,返回预定义信息(错误页面,空值等)(有损方案)
  • 情况2Redis 实例宕机,大量请求进入数据库

  • 解决方法:

    1. 事前预防:搭建高可用集群
    2. 多级缓存(搭建一个本地缓存):缺点是实现复杂度高
    3. 熔断:通过监控一旦雪崩出现,暂停缓存访问待实例恢复,返回预定义信息(有损方案)
    4. 限流:通过监控一旦发现数据库访问量超过阈值,限制访问数据库的请求数(有损方案)

缓存穿透

  • 缓存穿透是指:如果一个 key 在缓存和数据库都不存在,那么访问这个 key 每次都会进入数据库

    • 很可能被恶意请求利用
    • 缓存雪崩与缓存击穿都是数据库中有,但缓存暂时缺失
    • 缓存雪崩与缓存击穿都能自然恢复,但缓存穿透则不能
  • 解决方法

    1. 如果数据库没有,也将此不存在的 key 关联 null 值放入缓存缺点是这样的 key 没有任何业务作用,白占空间

    2. 布隆过滤器(一个插件)
      加在缓存和数据库之前
      在这里插入图片描述

    过滤器可以用来判定 key 不存在,发现这些不存在的 key,把它们过滤掉就好
    需要将所有的 key 都预先加载至布隆过滤器
    布隆过滤器不能删除,因此查询删除的数据一定会发生穿透(布谷鸟过滤器可以解决)

缓存一致性问题——旁路缓存

旁路缓存
  • 旁路缓存(Cache Aside),是一种常见的使用缓存的策略

  • 查询规则

    • 先读缓存
    • 如果命中,直接返回
    • 如果缺失,查 DB(数据库) 并将结果放入缓存,再返回
  • 增、删、改规则

    • 新增数据直接存 DB(数据库)
    • 修改、删除数据先更新DB(数据库),再删缓存【最终一致,数据一致性要求不高时,会有短暂的不一致,但最终一致】

    为什么要先操作库,再操作缓存?

    • 假设操作库和缓存均能成功,如果先操作缓存,会大几率出现数据库与缓存不一致的情况

    一致性分析 - 先清缓存,再更新库
    在这里插入图片描述

    一致性分析 - 先更新库,再清缓存
    在这里插入图片描述

    • 会有短暂不一致,但最终会一致
      在这里插入图片描述

    • 假设查询线程 A 查询数据时恰好缓存数据由于时间到期失效,或是第一次查询,会如上图所示出现不一致
      在这里插入图片描述
      但这种几率出现机会很小

用锁解决一致性

在这里插入图片描述

  • 缺点:影响吞吐量、分布式锁设计较为复杂

6. 缓存原子性

要求

  • 掌握 Redis 事务的局限性
  • 理解用乐观锁保证原子性
  • 理解用 lua 脚本保证原子性

Redis 事务局限性:不支持回滚

  • 单条命令是原子性,这是由 redis 单线程保障的
  • 多条命令能否用 multi + exec 来保证其原子性呢?

Redis 中 multi + exec 并不支持回滚,例如有初始数据如下

set a 1000
set b 1000
set c a

执行

multi
decr a   /*a减1*/
incr b   /*b加1*/
incr c
exec

执行 incr c 时,由于字符串不支持自增导致此条命令失败,但之前的两条命令并不会回滚上面的结果是前两条命令成功,后一条命令失败;

更为重要的是,multi + exec 中的读操作没有意义,因为读的结果并不能赋值给临时变量,用于后续的写操作,既然 multi + exec 中读没有意义,无法保证读 + 写的原子性(一个事务无法同时控制读和写),例如有初始数据如下

set a 1000
set b 1000

假设 a 和 b 代表的是两个账户余额,现在获取旧值,执行转账 500 的操作:

get a /* 存入客户端临时变量 */
get b /* 存入客户端临时变量 */
/* 客户端计算出 a 和 b 更新后的值 */
multi
set a 500
set b 1500
exec

但如果在 get 与 multi 之间其它客户端修改了 a 或 b,会造成丢失更新

乐观锁保证原子性

watch 命令,用来盯住 key(一到多个),如果这些 key 在事务期间:

  • 没有被别的客户端修改,则 exec 才会成功
  • 被别的客户端改了,则 exec 返回 nil,进而保证了原子性

还是上一个例子

get a /* 存入客户端临时变量 */
get b /* 存入客户端临时变量 */
/* 客户端计算出 a 和 b 更新后的值 */
watch a b /* 盯住 a 和 b */
multi
set a 500
set b 1500
exec

此时,如果其他客户端修改了 a 和 b 的值,那么 exec 就会返回 nil,并不会执行两条 set 命令,此时客户端可以进行重试

lua 脚本保证原子性

Redis 支持 lua 脚本,能保证 lua 脚本执行的原子性,可以取代 multi + exec

例如要解决上面的问题,可以执行如下命令

eval "local a = tonumber(redis.call('GET',KEYS[1]));local b = tonumber(redis.call('GET',KEYS[2]));local c = tonumber(ARGV[1]); if(a >= c) then redis.call('SET', KEYS[1], a-c); redis.call('SET', KEYS[2], b+c); return 1;else return 0; end" 2 a b 500
  • eval 用来执行 lua 脚本
  • 2 表示后面用空格分隔的参数中,前两个是 key,剩下的是普通参数
  • 脚本中可以用 keys[n] 来引用第 n 个 key,用 argv[n] 来引用第 n 个普通参数
  • 其中双引号内部的即为 lua 脚本,格式化如下
local a = tonumber(redis.call('GET',KEYS[1]));//tonumber把字符串转换成数字
local b = tonumber(redis.call('GET',KEYS[2]));
local c = tonumber(ARGV[1]); 
if(a >= c) then 
    redis.call('SET', KEYS[1], a-c); 
    redis.call('SET', KEYS[2], b+c); 
    return 1;
else 
    return 0; 
end

7. LRU Cache(淘汰策略)实现

要求

  • 掌握基于链表的 LRU Cache 实现
  • 了解 Redis 在 LRU Cache 实现上的变化

LRU Cache 淘汰规则

Least Recently Used,将最近最少使用的 key 从缓存中淘汰掉

  • 时间上,新的留下,老的淘汰
  • 如果访问了某个 key,则它就变成最新的

实现策略

  • 链表法,最近访问的 key 移动到链表头,不常访问的自然靠近链表尾,如果超过容量、个数限制,移除尾部的
  • 随机取样法(Redis采用的),链表法占用内存较多,redis 使用的是随机取样法,每次只抽 5 个 key,每个 key 记录了它们的最近访问时间,在这 5 个里挑出最老的移除

链表法为例,最近访问的 key 移动到链表头,不常访问的自然靠近链表尾,如果超过容量、个数限制,移除尾部的

  • 例如有原始数据如下,容量规定为 3
    在这里插入图片描述

  • 时间上,新的留下,老的淘汰,比如 put d,那么最老的 a 被淘汰
    在这里插入图片描述

  • 如果访问了某个 key,则它就变成最新的,例如 get b,则 b 被移动到链表头
    在这里插入图片描述

LRU Cache 链表实现(考代码)

  • 如何断开节点链接

在这里插入图片描述

  • 如何链入头节点
    在这里插入图片描述
参考代码一:(回答这个代码,体现了对链表的了解)
package day06;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

public class LruCache1 {
    static class Node {
       //节点的参数;
        Node prev;
        Node next;
        String key;
        Object value;

        public Node(String key, Object value) {
            this.key = key;
            this.value = value;
        }

        // 打印节点的信息:node的toString(prev <- node -> next)
        public String toString() {
            StringBuilder sb = new StringBuilder(128);
            sb.append("(");
            sb.append(this.prev == null ? null : this.prev.key);
            sb.append("<-");
            sb.append(this.key);
            sb.append("->");
            sb.append(this.next == null ? null : this.next.key);
            sb.append(")");
            return sb.toString();
        }
    }
    //断开节点链接的方法(删除一个节点)
    public void unlink(Node node) {
        node.prev.next = node.next;
        node.next.prev = node.prev;
    }
    //把新节点加入到head头结点之后
    public void toHead(Node node) {
        node.prev = this.head;
        node.next = this.head.next;
        this.head.next.prev = node;
        this.head.next = node;
    }

    int limit;       //元素上限,超出则删除较老的
    Node head;      //头结点
    Node tail;      //尾节点
    Map<String, Node> map;      //Map集合存放真正的键值,键存放key,值存放每个节点对象
    //初始化上面的这些成员
    public LruCache1(int limit) {
        this.limit = Math.max(limit, 2); //通过参数传进来,最小值设置成2;
        this.head = new Node("Head", null);
        this.tail = new Node("Tail", null);
        head.next = tail;//头结点尾结点相连;
        tail.prev = head;//头结点尾结点相连;
        this.map = new HashMap<>();//空Map
    }
    //删除逻辑
    public void remove(String key) {
        Node old = this.map.remove(key);//调用底层map的remove将key删掉;
        unlink(old);//断开节点的连接
    }
    //查询逻辑
    public Object get(String key) {
        Node node = this.map.get(key);
        if (node == null) {//如果key在链表中没有
            return null;
        }
        //如果key在链表中有,在链表中断开这个节点,并把它插入到头部;
        unlink(node);
        toHead(node);
        return node.value;//返回值;
    }

    //新增逻辑;
    public void put(String key, Object value) {
        Node node = this.map.get(key); //先查一下这个key在链表中有没有
        if (node == null) {      //没有
            node = new Node(key, value);     //创建一个新的节点;
            this.map.put(key, node);//存入map;
        } else {      //有
            node.value = value;//更新一下值
            unlink(node); //断开连接
        }
        toHead(node); //将节点移入头部
        if(map.size() > limit) {      //看看当前map的大小是否超出了上限,如果超出了
            Node last = this.tail.prev;//找到最后一个节点;
            this.map.remove(last.key);//将最后一个节点删掉;
            unlink(last); //链表中断开最后一个节点的链接;
        }
    }

    @Override
    //cache的toString
    public String toString() {   
        StringBuilder sb = new StringBuilder();
        sb.append(this.head);//拼接头结点
        Node node = this.head;
        while ((node = node.next) != null) {//遍历链表;
            sb.append(node);
        }
        return sb.toString();
    }

    public static void main(String[] args) {
        LruCache1 cache = new LruCache1(5); //创建 LruCache的实例,规定上限是5;
        System.out.println(cache);
        cache.put("1", 1);//添加元素
        System.out.println(cache);
        cache.put("2", 1);
        System.out.println(cache);
        cache.put("3", 1);
        System.out.println(cache);
        cache.put("4", 1);
        System.out.println(cache);
        cache.put("5", 1);
        System.out.println(cache);
        cache.put("6", 1);//此时将删除1结点
        System.out.println(cache);
        cache.get("2");//cache的get方法,此时2到达了头部
        System.out.println(cache);
        cache.put("7", 1);//cache的put方法,此时又将删除一个节点;
        System.out.println(cache);
    }
}

在这里插入图片描述

参考代码二

使用继承父类LinkedHashMap

package day06;

import java.util.LinkedHashMap;
import java.util.Map;

public class LruCache2 extends LinkedHashMap<String, Object> {

    private int limit; //作为元素个数的限制;

    public LruCache2(int limit) {
        // 1 2 3 4 false
        // 1 3 4 2 true ,此时调用2,就把2调到了右侧头部;按访问顺序调整;
        super(limit * 4 /3, 0.75f, true);//调用有参构造,参数:长度limit * 4 /3防止扩容,扩容因子,true
        this.limit = limit;
    }

    @Override
    //此方法把最老的键值对移除掉;返回true时则移除最老的
    protected boolean removeEldestEntry(Map.Entry<String, Object> eldest) {
        if (this.size() > this.limit) {
            return true;
        }
        return false;
    }

    public static void main(String[] args) {
        LruCache2 cache = new LruCache2(5);
        System.out.println(cache);
        cache.put("1", 1);
        System.out.println(cache);
        cache.put("2", 1);
        System.out.println(cache);
        cache.put("3", 1);
        System.out.println(cache);
        cache.put("4", 1);
        System.out.println(cache);
        cache.put("5", 1);
        System.out.println(cache);
        cache.put("6", 1);//此时由于新加了一个元素,超出了上限,会将最老的元素移除;
        System.out.println(cache);
        cache.get("2");
        System.out.println(cache);
        cache.put("7", 1);
        System.out.println(cache);
    }
}

Redis LRU Cache 实现

Redis 采用了随机取样法,较之链表法占用内存更少,每次只抽 5 个 key,每个 key 记录了它们的最近访问时间,在这 5 个里挑出最老的移除

  • 例如有原始数据如下,容量规定为 160,put 新 key a

在这里插入图片描述

  • 每个 key 记录了放入 LRU 时的时间,随机挑到的 5 个 key(16,78,90,133,156),会挑时间最老的移除(16)

  • 再 put b 时,会使用上轮剩下的 4 个(78,90,133,156),外加一个随机的 key(125),这里面挑最老的(78)

在这里插入图片描述

  • 如果 get 了某个 key,它的访问时间会被更新(下图中 90)这样就避免了它下一轮被移除
    在这里插入图片描述
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值