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

相关推荐
Hello.Reader6 小时前
Redis大Key问题全解析
数据库·redis·bootstrap
B1nna9 小时前
Redis学习(三)缓存
redis·学习·缓存
uccs14 小时前
go 第三方库源码解读---go-errorlint
后端·go
A227415 小时前
Redis——缓存雪崩
java·redis·缓存
weisian15115 小时前
Redis篇--应用篇3--数据统计(排行榜,计数器)
数据库·redis·缓存
言之。15 小时前
Redis单线程快的原因
数据库·redis·缓存
LYX369319 小时前
Docker 安装mysql ,redis,nacos
redis·mysql·docker
计算机毕设定制辅导-无忧学长19 小时前
Redis 持久化机制详解
redis
奋斗的老史1 天前
Spring Retry + Redis Watch实现高并发乐观锁
java·redis·spring
loop lee1 天前
Redis - Token & JWT 概念解析及双token实现分布式session存储实战
java·redis