学习记录篇章之 Redis
目录
学习记录篇章之 Redis
前言
一、Redis的常用数据结构以及使用场景
1.String
String的简单使用
2.list
list的简单使用
3.Hash
Hash的简单使用
4.zset
二、Redis的单线程模型和IO多路复用
Rdis中为什么不使用多线程
Redis的缓存过期策略
过期键的删除策略
Redis内存淘汰机制
总结
前言
Redis,一款用C语言编写的基于内存的NoSQL。在数据量日益增加的今天,Redis等缓存中间件的存在能够很好的帮我们后端数据库的压力。因为如果我们每一次的请求都要流入到数据库中去执行,那么即使性能再优越,效率再高的数据库也无法承担。所以我们可以利用缓存,简单地说就是把一些需要频繁查询的,不经常修改的数据,或者可以把一些频繁查询和修改,但是不需要立刻就存入到数据库中的数据放到缓存中进行操作。
一、Redis的常用数据结构以及使用场景
既然说Redis是一种NoSQL,那作为数据库来存储数据,他也拥有自己的数据结构,下面展示Redis部分主要的数据结构。
1.String
介绍
:
string
数据结构是简单的
key-value
类型。虽然
Redis
是⽤
C
语⾔写的,但是
Redis
并没有使⽤
C
的字符串表示,⽽是⾃⼰构建了⼀种
简单动态字符串
(
simple dynamic
string
,
SDS
)。相⽐于
C
的原⽣字符串,
Redis
的
SDS
不光可以保存⽂本数据还可以保存
⼆进制数据,并且获取字符串⻓度复杂度为
O(1)
(
C
字符串为
O(N)
)
,
除此之外
,Redis
的
SDS API
是安全的,不会造成缓冲区溢出。
2.
常⽤命令
:
set,get,strlen,exists,dect,incr,setex
等等。
3.
应⽤场景
:⼀般常⽤在需要计数的场景,⽐如⽤户的访问次数、热点⽂章的点赞转发数量等
等。
String的简单使用
127.0.0.1:6379> set key value #设置 key-value 类型的值
OK
127.0.0.1:6379> get key # 根据 key 获得对应的 value
"value"
127.0.0.1:6379> exists key # 判断某个 key 是否存在
(integer) 1
127.0.0.1:6379> strlen key # 返回 key 所储存的字符串值的⻓度。
(integer) 5
127.0.0.1:6379> del key # 删除某个 key 对应的值
(integer) 1
127.0.0.1:6379> get key
(nil)
批量设置
:
127.0.0.1:6379> mset key1 value1 key2 value2 # 批量设置 key-value 类型的值
OK
127.0.0.1:6379> mget key1 key2 # 批量获取多个 key 对应的 value
1) "value1"
2) "value2"
计数器(字符串的内容为整数的时候可以使⽤)
127.0.0.1:6379> set number 1
OK
127.0.0.1:6379> incr number # 将 key 中储存的数字值增⼀
(integer) 2
127.0.0.1:6379> get number
"2"
127.0.0.1:6379> decr number # 将 key 中储存的数字值减⼀
(integer) 1
127.0.0.1:6379> get number
"1"
过期策略
127.0.0.1:6379> expire key 60 # 数据在 60s 后过期
(integer) 1
127.0.0.1:6379> setex key 60 value # 数据在 60s 后过期 (setex:[set] + [ex]pire)
OK
127.0.0.1:6379> ttl key # 查看数据还有多久过期
(integer) 56
2.list
介绍
:
list
即是
链表
。链表是⼀种⾮常常⻅的数据结构,特点是易于数据元素的插⼊和删除
并且且可以灵活调整链表⻓度,但是链表的随机访问困难。许多⾼级编程语⾔都内置了链表
的实现⽐如
Java
中的
linkedList
,但是
C
语⾔并没有实现链表,所以
Redis
实现了⾃⼰
的链表数据结构。
Redis
的
list
的实现为⼀个
双向链表
,即可以⽀持反向查找和遍历,更⽅
便操作,不过带来了部分额外的内存开销。
常⽤命令
:
rpush,lpop,lpush,rpop,lrange,
llen
等。
应⽤场景
:
发布与订阅或者说消息队列、慢查询。
list的简单使用
127.0.0.1:6379> rpush myList value1 # 向 list 的头部(右边)添加元素
(integer) 1
127.0.0.1:6379> rpush myList value2 value3 # 向list的头部(最右边)添加多个元素
(integer) 3
127.0.0.1:6379> lpop myList # 将 list的尾部(最左边)元素取出
"value1"
127.0.0.1:6379> lrange myList 0 1 # 查看对应下标的list列表, 0 为 start,1为 end
1) "value2"
2) "value3"
127.0.0.1:6379> lrange myList 0 -1 # 查看列表中的所有元素,-1表示倒数第⼀
1) "value2"
2) "value3"
127.0.0.1:6379> rpush myList2 value1 value2 value3
(integer) 3
127.0.0.1:6379> rpop myList2 # 将 list的头部(最右边)元素取出
"value3"
通过
lrange
查看对应下标范围的列表元素
127.0.0.1:6379> rpush myList value1 value2 value3
(integer) 3
127.0.0.1:6379> lrange myList 0 1 # 查看对应下标的list列表, 0 为 start,1为 end
1) "value1"
2) "value2"
127.0.0.1:6379> lrange myList 0 -1 # 查看列表中的所有元素,-1表示倒数第⼀
1) "value1"
2) "value2"
3) "value3"
#通过 llen 查看链表⻓度:
127.0.0.1:6379> llen myList
(integer) 3
3.Hash
127.0.0.1:6379> set key value #设置 key-value 类型的值 OK 127.0.0.1:6379> get key # 根据 key 获得对应的 value "value" 127.0.0.1:6379> exists key # 判断某个 key 是否存在 (integer) 1 127.0.0.1:6379> strlen key # 返回 key 所储存的字符串值的⻓度。 (integer) 5 127.0.0.1:6379> del key # 删除某个 key 对应的值 (integer) 1 127.0.0.1:6379> get key (nil)批量设置 :
127.0.0.1:6379> mset key1 value1 key2 value2 # 批量设置 key-value 类型的值 OK 127.0.0.1:6379> mget key1 key2 # 批量获取多个 key 对应的 value 1) "value1" 2) "value2"计数器(字符串的内容为整数的时候可以使⽤)
127.0.0.1:6379> set number 1 OK 127.0.0.1:6379> incr number # 将 key 中储存的数字值增⼀ (integer) 2 127.0.0.1:6379> get number "2" 127.0.0.1:6379> decr number # 将 key 中储存的数字值减⼀ (integer) 1 127.0.0.1:6379> get number "1"过期策略
127.0.0.1:6379> expire key 60 # 数据在 60s 后过期 (integer) 1 127.0.0.1:6379> setex key 60 value # 数据在 60s 后过期 (setex:[set] + [ex]pire) OK 127.0.0.1:6379> ttl key # 查看数据还有多久过期 (integer) 56
2.list
介绍
:
list
即是
链表
。链表是⼀种⾮常常⻅的数据结构,特点是易于数据元素的插⼊和删除
并且且可以灵活调整链表⻓度,但是链表的随机访问困难。许多⾼级编程语⾔都内置了链表
的实现⽐如
Java
中的
linkedList
,但是
C
语⾔并没有实现链表,所以
Redis
实现了⾃⼰
的链表数据结构。
Redis
的
list
的实现为⼀个
双向链表
,即可以⽀持反向查找和遍历,更⽅
便操作,不过带来了部分额外的内存开销。
常⽤命令
:
rpush,lpop,lpush,rpop,lrange,
llen
等。
应⽤场景
:
发布与订阅或者说消息队列、慢查询。
list的简单使用
127.0.0.1:6379> rpush myList value1 # 向 list 的头部(右边)添加元素
(integer) 1
127.0.0.1:6379> rpush myList value2 value3 # 向list的头部(最右边)添加多个元素
(integer) 3
127.0.0.1:6379> lpop myList # 将 list的尾部(最左边)元素取出
"value1"
127.0.0.1:6379> lrange myList 0 1 # 查看对应下标的list列表, 0 为 start,1为 end
1) "value2"
2) "value3"
127.0.0.1:6379> lrange myList 0 -1 # 查看列表中的所有元素,-1表示倒数第⼀
1) "value2"
2) "value3"
127.0.0.1:6379> rpush myList2 value1 value2 value3
(integer) 3
127.0.0.1:6379> rpop myList2 # 将 list的头部(最右边)元素取出
"value3"
通过
lrange
查看对应下标范围的列表元素
127.0.0.1:6379> rpush myList value1 value2 value3
(integer) 3
127.0.0.1:6379> lrange myList 0 1 # 查看对应下标的list列表, 0 为 start,1为 end
1) "value1"
2) "value2"
127.0.0.1:6379> lrange myList 0 -1 # 查看列表中的所有元素,-1表示倒数第⼀
1) "value1"
2) "value2"
3) "value3"
#通过 llen 查看链表⻓度:
127.0.0.1:6379> llen myList
(integer) 3
3.Hash
127.0.0.1:6379> rpush myList value1 # 向 list 的头部(右边)添加元素 (integer) 1 127.0.0.1:6379> rpush myList value2 value3 # 向list的头部(最右边)添加多个元素 (integer) 3 127.0.0.1:6379> lpop myList # 将 list的尾部(最左边)元素取出 "value1" 127.0.0.1:6379> lrange myList 0 1 # 查看对应下标的list列表, 0 为 start,1为 end 1) "value2" 2) "value3" 127.0.0.1:6379> lrange myList 0 -1 # 查看列表中的所有元素,-1表示倒数第⼀ 1) "value2" 2) "value3" 127.0.0.1:6379> rpush myList2 value1 value2 value3 (integer) 3 127.0.0.1:6379> rpop myList2 # 将 list的头部(最右边)元素取出 "value3"通过 lrange 查看对应下标范围的列表元素
127.0.0.1:6379> rpush myList value1 value2 value3 (integer) 3 127.0.0.1:6379> lrange myList 0 1 # 查看对应下标的list列表, 0 为 start,1为 end 1) "value1" 2) "value2" 127.0.0.1:6379> lrange myList 0 -1 # 查看列表中的所有元素,-1表示倒数第⼀ 1) "value1" 2) "value2" 3) "value3" #通过 llen 查看链表⻓度: 127.0.0.1:6379> llen myList (integer) 3
3.Hash
Rdis中的Hash跟JDK1.8前里面的HashMap很像,内部实现也差不多(数组 + 链表)。
Redis 的 hash 做了更多优化。另外, hash 是⼀个 string 类型的 field 和 value 的映射表, 特别适合⽤于存储对象 ,后续操作的时候,你可以直接仅仅修改这个对象中的某个字段的 值。 ⽐如我们可以 hash 数据结构来存储⽤户信息,商品信息等等。 常⽤命令: hset,hmset,hexists,hget,hgetall,hkeys,hvals 等。 应⽤场景 : 系统中对象数据的存储。Hash的简单使用
127.0.0.1:6379> hset userInfoKey name "xiaoming" description "dev" age "24"
OK
127.0.0.1:6379> hexists userInfoKey name # 查看 key 对应的 value中指定的字段是否存
在。
(integer) 1
127.0.0.1:6379> hget userInfoKey name # 获取存储在哈希表中指定字段的值。
"guide"
127.0.0.1:6379> hget userInfoKey age
"24"
127.0.0.1:6379> hgetall userInfoKey # 获取在哈希表中指定 key 的所有字段和值
"name"
2) "xiaoming"
3) "description"
4) "dev"
5) "age"
6) "24"
127.0.0.1:6379> hkeys userInfoKey # 获取 key 列表
1) "name"
2) "description"
3) "age"
127.0.0.1:6379> hvals userInfoKey # 获取 value 列表
1) "guide"
2) "dev"
3) "24"
127.0.0.1:6379> hset userInfoKey name "honghong" # 修改某个字段对应的值
127.0.0.1:6379> hget userInfoKey name
"honghong"
4.zset
介绍:
和
set
相⽐,
zset
增加了⼀个权重参数
score
,使得集合中的元素能够按
score
进⾏有序排列,还可以通过
score
的范围来获取元素的列表。有点像是
Java
中
HashMap
和
TreeSet
的结合体。
常⽤命令:
zadd,zcard,zscore,zrange,zrevrange,zrem
等。
应⽤场景:
需要对数据根据某个权重进⾏排序的场景。⽐如在直播系统中,实时排⾏信息包
含直播间在线⽤户列表,各种礼物排⾏榜,弹幕消息(可以理解为按消息维度的消息排⾏
榜)等信息。
127.0.0.1:6379> zadd myZset 3.0 value1 # 添加元素到 sorted set 中 3.0 为权重
(integer) 1
127.0.0.1:6379> zadd myZset 2.0 value2 1.0 value3 # ⼀次添加多个元素
(integer) 2
127.0.0.1:6379> zcard myZset # 查看 sorted set 中的元素数量
(integer) 3
127.0.0.1:6379> zscore myZset value1 # 查看某个 value 的权重
"3"
127.0.0.1:6379> zrange myZset 0 -1 # 顺序输出某个范围区间的元素,0 -1 表示输出所有元
素1) "value3"
2) "value2"
3) "value1"
127.0.0.1:6379> zrange myZset 0 1 # 顺序输出某个范围区间的元素,0 为 start 1 为
stop
1) "value3"
2) "value2"
127.0.0.1:6379> zrevrange myZset 0 1 # 逆序输出某个范围区间的元素,0 为 start 1
为 stop
1) "value1"
2) "value2"
127.0.0.1:6379> zadd myZset 3.0 value1 # 添加元素到 sorted set 中 3.0 为权重 (integer) 1 127.0.0.1:6379> zadd myZset 2.0 value2 1.0 value3 # ⼀次添加多个元素 (integer) 2 127.0.0.1:6379> zcard myZset # 查看 sorted set 中的元素数量 (integer) 3 127.0.0.1:6379> zscore myZset value1 # 查看某个 value 的权重 "3" 127.0.0.1:6379> zrange myZset 0 -1 # 顺序输出某个范围区间的元素,0 -1 表示输出所有元 素1) "value3" 2) "value2" 3) "value1" 127.0.0.1:6379> zrange myZset 0 1 # 顺序输出某个范围区间的元素,0 为 start 1 为 stop 1) "value3" 2) "value2" 127.0.0.1:6379> zrevrange myZset 0 1 # 逆序输出某个范围区间的元素,0 为 start 1 为 stop 1) "value1" 2) "value2"
二、Redis的单线程模型和IO多路复用
Redis
基于
Reactor
模式来设计开发了⾃⼰的⼀套⾼效的事件处理模型
(
Netty
的线程模型也基
于
Reactor
模式,
Reactor
模式不愧是⾼性能
IO
的基⽯),这套事件处理模型对应的是
Redis
中的⽂件事件处理器(
file event handler
)。由于⽂件事件处理器(
file event handler
)是单线程
⽅式运⾏的,所以我们⼀般都说
Redis
是单线程模型。
既然是单线程,那怎么监听⼤量的客户端连接呢?
Redis
通过
IO
多路复⽤程序
来监听来⾃客户端的⼤量连接(或者说是监听多个
socket
),它会
将感兴趣的事件及类型
(
读、写)注册到内核中并监听每个事件是否发⽣。
这样的好处⾮常明显:
I/O
多路复⽤技术的使⽤让
Redis
不需要额外创建多余的线程来监听客户
端的⼤量连接,降低了资源的消耗
(和
NIO
中的
Selector
组件很像)。
《
Redis
设计与实现》有⼀段话是如是介绍⽂件事件的,我觉得写得挺不错。
Redis
基于
Reactor
模式开发了⾃⼰的⽹络事件处理器:这个处理器被称为⽂件事件处理器
(
file event handler
)。⽂件事件处理器使⽤
I/O
多路复⽤(
multiplexing
)程序来同时监听
多个套接字,并根据 套接字⽬前执⾏的任务来为套接字关联不同的事件处理器。
当被监听的套接字准备好执⾏连接应答(
accept
)、读取(
read
)、写⼊(
write
)、关 闭
(
close
)等操作时,与操作相对应的⽂件事件就会产⽣,这时⽂件事件处理器就会调⽤套
接字之前关联好的事件处理器来处理这些事件。
虽然⽂件事件处理器以单线程⽅式运⾏,但通过使⽤
I/O
多路复⽤程序来监听多个套接字
,
⽂件事件处理器既实现了⾼性能的⽹络通信模型,⼜可以很好地与
Redis
服务器中其他同样
以单线程⽅式运⾏的模块进⾏对接,这保持了
Redis
内部单线程设计的简单性。
那么,我们从中得知,文件事件处理器可以分为以下四个部分
1.多个 socket (客户端连接) 2.IO 多路复⽤程序(⽀持多个客户端连接的关键) 3.⽂件事件分派器(将 socket 关联到相应的事件处理器) 4.事件处理器(连接应答处理器、命令请求处理器、命令回复处理器)Rdis中为什么不使用多线程
对于这个问题,首先这个描述是不准确的,实际上Redis 4.0之后就引入了多线程,主要负责处理一些大键值对数据的删除等操作,大部分对于数据的操作还是单线程的。
那么Redis为什么不使用多线程去操作数据呢。
首先,Redis是基于内存操作的。所以Redis的性能几乎不受CPU的影响,主要与内存的大小以及网络传输方面的因素有关,可以说(多线程是可以有的,但是没有必要)
另外就是,多线程会带来死锁,数据不一致等各方面的问题,如果单线程既能保证效率,又不会带来潜在的危害,那还要什么多线程自行车呀。
这里有一个Redis 6.0的新特性,Redis 6.0引入了多线程操作,只不过不是基于数据读写的,而是在网络层的IO读写引入了多线程,提高了网络数据读写的效率(有兴趣的朋友们可以自行了解)
Redis的缓存过期策略
前面我们已经提到过了,Redis是拥有缓存过期特性的,也就是我们可以给Redis中的数据设置一个过期时间,时间到了我们就无法再使用这个数据了,必须要重新设置。
因为Redis是基于内存的,内存资源总是有限且宝贵的嘛,设置过期时间能很好的保证我们内存容量的健康,同时也可以在某些业务中给我们提供帮助(例如用户Token过期,登录验证码,二维码过期等等)。
过期键的删除策略
Redis中的过期键删除策略主要有以下两种,分别是惰性删除和定期删除。
1. 惰性删除 :只会在取出 key 的时候才对数据进⾏过期检查。这样对 CPU 最友好,但是可能会 造成太多过期 key 没有被删除。 2. 定期删除 : 每隔⼀段时间抽取⼀批 key 执⾏删除过期 key 操作。并且, Redis 底层会通过限 制删除操作执⾏的时⻓和频率来减少删除操作对 CPU 时间的影响。 定期删除对内存更加友好,惰性删除对 CPU 更加友好。两者各有千秋,所以 Redis 采⽤的是 定期 删除 + 惰性 / 懒汉式删除 。 但是,仅仅通过给 key 设置过期时间还是有问题的。因为还是可能存在定期删除和惰性删除漏掉 了很多过期 key 的情况。这样就导致⼤量过期 key 堆积在内存⾥,然后就 Out of memory 了。 怎么解决这个问题呢?答案就是: Redis 内存淘汰机制。Redis内存淘汰机制
Redis
提供
6
种数据淘汰策略:
1.
volatile-lru
(
least recently used
)
:从已设置过期时间的数据集(
server.db[i].expires
)
中挑选最近最少使⽤的数据淘汰
2.
volatile-ttl
:从已设置过期时间的数据集(
server.db[i].expires
)中挑选将要过期的数据淘汰
3.
volatile-random
:从已设置过期时间的数据集(
server.db[i].expires
)中任意选择数据淘汰
4.
allkeys-lru
(
least recently used
)
:当内存不⾜以容纳新写⼊数据时,在键空间中,移除
最近最少使⽤的
key
(这个是最常⽤的)
5.
allkeys-random
:从数据集(
server.db[i].dict
)中任意选择数据淘汰
6.
no-eviction
:禁⽌驱逐数据,也就是说当内存不⾜以容纳新写⼊数据时,新写⼊操作会报
错。这个应该没⼈使⽤吧!
4.0
版本后增加以下两种:
7.
volatile-lfu
(
least frequently used
)
:从已设置过期时间的数据集
(server.db[i].expires)
中
挑选最不经常使⽤的数据淘汰
8.
allkeys-lfu
(
least frequently used
)
:当内存不⾜以容纳新写⼊数据时,在键空间中,移
除最不经常使⽤的
key
总结
本篇文章就到这里啦,这篇文章首先认识了一下Redis的概念以及基本数据的使用方法。最后还了解了一下Redis的过期策略和内存淘汰策略。由于篇幅原因,Redis的持久化和使用场景我就下一篇再给大家展示咯。



