27-拓展 5:优胜劣汰 —— LRU
课程
1
开篇:授人以鱼不若授人以渔 —— Redis 可以用来做什么?
学习时长: 5分21秒
2
基础:万丈高楼平地起 —— Redis 基础数据结构
上次学到
学习时长: 16分14秒
3
应用 1:千帆竞发 —— 分布式锁
学习时长: 7分47秒
4
应用 2:缓兵之计 —— 延时队列
学习时长: 8分9秒
5
应用 3:节衣缩食 —— 位图
学习时长: 8分52秒
6
应用 4:四两拨千斤 —— HyperLogLog
学习时长: 14分17秒
7
应用 5:层峦叠嶂 —— 布隆过滤器
学习时长: 17分54秒
8
应用 6:断尾求生 —— 简单限流
学习时长: 4分37秒
9
应用 7:一毛不拔 —— 漏斗限流
学习时长: 7分22秒
10
应用 8:近水楼台 —— GeoHash
学习时长: 7分52秒
11
应用 9:大海捞针 —— Scan
学习时长: 8分42秒
12
原理 1:鞭辟入里 —— 线程 IO 模型
学习时长: 4分1秒
13
原理 2:交头接耳 —— 通信协议
学习时长: 3分34秒
14
原理 3:未雨绸缪 —— 持久化
学习时长: 5分27秒
15
原理 4:雷厉风行 —— 管道
学习时长: 3分51秒
16
原理 5:同舟共济 —— 事务
学习时长: 6分36秒
17
原理 6:小道消息 —— PubSub
学习时长: 7分7秒
18
原理 7:开源节流 —— 小对象压缩
学习时长: 7分14秒
19
原理 8:有备无患 —— 主从同步
学习时长: 4分9秒
20
集群 1:李代桃僵 —— Sentinel
学习时长: 3分52秒
21
集群 2:分而治之 —— Codis
学习时长: 7分28秒
22
集群 3:众志成城 —— Cluster
学习时长: 8分38秒
23
拓展 1:耳听八方 —— Stream
学习时长: 13分40秒
24
拓展 2:无所不知 —— Info 指令
学习时长: 4分4秒
25
拓展 3:拾遗补漏 —— 再谈分布式锁
学习时长: 2分18秒
26
拓展 4:朝生暮死 —— 过期策略
学习时长: 2分21秒
27
拓展 5:优胜劣汰 —— LRU
学习时长: 4分34秒
28
拓展 6:平波缓进 —— 懒惰删除
学习时长: 2分13秒
29
拓展 7:妙手仁心 —— 优雅地使用 Jedis
学习时长: 6分35秒
30
拓展 8:居安思危 —— 保护 Redis
学习时长: 2分19秒
31
拓展 9:隔墙有耳 —— Redis 安全通信
学习时长: 6分34秒
32
拓展 10:法力无边 —— Redis Lua 脚本执行原理
学习时长: 9分24秒
33
拓展 11:短小精悍 —— 命令行工具的妙用
学习时长: 9分21秒
34
源码 1:丝分缕析 —— 探索「字符串」内部
学习时长: 5分20秒
35
源码 2:循序渐进 —— 探索「字典」内部
学习时长: 7分24秒
36
源码 3:挨肩迭背 —— 探索「压缩列表」内部
学习时长: 10分42秒
37
源码 4:风驰电掣 —— 探索「快速列表」内部
学习时长: 3分49秒
38
源码 5:凌波微步 —— 探索「跳跃列表」内部
学习时长: 9分57秒
39
源码 6:破旧立新 —— 探索「紧凑列表」内部
学习时长: 2分42秒
40
源码 7:金枝玉叶 —— 探索「基数树」内部
学习时长: 5分36秒
41
源码 8:精益求精 —— LFU vs LRU
学习时长: 8分4秒
42
源码 9:如履薄冰 —— 懒惰删除的巨大牺牲
学习时长: 9分53秒
43
源码 10:跋山涉水 —— 深入字典遍历
学习时长: 9分24秒
44
源码 11:见缝插针 —— 探索 HyperLogLog 内部
学习时长: 13分3秒
45
尾声:百尺竿头 —— 继续深造指南
学习时长: 2分32秒
juejin_logo copyCreated with Sketch.

拓展 5:优胜劣汰 —— LRU

当 Redis 内存超出物理内存限制时,内存的数据会开始和磁盘产生频繁的交换 (swap)。交换会让 Redis 的性能急剧下降,对于访问量比较频繁的 Redis 来说,这样龟速的存取效率基本上等于不可用。

在生产环境中我们是不允许 Redis 出现交换行为的,为了限制最大使用内存,Redis 提供了配置参数 maxmemory 来限制内存超出期望大小。

当实际内存超出 maxmemory 时,Redis 提供了几种可选策略 (maxmemory-policy) 来让用户自己决定该如何腾出新的空间以继续提供读写服务。

noeviction 不会继续服务写请求 (DEL 请求可以继续服务),读请求可以继续进行。这样可以保证不会丢失数据,但是会让线上的业务不能持续进行。这是默认的淘汰策略。

volatile-lru 尝试淘汰设置了过期时间的 key,最少使用的 key 优先被淘汰。没有设置过期时间的 key 不会被淘汰,这样可以保证需要持久化的数据不会突然丢失。

volatile-ttl 跟上面一样,除了淘汰的策略不是 LRU,而是 key 的剩余寿命 ttl 的值,ttl 越小越优先被淘汰。

volatile-random 跟上面一样,不过淘汰的 key 是过期 key 集合中随机的 key。

allkeys-lru 区别于 volatile-lru,这个策略要淘汰的 key 对象是全体的 key 集合,而不只是过期的 key 集合。这意味着没有设置过期时间的 key 也会被淘汰。

allkeys-random 跟上面一样,不过淘汰的策略是随机的 key。

volatile-xxx 策略只会针对带过期时间的 key 进行淘汰,allkeys-xxx 策略会对所有的 key 进行淘汰。如果你只是拿 Redis 做缓存,那应该使用 allkeys-xxx,客户端写缓存时不必携带过期时间。如果你还想同时使用 Redis 的持久化功能,那就使用 volatile-xxx 策略,这样可以保留没有设置过期时间的 key,它们是永久的 key 不会被 LRU 算法淘汰。

LRU 算法

实现 LRU 算法除了需要 key/value 字典外,还需要附加一个链表,链表中的元素按照一定的顺序进行排列。当空间满的时候,会踢掉链表尾部的元素。当字典的某个元素被访问时,它在链表中的位置会被移动到表头。所以链表的元素排列顺序就是元素最近被访问的时间顺序。

位于链表尾部的元素就是不被重用的元素,所以会被踢掉。位于表头的元素就是最近刚刚被人用过的元素,所以暂时不会被踢。

下面我们使用 Python 的 OrderedDict(双向链表 + 字典) 来实现一个简单的 LRU 算法。

from collections import OrderedDict

class LRUDict(OrderedDict):

    def __init__(self, capacity):
        self.capacity = capacity
        self.items = OrderedDict()

    def __setitem__(self, key, value):
        old_value = self.items.get(key)
        if old_value is not None:
            self.items.pop(key)
            self.items[key] = value
        elif len(self.items) < self.capacity:
            self.items[key] = value
        else:
            self.items.popitem(last=True)
            self.items[key] = value

    def __getitem__(self, key):
        value = self.items.get(key)
        if value is not None:
            self.items.pop(key)
            self.items[key] = value
        return value

    def __repr__(self):
        return repr(self.items)


d = LRUDict(10)

for i in range(15):
    d[i] = i
print d

近似 LRU 算法

Redis 使用的是一种近似 LRU 算法,它跟 LRU 算法还不太一样。之所以不使用 LRU 算法,是因为需要消耗大量的额外的内存,需要对现有的数据结构进行较大的改造。近似 LRU 算法则很简单,在现有数据结构的基础上使用随机采样法来淘汰元素,能达到和 LRU 算法非常近似的效果。Redis 为实现近似 LRU 算法,它给每个 key 增加了一个额外的小字段,这个字段的长度是 24 个 bit,也就是最后一次被访问的时间戳。

上一节提到处理 key 过期方式分为集中处理和懒惰处理,LRU 淘汰不一样,它的处理方式只有懒惰处理。当 Redis 执行写操作时,发现内存超出 maxmemory,就会执行一次 LRU 淘汰算法。这个算法也很简单,就是随机采样出 5(可以配置) 个 key,然后淘汰掉最旧的 key,如果淘汰后内存还是超出 maxmemory,那就继续随机采样淘汰,直到内存低于 maxmemory 为止。

如何采样就是看 maxmemory-policy 的配置,如果是 allkeys 就是从所有的 key 字典中随机,如果是 volatile 就从带过期时间的 key 字典中随机。每次采样多少个 key 看的是 maxmemory_samples 的配置,默认为 5。

下面是随机 LRU 算法和严格 LRU 算法的效果对比图:

图中绿色部分是新加入的 key,深灰色部分是老旧的 key,浅灰色部分是通过 LRU 算法淘汰掉的 key。从图中可以看出采样数量越大,近似 LRU 算法的效果越接近严格 LRU 算法。同时 Redis3.0 在算法中增加了淘汰池,进一步提升了近似 LRU 算法的效果。

淘汰池是一个数组,它的大小是 maxmemory_samples,在每一次淘汰循环中,新随机出来的 key 列表会和淘汰池中的 key 列表进行融合,淘汰掉最旧的一个 key 之后,保留剩余较旧的 key 列表放入淘汰池中留待下一个循环。

扩展阅读

思考 & 作业

  1. 如果你是 Java 用户,试一试用 LinkedHashMap 实现一个 LRU 字典。
  2. 如果你是 Golang 用户,阅读一下 golang-lru 的源码。
留言
Ctrl + Enter
全部评论(44)
随波不逐流的头像
删除
后边的文章感觉好敷衍呀,一篇文章都没有几个字。
点赞
回复
BinK_1783的头像
删除
走的慢一些,但至少从未后退
点赞
回复
武汉大D哥的头像
删除
软件开发工程师 @ xx科技
有点灌水了
1
回复
行走的冷笑话的头像
删除
后端开发工程师
太菜了,随便水了一下,然后就把别人的内容砸你脸上
点赞
回复
小张和小赵的头像
删除
启蒙者 @ 无
LRU最好的代码就是用C++或者Java 用双向链表实现出来
点赞
回复
亽閑屁亊哆的头像
删除
Java Siege Lion @ IT Zoo
水的一塌糊涂
点赞
回复
杜阿的头像
删除
java程序媛
那么volatile-ttl和always-random也是懒惰淘汰吗?这点没说到
点赞
回复
daqiao的头像
删除
公众号: 大乔的幻想 | daqiao.world
volatile-random 跟上面一样,不过淘汰的 key 是过期 key 集合中随机的 key。
这句话有错吧,应该是淘汰设置了过期时间的key中的随机key而不是过期key中的。
点赞
回复
柳双六的头像
删除
Redis 4.0以后新增 LFU 算法。详细可以自行了解。
点赞
回复
已注销的头像
删除
redis淘汰池默认大小是16吧 , maxmemory_samples只是他每次取样的数量 并不是来淘汰池大小
1
1
删除
嗯 说的很对 淘汰吃的大小定义MAXMEMORY_EVICTION_POOL_SIZE为16
点赞
回复
DEDUCE的头像
删除
这个24bit的最后一次使用时间戳,具体有什么意义呢, 此文没提到?
点赞
2
删除
根据最够一次使用时间,来判断新旧
点赞
回复
删除
每个key都需要记录其闲置时间即空转时间,可以通过object idletime key查看,当进行缓存淘汰的时候,根据对应的淘汰策略去比较idletime
点赞
回复
zyfsuzy的头像
删除
研发工程师 @ 百度
我怎么觉得这个小册只能是学习笔记昵,吃瓜!
5
回复
复兴的头像
删除
后端程序员
使用了volatile-xx机制来删除key,但是还是超过maxmemory或者说没有设置过期时间的key,那么该怎么腾出空间呢。
2
1
删除
我看上面链接的文章,应该表现是和noeviction一样的。
点赞
回复
明天不想说话的头像
删除
越来越混,刚开始还用java+python,现在直接ptyhon,直接看不懂了
点赞
1
删除
(作者)
作业题
点赞
回复
小渣同学的头像
删除
高级工程师 @ 某咸鱼国企
不懂就问环节:
近似LRU是为每一个key维护了一个最后一次访问的时间戳,那么在redis中的热key是怎么实现的呢,是为每个key保存了一个访问次数的字段吗?
2
1
删除
每个key都会维护一个访问时间,可以通过object idletime key看到空闲时间;redis中数据类型为正数类型的时候引用可以被重复的利用,object refcount key查看
点赞
回复
freesia13279的头像
删除
工具人 @ 字节跳动
redis-4.0开始提供了LFU策略
点赞
回复
杨思翔的头像
删除
文章中说到:LRU 淘汰不一样,它的处理方式只有懒惰处理
那serverCron式的主动删除不算一种方式吗?
点赞
回复
金龟的头像
删除
LinkedHashMap 设置accessOrder 为true就可以实现lru了,dubbo,mybatis都是这样实现的
9
回复
Linker.Lin的头像
删除
TTL要好一些
点赞
回复
钱同志的头像
删除
空间有限,此时已满,有进必先出,出哪些呢?这就是淘汰算法要解决的问题啦!公司常用末尾淘汰的方式,缓存有一些方式常用LRU算法,最为复杂的应该是地球上各种各样生物的淘汰方式。
点赞
回复

查看全部 44 条回复