目录
Redis数据类型底层实现
【string】
【Hash】
【list】
【Set】
【Zset】
跳表:
IO多路复用
过期键删除策略
内存淘汰策略
Redis 6.0的新特性
先看一个问题:redis是单线程,为什么还那么快? redis6.0使用了多线程,目的是为了解决什么问题?
答案:Redis 是单线程,主要是指 Redis 的网络 IO 和键值对读写是由一个线程来完成的,但 Redis 的其他功能,比如持久化、异步删除、集群数据同步等,其实是由额外的线程执行的。另外,Redis 使用单线程模型还很快的原因还有下面几个方面:Redis 的大部分操作在内存上完成;采用了高效的数据结构,例如哈希表和跳表;Redis 采用了多路复用机制。
Redis 的基本对象结构 RedisObject,因为 Redis 键值对中的每一个值都是用 RedisObject 保存的。RedisObject 包括元数据和指针。其中元数据的一个功能就是用来区分不同的数据类型,指针用来指向具体的数据类型的值。RedisObject 的内部组成包括了 type,、encoding,、lru 和 refcount 4 个元数据,以及 1个*ptr指针。
type:表示值的类型,涵盖了我们前面学习的五大基本类型;
encoding:是值的编码方式,用来表示 Redis 中实现各个基本类型的底层数据结构, 例如 SDS、压缩列表、哈希表、跳表等;
lru:记录了这个对象最后一次被访问的时间,用于淘汰过期的键值对;
refcount:记录了对象的引用计数;
*ptr:是指向数据的指针。
redis的底层数据结构一共有 6 种,分别是简单动态字符串、双向链表、压缩列表、哈希表、跳表和整数数组。
使用 type命令 可以看到对外的数据类型:string、 hash、 list、 set、 zset;
使用 object encoding 命令可以看到内部实际保存的数据类型,如下:
Redis的string自己构建了简单动态字符串, sds。Sds内部又可以转为int、embstr、 raw。
下面演示,虽然type命令得到的都是string,但是用 object encoding 可以看到真实的类型。
127.0.0.1:6379> set data 100 //数值
OK
127.0.0.1:6379> type data
string
127.0.0.1:6379> object encoding data
"int"
127.0.0.1:6379>
127.0.0.1:6379> set name zhangsan //字符串
OK
127.0.0.1:6379> type name
string
127.0.0.1:6379> object encoding name
"embstr"
127.0.0.1:6379>
127.0.0.1:6379> set text asfsdfweqrwerfdsdfdascdfvsdfgwavdbqefcsdvdfsdasdsdfdbvcbxzdfg //很长的文本
OK
127.0.0.1:6379> type text
string
127.0.0.1:6379> object encoding text
"raw"
127.0.0.1:6379>
如何区分?
Redis的hash的底层是一个dict ,当数据量比较小或者数据值比较小时会采用ziplist(压缩列表),内容太多或者其中某个key的value值过大,会使用hashtable,无序的。
127.0.0.1:6379> hset user name zhangsan age 18 sex 1
(integer) 3
127.0.0.1:6379> hgetall user
1) "name"
2) "zhangsan"
3) "age"
4) "18"
5) "sex"
6) "1"
127.0.0.1:6379> type user
hash
127.0.0.1:6379> object encoding user
"ziplist"
127.0.0.1:6379>
127.0.0.1:6379> hset user hobby "this is user's hobby,it may be a long text, or may be very very long..."
(integer) 1
127.0.0.1:6379> hgetall user
1) "age"
2) "18"
3) "sex"
4) "1"
5) "name"
6) "zhangsan"
7) "hobby"
8) "this is user's hobby,it may be a long text, or may be very very long..."
127.0.0.1:6379> object encoding user
"hashtable"
关于 ziplist
Redis的list有序的数据结构,底层分为ziplist和quicklist
关于 quicklist:
127.0.0.1:6379> lpush queue A B C
(integer) 3
127.0.0.1:6379> type queue
list
127.0.0.1:6379> object encoding queue
"quicklist"
Redis的set是无序的,自动去重的数据类型,它的底层是一个字典dict。 当数据可以用整型并且数据元素小于配置文件中set-max- intset-entries是用intset ,否则用dict。
127.0.0.1:6379> sadd array 1 2 3 4 //全部是数值类型的元素
(integer) 4
127.0.0.1:6379> type array
set
127.0.0.1:6379> object encoding array
"intset"
127.0.0.1:6379>
127.0.0.1:6379> sadd array hello //新增一个字符串类型的元素
(integer) 1
127.0.0.1:6379> object encoding array
"hashtable"
Redis的zset是有序的,自动去重的数据类型。底层是由字典dict和跳表skiplist实现的。当数据较少时用ziplist来存储,ziplist可以在配置文件中通过zset-max-ziplist-entries和zset- max-ziplist-value来配置。
相当于给链表增加了索引,时间复杂度由原来的O(N)变成了O(logN),空间换时间。
【问题】redis的Zset类型为什么使用跳表,而不是红黑树或者二叉树?
【回答】跳表是用空间换时间,实现简单,区间查找快,跳表可以做到O(logN) 的时间复杂度定位区间的起点,然后在原始链表中顺序往后遍历就可以了。红黑树在插入和删除的时候可能需要做一些rebalance的操作,这样的操作可能会涉及到整个树的其他部分,而skiplist的操作显然更加局部性一些,需要锁住的节点更少,因此在这样的情况下性能好一些。
Redis是单线程的,只能在一个cpu内核上执行,假设是4核的,只会占用一个,其它三个不参与。即使是redis6.0版本redis的worker工作线程也依然是单线程。
Redis主要操作步骤分为三步:read(读取数据)、执行计算、write(返回数据)。如下图所示,c1、c2、c3表示redis的三个客户端,在redis6.0之前是下面的方式,串行执行。
redis6.0是下面这样的:
6.0版本以前执行上面的操作需要9个时间单位,6.0版本只需要5个时间单位。
打开 redis.conf 查找 # io-threads-do-reads no 部分,默认是关闭状态,如果需要开启,修改下面的内容:
Redis 回收内存空间的常用机制,本身就会引起 Redis 操作阻塞,导致性能变慢。如果大量的key同时失效,Redis 的线程就会一直执行删除,这样一来,就没办法正常服务其他的键值操作 了,就会进一步引起其他键值操作的延迟增加,Redis 就会变慢。注意, 删除操作是阻塞的(Redis 4.0 后可以用异步线程机制来减少阻塞影响)。
Redis设置key时,都会设置一个过期时间,那么当过期时间到了怎么处理?Redis同时使用了惰性过期和定期过期。
Redis默认每秒进行10次过期扫描:
1.从过期字典中随机20个key
2.删除这20个key中已过期的
3.如果超过25%的key过期,则重复第一步同时,为了保证业务不受影响, Redis还设置了3描的时间上限 ,默认不会超过25ms
假如内存不足时,redis会根据设置的淘汰策略,删除一些不常用的数据,保证redis的正常使用。
一般缓存的几种淘汰策略有下面几种
• 先进先出算法(FIFO):如果一个数据最先进入缓存,则应该最早淘汰掉。
• 最不经常使用(LRU):判断数据最近使用的时间,时间最远的数据优先被淘汰。
• 最近最少用(LFU):在一段时间内,数据被使用的次数最少,优先被淘汰。
Redis的淘汰策略有下面这些配置项:
1. noeviction: 当内存使用超过配置的时候会返回错误,不会回收任何键
2. allkeys-lru: 加入键的时候,如果过限,首先通过LRU算法回收最久没有使用的键
3. volatile-lru: 加入键的时候如果过限,首先从设置了过期时间的键集合中回收最久没有使用的键
4. allkeys-random: 加入键的时候如果过限,从所有key随机删除
5. volatile-random: 加入键的时候如果过限,从过期键的集合中随机回收
6. volatile-ttl: 从配置了过期时间的键中回收马上就要过期的键
7. volatile-lfu: 从所有配置了过期时间的键中回收使用频率最少的键
8. allkeys-lfu: 从所有键中回收使用频率最少的键
LRU最近最少使用:根据最近被使用的时间,离当前最远的数据优先被淘汰。新元素和刚被访问的元素都会被放到队尾,然后优先淘汰队头的元素。
但是可能存在问题,就是离当前最远的数据可能是访问次数最多的。因此在redis4.0之后新增了LFU策略(最不经常使用策略)。
主要新增的特性:多线程、客户端缓存与安全。从单线程处理网络请求到多线程处理:随着网络硬件的性能提升,Redis 的性能瓶颈有时会出现在网络 IO 的处理上,也就是说单个主线程处理网络请求的速度跟不上底层网络硬件的速度。Redis 的多 IO 线程只是用来处理网络请求的,对于读写命令,Redis 仍然使用单线 程来处理。
可以把主线程和多 IO 线程的 协作分成四个阶段:
阶段一:服务端和客户端建立 Socket 连接,并分配处理线程。
阶段二:IO 线程读取并解析请求。
阶段三:主线程执行请求操作。
阶段四:IO 线程回写 Socket 和主线程清空全局队列。
Redis 6.0 新增了一个重要的特性,就是实现了服务端协助的客户端缓存功能,也称为跟踪(Tracking)功能。有了这个功能,业务应用中的 Redis 客户端就可以把读取的数据缓存在业务应用本地了,应用就可以直接在本地快速读取数据了。
【如果你知道自己要去哪里,全世界都会为你让路,加油!】