redis 面试题

209 阅读5分钟

本文主要内容为BAT面试过程中设计到的redis知识。理解原理最重要,切勿死记硬背。作者语言组织不行,所以大部分都是复制的,已标明出处!!!

RDB(快照) 和 AOF(追加文件) 持久化得区别与优缺点

  • RDB会定时生成快照临时文件,然后替换旧的快照。所以可能会丢失最后一次生成快照以后的数据

  • AOF会在redis启动后记录每一个写操作,以追加命令的方式持久化。当AOF文件过大的时候会进行重写

  • 进行数据恢复时,Redis优先使用AOF,但RDB的性能会好于AOF(可能是AOF保存的数据较完整)

参考 Redis持久化

过期淘汰策略

redis 支持三种过期淘汰策略

  • 惰性过期:redis会记录key的过期时间,当访问这个key时判断是否过期,进行删除操作。节省cpu浪费内存。
  • 定时过期:每个设置过期时间的key会添加定时器,到时去自动清理。 浪费大量的cpu
  • 定期过期:redis的expires字典保存了所有设置了过期时间的key信息,定期扫描一定数量的key,并清理过期的key。cpu和内存相对较友好

redis 同时使用了定期过期和惰性过期策略

参考 Redis的过期策略和内存淘汰策略

redis 内存淘汰策略

  • redis启动时可以设置最大内存使用量,redis使用以下6种淘汰策略保证内存
  • noeviction:当内存不足以容纳新写入数据时,新写入操作会报错。
  • allkeys-lru:当内存不足以容纳新写入数据时,在键空间中,移除最近最少使用的key。
  • allkeys-random:当内存不足以容纳新写入数据时,在键空间中,随机移除某个key。
  • volatile-lru:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,移除最近最少使用的key。
  • volatile-random:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,随机移除某个key。
  • volatile-ttl:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,有更早过期时间的key优先移除。

参考 Redis的过期策略和内存淘汰策略

有序集合

因为有序集合涉及到的数据结构比较多,且不常用,所以一般考redis数据结构时会问此处。 有序集合的编码可以是 ziplist 或者 skiplist 。

压缩列表 ziplist

ziplist 编码的有序集合对象使用压缩列表作为底层实现,每个集合元素使用两个紧挨在一起的压缩列表节点来保存,第一个节点保存元素的成员(member),而第二个元素则保存元素的分值(score)。

压缩列表内的集合元素按分值从小到大进行排序,分值较小的元素被放置在靠近表头的方向, 而分值较大的元素则被放置在靠近表尾的方向。

跳表 skiplist

skiplist 编码的有序集合对象使用 zset 结构作为底层实现, 一个 zset 结构同时包含一个字典和一个跳跃表:

typedef struct zset {

    zskiplist *zsl;

    dict *dict;

} zset;

字典表保证了查找单个member的时候O(1)复杂度的操作,有序集合保证了范围查找的时间复杂度

存储示意图

实现难度不大,有兴趣可以自己撸下算法。随机插入性能很好,但顺序插入性能差的不行。抽空写的一个golang版本

package skiplist

import (
	"log"
	"math"
	"math/rand"
	"time"
)

const DefaultSkipListDepth = 32

type slNode struct {
	score   int
	forward []*slNode
}

func (sln *slNode) Score() int {
	return sln.score
}

type SkipList struct {
	Head     *slNode
	Tail     *slNode
	MaxDepth int
	depth    int
}

func (sl *SkipList) randDepth() int {
	var depth = 1
	rand.Seed(time.Now().UnixNano())

	for rand.Intn(2)%2 == 0 {
		depth++
	}

	if depth > sl.depth{
		depth = sl.depth + 1
	}

	return int(math.Min(float64(depth), float64(sl.MaxDepth)))
}

func (sl *SkipList) newNode(score int, depth int) slNode {
	node := slNode{
		score:   score,
		forward: make([]*slNode, depth+1),
	}

	return node
}

func (sl *SkipList) Search(score int) *slNode {
	startNode := sl.Head
	for i := sl.depth - 1; i >= 0; i-- {
		for p := startNode; p.forward[i] != sl.Tail; p = p.forward[i] {
			if p.forward[i].score < score {
				continue
			} else if p.forward[i].score > score {
				startNode = p
				break
			} else {
				return p.forward[i]
			}
		}
	}

	return nil
}

func (sl *SkipList) Delete(score int) {
	startNode := sl.Head
	for i := sl.MaxDepth - 1; i >= 0; i-- {
		for p := startNode; p.forward[i] != sl.Tail; p = p.forward[i] {
			if p.forward[i].score < score {
				continue
			} else if p.forward[i].score > score {
				startNode = p
				break
			} else {
				p.forward[i] = p.forward[i].forward[i]
				break
			}
		}
	}
}

func (sl *SkipList) Insert(score int) *slNode {
	depth := sl.randDepth()
	updates := make([]*slNode, depth+1)
	startNode := sl.Head

	for i:=sl.depth ; i < depth ; i++{

		updates[i] = sl.Head
	}

	for i := sl.depth - 1; i >= 0; i-- {
		for p := startNode; ; p = p.forward[i] {
			//log.Println("insert", "score",score, "depth",depth, "sl depth",sl.depth,  "level",i,"next", p.forward[i].score)
			if p.forward[i] ==  sl.Tail || p.forward[i].score > score{
				if depth > i{
					updates[i] = p
				}
				startNode = p
				break
			}else  if p.forward[i].score == score{
				return p.forward[i]
			}
		}
	}

	node := sl.newNode(score, depth)

	for i := 0; i < depth; i++ {
		node.forward[i] = updates[i].forward[i]
		updates[i].forward[i] = &node
	}

	if sl.depth < depth{
		sl.depth = depth
	}

	return &node
}
func (sl *SkipList) Display() {
	for i := sl.MaxDepth - 1; i >= 0; i-- {
		var values []int
		for p := sl.Head; p.forward[i] != sl.Tail; p = p.forward[i] {
			values = append(values, p.forward[i].score)
		}
		log.Println("level", i, values)
	}
}

func NewSkipList(depth int) *SkipList {
	sl := &SkipList{
		MaxDepth: depth,
		Head: &slNode{
			forward: make([]*slNode, depth),
		},
		Tail: &slNode{},
	}

	for i := 0; i < depth; i++ {
		sl.Head.forward[i] = sl.Tail
	}

	return sl
}

参考 有序集合对象

缓存穿透

当接口被恶意访问的时候,缓存命中率降低,大量请求会落到DB上,应用直接挂掉,通过BloomFilter布隆过滤器拦截非法请求

布隆过滤器使用多个hash函数将k映射到一个足够大内存中,如果判断某个key不存在,则一定不存在,如果判断某个key存在,则有一定概率不存在。