本文使用的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...