目录
什么是Redis
工作流程
数据结构
String—SDS简单动态字符串 O(1)
List—压缩列表、类似于LinkedList,支持队列的特性
Hash
Set—hashset不重复
Zset——压缩链表、跳表
数据过期机制
为什么需要设置过期时间?
怎么设置过期时间
判断过期
过期数据的删除策略
八种内存淘汰机制(大面积内存)
持久化机制
持久化操作
快照(snapshotting)持久化(RDB)
AOF(append-only file)持久化
Redis 是一个 C 语言开发的数据库,与传统数据库不同的是:Redis 的数据存在内存中。作为内存数据库,Redis读写速度非常快,因此被广泛应用于缓存方向。除了做缓存之外,Redis 也经常用来做分布式锁,甚至是消息队列。
Redis 提供了多种数据类型来支持不同的业务场景。Redis 还支持事务 、持久化、Lua 脚本、多种集群方案。
介绍
string 数据结构是简单的 key-value 类型。虽然 Redis 是用 C 语言写的,但是 Redis 并没有使用 C 的字符串表示,而是自己构建了一种 简单动态字符串(simple dynamic string,SDS)。相比于 C 的原生字符串,Redis 的 SDS 不光可以保存文本数据,还可以保存二进制数据,并且获取字符串长度的复杂度为 O(1)(C 字符串为 O(N))。除此之外,Redis 的 SDS API 是安全的,不会造成缓冲区溢出。
常用命令
set,get,exists,strlen,del,mset,mget,incr,decr,expire(exp),setex,ttl 等等。
//基本操作
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
应用场景
一般常用在需要计数的场景,比如用户的访问次数、热点文章的点赞转发数量等等
介绍
list 即链表。链表是一种非常常见的数据结构,特点是易于数据元素的插入和删除,并且可以灵活调整链表长度,但是随机访问困难。C 语言并没有实现链表,所以 Redis 实现了自己的链表数据结构:Redis 的 list 的实现为一个双向链表,即可以支持反向查找和遍历,更方便操作,不过也带来了额外的内存开销。
常用命令
rpush,lpop,lpush,rpop,lrange,llen 等。
//基本操作
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> 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"
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"
//通过 lrange 命令,可以实现分页查询
127.0.0.1:6379> llen myList # 查看链表长度
(integer) 2
//通过rpush、rpop实现栈(后进先出)
127.0.0.1:6379> rpush myList2 value1 value2 value3
(integer) 3
127.0.0.1:6379> rpop myList2 # 将 list的头部(最右边)元素取出
"value3"
应用场景
发布与订阅或者说消息队列、慢查询
介绍
hash 类似于 JDK1.8 前的 HashMap,内部实现也差不多(数组 + 链表)。不过,Redis 的 hash 做了更多优化。另外,hash 是一个 string 类型的 field 和 value 的映射表,特别适合用于存储对象,后续操作的时候,你可以仅仅修改这个对象中的某个字段的值。 比如我们可以 hash 数据结构来存储用户信息,商品信息等等。
常用命令
hset,hmset,hexists,hget,hgetall,hkeys,hvals 等。
127.0.0.1:6379> hset userInfoKey name "guide" 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 的所有字段和值
1) "name"
2) "guide"
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 "GuideGeGe" # 修改某个字段对应的值
127.0.0.1:6379> hget userInfoKey name
"GuideGeGe"
应用场景
系统中对象数据的存储。
介绍
set 类似于 Java 中的 HashSet ,是一种无序集合:集合中的元素没有先后顺序。当你需要存储一个列表数据,又不希望出现重复数据时,set 是一个很好的选择。并且 set 提供了判断某个成员是否在一个 set 集合内的重要接口,这个是 list 所不能提供的,可以基于 set 轻易实现交集、并集、差集的操作。比如:你可以将一个用户所有的关注人存在一个集合中,将其所有粉丝存在一个集合。Redis 可以非常方便的实现如共同关注、共同粉丝、共同喜好等功能。这个过程也就是求交集的过程。
常用命令
sadd,spop,smembers,sismember,scard,sinterstore,sunion 等。
127.0.0.1:6379> sadd mySet value1 value2 # 添加元素进去
(integer) 2
127.0.0.1:6379> sadd mySet value1 # 不允许有重复元素
(integer) 0
127.0.0.1:6379> smembers mySet # 查看 set 中所有的元素
1) "value1"
2) "value2"
127.0.0.1:6379> scard mySet # 查看 set 的长度
(integer) 2
127.0.0.1:6379> sismember mySet value1 # 检查某个元素是否存在set 中,只能接收单个元素
(integer) 1
127.0.0.1:6379> sadd mySet2 value2 value3
(integer) 2
127.0.0.1:6379> sinterstore mySet3 mySet mySet2 # 获取 mySet 和 mySet2 的交集并存放在 mySet3 中
(integer) 1
127.0.0.1:6379> smembers mySet3
1) "value2"
应用场景
需要存放的数据不能重复以及需要获取多个数据源交集和并集等场景
介绍
和 set 相比,sorted set 增加了一个权重参数 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"
应用场景
需要对数据根据某个权重进行排序的场景。比如在直播系统中,实时排行信息包含直播间在线用户列表,各种礼物排行榜,弹幕消息(可以理解为按消息维度的消息排行榜)等信息
Redis 自带了给缓存数据设置过期时间的功能,除了 String 类型有独有的设置过期时间的命令 setex 外,其他类型都要依靠 expire 命令来设置过期时间 。另外,persist 命令可以移除一个键的过期时间。
127.0.0.1:6379> exp 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
Redis 通过过期字典(可以看作是hash表)来保存数据过期的时间。过期字典的键指向Redis数据库中的某个key(键),过期字典的值是一个long long类型的整数,这个整数保存了key所指向的数据库键的过期时间(毫秒精度的UNIX时间戳)。
Redis 采用的是定期删除+惰性删除的策略。但是,仅仅通过给 key 设置过期时间还是有问题的:还是可能存在定期删除和惰性删除漏掉了很多过期 key 的情况,导致大量过期 key 堆积在内存里。所以,我们需要内存淘汰机制。
如何保证 Redis 中的数据都是热点数据?
- volatile-lru(least recently used)
从已设置过期时间的数据集(server.db[i].expires)中挑选最近最少使用的数据淘汰
- volatile-ttl
从已设置过期时间的数据集(server.db[i].expires)中挑选将要过期的数据淘汰
- volatile-random
从已设置过期时间的数据集(server.db[i].expires)中任意选择数据淘汰
- allkeys-lru(least recently used)
当内存不足以容纳新写入数据时,在键空间中,移除最近最少使用的 key(这个是最常用的)
- allkeys-random
从数据集(server.db[i].dict)中任意选择数据淘汰
- no-eviction
禁止驱逐数据,也就是说当内存不足以容纳新写入数据时,新写入操作会报错。这个不推荐哦!
4.0 版本后增加以下两种:
- volatile-lfu(least frequently used)
从已设置过期时间的数据集(server.db[i].expires)中挑选最不经常使用的数据淘汰
- allkeys-lfu(least frequently used)
当内存不足以容纳新写入数据时,在键空间中,移除最不经常使用的 key
注意:最近最少使用可能以前经常使用;最不经常使用不看时间,只看使用次数
为什么需要持久化(将内存中的数据写入到硬盘里面)?
为了之后重用数据(比如重启机器、机器故障之后恢复数据),或者为了防止系统故障而将数据备份到一个远程位置,保证 Redis 挂掉之后再重启,数据可以进行恢复。
Redis 可以通过创建快照来获得内存数据在某个时间点上的副本。Redis 创建快照之后,可以对快照进行备份,将快照复制到其他服务器,从而创建具有相同数据的服务器副本(Redis 主从结构,主要用来提高 Redis 性能),也可以将快照留在原地以便重启服务器的时候使用。
快照持久化是 Redis 默认采用的持久化方式,在 Redis.conf 配置文件中默认有此下配置:
- save 900 1 #在900秒(15分钟)之后,如果至少有1个key发生变化,Redis就会自动触发BGSAVE命令创建快照。
- save 300 10 #在300秒(5分钟)之后,如果至少有10个key发生变化,Redis就会自动触发BGSAVE命令创建快照。
- save 60 10000 #在60秒(1分钟)之后,如果至少有10000个key发生变化,Redis就会自动触发BGSAVE命令创建快照。
与快照持久化相比,AOF 持久化的实时性更好,因此已成为主流的持久化方案。默认情况下 Redis 没有开启 AOF(append only file)方式的持久化,可以通过 appendonly 参数开启:appendonly yes。
开启 AOF 持久化后每执行一条更改 Redis 中数据的命令,Redis 就会将该命令写入硬盘中的 AOF 文件。AOF 文件的保存位置和 RDB 文件相同,都是通过 dir 参数设置的,默认的文件名是 appendonly.aof。在 Redis 的配置文件中存在三种不同的 AOF 持久化方式,它们分别是:
- appendfsync always #每次有数据修改发生时都会写入AOF文件,这样会严重降低Redis的速度
- appendfsync everysec #每秒钟同步一次,显示地将多个写命令同步到硬盘
- appendfsync no #让操作系统决定何时进行同步
为了兼顾数据和写入性能,用户可以考虑 appendfsync everysec 选项 ,让 Redis 每秒同步一次 AOF 文件,Redis 性能几乎没受到任何影响。而且这样即使出现系统崩溃,用户最多只会丢失一秒之内产生的数据。当硬盘忙于执行写入操作的时候,Redis 还会优雅的放慢自己的速度以便适应硬盘的最大写入速度