Redis缓存淘汰策略

本文使用的Redis服务端和客户端版本如下:

shell 复制代码
$ redis-server -v
Redis server v=7.0.0 sha=00000000:0 malloc=libc bits=64 build=9b921e455b2f5c37
$ redis-cli -v
redis-cli 7.0.0

查看Redis的最大内存配置:

redis 复制代码
127.0.0.1:6379> config get maxmemory
1) "maxmemory"
2) "0"

如果不设置最大内存大小或者设置最大内存大小为0,在64位操作系统下不限制内存大小,在32位操作系统下最多使用3GB内存。

redis 复制代码
127.0.0.1:6379> config set maxmemory 1
OK
127.0.0.1:6379> set k1 abc
(error) OOM command not allowed when used memory > 'maxmemory'.

将最大内存设置为1字节,存储内容超过最大内存设置时,会报OOM错误。

没有设置过期时间的缓存数据过多,就容易达到最大内存设置,导致OOM报错,所以需要使用缓存淘汰策略。

Redis缓存淘汰策略:

shell 复制代码
# volatile-lru -> Evict using approximated LRU, only keys with an expire set.
# allkeys-lru -> Evict any key using approximated LRU.
# volatile-lfu -> Evict using approximated LFU, only keys with an expire set.
# allkeys-lfu -> Evict any key using approximated LFU.
# volatile-random -> Remove a random key having an expire set.
# allkeys-random -> Remove a random key, any key.
# volatile-ttl -> Remove the key with the nearest expire time (minor TTL)
# noeviction -> Don't evict anything, just return an error on write operations.

有2个维度:

  • 过期键中筛选

  • 所有键中筛选

4个方面:

  • lru,最近最少使用

  • lfu,最近最不常用

  • random,随机

  • ttl,删除马上要过期的

默认是noeviction,不驱逐任何key。一般设置为allkeys-lru,对所有key使用LRU算法进行删除,优先删除最近最不经常使用的key,用以保存新数据。

LRU Least Recently Used,最近最少使用页面置换算法,淘汰最长时间未被使用的页面,看页面最后一次被使用到发生调度的时间长短,首先淘汰最长时间未被使用的页面。

LFU Least Frequently Used:最近最不常用页面置换算法,淘汰一定时期内被访问次数最少的页,看一定时间内页面被使用的频率,淘汰一定时期内被访问次数最少的页。

例如依次打开了页面2、1、2、1、2、3、5,如果限制只能保存3页,按照LRU应该删除1,而按照LFU应该删除3。

实现LRU

用双向链表实现LRU,将访问到的键移动到链表的尾部,当达到容量限制时,就删除链表的第一个数据节点。这样越是近期被访问的数据,就越靠近链表尾,越不容易被删除。

go 复制代码
package main

import "fmt"

func main() {
	// 初始化一个值类型为整数,容量为3的lru缓存
	lru := NewLRUCache[int](3)
	lru.Set("a", 2)
	lru.Set("b", 1)
	lru.Set("c", 2)
	lru.Set("d", 1)

	// 此时键a已经被删掉了
	_, err := lru.Get("a")
	if err != nil {
		fmt.Println(err)
	}
	lru.PrintNodes() // b:1 c:2 d:1

	lru.Set("e", 2)
	lru.Set("f", 3)

	lru.PrintNodes() // d:1 e:2 f:3
}

// 双向链表中的节点
type DoubleListNode[T any] struct {
	Key  string
	Val  T
	Next *DoubleListNode[T]
	Prev *DoubleListNode[T]
}

// 创建链表节点,相当于类中的构造函数
func NewDoubleListNode[T any](key string, val T) DoubleListNode[T] {
	dln := DoubleListNode[T]{
		Key: key,
		Val: val,
	}
	return dln
}

type LRUCache[T any] struct {
	Head     *DoubleListNode[T]
	Tail     *DoubleListNode[T]
	Map      map[string]*DoubleListNode[T]
	Capacity int
}

// capacity 表示缓存中最多存放多少个数据项
func NewLRUCache[T any](capacity int) LRUCache[T] {
	var empty T
	nodeMap := make(map[string]*DoubleListNode[T])
	// 初始化双向链表
	head := NewDoubleListNode[T]("head", empty)
	tail := NewDoubleListNode[T]("tail", empty)
	head.Next = &tail
	tail.Prev = &head

	// 初始化lru缓存
	lru := LRUCache[T]{
		Head:     &head,
		Tail:     &tail,
		Map:      nodeMap,
		Capacity: capacity,
	}
	return lru
}

// 向缓存中放入数据
func (lru *LRUCache[T]) Set(key string, val T) {
	node, ok := lru.Map[key]
	if ok {
		// 如果缓存中已经存在这个键,就将这个键移动到链表的末尾
		lru.moveToTail(node, val)
	} else {
		// 已经达到容量限制了,就删除链表的第一个节点
		if len(lru.Map) == lru.Capacity {
			td := lru.Head.Next
			lru.deleteNode(td)

			delete(lru.Map, td.Key)
		}
		// 缓存中不存在这个键,就将这个键添加到链表的末尾
		// 并在map中新增这个键
		node := NewDoubleListNode[T](key, val)
		lru.insertToTail(&node)
		lru.Map[key] = &node
	}
}

// 从缓存中获取数据
func (lru *LRUCache[T]) Get(key string) (T, error) {
	node, ok := lru.Map[key]
	// 映射中不存在这个值,就返回这个类型的空值
	if !ok {
		var empty T
		return empty, fmt.Errorf("key not exist")
	}
	// 将这个节点移动到链表的尾部,并返回节点的值
	lru.moveToTail(node, node.Val)
	return node.Val, nil
}

// 打印节点的内容
func (lru *LRUCache[T]) PrintNodes() {
	list := lru.Head
	node := list.Next
	for node.Key != "tail" {
		fmt.Printf("%v:%v ", node.Key, node.Val)
		node = node.Next
	}
	fmt.Println("")
}

// 给节点赋新值,并移动到尾部
func (lru *LRUCache[T]) moveToTail(node *DoubleListNode[T], newVal T) {
	lru.deleteNode(node)
	node.Val = newVal
	lru.insertToTail(node)
}

// 删除节点
func (lru *LRUCache[T]) deleteNode(node *DoubleListNode[T]) {
	node.Prev.Next = node.Next
	node.Next.Prev = node.Prev
}

// 将节点放到链表尾部
func (lru *LRUCache[T]) insertToTail(node *DoubleListNode[T]) {
	lru.Tail.Prev.Next = node
	node.Prev = lru.Tail.Prev
	node.Next = lru.Tail
	lru.Tail.Prev = node
}

学习地址

Redis缓存淘汰策略:www.bilibili.com/video/BV13R...

相关推荐
AOwhisky几秒前
Kubernetes(K8s)学习笔记(第十四期):集群存储与有状态应用(下篇):StatefulSet 有状态应用管理
redis·笔记·mysql·云原生·kubernetes·云计算·k8s
风向决定发型丶8 小时前
redis集群搭建
数据库·redis·缓存
梦想的颜色10 小时前
硬核实践:使用 Docker 部署生产级 Redis(持久化 + 安全配置 + 高可用)
redis·docker·redis持久化·docker compose·redis哨兵·rdb aof
宠友信息12 小时前
多端数据互通场景下Spring Boot仿小红书源码结构设计
数据库·spring boot·redis·缓存·架构
长不胖的路人甲12 小时前
Redis 缓存的数据持久化方案讲解
数据库·redis·缓存
长不胖的路人甲12 小时前
Redis 单线程为什么速度很快
数据库·redis·缓存
彦为君13 小时前
算法思维与经典智力题
java·前端·redis·算法
彦为君14 小时前
Redis最新版本特性
java·数据库·redis·算法·bootstrap
长不胖的路人甲14 小时前
Redis 数据删除策略
数据库·redis·spring
Sinclair15 小时前
认识安企CMS-系统概述
开源·go