Redis官网
Redis命令参考
Redis源码Git地址
Redis版本 5.0.4
文章目录
1 前言
Redis is an open source (BSD licensed), in-memory data structure store, used as a database, cache and message broker. It supports data structures such as strings, hashes, lists, sets, sorted sets with range queries, bitmaps, hyperloglogs, geospatial indexes with radius queries 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是一个开源的,基于内存处理的数据结构存储系统,可以作为数据库、缓存和消息处理。它支持的数据类型有字符串、哈希、列表、集合、有序集合、位图、Hyperloglog、地理位置和stream。同时还提供了复制、Lua脚本、LRU驱动事件、事务和不同级别的磁盘持久化功能,并通过哨兵和集群分区功能实现高可用。
由于Redis是基于内存的,其速度是很快的,每秒处理的请求可达十万次,可以通过redis-benchmark -n 100000 -q
进行测试,官方文档结果如下:
- 使用内存读写
- 单线程
- 异步非阻塞IO,多路复用
2 Redis基础
在这部分主要介绍Redis的基础知识,包括内容如下
- Redis的安装
- Redis的数据类型
- Redis的操作命令
- Java客户端Jedis的使用
2.1 Redis的安装
-
下载Redis的压缩包
在官方的下载页面中提供了Redis的非稳定版、稳定版和Docker的镜像三种,我们下载其稳定版的压缩包
-
解压编译
tar zxf redis-5.0.4.tar.gz
命令解压Redis的压缩包
cd redis-5.0.4
进入redis目录执行make
命令 -
服务端和客户端的启动
启动命令在src/
目录下
启动服务端src/redis-server
。 想要在后台启动的话,需要将redis.conf
配置文件中的daemonize
参数设置为yes,默认为no
启动客户端src/redis-cli <-h host -p port>
redis.conf是redis的配置文件,可以在这个里面进行相应启动参数的修改
想要redis服务端在非本地访问,需要设置 bind 0.0.0.0
2.2 通用命令
exists key
判断某个key是否存在del key
删除指定的keyrename key newkey
重命名keys *
列出所有的key 会遍历所有的key 会导致阻塞 线上环境禁用dbsize
查看键个数type key
判断指定key的数据类型object encoding key
判断指定key的数据结构flushdb
清空当前数据库flushall
清空所有数据库expire key seconds
为key设置过期时间persist key
取消掉过期时间ttl key
查看key的过期时间 秒pttl key
返回key的过期时间 毫秒
2.3 数据类型及实现原理
在Redis中提供了多种数据类型,通过官方文档可以看到给出的有字符串、哈希、列表、集合和有序集合五种基本的数据类型,同时还有位图、HyperLogLogs和Streams(Redis 5新增)类型
Redis是一个key-value数据库,每设置一个键值对会创建一个dictEntry
对象来进行存储,在该对象中会存储键和值的信息,key是使用的Redis自己实现的字符串SDS
数据结构存储的,value是使用redisObject
对象来存储的
- dictEntry 可以在dict.h中找到对其定义,如下
typedef struct dictEntry {
void *key; // key的引用
union {
void *val; // value的引用
uint64_t u64;
int64_t s64;
double d;
} v;
struct dictEntry *next; // 下一个键值对的引用 用来解决hash冲突的情况,形成链表
} dictEntry;
- redisObject可以在server.h中找到对其的定义,如下
typedef struct redisObject {
unsigned type:4; // 对象类型 例如:string list hash set zset
unsigned encoding:4; // 底层编码
unsigned lru:LRU_BITS; // 记录对象最后一次呗命令程序访问的时间 lru(24位时间)和lfu(8位访问频率16位访问时间)算法存储的样式有区别
int refcount; // 引用计数 该值为0时对象所占的内存会被释放 可以用来实现对象共享
void *ptr; // 执行底层实现数据结构的指针
} robj;
数据的类型可以通过
type key
命令来查看,底层编码可以使用object encoding key
命令进行查看
object idletime key
可以用来查看lru值 上次访问该key距离现在的时长
2.3.1 String 字符串
字符串是Redis的基础类型,该类型是二进制安全的,可以存储数字、文本、图片和视频等。
没有使用c语言的字符串,而是使用的自己实现的SDS(simple dynamic string 简单动态字符串)来存储。一个字符串的最大长度为512M
,其数据结构的定义在sds.h
中可以查看,其中定义了多种长度的sdshdr的结构如下
struct __attribute__ ((__packed__)) sdshdr5 {
unsigned char flags; /* 3 lsb of type, and 5 msb of string length */
char buf[];
};
struct __attribute__ ((__packed__)) sdshdr8 {
uint8_t len; // 长度
uint8_t alloc; // 内存大小
unsigned char flags; // 当前字符数组的属性
char buf[]; // 字符串的值
};
struct __attribute__ ((__packed__)) sdshdr16 {
uint16_t len; /* used */
uint16_t alloc; /* excluding the header and null terminator */
unsigned char flags; /* 3 lsb of type, 5 unused bits */
char buf[];
};
struct __attribute__ ((__packed__)) sdshdr32 {
uint32_t len; /* used */
uint32_t alloc; /* excluding the header and null terminator */
unsigned char flags; /* 3 lsb of type, 5 unused bits */
char buf[];
};
struct __attribute__ ((__packed__)) sdshdr64 {
uint64_t len; /* used */
uint64_t alloc; /* excluding the header and null terminator */
unsigned char flags; /* 3 lsb of type, 5 unused bits */
char buf[];
};
2.3.1.1 SDS对C语言字符串的改进
- 在SDS中维护了一个字符串长度的字段,在获取字符串长度时可以直接得到而不用进行遍历 时间复杂度将为0(1)
- 在对SDS进行修改时,SDS的操作中会先检查空间是否满足,不满足的话会先进行扩容,可以避免缓冲区溢出
- 在SDS中有内存预分配和惰性空间释放,减少在对字符串修改时对空间的分配次数
- 二进制安全,SDS不仅能存文本还能存图片、视频等二进制数据
2.3.1.2 常用的命令
-
set key value
设置key 可以增加一些参数EX seconds
为key设置过期时间 单位为秒 等同于expire key seconds
的效果PX millseconds
为key设置过期时间 单位为毫秒NX
加上该参数只有当key不存在时才会添加成功XX
加上该参数表示 只有当key存在时才能设置成功
使用该命令可以实现分布式锁
-
setnx key value
key不存在时设置成功 -
setex key seconds value
设置key同时制定存活时间 -
get key
获取一个key -
getset key value
为key设置值并返回旧值 -
mset k1 v1 k2 v2
批量设置key -
mget k1 k2
批量获取key的值 -
incr k
key的值增加1 -
decr k
key的值减1 -
incrby k num
key的值增加指定的值 -
decrby k num
key的值减少指定的值 -
strlen key
获取key的值的长度 -
append key value
拼接key的值 -
setrange key offset value
从索引为offset的位置其使用value进行覆盖 -
getrange key start end
获取指定范围的字符串
2.3.1.3 底层编码
字符串对象的编码有int、embstr和raw三种
int
存储的数据为整数,且长度不超过8个字节embstr
存储的数据为长度小于44个字节的字符串或者长度超过8个字节的整数raw
存储长度大于44字节的字符串
测试如下图所示
embstr和raw都是使用redisObject和sdshdr结构来存储字符串,raw编码会调用两次内存分配函数来创建这两个对象,embstr编码调用一次内存分配来分配一个包含这两个结构的连续的空间。
int和embstr编码在执行append操作会变为raw编码
2.3.2 hash 哈希
哈希可以用来存储多个简直对的映射。
2.3.2.1 常用命令
hset key field value
设置一个key的字段值hmset key field1 value1 field2 value2
批量设置一个key的字段值hsetnx key field value
当字段不存在时才设置hget key field
获取一个key的某个字段值hmget key field1 field2
批量获取某个key的字段值hexists key field
判断一个key下面是否存在指定的字段hdel key field1 field2
删除一个key下的某些字段hlen key
获取一个key下的字段数量hstrlen key field
获取一个key下摸个字段值的长度hincrby key field num
为一个key下的某个字段值增加指定的值hincrbyfloat key field num
hkeys key
获取某一key下的所有字段名hvals key
获取key下的所有字段值hgetall key
获取key下的所有字段名和值
2.3.2.1 内部编码
哈希的编码可以是ziplist或者hashtable
- ziplist 压缩列表 键值对的键和值的字符串长度小于等于64字节且键值对数量小于512个
在
ziplist.c
中对压缩列表的解释如下:
是一个经过特殊编码的双向链表,它不存储指向上一个链表节点和指向下一 个链表节点的指针,而是存储上一个节点长度和当前节 点长度,通过牺牲部分读写性能, 来换取高效的内存空间利用率,是一种时间换空间的思想。只用在字段个数少,字段值 小的场景里面。
ziplist的存储结构如下图所示:
zlbytes
记录整个压缩列表占用的内存字节数zltail
记录压缩列表尾节点举例压缩列表起始地址有多少字节 通过该偏移量可以确定尾节点的地址zllen
记录压缩列表包含的节点数量entry
压缩列表的节点对象zlend
用来标记压缩列表的尾端
列表节点对象的定义如下
typedef struct zlentry {
unsigned int prevrawlensize; // 上一个节点占用的空间
unsigned int prevrawlen; // 存储上一个节点大小所需要的空间
unsigned int lensize; // 存储当前列表长度值所占用的空间
unsigned int len; // 当前节点占用的长度
unsigned int headersize; // prevrawlensize + prevrawlen
unsigned char encoding; // 编码方式
unsigned char *p; // 指向当前节点的起始位置
} zlentry;
- hashtable 哈希表 当超出ziplist的范围后,底层编码会变为hashtable
在Redis中hashtable被称为字典,是通过数组加链表的结构实现的
在dict.h
中定义了这种数据结构,如下
/* This is our hash table structure. Every dictionary has two of this as we
* implement incremental rehashing, for the old to the new table. */
typedef struct dictht {
dictEntry **table; // 哈希表数组 其源码已在上面给出
unsigned long size; // 哈希表大小
unsigned long sizemask; // 哈希表掩码,用于计算索引值 等于size-1
unsigned long used; // 该哈希表已有节点的数量
} dictht;
typedef struct dict {
dictType *type; // 类型特定函数 指向dictType结构的指针
void *privdata; // 私有数据 需要传递给特点函数的参数
dictht ht[2]; // 哈希表 两个 为了rehash
long rehashidx; // rehash索引 不在进行rehash时为-1
unsigned long iterators; // 当前正在使用的迭代器的数量
} dict;
typedef struct dictType {
uint64_t (*hashFunction)(const void *key); // 计算哈希值
void *(*keyDup)(void *privdata, const void *key); // 复制键
void *(*valDup)(void *privdata, const void *obj); // 复制值
int (*keyCompare)(void *privdata, const void *key1, const void *key2); // 比较键
void (*keyDestructor)(void *privdata, void *key); // 销毁键
void (*valDestructor)(void *privdata, void *obj); //销毁值
} dictType;
其结构如下图所示
rehash过程
- 为ht[1]哈希表分配空间,这个哈希表的空间大小取决于要执行的操作以及ht[0]当前包含的键值对数量
- 扩容的情况:大小设置为第一个大于等于ht[0].used*2且为2的n次方的值
- 收缩情况:大小设置为第一个大于等于ht[0].userd且为2的n次方的值
- 将保存在ht[0]中的所有键值对rehash(重新计算键的哈希值和索引值)到ht[1]中 指定的位置
- 释放ht[0] 将ht[1]设置为ht[0],并将ht[1]设置为空
渐进式rehash
当节点数量太大时,为了避免rehash对服务器性能造成影响,会采用分多次、渐进式地将ht[0]里面的键值对慢慢地rehash到ht[1]中,其过程如下
1、为ht[1]分配空间
2、将字典中的rehashidx设置为0
3、在rehash进行期间,每次对字典执行添加、删除、查找或者更新操作时,除了执行响应的操作外,还会将ht[0]上rehashidx索引上的键值对移动到ht[1]上,完成后,将rehashidx加一
4、当ht[0]上的所有数据都移动到ht[1]上后将rehashidx的值设置为-1
ziplist和hashtable编码之间的转化限制可以通过
list-max-ziplist-value 和 list-max-ziplist-entries参数设定
2.3.3 List 列表
列表用来存储一系列有序的字符串数据 有序 可重复 有索引
2.3.3.1 常用命令
lpush key value
lpushx key value
rpush key value
rpushx key value
lpop key value
rpop key value
lset key index value
lrange key start end
ltrim key start end
2.3.3.2 底层编码
在早期的版本中使用的ziplist和hashtable编码,3.2版本后使用的是quicklist编码,其结构源码在quicklist.h
中定义如下:
typedef struct quicklist {
quicklistNode *head; // 双向链表的头节点
quicklistNode *tail; // 双向链表的尾节点
unsigned long count; // 在所有ziplist中节点数
unsigned long len; // 双向链表的节点数
int fill : 16; /* fill factor for individual nodes */
unsigned int compress : 16; /* depth of end nodes not to compress;0=off */
} quicklist;
typedef struct quicklistNode {
struct quicklistNode *prev; // 前一个节点
struct quicklistNode *next; // 后继节点
unsigned char *zl; // 执行ziplist
unsigned int sz; // 当前ziplist占用的字节
unsigned int count : 16; // ziplist中存储了多少个元素 占16个字节
unsigned int encoding : 2; // 是否采用LZF压缩算法
unsigned int container : 2; /* NONE==1 or ZIPLIST==2 */
unsigned int recompress : 1; /* was this node previous compressed? */
unsigned int attempted_compress : 1; /* node can't compress; too small */
unsigned int extra : 10; /* more bits to steal for future usage */
} quicklistNode;
如下图所示
2.3.4 set 集合
无序 元素不可重复
2.3.4.1 常用命令
sadd key v1 v2 v3
向集合中插入数据smembers key
获取集合中的所有元素scard key
获取集合中的元素数量srandmember key count
从集合中随机获取count个数据 count省略取一个spop key count
从集合中弹出count个数据 省略弹出一个srem key v1 v2
从集合中删除指定的元素 可以指定多个sismember key v
判断是否为集合的元素smove source target v
从source集合中移动元素到target集合中sdiff set1 set2 set3
多个集合的差集sinter set1 set2
两个集合的交集sunion set1 set2
两个结合的并集sinterstore key set1 set2
交集存入一个新的集合sdiffstore key set1 set2
差集存入一个新的集合sunionstore key set1 set2
并集存入一个新的集合
2.4.4.2 底层编码
底层编码为intset
或者hashtable
,当为纯整数且数量小于513个时使用的是intset
编码实现,底层是一个整数集合,否则使用hashtable
2.3.5 zset 有序集合
元素不可重复 有序 通过制定score排序
2.3.5.1 常用命令
2.3.5.2 底层编码
有序集合的底层编码使用的是ziplist或者skiplist实现
2.3.6 其他类型数据
前面几部分我们介绍了Redis中的5种基本数据类型,除了这几种类型,在Redis中还有另外的集中类型,如下
- Bitmap 位图
- HyperLogLogs
- geo 地理位置
- stream
2.3.6.1 位图
位图不是实际的数据类型,而是在String类型上定义的一组面向比特的操作,我们可以使用这种操作对一个字符串的某位的字节数进行修改(0或1)。
常用命令
bitcount key
setbit
getbit
bitop
bitpos
bitfield
2.3.6.2 HyperLogLog
HyperLogLog是一种概率统计的数据结构,用于对数据集合中的数据进行唯一性统计。
2.3.6.3 地理位置
地理位置是一种特殊的有序集合,存储经纬度、名称、地理空间等信息。
2.4 Redis配置
在Redis的安装目录下有redis.conf
文件,这是Redis的配置文件
2.5 Java客户端
Redis提供了大量的客户端支持
Java的客户端也有很多个,如下图
Jedis客户端的使用
- 添加Jedis的Maven依赖
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>3.1.0</version>
<type>jar</type>
<scope>compile</scope>
</dependency>
- 客户端使用
通过服务端的ip和端口创建一个Jedis对象,使用该对象对redis进行操作,代码如下
public static void main(String[] args) {
Jedis jedis = new Jedis("127.0.0.1", 6379);
jedis.set("name", "xiehua");
System.out.println(jedis.get("name"));
}
3 发布/订阅
在Redis中提供了发布/订阅模式,过程是消费者订阅所需的频道,生产者向频道中发布消息,这样实现了生产者和消费者之间的解耦。
3.1 相关命令
subscribe channel1 channel2
订阅频道,可以指定多个psubscribe pattern
使用匹配规则订阅频道?
一个字符*
多个字符publish channel message
向频道中发布消息unsubscribe channel1 channel2
取消订阅某些频道punsubscribe pattern
使用匹配规则取消订阅频道pubsub channels pattern
列出满足匹配规则的频道 没有匹配规则列出所有pubsub numsub channel1 channel2
返回频道的订阅数量 支持同时查询多个频道
命令演示如下所示:
4 事务
事务相关的命令
multi
开始事务exec
提交事务discard
取消事务watch key
监视一个键 当有其他客户端对该值进行修改后,事务会被打断 演示如下
Redis中的事务和Mysql中的事务是有区别的,在Redis事务中若发生异常,其他的操作是不会进行回滚的,这样发生的脏数据需要手动进行处理,因此无法通过Redis事务来保证多个操作的原子性
,演示如下:
在上面的演示中我们创建了一个字符串键和一个哈希键,在事务中均使用set命令修改这两个键,当提交事务后,发现字符串键已被修改
5 持久化
为了防止在Redis服务宕机后数据丢失的问题,Redis里提供了RDB和AOF两种持久化方式。
5.1 RDB持久化
RDB是Redis的默认持久化方案。将内存的数据生成一个.rdb文件,在服务重启时加载该rdb文件进行数据恢复。
有如下配置
dbfilename
文件名称 默认为dump.rdbdir
文件存储路径rdbcompression
是否开启压缩 开启可以节省存储空间,但会消耗一些CPU 默认开启rdbchecksum
使用CRC64算法进行数据校验 默认开启
5.1.1 RDB的快照生成方式
- 手动触发
save
该命令在生成快照时会阻塞当前服务器,Redis不能处理其他命令bgsave
使用该命令Redis会在后台异步进行快照操作,Redis服务器还能处理其他请求
- 自动触发
-
配置文件制定规则 在配置文件的
SNAPSHOTTING
下有触发频率的配置,其格式为save <seconds> <changes>
,默认配置如下save 900 1 900秒内至少有一个key被修改
save 300 10 300秒内至少有10个key被修改
save 60 10000 60秒内至少有10000个key被修改 -
调用
shutdown
命令
-
5.1.2 RDB持久化的特点
优点
- RDB是一个紧凑的文件,保存了Redis在某个时间点上的数据集,适合用于进行备份和灾难恢复
- 生成RDB文件的时候,Redis主进程会fork()一个子进程来处理备份工作,主进程不需要进行任何磁盘IO操作
- RDB在恢复数据的速度要快
缺点
- 无法做到实时持久化
- 意外发生宕机的情况会吊事最后一次快照之后的所有修改
5.2 AOF持久化
AOF持久化默认是关闭的,该持久化方式是通过记录每个写操作,将其保存到文件中,在Redis重启时会根据日志文件的内容把写指令执行一遍。
5.2.1 开启AOF
通过redis.conf
中的appendonly
参数进行开启,通过appendfilename
参数设置文件名,文件路径也是由dir
参数指定
触发条件由appendfsync
参数设置,有如下三种
no
由操作系统保证数据同步 速度快 不安全always
每次写入都执行同步 效率低 发生宕机最多丢失一个命令的数据everysec
每秒同步一次 可能会丢失一秒的数据 通常使用该方式
5.2.2 重写机制
由于AOF持久化是通过记录操作指令的方式工作的,随着时间的推迟,生成的AOF文件会越来越大,为了解决这个问题,Redis中增加了重写机制,当文件达到一定大小后,Redis就会对其进行重写,只保留可以恢复数据的最小指令集。
auto-aof-rewrite-percentage
当目前aof文件大小超过上一次重写的aof文件大小的百分比进行重写。默认为100auto-aof-min-size
指定允许进行重写操作的最小大小,避免达到百分比但文件很小的情况进行重写 默认为64m
5.2.3 AOF持久化的特点
优点
- AOF持久化的方法提供了多种同步频率,对数据的完整性保证更高,采用默认的同步频率,最多丢失1秒的数据
缺点
- AOF的文件较大
- 在高并发情况下,RDB比AOF具有更好的性能
5.3 持久化方式的选择
了解了RDB和AOF两种持久化策略的特点后,我们应该如何选择呢
- 在对数据完整性要求不是太高的情况下,优先使用RDB进行持久化
- 一般情况下两种同步方式同时开启,这种情况下,Redis重启的时候会优先载入AOF文件进行数据恢复
6 内存回收
在这部分我们了解一下Redis的内存回收,在Redis中内存回收包含两类
- 设置了过期时间的键的删除
- 内存使用达到上限触发的内存淘汰
6.1 过期键删除策略
我们可以通过expire key
命令为key设置存活时间,这个键是是何时真正的删除并释放内存呢?在这一部分我们将对其进行介绍
在说Redis的过期键删除策略前我们先来了解下三种删除方式,如下:
-
定时删除 在设置键的过期时间的同时,创建一个定时器,当键的存活时间到后立即删除
能够及时的释放内存,但当过期键比较多时会占用较长的cpu资源,影响服务器的响应时间和吞吐量
-
惰性删除 每次从键空间获取键时,判断取得的键是否过期,如果过期的话,就删除该键,否则返回
不会占用过多的cpu资源,但有些键一直不被访问的情况下,会一直占用内存
-
定期删除 每隔一段时间,程序对数据库进行一次检查,删除里面过期的键
这种方式是前两种方式的折中方案,但需要根据情况设置合理的删除量和执行频率
6.1.1 Redis中的过期键删除策略
在前面我们了解了三种删除策略的特点,那么在Redis中是使用的哪种策略呢?
官网文档给出的说明如下:
通过官网的说明,我们可以看出在Redis中使用的是惰性删除和定期删除两种策略,通过这两种策略的配合使用,可以在使用CPU时间和占用内存之间取得平衡
1、Redis中惰性策略的实现
Redis中惰性删除策略的逻辑在db.c#expireIfNeeded
方法中定义,其源码如下
int expireIfNeeded(redisDb *db, robj *key) {
if (!keyIsExpired(db,key)) return 0;
if (server.masterhost != NULL) return 1;
server.stat_expiredkeys++;
propagateExpire(db,key,server.lazyfree_lazy_expire);
notifyKeyspaceEvent(NOTIFY_EXPIRED,
"expired",key,db->id);
return server.lazyfree_lazy_expire ? dbAsyncDelete(db,key) :
dbSyncDelete(db,key);
}
2、Redis中的定期删除策略
从上面的文档截图中我们可以看到在Redis中过期键的定期删除过程为
- 1、从设置了过期时间的键中抽取20个
- 2、删除其中已经过期的键
- 3、如果有超过25%的键过期了会重复执行该步骤一
6.2 Redis淘汰策略
在Redis中我们可以通过maxmemory
参数来配置Redis可以使用的最大内存,当达到该值后会对内存进行回收,支持的淘汰策略如下:
volatile-lru
设置了过期时间的中最近最少访问 Least Recently Usedallkeys-lru
最近最少访问volatile-lfu
设置了过期时间中最不常用的 Least Frequently Usedallkeys-lfu
最不常用的volatile-random
设置了过期时间中随机allkeys-random
随机volatile-ttl
根据过期时间删除noeviction
不进行淘汰,无法再进行写操作
通过maxmemory-policy
参数指定 默认为noeviction
7 主从复制
在Redis中,我们可以通过执行slaveof
命令,让一个服务器去复制另一个服务器,我们称被复制的服务器为主服务器,复制操作的服务器称为从服务器,从服务器只能进行读操作,示例如下
replicaof/slaveof ip port
使其成为某个服务的从节点
replicaof/slaveof no one
使其脱离节点
info replicaton
查看集群信息
7.1 实现原理
- 当一个服务器称为某个服务的从服务后,首先需要同步主服务上的数据,这步是全量复制。主节点通过bgsave命令在本地生成一份RDB文件,并将该文件发送给从节点,从节点首先清除自己的旧数据,然后使用该RDB文件加载数据
- 之后在主节点的更新操作会通过命令传播的方式发送给从服务器
- 从服务器宕机后重新连接主节点,此时无需执行全量复制,在从服务器中会存储一个
master_repl_offset
的偏移量数据,只需同步之后的操作即可
8 Sentinel 哨兵
Sentinel是Redis的高可用解决方案