redis深入浅出

image-20190611102836454

初识

官网介绍

​ 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

简单翻译下

​ 内存数据结构存储 ,被当成数据库、缓存、消息代理使用

​ 支持的数据结构,字符串 、哈希、列表、集合等

​ 副本集(主从复制)、Lua脚本、LRU淘汰策略(Least Recently Used、Least Frequently Used)、事务(不可回滚)、不同级别的磁盘持久、用Sentinel保证高可用、用集群来自动分区

单线程 、简单、高性能的key-value数据库

Redis 的特性:
1)更丰富的数据类型

2)进程内与跨进程;单机与分布式

3)功能丰富:持久化机制、过期策略

4)支持多种编程语言

5)高可用,集群

版本特征

1.Redis2.6

Redis2.6在2012年正式发布,相对于Redis2.4,主要特性如下:

  • 支持Lua脚本
  • 键的过期时间支持毫秒
  • 从节点支持只读功能

2.Redis2.8

Redis2.8在2013年11月22日正式发布,相比于Redis2.6,主要特性如下:

  • PSYNC 添加部分主从复制的功能
  • Redis Sentinel生产可用
  • 新增命令:SCAN、SSCAN、HSCAN和ZSCAN

3.Redis3.0(里程碑)

Redis3.0在2015年4月1日正式发布,相比于Redis2.8主要特性如下:

  • Redis Cluster:Redis的官方分布式实现
  • 全新的embedded string对象编码结果,优化小对象内存访问,在特定的工作负载下载速度大幅提升

4.Redis3.2

Redis3.2在2016年5月6日正式发布,相比于Redis3.0主要特征如下:

  • 新的List编码类型:quicklist。

5.Redis4.0

Redis4.0在2016年底正式发布,相比于Redis3.2主要特征如下: http://antirez.com/news/110

  • 模块化
  • PSYNC2.0:优化了之前版本中,主从节点切换必然引起全量复制的问题。
  • 提供了新的缓存剔除算法:LFU(Last Frequently Used),并对已有算法进行了优化。
  • 提供了非阻塞del和flushall/flushdb功能,有效解决删除了bigkey可能造成的Redis阻塞。
  • 提供了RDB-AOF混合持久化格式,充分利用了AOF和RDB各自优势。

6.Redis5.0

Redis5.0在2018年5月正式发布相比于Redis4.0主要特征如下:

  • Stream data type

more: https://raw.githubusercontent.com/antirez/redis/5.0/00-RELEASENOTES

数据结构

String

​ Redis 的字符串是动态字符串,是可以修改的字符串,内部结构实现上类似于 Java 的 ArrayList,采用预分配冗余空间的方式来减少内存的频繁分配。 当字符串长度小于 1M 时,扩容都是加倍现有的空间,如果超过 1M,扩容时一次只会多扩 1M 的空间。需要注意的是字符串最大长度为 512M。

存储(实现)原理

数据模型
/*dict.h*/
typedef struct dictEntry {
    void *key; /* key 关键字定义 */
    union {
        void *val; uint64_t u64; /* value 定义 */
    int64_t s64; double d;
    } v;
    struct dictEntry *next; /* 指向下一个键值对节点 */
} dictEntry;

以set hello world 为例子

image-20200225165251801

key 是字符串,但是 Redis 没有直接使用 C 的字符数组,而是存储在自定义的 SDS 中。

value 既不是直接作为字符串存储,也不是直接存储在 SDS 中,而是存储在 redisObject 中。实际上五种常用的数据类型的任何一种,都是通过 redisObject 来存储 的。

redisObject
/* server.h */
typedef struct redisObject {
    unsigned type:4; /* 对象的类型,包括:OBJ_STRING、OBJ_LIST、OBJ_HASH、OBJ_SET、OBJ_ZSET */ 			                       	 unsigned encoding:4; /* 具体的数据结构 */
    unsigned lru:LRU_BITS; /* 24 位,对象最后一次被命令程序访问的时间,与内存回收有关 */
    int refcount; /* 引用计数。当refcount为0的时候,表示该对象不被任何对象引用,可以进行垃圾回收了*/
    void *ptr; /* 指向对象实际的数据结构 */
} robj;

可以使用 type 命令来查看对外的类型。

可以使用 object encoding 来查看具体的数据结构。

内部编码
image-20200225170138936

字符串类型的内部编码有三种:
1、int,存储 8 个字节的长整型(long,2^63-1)。
2、embstr, 代表 embstr 格式的 SDS(Simple Dynamic String 简单动态字符串),

存储小于 44 个字节的字符串。
3、raw,存储大于 44 个字节的字符串(3.2 版本之前是 39 字节)

问题 1、什么是 SDS? Redis 中字符串的实现?

​ 在 3.2 以后的版本中,SDS 又有多种结构(sds.h): sdshdr5、sdshdr8、sdshdr16、sdshdr32、sdshdr64,用于存储不同的长度的字符串,分别代表 2^5=32byte, 2^8=256byte,2^16=65536byte=64KB,2^32byte=4GB。

/* sds.h */
struct __attribute__ ((__packed__)) sdshdr8 {
    uint8_t len; /* 当前字符数组的长度 */
    uint8_t alloc; /*当前字符数组总共分配的内存大小 */
    unsigned char flags; /* 当前字符数组的属性、用来标识到底是 sdshdr8 还是 sdshdr16 等 */ 
    char buf[]; /* 字符串真正的值 */
};

问题 2、为什么 Redis 要用 SDS 实现字符串?

C 字符串 SDS
获取字符串长度的复杂度为 O(N) 获取字符串长度的复杂度为 O(1)
API 是不安全的,可能会造成缓冲区溢出 API 是安全的,不会造成缓冲区溢出
修改字符串长度 N 次必然需要执行 N 次内存重分配 修改字符串长度 N 次最多需要执行 N 次内存重分配
只能保存文本数据 可以保存文本或者二进制数据
可以使用所有<string.h>库中的函数 可以使用一部分<string.h>库中的函数

问题 3、embstr 和 raw 的区别?

​ embstr 的使用只分配一次内存空间(因为 RedisObject 和 SDS 是连续的,只读),而 raw 需要分配两次内存空间(分别为 RedisObject 和 SDS 分配空间)。

问题 4、int 和 embstr 什么时候转化为 raw?

​ 当int数据不再是整数,或大小超过了 long 的范围 (2^63-1=9223372036854775807)时,自动转化为 raw

​ 对于 embstr,由于其实现是只读的,因此在对 embstr对象进行修改时,都会先转化为raw 再进行修改。

应用场景

缓存

数据共享分布式

分布式锁

​ STRING 类型 setnx 方法,只有不存在时才能添加成功,返回 true。

全局 ID

​ INT 类型,INCRBY,利用原子性

计数器

​ INT 类型,INCR 方法

限流

​ INT 类型,INCR 方法

位统计

​ bit count

Hash

​ Redis 的字典相当于 Java 语言里面的 HashMap,它是无序字典。不同的是,Redis 的字典的值只能是字符串,另外它们 rehash 的方式不一样,因为 Java 的 HashMap 在字典很大时,rehash 是个耗时的操作,需要一次性全部 rehash(在concurrentHashMap中也是渐进式,而且是多线程)。

Redis 为了高性能,不能堵塞服务,所以采用了渐进式 rehash 策略。(操作的时候会触发rehash,同时后台会有个定时任务在执行)

​ 相比字符串 ,可以进行部分获取,减少网络传输. 但hash 结构的存储消耗要高于单个字符串.

存储(实现)原理

​ 外层的哈希(Redis KV 的实现)只用到了 hashtable。当存储 hash 数据类型时, 我们把它叫做内层的哈希。

内层的哈希底层可以使用两种数据结构实现:

ziplist:OBJ_ENCODING_ZIPLIST(压缩列表)

hashtable:OBJ_ENCODING_HT(哈希表)

ziplist 压缩列表

ziplist 是一个经过特殊编码的双向链表,它不存储指向上一个链表节点和指向下一个链表节点的指针,而是存储上一个节点长度和当前节点长度,通过牺牲部分读写性能, 来换取高效的内存空间利用率,是一种时间换空间的思想。只用在字段个数少,字段值 小的场景里面。

内部结构

typedef struct zlentry {
    unsigned int prevrawlensize; 	/* 上一个链表节点占用的长度 */
    unsigned int prevrawlen; 			/* 存储上一个链表节点的长度数值所需要的字节数 */ 
    unsigned int lensize; 				/* 存储当前链表节点长度数值所需要的字节数 */
    unsigned int len; 						/* 当前链表节点占用的长度 */
    unsigned int headersize; /* 当前链表节点的头部大小(prevrawlensize + lensize),即非数据域的大小 */
    unsigned char encoding;  /* 编码方式 */
    unsigned char *p; /* 压缩链表以字符串的形式保存,该指针指向当前节点起始位置 */
} zlentry;

编码 encoding(ziplist.c 源码第 204 行)

#define ZIP_STR_06B (0 << 6) //长度小于等于 63 字节
#define ZIP_STR_14B (1 << 6) //长度小于等于 16383 字节 
#define ZIP_STR_32B (2 << 6) //长度小于等于 4294967295 字节
image-20200225180434113

什么时候使用?

当 hash 对象同时满足以下两个条件的时候,使用 ziplist 编码:

1)所有的键值对的健和值的字符串长度都小于等于 64byte(一个英文字母一个字节);

2)哈希对象保存的键值对数量小于 512 个。

hashtable(dict)

在 Redis 中,hashtable 被称为字典(dictionary),它是一个数组+链表的结构。

image-20200225182411033

dictht
/* 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;
dict
typedef struct dict {
    dictType *type; /* 字典类型 */
    void *privdata; /* 私有数据 */
    dictht ht[2]; /* 一个字典有两个哈希表 */
    long rehashidx; /* rehash 索引 */
    unsigned long iterators; /* 当前正在使用的迭代器数量 */
} dict;
rehash

步骤:
1、为字符 ht[1]哈希表分配空间,这个哈希表的空间大小取决于要执行的操作,以及 ht[0]当前包含的键值对的数量。
扩展:ht[1]的大小为第一个大于等于 ht[0].used*2。
2、将所有的 ht[0]上的节点 rehash 到 ht[1]上,重新计算 hash 值和索引,然后放入指定的位置。
3、当 ht[0]全部迁移到了 ht[1]之后,释放 ht[0]的空间,将 ht[1]设置为 ht[0]表,并创建新的 ht[1],为下次 rehash 做准备

https://blog.csdn.net/wangmaohong0717/article/details/84611426

http://redisbook.com/preview/dict/incremental_rehashing.html

扩容缩容?

负载因子

static int dict_can_resize = 1;
static unsigned int dict_force_resize_ratio = 5;

ratio = used / size,已使用节点与字典大小的比例

dict_can_resize 为 1 并且 dict_force_resize_ratio 已使用节点数和字典大小之间的 比率超过 1:5,触发扩容

应用场景

string

存储对象类型的数据

购物车

List

​ Redis 的列表相当于 Java 语言里面的 LinkedList,注意它是链表而不是数组。这意味着 list 的插入和删除操作非常快,时间复杂度为 O(1),但是索引定位很慢,时间复杂度为 O(n),这点让人非常意外。 常用于异步队列。

image-20190611185500346

在列表元素较少的情况下会使用一块连续的内存存储,这个结构是 ziplist,也即是压缩列表。

当数据量比较多的时候才会改成 quicklist。因为普通的链表需要的附加指针空间太大,会比较浪费空间,而且会加重内存的碎片化。

比如这个列表里存的只是 int 类型的数据,结构上还需要两个额外的指针 prevnext

所以 Redis 将链表和 ziplist 结合起来组成了 quicklist。也就是将多个 ziplist 使用双向指针串起来使用。这样既满足了快速的插入删除性能,又不会出现太大的空间冗余。

存储(实现)原理

在早期的版本中,数据量较小时用 ziplist 存储,达到临界值时转换为 linkedlist 进 行存储,分别对应 OBJ_ENCODING_ZIPLIST 和 OBJ_ENCODING_LINKEDLIST 。

3.2 版本之后,统一用 quicklist 来存储。quicklist 存储了一个双向链表,每个节点 都是一个 ziplist。

image-20200225203545120

quicklist

quicklist(快速列表)是 ziplist 和 linkedlist 的结合体。

typedef struct quicklist {
    quicklistNode *head; /* 指向双向列表的表头 */ 
    quicklistNode *tail; /* 指向双向列表的表尾 */
    unsigned long count; /* 所有的 ziplist 中一共存了多少个元素 */ 
    unsigned long len;	/* 双向链表的长度,node 的数量 */
    int fill : 16; /* fill factor for individual nodes */
    unsigned int compress : 16;  /* 压缩深度,0:不压缩; */
} quicklist;	
quicklistNode

quicklistNode 中的*zl 指向一个 ziplist,一个 ziplist 可以存放多个元素。

typedef struct quicklistNode {
    struct quicklistNode *prev; /* 前一个节点 */
    struct quicklistNode *next; /* 后一个节点 */
    unsigned char *zl; /* 指向实际的 ziplist */
    unsigned int sz; /* 当前 ziplist 占用多少字节 */
    unsignedintcount:16;/* 当前ziplist中存储了多少个元素,占16bit(下同),最大65536个*/ unsigned int   	 encoding : 2; /* 是否采用了 LZF 压缩算法压缩节点,1:RAW 2:LZF */
    unsigned int   container : 2; /* 2:ziplist,未来可能支持其他结构存储 */
    unsigned int   recompress : 1; /* 当前 ziplist 是不是已经被解压出来作临时使用 */
    unsigned int   attempted_compress : 1; /* 测试用 */
    unsigned int   extra : 10; /* 预留给未来使用 */
} quicklistNode;

应用场景

用户消息时间线 timeline

消息队列

Set

​ Redis 的集合相当于 Java 语言里面的 HashSet,它内部的键值对是无序的唯一的。它的内部实现相当于一个特殊的字典,字典中所有的 value 都是一个值NULL

存储(实现)原理

​ Redis 用 intset 或 hashtable 存储 set。

如果元素都是整数类型,就用 inset 存储。

如果不是整数类型,就用 hashtable(数组+链表的存来储结构)。

应用场景

抽奖

​ 随机获取元素 spop myset

点赞、签到、打卡

​ 这条微博的 ID 是 t1001,用户 ID 是 u3001。

​ 用 like:t1001 来维护 t1001 这条微博的所有点赞用户。

​ 点赞了这条微博:sadd like:t1001 u3001

​ 取消点赞:srem like:t1001 u3001

​ 是否点赞:sismember like:t1001 u3001

​ 点赞的所有用户:smembers like:t1001

​ 点赞数:scard like:t1001

商品标签

用 tags:i5001 来维护商品所有的标签。

sadd tags:i5001 画面清晰细腻 sadd tags:i5001 真彩清晰显示屏 sadd tags:i5001 流畅至极

商品筛选

​ 获取差集

​ sdiff set1 set2

​ 获取交集(intersection )

​ sinter set1 set2
获取并集
​ sunion set1 set2

sadd brand:apple iPhone11
sadd brand:ios iPhone11
sad screensize:6.0-6.24 iPhone11 sad screentype:lcd iPhone11

筛选商品,苹果的,iOS 的,屏幕在 6.0-6.24 之间的,屏幕材质是 LCD 屏幕

​ sinter brand:apple brand:ios screensize:6.0-6.24 screentype:lcd

用户关注、推荐模型

思考

1)相互关注? 2)我关注的人也关注了他? 3)可能认识的人?

Zset

​ zset 可能是 Redis 提供的最为特色的数据结构。它类似于 Java 的 SortedSet 和 HashMap 的结合体,一方面它是一个 set,保证了内部 value 的唯一性,另一方面它可以给每个 value 赋予一个 score,代表这个 value 的排序权重。

​ 我们需要这个链表按照 score 值进行排序。这意味着当有新元素需要插入时,要定位到特定位置的插入点,这样才可以继续保证链表是有序的。通常我们会通过二分查找来找到插入点,但是二分查找的对象必须是数组,所以 它的内部实现用的是一种叫做「跳跃列表」的数据结构。

数据结构对比:

数据结构 是否允许重复元素 是否有序 有序实现方式
列表 list 索引下标
集合 set
有序集合 zset 分值 score

存储(实现)原理

同时满足以下条件时使用 ziplist 编码:
元素数量小于 128 个
所有 member 的长度都小于 64 字节

超过阈值之后,使用 skiplist+dict 存储。

image-20200429105750619

什么是 skiplist?

二分查找法只适用于有序数组,不适用于链表。所以为了解决这个问题

插入一个数据的时候,决定要放到那一层,取决于一个算法 ,zslRandomLevel

/* t_zset.c */
int zslRandomLevel(void) {
    int level = 1;
    while ((random()&0xFFFF) < (ZSKIPLIST_P * 0xFFFF))
    level += 1;
    return (level<ZSKIPLIST_MAXLEVEL) ? level : ZSKIPLIST_MAXLEVEL;
}

为什么不用 AVL 树或者红黑树?因为 skiplist 更加简洁。

typedef struct zskiplistNode {
    sds ele; /* zset 的元素 */
    double score; /* 分值 */
    struct zskiplistNode *backward; /* 后退指针 */ 
  
  struct zskiplistLevel {
        struct zskiplistNode *forward; /* 前进指针,对应 level 的下一个节点 */
        unsigned long span; /* 从当前节点到下一个节点的跨度(跨越的节点数) */ 
  } 
  level[]; /* 层 */
} zskiplistNode; 

typedef struct zskiplist {
    struct zskiplistNode *header, *tail; /* 指向跳跃表的头结点和尾节点 */ 
  unsigned long length; /* 跳跃表的节点数 */
    int level; /* 最大的层数 */
} zskiplist; 

typedef struct zset {
    dict *dict;
    zskiplist *zsl; 
} zset;

结构图

image-20200414155358585

应用场景

排行榜

其他数据结构

BitMaps

Bitmaps 是在字符串类型上面定义的位操作。一个字节由 8 个二进制位组成。

应用场景:
用户访问统计
在线用户统计

Hyperloglogs

Hyperloglogs:提供了一种不太准确的基数统计方法,比如统计网站的 UV,存在 一定的误差

Streams

5.0 推出的数据类型。支持多播的可持久化的消息队列,用于实现发布订阅功能,借 鉴了 kafka 的设计。

总结

数据结构总结

image-20200225212533444

编码转换总结

image-20200225212555266

使用

  • 管道(Pipelining)

    一次发送多个命令,可以包含Batch命令,节省往返时间。

  • 淘汰策略 LRU LFU

    Least Recently Used 按照访问时间 这个可以用LinkHashMap 简单实现
    Least Frequently Used 按照访问次数 需要额外的队列记录访问频率

  • Batch 命令

    批量相同指令

  • 事务

    Redis 事务保证了其中的一致性(C)和隔离性(I),但并不保证原子性(A)和持久性(D)。

  • 分布式锁

    Lua脚本实现原子性

  • TTL 过期删除

    惰性、异步 删除, 注意过期时间要采用 一个常量加一个随机时间戳 防止大批量的key同一时间过期

发布订阅

传统list队列局限

1、如果生产者生产消息的速度远大于消费者消费消息的速度,List 会占用大量的内 存。

2、消息的实时性降低。

基于 list 实现的消息队列,不支持一对多的消息分发。

发布订阅模式

订阅频道

​ 我们有很多的频道(channel),我们也可以把这个频道理解成 queue。订 阅者可以订阅一个或者多个频道。消息的发布者(生产者)可以给指定的频道发

订阅

​ subscribe channel-1 channel-2 channel-3

发布

​ publish channel-1 2673

按规则(Pattern)订阅频道

支持?和占位符。?代表一个字符,代表 0 个或者多个字符。

Redis事务

Redis 的事务有两个特点:

1、按进入队列的顺序执行。

2、不会受到其他客户端的请求的影响。

Redis 的事务涉及到四个命令:multi(开启事务),exec(执行事务),discard (取消事务),watch(监视)

watch命令

它可以为 Redis 事务提供 CAS 乐观锁行为(Check and Set / Compare and Swap)

我们可以用 watch 监视一个或者多个 key,如果开启事务之后,至少有一个被监视 key 键在 exec 执行之前被修改了, 那么整个事务都会被取消(key 提前过期除外)。可 以用 unwatch 取消。

事务可能遇到的问题

在执行 exec 之前发生错误

比如:入队的命令存在语法错误,包括参数数量,参数名等等(编译器错误)。

在这种情况下事务会被拒绝执行,也就是队列中所有的命令都不会得到执行。

在执行 exec 之后发生错误

比如,类型错误,比如对 String 使用了 Hash 的命令,这是一种运行时错误。

为什么在一个事务中存在错误,Redis 不回滚?

  • Redis 命令只会因为错误的语法而失败(并且这些问题不能在入队时发现),或是命令用在了错误类型的键上面:这也就是说,从实用性的角度来说,失败的命令是由编程错误造成的,而这些错误应该在开发的过程中被发现,而不应该出现在生产环境中。
  • 因为不需要对回滚进行支持,所以 Redis 的内部可以保持简单且快速。

Lua脚本

Lua/ˈluə/是一种轻量级脚本语言,它是用 C 语言编写的,跟数据的存储过程有点类 似。 使用 Lua 脚本来执行 Redis 命令的好处:

1、一次发送多个命令,减少网络开销。

2、Redis 会将整个脚本作为一个整体执行,不会被其他请求打断,保持原子性。

3、对于复杂的组合命令,我们可以放在文件中,可以实现程序之间的命令集复用。

Lua应用

分布式锁

限流

管理

持久化

  • 快照RDB

    全量备份

  • AOF 指令

    • 1条指令执行一次刷盘
    • 1段时间执行一次刷盘
    • 1s执行一次刷盘
  • RDB快照 +AOF同步

集群

​ 高可用 主从同步(RDB快照 +AOF同步) 、raft 一致性算法 、gossip 通信协议

​ 高性能 读写分离

​ 分片 横向扩展 算法槽

https://redis.io/topics/cluster-spec

参考:

redis深度历险


转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 951488791@qq.com

文章标题:redis深入浅出

字数:5.3k

本文作者:zhengyumin

发布时间:2019-06-10, 18:33:40

最后更新:2020-06-25, 21:51:57

原始链接:http://zyumin.github.io/2019/06/10/Overview_Redis/

版权声明: "署名-非商用-相同方式共享 4.0" 转载请保留原文链接及作者。