概述
Redis 是一个开源(BSD许可)的,内存中的数据结构存储系统,它可以用作 数据库、缓存和消息中间件。
它支持多种类型的数据结构,如 字符串(strings)
, 散列(hashes)
, 列表(lists)
, 集合(sets)
, 有序集合(sorted sets)
与范围查询, bitmaps, hyperloglogs 和 地理空间(geospatial) 索引半径查询。
Redis 内置了 复制(replication),LUA脚本(Lua scripting), LRU驱动事件(LRU eviction),事务(transactions) 和不同级别的 磁盘持久化(persistence), 并通过 Redis哨兵(Sentinel)和自动 分区(Cluster)提供高可用性(high availability)。
String
类型详解
基本的数据类型,字符串
127.0.0.1:6379> set key1 v1 # 设置值
OK
127.0.0.1:6379> get key1 # 获取值
"v1"
127.0.0.1:6379> keys * # 获取所有的 key
1) "key1"
2) "age"
127.0.0.1:6379> EXISTS key1 # 判断一个 key 是否存在
(integer) 1
127.0.0.1:6379> APPEND key1 "hello" # 追加字符串,如果当前 key 不存在,就相当于 set key
(integer) 7
127.0.0.1:6379> get key1
"v1hello"
127.0.0.1:6379> APPEND key1 "zzzzkkkk"
(integer) 15
127.0.0.1:6379> STRLEN key1 # 获取字符串的长度
(integer) 15
127.0.0.1:6379> get key1
"v1hellozzzzkkkk"
# 设置值
127.0.0.1:6379> set views 0
OK
127.0.0.1:6379> get views
"0"
127.0.0.1:6379> incr views # 自加 1
(integer) 1
127.0.0.1:6379> get views
"1"
127.0.0.1:6379> incr views
(integer) 2
127.0.0.1:6379> get views
"2"
127.0.0.1:6379> decr views # 自减 1
(integer) 1
127.0.0.1:6379> get views
"1"
127.0.0.1:6379> incr views
(integer) 2
127.0.0.1:6379> get views
"2"
127.0.0.1:6379> INCRBY views 10 # 可以设置增加的步长
(integer) 12
127.0.0.1:6379> INCRBY views 10
(integer) 22
127.0.0.1:6379> DECRBY views 5 # 设置减少的步长
(integer) 17
127.0.0.1:6379> get views
"17"
# 字符串范围 range
127.0.0.1:6379> flushdb
OK
127.0.0.1:6379> keys *
(empty list or set)
127.0.0.1:6379> set key1 "Hello World"
OK
127.0.0.1:6379> get key1
"Hello World"
127.0.0.1:6379> GETRANGE key1 0 3 # 截取字符串 [0, 3]
"Hell"
127.0.0.1:6379> GETRANGE key1 0 -1 # 获取全部字符串 == get
"Hello World"
# 替换
127.0.0.1:6379> set key2 ahuoffdewe
OK
127.0.0.1:6379> get key2
"ahuoffdewe"
127.0.0.1:6379> SETRANGE key2 1 xx # 替换指定位置开始的字符串
(integer) 10
127.0.0.1:6379> get key2
"axxoffdewe"
# setex (set with expire) # 设置过期时间
# setnx (set with exist) # 不存在设置(在分布式锁中常常使用)
127.0.0.1:6379> setex key3 60 "hello" # 设置 key3 ,60s后过期
OK
127.0.0.1:6379> ttl key3
(integer) 55
127.0.0.1:6379> get key3
"hello"
127.0.0.1:6379> setnx mykey "redis" # mykey 如果不存在,则设置
(integer) 1
127.0.0.1:6379> keys *
1) "key3"
2) "key1"
3) "mykey"
4) "key2"
127.0.0.1:6379> keys *
1) "key1"
2) "mykey"
3) "key2"
127.0.0.1:6379> ttl key3
(integer) -2
127.0.0.1:6379> setnx mykey "MongoDB" # 修改失败,返回值 0
(integer) 0
127.0.0.1:6379> get mykey
"redis"
# 批量设置
127.0.0.1:6379> flushdb
OK
127.0.0.1:6379> mset k1 v1 k2 v2 k3 v3 # 同时设置多个值
OK
127.0.0.1:6379> keys *
1) "k3"
2) "k2"
3) "k1"
127.0.0.1:6379> mget k1 k2 k3 # 同时获取多个值
1) "v1"
2) "v2"
3) "v3"
127.0.0.1:6379> msetnx k1 v1 k4 v4 # msetnx 是一个原子性的操作,要么一起失败,要么一起成功
(integer) 0
127.0.0.1:6379> keys *
1) "k3"
2) "k2"
3) "k1"
# 对象
set user:1:{name:zhangsan, age:3} # 设置一个 user:1 对象 值为 json 字符来保存一个对象
# user:{id}:{field} 是可以的
127.0.0.1:6379> mset user:1:name zhangsan user:1:age 2
OK
127.0.0.1:6379> mget user:1:name user:1:age
1) "zhangsan"
2) "2"
# getset 先 get 再 set
127.0.0.1:6379> getset db redis # 如果不存在值,就返回 nil
(nil)
127.0.0.1:6379> get db
"redis"
127.0.0.1:6379> getset db mongodb # 如果存在值,获取原来的值,并设置新的值
"redis"
127.0.0.1:6379> get db
"mongodb"
String 数据类型的底层实现
Redis 中所有场景中出现的字符串,基本都是由 SDS (动态字符串)来实现的;
SDS 是 “simple dynamic string” 的缩写。
SDS 的源码
// 3.0及以前
struct sdshdr {
// 记录buf数组中已使用字节数量
unsigned int len;
// 记录buf数组中未使用的字节数量
unsigned int free;
// 字节数组,存储字符串
char buf[];
};
// >=3.2
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; /* used */
uint8_t alloc; /* excluding the header and null terminator */
unsigned char flags; /* 3 lsb of type, 5 unused bits */
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[];
};
//在3.2以后的版本,redis 的SDS分为了5种数据结构,分别应对不同长度的字符串需求,具体的类型选择如下。
static inline char sdsReqType(size_t string_size) { // 获取类型
if (string_size < 1<<5) // 32
return SDS_TYPE_5;
if (string_size < 1<<8) // 256
return SDS_TYPE_8;
if (string_size < 1<<16) // 65536 64k
return SDS_TYPE_16;
if (string_size < 1ll<<32) // 4294967296 4GB
return SDS_TYPE_32;
return SDS_TYPE_64;
}
SDS 有两种存储形式:一种是 raw,一种是 embstr
所有的 Redis 对象都有一个 Redis 对象头结构体
struct RedisObject { // 一共占用16字节
int4 type; // 4bits 类型
int4 encoding; // 4bits 存储格式
int24 lru; // 24bits 记录LRU信息
int32 refcount; // 4bytes
void *ptr; // 8bytes,64-bit system
} robj;
raw 在分配内存时,RedisObject 和 SDS 各分配一块内存,需要两次分配内存;两个对象头在内存地址一般是不连续的。
而 embstr 是 RedisObject 和 SDS 同处于一块内存,分配内存一次就分配就实现了; RedisObject 对象头和 SDS 对象头是连续存在一起的
-
空间预分配
为减少修改字符串带来的内存重分配次数,SDS 采用了“一次管够”的策略;
若修改之后,SDS 长度小于 1MB,则多分配现有 len 长度的空间;
若修改之后,SDS 长度大于等于 1MB,则扩充除了满足修改之后的长度外,额外多 1MB 空间 -
惰性空间释放
为避免缩短字符串时候的内存重分配操作,SDS 在数据减少时,并不立刻释放空间。
List
类型详解
基本的数据类型,列表
在 Redis 中,可以把 list 当作栈、队列、阻塞队列使用
Redis 的命令不区分大小写
127.0.0.1:6379> flushdb
OK
127.0.0.1:6379> keys *
(empty list or set)
127.0.0.1:6379> LPUSH list one # 将一个值或者多个值插入到 列表的头部(左)
(integer) 1
127.0.0.1:6379> LPUSH list two
(integer) 2
127.0.0.1:6379> LPUSH list three
(integer) 3
127.0.0.1:6379> LRANGE list 0 -1 # 获取 list 中的值
1) "three"
2) "two"
3) "one"
127.0.0.1:6379> LRANGE list 0 1 # 获取值
1) "three"
2) "two"
127.0.0.1:6379> RPUSH list four # 将一个值或者多个值插入到 列表的头部(右)
(integer) 4
127.0.0.1:6379> LRANGE list 0 -1
1) "three"
2) "two"
3) "one"
4) "four"
127.0.0.1:6379> LRANGE list 0 -1
1) "three"
2) "two"
3) "one"
4) "four"
127.0.0.1:6379> Lpop list # 移除第一个元素
"three"
127.0.0.1:6379> Rpop list # 移除最后一个元素
"four"
127.0.0.1:6379> LRANGE list 0 -1
1) "two"
2) "one"
127.0.0.1:6379> lindex list 1 # 通过下标获取值
"one"
127.0.0.1:6379> lindex list 0
"two"
127.0.0.1:6379> flushdb
OK
127.0.0.1:6379> Lpush list one
(integer) 1
127.0.0.1:6379> Lpush list two
(integer) 2
127.0.0.1:6379> Lpush list three
(integer) 3
127.0.0.1:6379> Lpush list four
(integer) 4
127.0.0.1:6379> llen list # 获取 list 长度
(integer) 4
127.0.0.1:6379> lrem list 1 one # 移除 list 集合中指定个数的 value,精确匹配
(integer) 1
127.0.0.1:6379> LRANGE list 0 -1
1) "four"
2) "three"
3) "two"
127.0.0.1:6379> Lpush list three
(integer) 4
127.0.0.1:6379> Lpush list three
(integer) 5
127.0.0.1:6379> LRANGE list 0 -1
1) "three"
2) "three"
3) "four"
4) "three"
5) "two"
127.0.0.1:6379> lrem list 2 three
(integer) 2
127.0.0.1:6379> LRANGE list 0 -1
1) "four"
2) "three"
3) "two"
127.0.0.1:6379> flushdb
OK
127.0.0.1:6379> Rpush mylist "hello"
(integer) 1
127.0.0.1:6379> Rpush mylist "hello1"
(integer) 2
127.0.0.1:6379> Rpush mylist "hello12"
(integer) 3
127.0.0.1:6379> Rpush mylist "hello123"
(integer) 4
127.0.0.1:6379> ltrim mylist 1 2 # 通过下标截取指定的长度,这个list 已经被改变了,只剩下截取后的不放呢
OK
127.0.0.1:6379> lrange mylist 0 -1
1) "hello1"
2) "hello12"
127.0.0.1:6379> flushdb
OK
127.0.0.1:6379> rpush mylist "hello"
(integer) 1
127.0.0.1:6379> rpush mylist "hello1"
(integer) 2
127.0.0.1:6379> rpush mylist "hello12"
(integer) 3
127.0.0.1:6379> rpush mylist "hello123"
(integer) 4
127.0.0.1:6379> rpoplpush mylist myother # 移除列表的最后一个元素,并移动到新的列表
"hello123"
127.0.0.1:6379> lrange mylist 0 -1 # 查看原来列表
1) "hello"
2) "hello1"
3) "hello12"
127.0.0.1:6379> lrange myother 0 -1 # 查看新的列表
1) "hello123"
127.0.0.1:6379> exists list # 判断list 是否存在
(integer) 0
127.0.0.1:6379> lset list 0 item # 如果不存在列表,则会报错
(error) ERR no such key
127.0.0.1:6379> lpush list value1
(integer) 1
127.0.0.1:6379> lrange list 0 0
1) "value1"
127.0.0.1:6379> lset list 0 item # 如果存在,更新当前下标的值
OK
127.0.0.1:6379> lrange list 0 0 # 如果下标不存在,则会报错
1) "item"
# insert 将某个具体的 value 插入到列表中 某个元素的前面或者后面
127.0.0.1:6379> flushdb
OK
127.0.0.1:6379> rpush mylist "hello"
(integer) 1
127.0.0.1:6379> rpush mylist zzz
(integer) 2
127.0.0.1:6379> linsert mylist before "zzz" "other"
(integer) 3
127.0.0.1:6379> lrange mylist 0 -1
1) "hello"
2) "other"
3) "zzz"
127.0.0.1:6379> linsert mylist after "zzz" "new"
(integer) 4
127.0.0.1:6379> lrange mylist 0 -1
1) "hello"
2) "other"
3) "zzz"
4) "new"
List 的底层实现
在 Redis 3.2 版本之前,List 使用 ziplist 或者 linkedlist 实现,如下图1
但是,在之后的版本中,List 使用 quicklist 实现
-
ziplist
压缩列表,适合用于长度较小的值;由连续空间组成(会保存每个值得长度信息,可以依次找到各个值)
存取效率高,内存占用小,但是由于是连续内存,修改操作需要重新分配内存
-
linkedlist
双向链表,修改效率高,但是由于需要保存前后指针,占用内存比较多 -
quicklist
可以看作是一种混合结构,quicklist
是ziplist
与linkedlist
的混合体将
linkedlist
按段切分,每一段使用ziplist
来紧凑存储,多个ziplist
之间使用双向指针串连起来既满足了快速的插入删除功能,又不会出现太大的空间冗余
Set
类型详解
set
是一个 无序不重复 集合,set
中的值不能重复
127.0.0.1:6379> flushdb
OK
127.0.0.1:6379> sadd myset "hello" # 往 set 中添加元素
(integer) 1
127.0.0.1:6379> sadd myset "zzz"
(integer) 1
127.0.0.1:6379> sadd myset "love"
(integer) 1
127.0.0.1:6379> smembers myset # 查看指定 set 的所有值
1) "love"
2) "zzz"
3) "hello"
127.0.0.1:6379> sismember myset hello # 判断某一个值是否在 set 集合中,存在
(integer) 1
127.0.0.1:6379> sismember myset hello1 # 不存在
(integer) 0
127.0.0.1:6379> scard myset # 查看set 集合中元素的个数
(integer) 3
127.0.0.1:6379> srem myset hello # 移除 set 集合中的指定元素
(integer) 1
127.0.0.1:6379> scard myset
(integer) 2
127.0.0.1:6379> smembers myset
1) "love"
2) "zzz"
127.0.0.1:6379> sadd myset "hello"
(integer) 1
127.0.0.1:6379> sadd myset "world"
(integer) 1
127.0.0.1:6379> sadd myset "earth"
(integer) 1
127.0.0.1:6379> smembers myset
1) "love"
2) "zzz"
3) "world"
4) "hello"
5) "earth"
127.0.0.1:6379> srandmember myset # 随机抽取一个元素
"world"
127.0.0.1:6379> srandmember myset
"hello"
127.0.0.1:6379> srandmember myset 2 # 随机抽取指定个数的元素
1) "zzz"
2) "hello"
127.0.0.1:6379> srandmember myset 2
1) "zzz"
2) "hello"
127.0.0.1:6379> srandmember myset 2
1) "world"
2) "earth"
127.0.0.1:6379> smembers myset
1) "hello"
2) "zzz"
3) "love"
4) "world"
5) "earth"
127.0.0.1:6379> spop myset # 随机删除一些 set 集合中的元素
"hello"
127.0.0.1:6379> spop myset
"earth"
127.0.0.1:6379> smembers myset
1) "zzz"
2) "love"
3) "world"
127.0.0.1:6379> flushdb
OK
127.0.0.1:6379> sadd myset "hello"
(integer) 1
127.0.0.1:6379> sadd myset "world"
(integer) 1
127.0.0.1:6379> sadd myset "earth"
(integer) 1
127.0.0.1:6379> sadd myset2 "water"
(integer) 1
127.0.0.1:6379> smove myset myset2 "earth" # 将一个指定的值,移动到另外一个 set 集合中
(integer) 1
127.0.0.1:6379> smembers myset
1) "world"
2) "hello"
127.0.0.1:6379> smembers myset2
1) "water"
2) "earth"
127.0.0.1:6379> smembers myset
1) "world"
2) "hello"
127.0.0.1:6379> smembers myset2
1) "water"
2) "earth"
127.0.0.1:6379> sadd myset "earth"
(integer) 1
127.0.0.1:6379> sdiff myset myset2 # 差集
1) "world"
2) "hello"
127.0.0.1:6379> sinter myset myset2 # 交集
1) "earth"
127.0.0.1:6379> sunion myset myset2 # 并集
1) "water"
2) "world"
3) "earth"
4) "hello"
Set 的底层实现
Set
的底层使用 intset
或者 hashtable
实现
当集合中的数据都是整数时,且数量不超过 512 个,使用 intset
方式;
intset
是有序不重复的连续空间,可以节约内存,但是也由于是连续的空间存储,修改效率不高
当 Set
使用 hashtable
实现:其中 value
使用 NULL
填充
哈希表的制作方法一般有两种,一种是开放寻址法,一种是拉链法。Redis 的哈希表的制作使用的是拉链法。
Hash
类型详解
可以等同于一个 Map 集合,还是以 key-value 的形式存在,key-map 此时的值是一个集合
127.0.0.1:6379> hset myhash field1 zzz # set 一个具体的 key-value
(integer) 1
127.0.0.1:6379> hget myhash field1 # 获取一个字段值
"zzz"
127.0.0.1:6379> hmset myhash field2 hello field3 world # set 多个 key-value
OK
127.0.0.1:6379> hmget myhash field1 field2 field3 # 获取多个字段值
1) "zzz"
2) "hello"
3) "world"
127.0.0.1:6379> hgetall myhash # 获取全部数据
1) "field1"
2) "zzz"
3) "field2"
4) "hello"
5) "field3"
6) "world"
127.0.0.1:6379> hdel myhash field1 # 删除 hash 指定的 key 字段,对应的 value 也就消失
(integer) 1
127.0.0.1:6379> hgetall myhash
1) "field2"
2) "hello"
3) "field3"
4) "world"
127.0.0.1:6379> hgetall myhash
1) "field1"
2) "hello"
3) "field2"
4) "world"
5) "field3"
6) "hello"
7) "field4"
8) "earth"
127.0.0.1:6379> hlen myhash # 获取hash 表的字段数量
(integer) 4
127.0.0.1:6379> hexists myhash field2 # 判断 hash 中指定字段是否存在
(integer) 1
127.0.0.1:6379> hexists myhash field5 # 不存在
(integer) 0
127.0.0.1:6379> hkeys myhash # 只获得所有的 field
1) "field1"
2) "field2"
3) "field3"
4) "field4"
127.0.0.1:6379> hvals myhash # 只获得所有的 value
1) "hello"
2) "world"
3) "hello"
4) "earth"
127.0.0.1:6379> hset myhash field5 5 # 指定增量
(integer) 1
127.0.0.1:6379> hincrby myhash field5 1 # 加 1
(integer) 6
127.0.0.1:6379> hget myhash field5
"6"
127.0.0.1:6379> hincrby myhash field5 -1 # 减 1
(integer) 5
127.0.0.1:6379> hget myhash field5
"5"
127.0.0.1:6379> hsetnx myhash field6 lll # 如果不存在则设置
(integer) 1
127.0.0.1:6379> hsetnx myhash field6 ooo # 如果存在,则不可以设置
(integer) 0
hash
的应用:
- 存储变更的数据,尤其是用户的信息等经常变动的信息
hash
更适合对象的存储,而string
更适合字符串的存储
hash 的底层实现
Zset
类型详解
Zset
是一个有序集合
127.0.0.1:6379> zadd myset 1 one
(integer) 1
127.0.0.1:6379> zadd myset 2 two 3 three
(integer) 2
127.0.0.1:6379> zrange myset 0 -1
1) "one"
2) "two"
3) "three"
127.0.0.1:6379> zadd salary 2500 xiaoh # 添加用户
(integer) 1
127.0.0.1:6379> zadd salary 3000 zhangss
(integer) 1
127.0.0.1:6379> zadd salary 200 kuahshen
(integer) 1
127.0.0.1:6379> zrangebyscore salary -inf +inf # 显示所有用户 从小到大
1) "kuahshen"
2) "xiaoh"
3) "zhangss"
127.0.0.1:6379> zrangebyscore salary -inf +inf withscores # 显示所有用户 从小到大,附带成绩
1) "kuahshen"
2) "200"
3) "xiaoh"
4) "2500"
5) "zhangss"
6) "3000"
127.0.0.1:6379> zrangebyscore salary -inf 2500 withscores # 显示所有用户 从小到大,salary 小于 2500
1) "kuahshen"
2) "200"
3) "xiaoh"
4) "2500"
127.0.0.1:6379> zrevrange salary 0 -1 # 从大到小 排序
1) "zhangss"
2) "kuahshen"
127.0.0.1:6379> zrange salary 0 -1
1) "kuahshen"
2) "xiaoh"
3) "zhangss"
127.0.0.1:6379> zrem salary xiaoh # 移除 有序集合中的指定元素
(integer) 1
127.0.0.1:6379> zrange salary 0 -1
1) "kuahshen"
2) "zhangss"
127.0.0.1:6379> flushdb
OK
127.0.0.1:6379> zadd myset 1 hello
(integer) 1
127.0.0.1:6379> zadd myset 2 world 3 zzz
(integer) 2
127.0.0.1:6379> zcount myset 1 3 # 获取指定区间的成员数量
(integer) 3
127.0.0.1:6379> zcount myset 1 2
(integer) 2
Zset
的底层实现
Zset
的底层存储结构使用 ziplist
(压缩双向链表) 或者 skiplist
(跳表)
如果 Zset
储存的数据中,存储元素数量超过 128 个,每个节点最大存储字节数超过 64 字节,则从 ziplist
转换到 skiplist
对于零散数据,数据量比较少,使用 ziplist
可以节省内存的使用
ziplist
作为 Zset
的底层存储结构时候,每个集合元素使用两个紧挨在一起的压缩列表节点来保存,第一个节点保存元素的成员,第二个元素保存元素的分值
skiplist
作为 Zset
的底层存储结构的时候,使用 skiplist
按序保存元素及分值,使用 dict
来保存元素和分值的映射关系