Redis基础

Redis是非关系型键值对数据库,可根据键以O(1)时间复杂度取出插入关联值,Redis数据是存在内存中,键值对中可以是字符串整型浮点型等且键唯一的类型可以是stringhashlistsetsorted set等。内置了复制、磁盘持久化、LUA脚本、事务、SSL、ACLs、客户端缓存、客户端代理等功能,通过哨兵模式Cluster模式提供高可用。

Redis的速度非常的快,单机Redis就可以支撑每秒10几万的并发,相对于MySQL来说,性能是MySQL的几十倍。

  1. 完全基于内存操作
  2. C语言实现优化过的数据结构,基于几种基础的数据结构,Redis做了大量的优化,性能极高
  3. 使用单线程,无上下文的切换成本
  4. 基于非阻塞的IO多路复用机制

Redis的单线程主要是指Redis的网络IO键值对读写由一个线程来完成的,但Redis的其他功能如持久化异步删除集群数据同步由额外线程执行

Redis单线程之所以这么快,是由于Redis所有数据都在内存中,所有运算都是内存级别的运算,且单线程避免了多线程的切换性能损耗问题。正因为Redis是单线程,故需小心使用Redis指令,对于耗时的指令keys,一定要谨慎使用,否则可能会导致Redis卡顿

Redis单线程之所以能处理很高的并发的客户端链接,其是利用epoll来实现的IO多路复用,将连接信息事件放到队列中,依次放到文件事件分派器事件分派器将事件分发给事件处理器

Redis epoll IO多路复用

虽然6.0后改用多线程,但并非是完全摒弃单线程,redis还是使用单线程模型来处理客户端的请求,只是使用多线程来处理数据的读写和协议解析,因为redis性能瓶颈在于网络IO而非CPU,使用多线程能提升IO读写的效率,从而整体提高redis的性能,执行命令还是使用单线程

常用命令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
SET key value 					# 存入字符串键值对
MSET key value [key value ...] # 批量存储字符串键值对
SETNX key value # 存入一个不存在的字符串键值对
GET key # 获取一个字符串键值
MGET key [key ...] # 批量获取字符串键值
DEL key [key ...] # 删除一个键
EXPIRE key seconds # 设置一个键的过期时间(秒)

# 原子加减
INCR key # 将key中储存的数字值加1
DECR key # 将key中储存的数字值减1
INCRBY key increment # 将key所储存的值加上increment
DECRBY key decrement # 将key所储存的值减去decrement

# 单值缓存
SET key value
GET key

# 对象缓存
SET user:1 value(json格式数据)
MSET user:1:name zhuge user:1:balance 1888
MGET user:1:name user:1:balance
# 分布式锁
SETNX product:10001 true # 返回1代表获取锁成功
SETNX product:10001 true # 返回0代表获取锁失败
DEL product:10001 # 执行完业务释放锁
SET product:10001 true ex 10 nx # 防止程序意外终止导致死锁

# Hash常用操作
HSET key field value # 存储一个哈希表key的键值
HSETNX key field value # 存储一个不存在的哈希表key的键值
HMSET key field value [field value ...] # 在一个哈希表key中存储多个键值对
HGET key field # 获取哈希表key对应的field键值
HMGET key field [field ...] # 批量获取哈希表key中多个field键值
HDEL key field [field ...] # 删除哈希表key中的field键值
HLEN key # 返回哈希表key中field的数量
HGETALL key # 返回哈希表key中所有的键值

HINCRBY key field increment # 为哈希表key中field键的值加上增量increment

# List常用操作
LPUSH key value [value ...] # 将一个或多个值value插入到key列表的表头(最左边)
RPUSH key value [value ...] # 将一个或多个值value插入到key列表的表尾(最右边)
LPOP key # 移除并返回key列表的头元素
RPOP key # 移除并返回key列表的尾元素
LRANGE key start stop # 返回列表key中指定区间内的元素,区间以偏移量start和stop指定
BLPOP key [key ...] timeout # 从key列表表头弹出一个元素,若列表中没有元素,阻塞等待timeout秒,如果timeout=0,一直阻塞等待
BRPOP key [key ...] timeout # 从key列表表尾弹出一个元素,若列表中没有元素,阻塞等待timeout秒,如果timeout=0,一直阻塞等待

# 常用数据结构
Stack(栈) = LPUSH + LPOP
Queue(队列)= LPUSH + RPOP
Blocking MQ(阻塞队列)= LPUSH + BRPOP

# Set常用操作
SADD key member [member ...] # 往集合key中存入元素,元素存在则忽略,若key不存在则新建
SREM key member [member ...] # 从集合key中删除元素
SMEMBERS key # 获取集合key中所有元素
SCARD key # 获取集合key的元素个数
SISMEMBER key member # 判断member元素是否存在于集合key中
SRANDMEMBER key [count] # 从集合key中选出count个元素,元素不从key中删除
SPOP key [count] # 从集合key中选出count个元素,元素从key中删除

# Set运算操作
SINTER key [key ...] # 交集运算
SINTERSTORE destination key [key ..] # 将交集结果存入新集合destination中
SUNION key [key ..] # 并集运算
SUNIONSTORE destination key [key ...] # 将并集结果存入新集合destination中
SDIFF key [key ...] # 差集运算
SDIFFSTORE destination key [key ...] # 将差集结果存入新集合destination中

# ZSet常用操作
ZADD key score member [[score member]…] # 往有序集合key中加入带分值元素
ZREM key member [member …] # 从有序集合key中删除元素
ZSCORE key member # 返回有序集合key中元素member的分值
ZINCRBY key increment member # 为有序集合key中元素member的分值加上increment
ZCARD key # 返回有序集合key中元素个数
ZRANGE key start stop [WITHSCORES] # 正序获取有序集合key从start下标到stop下标的元素
ZREVRANGE key start stop [WITHSCORES] # 倒序获取有序集合key从start下标到stop下标的元素

# Zset集合操作
ZUNIONSTORE destkey numkeys key [key ...] # 并集计算
ZINTERSTORE destkey numkeys key [key …] # 交集计算

应用场景

计数器:可对String进行自增自减运算从而实现计数器功能,这种内存型数据库读写性能非常高,很适合存储频繁读写的计数量

分布式ID生成:利用自增特性,一次请求一个大一点的步长incr 2000,缓存在本地使用,用完再请求

海量数据统计:通过位图bitmap存储是否参过某次活动,是否已读谋篇文章,用户是否为会员,日活统计

Session共享:可统一存储多台应用服务器会话信息,一个用户可请求任意一个应用服务器,从而更容易实现高可用性以及可伸缩性

分布式队列、阻塞队列:List双向链表可通过lpush/rpushrpop/lpop写入和读取消息,可通过使用brpop/blpop来实现阻塞队列

分布式锁实现:使用Redis自带的SETNX命令实现分布式锁

热点数据存储:最新评论,最新文章列表,使用list存储,ltrim取出热点数据,删除老数据

社交类需求:可通过Set交集实现共同好友等功能,可通过Set求差集进行好友推荐、文章推荐

排行榜sorted_set可实现有序性操作,从而实现排行榜等功能

延迟队列:通过sorted_set使用当前时间戳 + 需要延迟的时长做score,消息内容作为元素,调用zadd来生产消息,消费者使用zrangbyscore获取当前时间之前的数据做轮询处理。消费完再删除任务rem key member

通过Hash实现电商购物车
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 电商购物车
# 以用户id为key
# 商品id为field
# 商品数量为value

# 购物车操作
# 添加商品
hset cart:1001 10088 1
# 增加数量
hincrby cart:1001 10088 1
# 商品总数
hlen cart:1001
# 删除商品
hdel cart:1001 10088
# 获取购物车所有商品
hgetall cart:1001

通过Set实现抽奖
1
2
3
4
5
6
7
#微信抽奖小程序
# 点击参与抽奖加入集合
SADD key {userlD}
# 查看参与抽奖所有用户
SMEMBERS key
# 抽取count名中奖者
SRANDMEMBER key [count] / SPOP key [count]

Set实现微博点赞、收藏、标签
1
2
3
4
5
6
7
8
9
10
# 点赞
SADD like:{消息ID} {用户ID}
# 取消点赞
SREM like:{消息ID} {用户ID}
# 检查用户是否点过赞
SISMEMBER like:{消息ID} {用户ID}
# 获取点赞的用户列表
SMEMBERS like:{消息ID}
# 获取点赞用户数
SCARD like:{消息ID}
Set实现微博关注模型
1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 集合操作实现微博微信关注模型
# ccc关注的人
cccSet-> {aaa, bbb}
# fff关注的人
fffSet--> {ccc, ddd, aaa, bbb}
# aaa关注的人
aaaSet-> {ccc, fff, ddd, bbb, eee}
# ccc和fff共同关注
SINTER cccSet fffSet--> {aaa, bbb}
# aaa关注的人也关注bbb
SISMEMBER aaaSet fff
SISMEMBER bbbSet fff
# ccc可能认识的人:
SDIFF fffSet cccSet->{ccc, ddd}

Zset实现排行榜
1
2
3
4
5
6
7
8
9
# 点击新闻
ZINCRBY hotNews:20190819 1 守护香港
# 展示当日排行前十
ZREVRANGE hotNews:20190819 0 9 WITHSCORES
# 七日搜索榜单计算
ZUNIONSTORE hotNews:20190813-20190819 7
hotNews:20190813 hotNews:20190814... hotNews:20190819
# 展示七日排行前十
ZREVRANGE hotNews:20190813-20190819 0 9 WITHSCORES

Redis安装

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
# 安装gcc
yum install gcc

# 把下载好的redis-5.0.3.tar.gz放在/usr/local文件夹下,并解压
wget http://download.redis.io/releases/redis-5.0.3.tar.gz
tar xzf redis-5.0.3.tar.gz
cd redis-5.0.3

# 进入到解压好的redis-5.0.3目录下,进行编译与安装
make

# 修改配置
daemonize yes # 后台启动
protected-mode no # 关闭保护模式,若开启只有本机才可访问redis
# bind 127.0.0.1 绑定机器网卡ip,若有多块网卡可配多个ip,代表允许客户端通过机器哪些网卡ip去访问,内网一般可不配置bind,注释掉即可

# 启动服务
src/redis-server redis.conf

# 验证启动是否成功
ps -ef | grep redis

# 进入redis客户端
src/redis-cli

# 退出客户端
quit

# 退出redis服务
pkill redis-server
kill 进程号
src/redis-cli shutdown

# 查看redis支持的最大连接数,在redis.conf文件中可修改,默认maxclients 10000
CONFIG GET maxclients

Redis基本数据类型

Redis是基于数组链表来存储海量数据的,通过对key进行hash取模定位到数组下标,产生hash冲突则通过链表来存储冲突的数据,Redis有16个DB。

1
2
3
4
5
6
7
8
9
10
11
typedef struct redisDb {
dict *dict; /* The keyspace for this DB 存储K-V数据*/
dict *expires; /* Timeout of keys with a timeout set 过期时间字典 */
dict *blocking_keys; /* Keys with clients waiting for data (BLPOP) 阻塞队列的处理*/
dict *ready_keys; /* Blocked keys that received a PUSH key跟客户端链接之间的关系*/
dict *watched_keys; /* WATCHED keys for MULTI/EXEC CAS */
int id; /* Database ID */
long long avg_ttl; /* Average TTL, just for stats */
unsigned long expires_cursor; /* Cursor of the active expire cycle. */
list *defrag_later; /* List of key names to attempt to defrag one by one, gradually. */
} redisDb;

Redis是非关系型键值对数据库,可根据键以O(1)的时间复杂度取出或插入关联值数据是基于内存的,键值对中键的类型可以是字符串整型浮点型等且键唯一,值的类型可以是stringhashlistsetsorted set等。

用于保存键值对的抽象数据结构,redis使用hash表作为底层实现,每个字典带有两个hash,供平时使用rehash时使用,hash表使用链地址法解决键冲突,被分配到同一个索引位置的多个键值对会形成一个单向链表,在对hash表进行扩容或者缩容的时候,为了服务的可用性,rehash的过程不是一次性完成的而是渐进式的

每个dict字典有两个dictht,是为了通过渐进式rehash提升扩容时的性能,若发生扩容会创建新的dictht且将旧的dictht数据向新的dictht拷贝,最终释放掉就的dictht,但数据量非常大的情况会非常耗时,而redis执行命令是单线程的,会导致整个性能降低,故redis使用了渐进式的rehash,即扩容是会创建新的dictht,但不会一次全部将旧dictht的数据搬运到dictht上,而是访问到某个key时才选择一部分搬运,当没有访问时也会通过事件轮询来搬数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
typedef struct dict { // redis中的hashtable
dictType *type; // 类型
void *privdata;
dictht ht[2];// ht[0] , ht[1] =null
long rehashidx; /* rehashing not in progress if rehashidx == -1 */
unsigned long iterators; /* number of iterators currently running */
} dict;
/* 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 { // hashtable数据结构,每个字典都有两个dictht,用于在旧表到新表时通过渐进式的rehash
dictEntry **table;
unsigned long size; // hashtable 容量
unsigned long sizemask; // size -1
unsigned long used; // hashtable 元素个数 used / size =1
} dictht;
typedef struct dictType {
uint64_t (*hashFunction)(const void *key); // 使用的hash函数封装在hashFunction
void *(*keyDup)(void *privdata, const void *key);
void *(*valDup)(void *privdata, const void *obj);
int (*keyCompare)(void *privdata, const void *key1, const void *key2); // keyCompare类似equals方法
void (*keyDestructor)(void *privdata, void *key);
void (*valDestructor)(void *privdata, void *obj);
} dictType;
typedef struct dictEntry {
void *key; // string对象其实就是sds
union { // 用于存储值
void *val; // 可能是string、list、hash等
uint64_t u64; // 占8 byte
int64_t s64; // 占8 byte
double d; // 占8 byte
} v;
struct dictEntry *next; // 链表,用于解决hash冲突
} dictEntry;
// redisObject对象: string, list, set, hash, zset
typedef struct redisObject {// 总共占用16个byte,缓存行为64byte,
unsigned type:4; // 4 bit: 表示数据类型:string, list, set, hash, zset
unsigned encoding:4; // 4 bit, 内存底层类型:int、row、embstr、quicklist、hashtable、ziplist、intset、skiplist
unsigned lru:LRU_BITS; /* LRU time (relative to global lru_clock) or
* LFU data (least significant 8 bits frequency
* and most significant 16 bits access time). 24 bit
*/
int refcount; // 4 byte 用于引用计数法来管理内存
void *ptr; // 8 byte 总空间: 4 bit + 4 bit + 24 bit + 4 byte + 8 byte = 16 byte
} robj;

对外提供的数据结构

基于基础的数据结构,Redis封装了自己的对象系统,包含字符串对象string列表对象list哈希对象hash集合对象set有序集合对象zset,每种对象都用到了至少一种基础的数据结构Redis通过encoding属性设置对象的编码形式提升灵活性和效率,基于不同的场景Redis会自动做出优化。不同对象的编码如下:

  1. 字符串对象stringint整数、embstr编码的简单动态字符串、raw简单动态字符串
  2. 列表对象list:ziplistquicklist
  3. 哈希对象hashziplisthashtable
  4. 集合对象setintsethashtable
  5. 有序集合对象zsetziplistskiplist

字符串对象string

可用于单值缓存、对象缓存、分布式锁的实现、计数器、Web集群Session共享、分布式系统全局序列号生成可批量生成提升性能。若大量数据储存成string可能导致频繁rehash

Redis没有直接使用C语言传统的字符串表示,而是自己实现的叫做简单动态字符串SDS的抽象类型。C语言的字符串不记录自身的长度信息,而SDS保存了长度信息,这样将获取字符串长度的时间由O(N)降低到了O(1),同时可避免缓冲区溢出减少修改字符串长度时所需的内存重分配次数

Redis并没有直接使用C语言char数组作为字符串的实现,而是自定义了一个simple dynamic stringSDS,且C语言作为字符串结束是通过\0作为字符串结束,在redis中可能会导致数据问题。

二进制安全的数据结构即自定义SDS防止\0导致数据丢失,提供了内存预分配机制避免频繁内存分配,在字符串末尾加上\0兼容C语言的函数库

1
2
3
4
5
6
7
8
9
# 并不是每次都成倍扩容,当达到1M时不会再成倍扩容,而是每次加1M
sds:
free: 0 # 剩余容量
len: 4 # 最大长度
char buf[]="test" # 当需要append为test123时,使用(4 + 3) * 2 = 14
sds: # 适用于append、setbit命令
free: 7 # 剩余容量,认为修改可能还会再次被修改,且只扩大不缩小
len: 14 # 最大长度
char buf[]="test123" -> test123

这里Key长度为8即其本身占用8byte+\01byte,这里key明显使用的是sdshdr5,其数据结构本身额外占1byte,值是使用的sdshdr8,数据结构本身占3byte,数据长度为4即占5byteredisObject数据结构本身占16bytedictEntry 数据结构本身占24byte,故总共占8 + 1 + 1 + 3 + 5 + 16 + 24 = 58

1
2
3
4
5
set sds_test aaaa
memory usage sds_test # 58,实际数据长度为4 + \0即5个byte
debug object sds_test
append sds_test b # (1 + 5) * 2 = 12 增长了8
memory usage sds_test # 66 = 58 + 8

String在Redis中使用得非常多,redis 3.2以前由于int4个字节可表示0到2^32 -1即42亿多数据,通常可能不用到这么大,在redis 3.2之后lenfree进行了优化,增加了很多数据类。对于不同的范围使用不同的数据结构进行描述。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
// redis 3.2以前,由于int是4个字节可表示0到2^32 -1即42亿多数据,通常可能不用到这么大
struct sdshdr {
int len;
int free;
char buf[];
};
// redis 3.2之后,增加了很多数据类,len表示长度,alloc分配的空间,
typedef char *sds;
struct __attribute__ ((__packed__)) sdshdr5 { // 0 - 2 ^ 5 - 1
unsigned char flags; /* 3 lsb of type, and 5 msb of string length */
char buf[];// buf[0]: z: 0101001
};
struct __attribute__ ((__packed__)) sdshdr8 { // 1个字节长度
uint8_t len; /* used 1byte*/
uint8_t alloc; /* excluding the header and null terminator 1byte*/
unsigned char flags; /* 3 lsb of type, 5 unused bits 1byte*/
char buf[]; // 由于会兼容c语言会加上\0占用一个byte
};
struct __attribute__ ((__packed__)) sdshdr16 { // 2个字节长度
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 { // 4个字节长度
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 { // 8个字节长度
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[];
};
#define SDS_TYPE_5 0 // flags前三位表示的值,sdshdr5
#define SDS_TYPE_8 1 // flags前三位表示的值,sdshdr8
#define SDS_TYPE_16 2 // flags前三位表示的值,sdshdr16
#define SDS_TYPE_32 3 // flags前三位表示的值,sdshdr32
#define SDS_TYPE_64 4 // flags前三位表示的值,sdshdr64

String类型在Redis中是非常紧凑的,flags用于表示数据类型,占一个字节8sdshdr5前三位用于表示类型,后五位用于表示长度,这也是sdshdr5的由来。对于剩余的sdshdr8sdshdr16sdshdr32sdshdr64等类型,flags前三位依然表示数据类型5位闲置未使用

sdshsr5

sdshsr8

sdshsr16

若是encoding编码为int,则*ptr直接用于存储数据,对于embstr编码类型,由于操作系统缓存行为64byte,而redisObject对象占16byte,要利用缓存行的优化,还剩余48byte,对于sdshdr8类型的数据,结构占用3字节为了兼容\0故总共占4字节,还剩余44个字节可用,故若字符长度小于等于44byte的数据,可利用操作系统缓存行特性,一次IO就获取到数据。长度超过44则编码类型为row

1
2
3
4
5
6
7
8
set intstr 55
type intstr
object encoding intstr

set emb_str aaaa
set emb_str aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
set emb_str aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaab
object encoding emb_str

列表对象list

list是一个按加入的时间顺序排序的有序数据结构,Redis采用双端链表quicklistziplist作为list的底层实现。可以通过设置每个ziplist最大容量quicklist数据压缩范围提升数据存取效率。

1
2
list-max-ziplist-size -2 # 单个ziplist节点最大能存储8kb,超过则进行分裂将数据存储在新的ziplist
list-compress-depth 1 # 0表示所有节点都不压缩,1表示头结点和尾节点不压缩其他节点压缩
压缩列表ziplist

压缩列表是为节约内存而开发的顺序性数据结构,可包含多个节点,每个节点可保存一个字节数组整数值zlbytes能存储元素个数zltail尾节点索引位置即在ziplist中的便宜字节数,可以很方便找到最后一项数据,从而在ziplist尾端快速地执行push或pop操作,zllen当前ziplist有多少个元素zlend标识ziplist最优一个字节即数据的结尾恒等于255prerawlen前一个元素的数据长度,len表示entry中数据的长度,data即真实数据存储。

quicklist

是一个双向无环链表结构,很多发布订阅慢查询监视器功能都是使用到了链表来实现,每个链表的节点由一个listNode结构来表示,每个节点都有指向前置节点后置节点的指针,表头节点的前置和后置节点都指向NULL

可通过LPUSH + LPOP实现StackLPUSH + RPOP实现Queue队列LPUSH + BRPOP实现阻塞队列,可应用于类似微博、微信公众号消息流

哈希对象hash

hash数据结构底层实现为一个字典数据量较小时底层使用ziplist存储数据量较大时将改为HashTable编码存储同类数据归类整合储存方便数据管理,相比string操作消耗内存与CPU更小更节省空间,但过期功能不能使用在field只能用在key上,集群架构下不适合大规模使用。可应用于如电商购物车,用户id为key,商品id为field,商品数量为value。

1
2
hash-max-ziplist-entries 512 # 元素个数超过512,将改为HashTable编码
hash-max-ziplist-value 64 # 单个元素大小超过64byte,将改为HashTable编码

可将同类型数据整合存储,方便数据管理,相比string操作消耗内存和CPU更小,但过期功能不能使用在field上,只能用在key上,集群架构下不适合大规模使用

1
2
3
4
hset hash_test field1 value1
hset hash_test field2 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
hset hash_test field2 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaab
object encoding hash_test

集合对象set

set是无序的自动去重的集合数据结构,底层实现为一个valuenull的字典,当数据可用整型表示时,set集合将被编码为intset数据结构,当数据无法用整型表示时或元素个数大于set-max-intset-entries配置值时将使用HashTable存储数据

1
set-max-intset-entries 512 # 存储元素超过512时,使用HashTable编码
整数集合intset

用于保存整数值的集合抽象数据结构,整数集合是一个有序的整型数据结构,整型集合在Redis中可保存int16_tint32_tint64_t类型的整型数据,底层实现为数组,且可保证集合中不会出现重复数据

1
2
3
4
5
6
7
8
9
typedef struct intset {
uint32_t encoding; // 编码类型
uint32_t length; // 元素个数
int8_t contents[]; // 元素存储
} intset;

#define INTSET_ENC_INT16 (sizeof(int16_t))
#define INTSET_ENC_INT32 (sizeof(int32_t))
#define INTSET_ENC_INT64 (sizeof(int64_t))

1
2
3
4
5
sadd set_test 11 12 13
type set_test
sadd set_test aa
smembers set_test
object encoding set_test

SINTER set1 set2 set3 得到集合{c}SUNION set1 set2 set3得到集合{a,b,c,d,e}SDIFF set1 set2 set3得到集合{a}

可应用于类似抽奖小程序SADD key {uid}加入抽奖集合,SMEMBERS key查看参与抽奖所有用户,SRANDMEMBER key [count]SPOP key [count]抽取count名中奖者。

微信微博点赞、收藏、标签等功能SADD like:{msgID} {userID}点赞,SREM like:{msgID} {userID}取消点赞,SISMEMBER like:{msgID} {userID}检查用户是否点过赞,SMEMBERS like:{msgID}获取点赞的用户列表,SCARD like:{msgID}获取点赞用户数

有序集合对象zset

zset是有序的自动去重的集合数据结构,底层实现为字典 + 跳表skiplist,当数据比较少时ziplist编码结构存储

1
2
zset-max-ziplist-entries 128 # 元素个数超过128,将用skiplist编码
zset-max-ziplist-value 64 # 单个元素大小超过64byte,将用skiplist编码

zset-ziplist

跳跃表skiplist

有序集合的底层实现之一redis中实现有序集合键集群节点的内部结构中都是使用跳跃表。redis跳跃表由zskiplistzskiplistNode组成,zskiplist用于保存跳跃表信息(表头、表尾节点、长度等),zskiplistNode用于表示表跳跃节点,每个跳跃表的层高都是1-32随机数层级越高的层出现概率越低,随机层高大于当前的最大层高,则初始化新的层高,在同一个跳跃表中,多个节点可以包含相同的分值,但是每个节点的成员对象必须是唯一的,节点按照分值大小排序,如果分值相同,则按照成员对象的大小排序

可应用于排行榜,如展示当日排行前十、七日搜索榜单计算、七日排行前十等

1
2
3
4
zadd zset_test 1 member1 2 member2
object encoding zset_test
zadd zset_test 1 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
zadd zset_test 1 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaab

元素插入过程,首先通过zskiplistheader找到索引层level记录最高层高,首先从最高层往下找到具体的层,然后判断当前数据分值是否大于当前层起始分值,若大于继续通过forward指针往下找,若forward指向null,则往下沉一层继续找。

GeoHash

geohash是一种地理位置编码方法,它将地理位置编码为一串简短字母数字是一种分层的空间数据结构,将空间细分为网格形状的桶,是Z顺序曲线的众多应用之一,通常是空间填充曲线。

geohash

通过GeoHash算法,可将经纬度的二维坐标变成一个可排序、可比较的字符串编码,在编码中每个字符代表一个区域,且前面的字符是后面字符的父区域

GeoHash利用Z阶曲线进行编码,可将二维所有点转换成一阶曲线,地理位置坐标点通过编码转换成一维值,利用有序数据结构B树SkipList等均可进行范围搜索,因此利用GeoHash算法查找邻近点比较快。Z阶曲线虽然有局部保序性,但它也有突变性,在每个Z字母的拐角都可能出现顺序的突变。

BitMap

基本思想是用一个bit位来标记某个元素对应的Value,可大大节省存储空间,可用于快速排序快速去重快速查找亿级日活统计等,在数据比较密集时才有优势布隆过滤器就是使用的BitMap。可通过与操作连续多少天活跃统计

1
2
3
4
5
setbit key offset value # 设置bigmap下标位置offset的value,value只能为0或1
getbit key offset # 获取bigmap下标位置offset的value
bitop or after_key key1 key2 # 将key1 和 key2做按位或操作且将结果保存到 after_key中
bitop and after_key key1 key2 # 将key1 和 key2做按位与操作且将结果保存到after_key中
bitcount key # 统计key中位为1的个数

Redis持久化

RDB快照

默认情况下Redis将内存数据库快照保存在名字为dump.rdb的二进制文件中,可对Redis进行设置,让它在N秒内数据集至少有M个改动这一条件被满足时, 自动保存一次数据集快照。如以下设置会让Redis在满足60秒内有至少有1000个键被改动时, 自动保存一次数据集,还可手动执行命令生成RDB快照,执行命令savebgsave可生成dump.rdb文件,每次命令执行都会将所有Redis内存快照到一个新的rdb文件里,并覆盖原有rdb快照文件

1
# save 60 1000    // 关闭RDB只需要将所有的save保存策略注释掉即可

Redis借助操作系统提供的写时复制技术Copy-On-Write, COW,在生成快照同时依然可正常处理写命令bgsave子进程由主线程fork生成,可共享主线程所有内存数据。bgsave子进程运行后,开始读取主线程内存数据,并把它们写入RDB文件。此时若主线程对这些数据也都是读操作,则主线程和bgsave子进程相互不影响。若主线程要修改一块数据,则这块数据就会被复制一份生成该数据的副本。然后bgsave子进程会把这个副本数据写入RDB文件,这个过程中主线程仍可直接修改原来的数据

savebgsave命令的IO类型一个是同步一个是异步,save会阻塞redis其他命令,bgsave不会阻塞,只是在生成子进程执行调用fork函数时会有短暂阻塞;它们时间复杂度都是O(n)save命令不会消耗额外内存bgsave命令需要fork子进程需要消耗额外内存。

AOF

快照功能并不是非常耐久durable,若Redis因某些原因故障停机,服务器将丢失最近写入且仍未保存到快照中的数据。从1.1版本开始,Redis增加了一种完全耐久的持久化方式AOF持久化,将修改的每一条指令记录进appendonly.aof文件中,先写入os cache,每隔一段时间fsync磁盘,每执行增删改命令时就会被aof文件记录。

可通过修改配置文件来打开AOF功能以及设置fsync策略,默认并推荐的措施为每秒fsync一次, 这种fsync策略可兼顾速度和安全性,AOF文件中可能有很多无用指令,故AOF会定期根据内存的最新数据生成aof文件。也可以通过redis客户端执行命令bgrewriteaof手动重写AOF。AOF重写redis会fork出一个子进程去做与bgsave命令类似,不会对redis正常命令处理有太多影响。

1
2
3
4
5
6
appendonly yes 		 # 打开AOF功能
appendfsync always # 每次有新命令追加到AOF文件时就执行一次fsync,非常慢也非常安全
appendfsync everysec # 每秒fsync一次,足够快且在故障时只会丢失1秒钟的数据
appendfsync no # 从不fsync,将数据交给操作系统来处理。更快,也更不安全的选择
auto-aof-rewrite-min-size 64mb # aof文件至少达到64M才会自动重写,文件太小恢复速度本来就很快,重写意义不大
auto-aof-rewrite-percentage 100 # aof文件自上一次重写后文件大小增长了100%则再次触发重写

RDB快照AOF两种持久化策略生产环境可以都启用,redis启动时若既有RDB文件又有AOF文件则优先选择aof文件恢复数据,因为AOF一般来说数据更全一点

命令 RDB AOF
启动优先级
体积
恢复速度
数据安全性 容易丢数据 根据策略决定
混合持久化

重启Redis时,很少使用RDB来恢复内存状态,因为RDB会丢失大量数据通常使用AOF日志重放,但重放AOF日志性能相对RDB来说要很多,这样在Redis实例很大的情况下,启动需要花费很长的时间。 Redis 4.0为了解决该问题引入了混合持久化

1
aof-use-rdb-preamble yes # 开启混合持久化,注意必须先开启aof

若开启了混合持久化,AOF在重写时不再是单纯将内存数据转换为RESP命令写入AOF文件,而是将重写这一刻之前的内存做RDB快照处理,且将RDB快照内容和增量的AOF修改内存数据的命令存在一起,都写入新的AOF件,新的文件一开始不叫appendonly.aof,等到重写完新的AOF文件才会进行改名覆盖原有的AOF文件,完成新旧两个AOF文件的替换。

混合模式下Redis重启时,可先加载RDB内容,然后再重放增量AOF日志就可以完全替代之前的AOF全量文件重放,因此重启效率大幅得到提升。

混合持久化AOF文件结构

Redis存储数据序列化

StringRedisTemplate继承自RedisTemplate一般用来存储字符串默认序列化机制为StringRedisSerializer。若需存储对象一般用RedisTemplate,其底层用序列化机制是JdkSerializationRedisSerializer,这种存储对象要求对象实现Serializable接口,底层存的是二进制的序列化数组,不便在redis中查看,故一般用Jackson2JsonRedisSerializer序列化机制,其能将对象转成JSON存储,且不需要对象实现Serializable接口,且方便在redis中查看。若无需在redis中查看数据且对性能要求较高,可以采用protobuf序列化。

1
2
3
4
5
6
7
8
RedisTemplate<Object, Object> template = new RedisTemplate<>();
Jackson2JsonRedisSerializer<Object> jsonRedisSerializer = new Jackson2JsonRedisSerializer<Object>(Object.class);
// value的序列化采用jsonRedisSerializer
template.setValueSerializer(jsonRedisSerializer);
template.setHashValueSerializer(jsonRedisSerializer);
// key的序列化采用StringRedisSerializer
template.setKeySerializer(new StringRedisSerializer());
template.setHashKeySerializer(new StringRedisSerializer());

Redis备份策略

crontab定时调度脚本,每小时都copy一份rdb或aof的备份到一个目录中去,仅仅保留最近48小时的备份

每天都保留一份当日的数据备份到一个目录中去,可以保留最近1个月的备份,每次copy备份的时候,都把太旧的备份删除

每天晚上将当前机器上的备份复制一份到其他机器上以防机器损坏

RedisTemplate方法与命令对应关系

String类型
Redis命令 RedisTemplate方法 备注
set key value rt.opsForValue().set(“key”, “value”) 存入字符串键值对
get key rt.opsForValue().get(“key”) 获取一个字符串键值
del key rt.delete(“key”) 删除一个键
strlen key rt.opsForValue().size(“key”)
getset key value rt.opsForValue().getAndSet(“key”, “value”)
getrange key start end rt.opsForValue().get(“key”, start, end)
append key value rt.opsForValue().append(“key”, “value”)
Hash类型
Redis命令 RedisTemplate方法 备注
hmset key field1 value1 field2 value2… rt.opsForHash().putAll(“key”, map) 在一个哈希表key中
存储多个键值对
hset key field value rt.opsForHash().put(“key”,”field”,”value”)
hexists key field rt.opsForHash().hasKey(“key”,”field”)
hgetall key rt.opsForHash().entries(“key”) 返回Map对象
hvals key rt.opsForHash().values(“key”) 返回List对象
hkeys key rt.opsForHash().keys(“key”) 返回List对象
hmget key field1 field2… rt.opsForHash().multiGet(“key”, keyList)
hsetnx key field value rt.opsForHash().putIfAbsent(“key”, “field”, “value”
hdel key field1 field2 rt.opsForHash().delete(“key”, “field1”, “field2”)
hget key field rt.opsForHash().get(“key”, “field”)
List类型
Redis命令 RedisTemplate方法 备注
lpush list node1 node2 node3… rt.opsForList().leftPush(“list”,”node”)
rt.opsForList().leftPushAll(“list”, list)
rpush list node1 node2 node3… rt.opsForList().rightPush(“list”,”node”)
rt.opsForList().rightPushAll(“list”,list)
lindex key index rt.opsForList().index(“list”, index)
llen key rt.opsForList().size(“key”)
lpop key rt.opsForList().leftPop(“key”)
rpop key rt.opsForList().rightPop(“key”)
lpushx list node rt.opsForList().leftPushIfPresent(“list”,”node”)
rpushx list node rt.opsForList().rightPushIfPresent(“list”,”node”)
lrange list start end rt.opsForList().range(“list”,start,end)
lrem list count value rt.opsForList().remove(“list”,count,”value”)
lset key index value rt.opsForList().set(“list”,index,”value”)
Set类型
Redis命令 RedisTemplate方法 备注
sadd key member1 member2… rt.boundSetOps(“key”).add(“member1”,”member2”,…)
rt.opsForSet().add(“key”, set)
scard key rt.opsForSet().size(“key”)
sidff key1 key2 rt.opsForSet().difference(“key1”, “key2”)
sinter key1 key2 rt.opsForSet().intersect(“key1”, “key2”)
sunion key1 key2 rt.opsForSet().union(“key1”, “key2”)
sdiffstore des key1 key2 rt.opsForSet().differenceAndStore(“key1”, “key2”, “des”)
sinter des key1 key2 rt.opsForSet().intersectAndStore(“key1”, “key2”, “des”)
sunionstore des key1 key2 rt.opsForSet().unionAndStore(“key1”, “key2”, “des”)
sismember key member rt.opsForSet().isMember(“key”, “member”)
smembers key rt.opsForSet().members(“key”)
spop key rt.opsForSet().pop(“key”)
srandmember key count rt.opsForSet().randomMember(“key”, count)
srem key member1 member2… rt.opsForSet().remove(“key”, “member1”, “member2”, …)