为什么单线程的redis如此之快

本文深入探讨了Redis的高性能秘诀,包括纯内存操作、单线程设计、高效数据结构如SDS和跳跃表、合理数据编码及渐进式rehash策略。同时,解析了常见数据结构的应用场景,以及删除过期数据的三种策略。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

Redis作为一种性能优越的KV缓存服务器被广泛使用在各种后端项目中,在校招的面试题中存在感也特别高,Redis是高频问题。作为一只菜鸡,我知道的仅仅是:它的快是因为将数据存储在了内存中。然后就陷入和面试官互相注视的尴尬处境中,其实我们心中都明白这仅仅是Redis卓越性能原因的一小部分。几个月后的今天,作为一个看过很多大佬文章的菜鸡,我决定把大佬的东西复读一遍。
Redis性能之所以如此之高,得益于:

  • 纯内存操作
  • redis是单线程的
  • 高效的数据结构
  • 合理的数据编码
  • 其他优化

Redis常用数据结构及其应用场景:

  • String:缓存、计数器、分布式锁等;
  • List:链表、队列等;
  • Hash:用户信息、哈希表等;
  • Set:去重;
  • Zset:有序集合,常数查找时间。

SDS(Simple Dynamic String)

Redis使用C语言开发,但却没有使用C语言中的字符串,而是使用了一种称为SDS(Simple Dynamic String)的结构体来保存字符串

struct sdshdr{
	int len;//用于记录buf中已经使用的空间的长度
	int free;//buf中空闲的空间的长度
	char buf[];//存储实际内容
}

相比于C语言字符串,SDS有如下优势:

  1. 常数时间内获得字符串长度:C语言的字符串并不记录自身长度,需要时则遍历获取,时间复杂度O(N),SDS直接返回len;
  2. 避免缓存区溢出:SDS的API在修改字符串时会事先检查空间是否充足,如果不足则重新分配内存;
  3. 减少字符串修改时带来的内存重新分配的次数;
  4. 二进制安全:SDS可以安心地存储一些特殊字符,例如C语言中表示字符串结束的’\0’。

字典

Redis中字典与Java中的HashMap类似,但比HashMap高级,字典是Redis中Hash的底层实现。

	//最重要的字段是dictht 和 trehashidx,这两个字段与rehash相关
typedef struct dict{
	dictType *type;
	void *privdata;
	dictht ht[2];
	int trehashidx;
}

typedef struct dictht{
	//哈希表数组
	dectEntrt **table;
	//哈希表大小
	unsigned long size;
	unsigned long sizemask;
	//哈希表已有结点数量
	unsigned long used;
}

Rehash

我们重点聊一聊rehash,在上述代码中,我们看到dict中存储了一个dictht数组,长度为2,表明这个数据结构中实际存储着两个哈希表ht[1]和ht[0],两个表的存在未rehash过程提供了便利,其过程如下:

  • 为ht[1]分配空间,如果是扩容操作,ht[1]的大小为第一个大于等于ht[0].used*2的2^n; 如果是缩容操作,ht[1]的大小为第一个大于等于ht[0].used的2^n;
  • 将ht[0]的键值rehash到ht[1]中;
  • 当ht[0]迁移至ht[1]中后,释放ht[0],将ht[1]置为ht[0],并为ht[1]创建一张新表,为下次rehash做准备。

渐进式Rehash

上述的rehash方法在数据量很大时,会影响到服务器的性能,因此,Redis采用了一种渐进式的Rehash策略,分多次,渐进地将ht[0]迁移至ht[1]中:

  • 为ht[1]分配内存,同时维护ht[0]和ht[1];
  • 字典中维护了一个rehashidx,并将它置为0,表示rehash开始;
  • 在Rehash期间,每次对字典操作时,程序还顺便将ht[0]在rehashidx索引上的所有键值对rehash到ht[1]中,当全部rehash完成后,将rehashidx置为-1,表示rehash完成。

由于同时维护了两张哈希表,所以rehash过程中内存占用率会增长;删改查操作会在两张表中进行,表一没有则查表二,但是新增操作则只在ht[1]中进行,确保ht[0]只减不增。

跳跃表

Zset是一个有序的链表结构,其底层的数据结构是跳跃表skiplist:

typedef struct zskiplistNode{
	//成员对象
	robj *obj;
	//分值
	double score;
	//后退指针
	struct zskiplistNode *backforward;

	struct zskiplistlevel{
		struct zskiplistNode *forward;//前进指针
		unsigned int span;//跨度,当前结点和下一结点的距离
	}level[];
}

typedef struct zskiplist{
	struct zskiplistNode *header,*tail;
	//表中结点的数量
	unsigned long length;
	//表中层数最大的结点层数
	int level;
}

压缩列表

压缩列表ziplist用于为Redis节约内存,是列表键和字典键的底层实现之一。
当元素个数较少时,链表键中会把ziplist转化为linkedlist,字典键中会将ziplist转化为Hashtable。
ziplist是由一系列特殊编码的连续内存块组成的顺序型数据结构,可以分为多个entry,每个entry中均可存储整数或者字符串,因为是连续内存,所以遍历速度较快。

编码转化

String对象的编码转化

String对象的编码可以是int或者raw,对于String类的键值,如果存储的是非数字,采用int编码,否则采用raw编码。

List对象的编码转化

ziplist编码:

  • 列表对象保存的所有字符串元素长度都小于64字节。
  • 列表对象保存的元素小于512个。

其他情况采用linkedlist编码。

Set类型的编码转化

可以死intset和hashtable,整数元素使用intset,如果集合中右非整数类型,Redis会将其转化为hashtable编码。

Hash对象的编码转化

ziplist编码:

  • 对象保存的键和值的字符串元素长度都小于64字节。
  • 对象保存的元素小于512个。

否则为hashtable编码。

Zset对象的编码转化

ziplist编码:

  • 保存元素个数少于128;
  • 元素的成员长度都小于64。

否则zkiplist。

删除过期数据对性能的影响

三种策略

  • 定时删除:创建键的同时,创建timer计时。对内存友好,对CPU不友好,当删除任务过多时,删除行为占据CPU资源,影响Redis的吞吐量。
  • 惰性删除:过期后不管,每次读取该键时判断是否过期,过期则删除并返回空。对CPU友好,内存不友好,大量过期的键会在内存中堆积。
  • 定期删除:每个一段时间对数据库中的过期键进行一次检查。这是一种折中的策略。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值