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...

相关推荐
不会写DN2 小时前
其实跨域问题是后端来解决的? CORS
服务器·网络·面试·go
NCIN EXPE7 小时前
redis 使用
数据库·redis·缓存
hERS EOUS8 小时前
nginx 代理 redis
运维·redis·nginx
NoSi EFUL9 小时前
redis存取list集合
windows·redis·list
Deepincode10 小时前
Redis源码探究系列—SDS 扩容策略与内存预分配机制
redis
不会写DN10 小时前
Golang中的map的key可以是哪些类型?可以嵌套map吗?
后端·golang·go
程序员老邢11 小时前
【技术底稿 19】Redis7 集群密码配置 + 权限锁死 + 磁盘占满连锁故障真实排查全记录
java·服务器·经验分享·redis·程序人生·微服务
审判长烧鸡11 小时前
GO分层架构【4】Repository获取 *gorm.DB
go·分层架构·结构体注入
coNh OOSI12 小时前
Redis——Windows安装
数据库·windows·redis
Fᴏʀ ʏ꯭ᴏ꯭ᴜ꯭.12 小时前
Redis主从复制配置全攻略
数据库·redis·笔记