Redis
一、Redis是什么?
Redis is an open source (BSD licensed), in-memory data structure store, used as a database, cache, and message broker. Redis provides data structures such as strings, hashes, lists, sets, sorted sets with range queries, bitmaps, hyperloglogs, geospatial indexes, and streams. Redis has built-in replication, Lua scripting, LRU eviction, transactions, and different levels of on-disk persistence, and provides high availability via Redis Sentinel and automatic partitioning with Redis Cluster.
Redis是一种开放源代码(BSD许可)的内存中数据结构存储,用作数据库,缓存和消息代理。Redis提供数据结构,例如字符串,哈希,列表,集合,带范围查询的排序集合,位图,超日志,地理空间索引和流。Redis具有内置的复制,Lua脚本,LRU逐出,事务和不同级别的磁盘持久性,并通过Redis Sentinel和Redis Cluster自动分区提供了高可用性。
内存中的数据结构存储系统,它可以用作数据库、缓存和消息中间件
基于内存亦可持久化的日志型、Key-Value数据库
Redis官网, Redis中文网 ,Redis下载地址,本文使用版本
二、Redis为什么效率快?
- Redis是纯内存数据库,一般都是简单的存取操作,线程占用的时间很多,时间的花费主要集中在IO上,所以读取速度快。(纯内存操作)
- 使用的是非阻塞IO,IO多路复用,使用了单线程来轮询描述符,将数据库的开、关、读、写都转换成了事件,减少了线程切换时上下文的切换和竞争。(单线程操作,避免了频繁的上下文切换)
- 采用了单线程的模型,保证了每个操作的原子性,也减少了线程的上下文切换和竞争。(非阻塞I/O多路复用机制)
- 采用自己实现的事件分离器,效率比较高,内部采用非阻塞的执行方式,吞吐能力比较大。
- 全程使用hash(key-value)结构,读取速度快,还有一些特殊的数据结构,对数据存储进行了优化,如压缩表,对短数据进行压缩存储,再如,跳表,使用有序的数据结构加快读取的速度。
三、Redis的五种数据结构
数据类型和编码方式
Redis的数据对象的定义:
typedef struct redisObject {
unsigned type:4; // OBJ_STRING 0
// OBJ_LIST 1
// OBJ_SET 2
// OBJ_ZSET 3
// OBJ_HASH 4
unsigned encoding:4; // OBJ_ENCODING_RAW 0 /* Raw representation */
// OBJ_ENCODING_INT 1 /* Encoded as integer */
// OBJ_ENCODING_HT 2 /* Encoded as hash table */
// OBJ_ENCODING_ZIPMAP 3 /* Encoded as zipmap */ // 已废弃
// OBJ_ENCODING_LINKEDLIST 4 /* Encoded as regular linked list */
// OBJ_ENCODING_ZIPLIST 5 /* Encoded as ziplist */
// OBJ_ENCODING_INTSET 6 /* Encoded as intset */
// OBJ_ENCODING_SKIPLIST 7 /* Encoded as skiplist */
// OBJ_ENCODING_EMBSTR 8 /* Embedded sds string encoding */
// OBJ_ENCODING_QUICKLIST 9 /* Encoded as linked list of ziplists */
void *ptr; // 指向底层实现数据结构的指针
unsigned lru:REDIS_LRU_BITS;
int refcount;
} robj;
type和encoding的对应关系如下图:
编码与数据结构实现:encoding与ptr的对应关系
3.1 字符串(String)
命令 | 描述 |
---|---|
set key value | 设置指定 key 的值 |
get key | 获取指定 key 的值。 |
del key | 通过 key,删除键值对 |
setnx key value | 只有在 key 不存在时设置 key 的值。 |
setex key seconds value | 将值 value 关联到 key ,并将 key 的过期时间设为 seconds (以秒为单位)。 |
strlen key | 返回 key 所储存的字符串值的长度。 |
append key value | 如果 key 已经存在并且是一个字符串, APPEND 将 value 追加到 key 原来的值的末尾。 |
ttl key | 查看key还有多长时间过期 |
字符串底层存储的是字符数组。
什么情况下选择哪种encoding?(可以使用 OBJECT encoding key 命令来查看编码)
-
int
设置为long类型的整数,那么encoding是int。
-
embstr
当我们保存的值小于40的时候,则使用的encoding是embstr。(Redis3.0版本是40)
embstr没有提供修改函数,所以是只读的。要想对它进行修改,首先会转换成raw,再执行修改,然后结束。
专门用于保存短字符串的一种编码方式,优点是速度快。- raw调用两次的内存分配函数来分别创建redisObject和sdsstr;
- embstr通过一次的内存分配函数来分配一块连续的空间,包含着redisObject和sdsstr,所以执行速度是快的。
- raw
当我们保存的值大于等于40的时候,则使用的encoding是raw。(Redis3.0版本是40) - SDS
简单的动态的字符串,是redis自己开发的一个字符串的抽象类型。
为什么要自己开发一个字符串类型,而不使用C的字符串?
- 计算字符串的长度:C字符串会一个一个元素去遍历,直到\0结束符号,复杂度O(n);SDS复杂度O(1),执行更快。
- C字符串有一个问题,缓冲区溢出。C字符串如果是在增加元素之前,没有进行足够的内存分配,那么就会出现缓冲区溢出的请款;SDS是动态的执行空间的扩充,API会自动的进行空间扩展。
- C字符串内存的重新分配是很耗性能的。
SDS采用两种方式解决:
a. 第一种是空间预分匹配;
1> 如果对SDS进行修改后,SDS的长度<1M, 那么此时分配的len=free;
2> 如果对SDS进行修改后,SDS的长度>=1M,len只会按照1M去分配。
b. 第二种是惰性空间释放。
1> 删除字符时,不及时释放,还保留空间。 - C字符串是以\0结尾的,并且字符串里不能包含空字符串,适用范围比较小;SDS是通过len来判断结尾的,可以保存任意的数据。
struct sdshdr {
unsigned int len; // 已使用的字符长度
unsigned int free; // 未使用的字符长度
char buf[]; // 数组
}
3.2 散列(Hash)
命令 | 描述 |
---|---|
hset key field value | 将哈希表 key 中的字段 field 的值设为 value 。 |
hgetall key | 获取在哈希表中指定 key 的所有字段和值 |
hget key field1 [field2] | 获取存储在哈希表中指定字段的值/td> |
hdel key field1 [field2] | 删除一个或多个哈希表字段 |
hexists key field | 查看哈希表 key 中,指定的字段是否存在。 |
hlen key | 获取哈希表中字段的数量 |
hvals key | 获取哈希表中所有值 |
hkeys key | 获取所有哈希表中的字段 |
hsetnx key field value | 只有在字段 field 不存在时,设置哈希表字段的值。 |
- ziplist 编码
同时满足以下两种情况,那么就是ziplist 编码,否则就是hashtable 编码:
- hash里面的键值对中,key和value的长度全部小于46个字节;
- hash里面的键值对中,元素个数小于512个。
- hashtable 编码
3.3 列表(List)
场景:消息队列;
特点:有序,可重复,插入和删除快,查找比较慢。
命令 | 描述 |
---|---|
lindex key index | 通过索引获取列表中的元素 |
rpush key value1 [value2] | 在列表中添加一个或多个值 |
lrange key start stop | 获取列表指定范围内的元素 |
brpoplpush | 从列表中弹出一个值,将弹出的元素插入到另外一个列表中并返回它; 如果列表没有元素会阻塞列表直到等待超时或发现可弹出元素为止。 |
lrem key count value | 移除列表元素 |
llen key | 获取列表长度 |
ltrim key start stop | 对一个列表进行修剪(trim),就是说,让列表只保留指定区间内的元素,不在指定区间之内的元素都将被删除。 |
lpop key | 移出并获取列表的第一个元素 |
lpushx key value | 将一个或多个值插入到已存在的列表头部 |
linsert key BEFORE | AFTER pivot value |
rpop key | 移除并获取列表最后一个元素 |
lset key index value | 通过索引设置列表元素的值 |
lpush key value1 [value2] | 将一个或多个值插入到列表头部 |
rpushx key value | 为已存在的列表添加值 |
-
ziplist 编码
-
linkedlist 编码
3.4 集合(Set)(无序集合)
场景:去重;
(存数值时是有序的)
命令 | 描述 |
---|---|
sadd key member1 [member2] | 向集合添加一个或多个成员 |
smembers key | 返回集合中的所有成员 |
sismember key member | 判断 member 元素是否是集合 key 的成员 |
scard key | 获取集合的成员数 |
spop key | 取出集合中的一个元素 |
del key | 删除集合 |
sinter key1 [key2] | 返回给定所有集合的交集 |
srem key member1 [member2] | 移除集合中一个或多个成员 |
spop key | 移除并返回集合中的一个随机元素 |
- intset 编码
针对整形集合作为底层实现的。
什么情况下使用intset编码?
- 集合对象保存的所有元素都是整数值。
- 集合对象保存的元素<=512个。
- hashtable 编码
底层是字典。每个键都是字符串对象。字典对应的值都为NULL。
3.5 有序集合(ZSet)
命令 | 描述 |
---|---|
zadd key score1 member1 [score2 member2] | 向有序集合添加一个或多个成员,或者更新已存在成员的分数 |
zscore key member | 返回有序集中,成员的分数值 |
zrange key start stop [WITHSCORES] | 正序输出 |
zrangebyscore key -inf +inf | 正序输出 |
zrevrange key 0 -1 | 倒序输出 |
zrem key member [member …] | 移除有序集合中的一个或多个成员 |
zcard key | 查看key中的元素个数 |
zrevrank key member | 返回有序集合中指定成员的排名,有序集成员按分数值递减(从大到小)排序 |
zcount key min max | 计算在有序集合中指定区间分数的成员数 |
- ziplist 编码
- skiplist 编码
全局常用命令
命令 | 描述 |
---|---|
keys * | 查询所有key值 |
type key | 查询key的类型 |
pexpire key milliseconds | 设置 key 的过期时间以毫秒计。 |
exists key | 检查给定 key 是否存在。 |
expire key seconds | 为给定 key 设置过期时间,以秒计。 |
四、应用
4.1 分布式锁
如何实现?
- 加锁和解锁的key要一致;
- 不用永久加锁,设置过期时间;
- 一定保证加锁与设置过期时间的原子性;
- 要支持过期续租,或者是重入(ThreadLocal);
加锁: setnx + 过期时间;
解锁:del key , 要保证原子性;
锁过期问题:
重叠解锁问题(在锁过期的问题基础上):
单点的问题:(redlock算法解决)
4.2 消息队列
- 消息类型
- 实时类消息;
- 延时类消息;(通过score实现延时)
- 消费者怎么消费数据?
- blpop和brpop拉取数据,有数据就拉取,没数据就一直blocking阻塞,直到有数据。此时redis会检查阻塞的消费者一直没做任何事情,就会断开消费者的连接,此时需要try catch住,继续执行第二次连接;
4.3 位图
五、持久化
Redis本身运行时数据保存在内存中,支持RDB和AOF两种持久化机制(这两种方式可以单独使用其中一种,或者混合使用),持久化功能有效地避免因进程退出造成的数据丢失问题,当下次重启时利用之前持久化的文件即可实现数据恢复。Redis默认就采用了一种持久化方式,即RDB。(Redis持久化的优先级:AOF>RDB)
5.1 RDB持久化(默认)
支持手工执行和服务器定期执行。二进制文件保存数据。
手动执行,命令:SAVE或BGSAVE。
RDB方式是通过快照完成的,一段时间内Redis会自动将内存中的所有数据进行快照,并且存储到硬盘上(进行快照的条件在配置文件中指定)。当下次启动redis的时候,回去读取硬盘上的快照恢复数据,进而实现Redis的持久化。
持久化时,会在指定的目录中生成一个.rdb结尾的快照文件。
redis.h
struct redisServer {
...
// 保存saveparam的数组
struct saveparam *saveparam;
// 修改计数器,记录上一次成功执行SAVE或BGSAVE后数据进行多少次修改(包括写入、删除、更新等操作)。
// sadd name "zhangsan" dirty计数器+1
// sadd name "zhangsan" "lisi" "wangwu" "zhaoliu" dirty计数器+4
long long dirty;
// 上一次执行保存的时间,记录上一次成功执行SAVE或BGSAVE的时间
time_t lastsave;
...
}
redis.h
struct saveparam { // 定时操作的定时配置
// 执行的秒数
time_t seconds;
// 修改的次数
int changes;
}
redis.conf配置文件
- 设置触发条件:
- 设置rdb文件路径:
测试
-
修改配置文件
-
启动Redis服务,指定配置文件
-
启动客户端,触发持久化
-
验证
-
关闭服务重新打开(直接获取可获得值)
5.2 AOF持久化(耗内存)
记录redis来记录数据库的变更。
RDB方式不能提供强一致性,如果Redis进程崩溃,那么两次RDB之间的数据也随之消失。那么AOF的出现很好的解决了数据持久化的实时性,AOF以独立日志的方式记录每次写命令,重启时再重新执行AOF文件中的命令来恢复数据。AOF会先把命令追加在AOF缓冲区,然后根据对应策略写入硬盘(appendfsync)。
客户端 -> Redis服务器 -> 执行命令 -> 保存被执行的命令 -> AOF文件中。
aof_buff : 打开AOF开关以后每次执行完一个写命令,都会把写命令以请求协议格式保存到aof_buff缓冲区中。
-
写入和同步
- appendfsync always:将aof_buf里的内容写入并同步到AOF文件中,真正的把指令存入磁盘。优点:数据不丢失;缺点:效率低。
- appendfsync everysec:将aof_buf里的内容写入到AOF文件中,上次同步时间,距离现在超过一秒,进行AOF同步。
- appendfsync no:将aof_buf里的内容写入到AOF文件中,但是不对AOF文件进行同步。同步操作由操作系统负责,通常最长30s。
-
启动Redis服务时指定配置文件
-
启动客户端,触发持久化
-
验证
-
关闭服务重新打开(直接获取可获得值)
AOF的缺点:
- AOF文件越来越大,造成空间的大量浪费,数据加载也非常的慢。
- 多条执行命令的保存,有很大几率都是浪费的。(采用AOF重写解决)
AOF重写
auto-aof-rewrite-percentage 100 // 比上次重写后的体积增加了100%
auto-aof-rewrite-min-size 64mb // aof文件体积超过64MB
同时满足满足上边两个条件就会将多条语句,通过一条语句实现数据的还原。
Redis服务器fork了一个子进程去执行AOF重写,这样主进程不会阻塞。
针对数据不一致的情况,Redis服务器设置了一个AOF重写缓冲区,在子进程建立了的时候开始用。
Redis主进程fork子进程来执行AOF重写,这个子进程创建新的AOF文件来存储重写结果,防止影响旧文件。因为fork采用了写时复制机制,子进程不能访问在其被创建出来之后产生的新数据。Redis使用“AOF重写缓冲区”保存这部分新数据,最后父进程将AOF重写缓冲区的数据写入新的AOF文件中然后使用新AOF文件替换老文件。
以日志的形式记录每个写操作(读操作不记录),只需追加文件但不可以改写文件,Redis启动时会根据日志从头到尾全部执行一遍以完成数据的恢复工作。包括flushDB也会执行。
主要有两种方式触发:有写操作就写、每秒定时写(也会丢数据)。
因为AOF采用追加的方式,所以文件会越来越大,针对这个问题,新增了重写机制,就是当日志文件大到一定程度的时候,会fork出一条新进程来遍历进程内存中的数据,每条记录对应一条set语句,写到临时文件中,然后再替换到旧的日志文件(类似rdb的操作方式)。默认触发是当aof文件大小是上次重写后大小的一倍且文件大于64M时触发。
当两种方式同时开启时,数据恢复Redis会优先选择AOF恢复。一般情况下,只要使用默认开启的RDB即可,因为相对于AOF,RDB便于进行数据库备份,并且恢复数据集的速度也要快很多。
开启持久化缓存机制,对性能会有一定的影响,特别是当设置的内存满了的时候,更是下降到几百reqs/s。所以如果只是用来做缓存的话,可以关掉持久化。