文章目录
1、简介
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)。
2、Redis集群架构
2.1、Replication+Sentinel
Sentinel的作用:
- 监控:监控主服务器和从服务器是否运行正常
- 通知:当某个Redis服务器出现异常后,能够及时向管理员或者其他程序发送通知
- 自动的故障转移
- 通过选举将某个slave升级为master,原master的其他slave修改为新master的slave
- 根据Sentinel配置(client-reconfig-script)动态修改VIP(虚拟IP),将VIP指向新的master
- 客户端修改连接
- 老的master如重启成功,变成新master的slave
缺点在于主从切换过程中会丢失数据,只能够实现单点写,不能水平扩容
2.2、Proxy+Replication+Sentinel
以Twemproxy为例:
- 前端使用Twemproxy+KeepAlived做代理,将其后端的多台Redis实例分片进行统一管理与分配
- 每一个分片节点的Slave都是Master的副本且只读
- Sentinel监控每个分片节点的Master,当Master出现故障时,Sentinel会发出通知/启动自动故障转移
- Sentinel 可以在发生故障转移动作后触发相应脚本(通过 client-reconfig-script 参数配置 ),脚本获取到最新的Master来修改Twemproxy配置
缺点:
- 部署麻烦,不易运维
- 可扩展性差,进行扩缩容需要手动干预
2.3、Redis Cluster
3、Redis数据存储
Redis的数据存储涉及到内存分配器如jemalloc、简单动态字符串SDS、redisObject以及五种对象类型及内部编码等等。
3.1、jemalloc
Redis编译时候会指定内存分配器,默认是jemalloc;在64位的操作系统中,jemalloc将内存空间划分成小、大、巨大三个范围;每个范围内又被划分成了许多小的内存块单位;当Redis存储数据时候就会选择最合适的内存块进行存储。
3.2、redisObject
typedef struct redisObject {
unsigned type:4;
unsigned encoding:4;
unsigned lru:REDIS_LRU_BITS; /* lru time (relative to server.lruclock) */
int refcount;
void *ptr;
} robj;
type字段:表示的是对象的类型占四个bit;其中包括REDIS_STRING(字符串)、REDIS_LIST (列表)、REDIS_HASH(哈希)、REDIS_SET(集合)、REDIS_ZSET(有序集合)。
encoding:表示对象内部编码占四个bit;比如字符串对象,有int、embstr、raw三种编码;encoding属性优点是可以根据不同场景去设置不同的编码,从而可提高Redis的灵活性和效率。
Lru字段:最后一次被命令访问的时间,4.0版本占24bit;
refCount:记录该对象被引用的次数,整型,4Byte;作用主要是引用计数与内存回收
ptr字段:指向具体数据的指针,如set hello world,ptr指向包含world的SDS
小结:redisObject的结构和对象类型、编码、内存回收、共享对象都有关系。一个redisObject对象的大小是16个字节:4bit+4bit+24bit+4Byte+8Byte=16Byte
3.3、SDS
redis没有直接使用C字符串,而是使用了SDS(Simple Dynamic String,简单动态字符串),结构如下:
struct sdshdr {
int len; // buf已使用的长度
int free; // buf未使用的长度
char buf[]; // 字节数组,用来存储字符串
};
占用空间:
free所占长度+len所占长度+ buf数组的长度=4+4+free+len+1 = free+len+9 字节。
4、五种数据类型和内部编码
redis支持5种对象类型,每种结构都有多种编码方式,其优点在于接口与实现分离,当需要增加或改变内部编码时,用户使用不受影响,另一方面可以根据不同的应用场景切换内部编码,提高效率。
4.1、string:用于缓存、计数器、分布式锁等。
- 字符串是redis中最基本的数据类型,所有的键(key)都是字符串类型,且字符串外其他几种复杂类型的元素也是字符串;长度不超过512M;
- 三种内部编码:
a、int:8个字节的长整型。字符串值是整型时,这个值使用long整型表示。
b、embstr:<=39字节的字符串。embstr与raw都使用redisObject和sds保存数据,区别在于,embstr的使用只分配一次内存空间(因此redisObject和sds是连续的),而raw需要分配两次内存空间(分别为redisObject和sds分配空间)。因此与raw相比,embstr的好处在于创建时少分配一次空间,删除时少释放一次空间,以及对象的所有数据连在一起,寻找方便。而embstr的坏处也很明显,如果字符串的长度增加需要重新分配内存时,整个redisObject和sds都需要重新分配空间,因此redis中的embstr实现为只读。
c、raw:大于39个字节的字符串
4.2、hash:用于用户信息、Hash 表等。
- 哈希(作为一种数据结构),不仅是redis对外提供的5种对象类型的一种(与字符串、列表、集合、有序结合并列),也是Redis作为Key-Value数据库所使用的数据结构。为了说明的方便,在本文后面当使用“内层的哈希”时,代表的是redis对外提供的5种对象类型的一种;使用“外层的哈希”代指Redis作为Key-Value数据库所使用的数据结构。
- 内部编码
a. 压缩列表(ziplist):用于元素个数少、长度小的场景;优势在于集中存储,节省空间;操作复杂度O(n);
使用条件:元素数量小于512,且所有键值字符串的长度都小于64Byte;
b. 哈希表(hashtable):
从上到下依次是:
dictEntry
typedef struct dictEntry{
void *key; // 键,8Byte
union{
void *val;
uint64_tu64;
int64_ts64;
}v; // 值,指针、64位整型或无符号64位整型,8Byte
struct dictEntry *next; // 指向next,用于解决hash冲突问题,8Byte
}dictEntry;
bucket:数组,各元素是指向dictEntry的指针,大小取2^n
dictht
typedef struct dictht{
dictEntry **table; // 指向bucket的指针
unsigned long size; // 记录bucket的大小
unsigned long sizemask; // size-1
unsigned long used; // 已使用的dictEntry的数目
}dictht;
dict
typedef struct dict{
dictType *type; // 适应不同类型的键值对
void *privdata; // 适应不同类型的键值对
dictht ht[2]; // 数据存在ht[0]中,ht[1]在rehash时使用
int trehashidx; // 用于rehash
} dict;
4.3、list:用于表、队列、微博关注人时间轴列表等。
- 列表(list)用来存储多个有序的字符串,每个字符串称为元素;一个列表可以存储2^32-1个元素。Redis中的列表支持两端插入和弹出,并可以获得指定位置(或范围)的元素,可以充当数组、队列、栈等
- 内部编码
双端链表:
双端链表同时保存了表头指针和表尾指针,并且每个节点都有指向前和指向后的指针;
链表中保存了列表的长度;
dup、free和match与节点值类型设置有关,链表可以用于保存各种不同类型的值。
链表中每个节点指向的是type为字符串的redisObject。
压缩链表:
优点是节省内存空间,缺点是不方便进行修改或插入删除操作;在节点数较少时使用,条件同hash使用压缩列表的条件,节点数量多时还是使用双端链表;
4.4、set:用于去重、赞、踩、共同好友等
- 集合(set)与列表类似,都可以用来保存多个字符串,但集合与列表有两点不同:集合中的元素是无序的,因此不能通过索引来操作元素;集合中的元素不能有重复。一个集合中最多可以存储2^32-1个元素;除了支持常规的增删改查,Redis还支持多个集合取交集、并集、差集。
- 内部编码
a.哈希表(hashtable):类似hash中使用的hashtable,区别在于值有key,值全部被置为null
b.整数集合:适用于元素数量较小的情况,优势是集中存储,节省空间,操作复杂度O(n);使用条件是元素数量小于512,且所有元素都是整数;
typedef struct intset{
uint32_t encoding; // contents中存储内容的类型
uint32_t length; // contents中元素个数
int8_t contents[]; // 实际存储的值是int16_t、int32_t或int64_t
} intset;
- 单向编码转换:整数集合转化为哈希表
4.4、zset(sorted set,有序集合):用于访问量排行榜和点击量排行榜等
- 有序集合与集合一样,元素都不能重复;但与集合不同的是,有序集合中的元素是有顺序的。与列表使用索引下标作为排序依据不同,有序集合为每个元素设置一个分数(score)作为排序依据。
- 内部编码
a. 压缩列表(ziplist):同列表和hash
b. 跳表(skiplist):支持平均O(logN),最坏O(N)的节点查找;跳表实现由zskiplist和zskiplistNode两个结构组成:前者用于保存跳跃表信息(如头结点、尾节点、长度等),后者用于表示跳跃表节点;
#define ZSKIPLIST_MAXLEVEL 32 //最大层数
#define ZSKIPLIST_P 0.25 //P
typedef struct zskiplistNode {
robj *obj;
double score;
struct zskiplistNode *backward; //后向指针
struct zskiplistLevel {
struct zskiplistNode *forward; //每一层中的前向指针
unsigned int span; //x.level[i].span 表示节点x在第i层到其下一个节点需跳过的节点数。注:两个相邻节点span为1
} level[];
} zskiplistNode;
typedef struct zskiplist {
struct zskiplistNode *header, *tail; // 头节点,尾节点
unsigned long length; // 节点总数
int level; // 表内节点的最大层数
} zskiplist;