Redis基本介绍

Redis基本介绍

为什么叫REDIS呢?它的全称是REmote Dlctionary Service,直接翻译过来是远程字典服务。

Redis定位与特性

SQL与NoSQL

在绝大部分时候,我们都会首先考虑用关系型数据库来存储业务数据,比如SQLServer,Oracle,MySQL等等。

这些关系型数据库的特点:

1、它以表格的形式,基于行存储数据,是一个二维的模式。

2、它存储的是结构化的数据,数据存储有固定的模式(schema),数据需要适应表结构。

3、表与表之间存在关联(Relationship)。

4、大部分关系型数据库都支持SQL(结构化查询语言)的操作,支持复杂的关联查询。

5、通过支持事务(ACID)来提供严格或者实时的数据一致性。

但是使用关系型数据库也存在一些限制,比如:

1、要实现扩容的话,只能向上(垂直)扩展,比如磁盘限制了数据的存储,就要扩大磁盘容量,通过堆硬件的方式,不支持动态的扩缩容,水平扩容需要复杂的技术来实现,比如分库分表。

2、表结构修改困难,因此存储的数据格式也受到限制。

3、关系型数据库通常会把数据持久化到磁盘,在高并发和高数据量的情况下,基于磁盘的读写压力比较大。

为了规避关系型数据库的一系列问题,就有了非关系型的数据库,我们一般把它叫做“non-relational”或者“Not Only SQL”。NoSQL最开始是不提供SQL(Structured Query Language结构化查询语言)的数据库的意思,但是后来意思慢慢地发生了变化。

非关系型数据库的特点:

1、存储非结构化的数据,比如文本、图片、音频、视频。

2、表与表之间没有关联,可扩展性强。

3、保证数据的最终一致性,遵循BASE理论,Basically Available(基本可用)、Soft-state(软状态)、Eventually Consistent(最终一致性)。

4、支持海量数据的存储和高并发的高效读写。

5、支持分布式,能够对数据进行分片存储,扩缩容简单。

对于不同的存储类型,又有各种各样的非关系型数据库,比如有几种常见的类型:

1、KV存储:Redis和Memcached。

2、文档存储:MongoDB。

3、列存储:HBase。

4、图存储:Neo4j。

5、对象存储。

6、XML存储等等等等。

这个网页列举了各种各样的NoSQL数据库http://nosql-database.org/

能不能把SQL和NoSQL的特性结合在一起呢?

当然可以。所以现在有有了所谓的NewSQL数据库。

NewSQL结合了SQL和NoSQL的特性。例如TiDB(PingCAP)、VoltDB、ScaleDB。

Redis特性

对于Redis,大部分时候的认识是一个缓存的组件,当然从它的发展历史也可以看到,它最开始并不是作为缓存使用的。只是在很多的互联网应用里面,它作为缓存发挥了最大的作用。

Redis的主要特性有哪些,为什么要使用它作为数据库的缓存。

如果要了解Redis的特性,先了解几个问题:

1、为什么要把数据放在内存中?

1)内存的速度更快,10wQPS

2)减少计算的时间,减轻数据库压力

2、如果是用内存的数据结构作为缓存,为什么不用HashMap或者Memcached?

1)更丰富的数据类型

2)支持多种编程语言

3)功能丰富:持久化机制、内存淘汰策略、事务、发布订阅、pipeline、lua

4)支持集群、分布式

Memcached和Redis的主要区别是什么?

Memcached只能存储KV、没有持久化机制、不支持主从复制、是多线程的。

redis 安装启动参考

Redis的存储叫做key-value存储,或者叫做字典结构。key的最大长度限制是他512M,值的限制不同,有的是用长度限制的,有的是用个数限制的。

Redis基本数据类型

Redis一共有8种数据类型

String、 Hash、 Set、 List、Zset、Hyperloglog、Geo、 Streams

String字符串

存储类型

可以用来存储int(整数)、float(单精度浮点数)、String(字符串)。

操作命令

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
#设值
set abc 123456

#获取指定范围的字符
getrange abc 0 1

#获取值长度
strlen abc

#字符串追加内容
append abc good

#设置多个值(批量操作,原子性)
mset def 2673 ghi 666

#获取多个值
mget def ghi

#设置值,如果key存在,则不成功
setnx key1 pyy
#基于此可实现分布式锁,用del key释放锁。
#但如果释放锁的操作失败了,导致其他节点永远获取不到锁,怎么办?
#加过期时间。单独用expire加过期,也失败了,无法保证原子性,怎么办?多参数
set key value [expiration EX seconds|PX milliseconds][NX|XX]
#使用参数的方式
set kl vl EX 10 NX

#(整数)值递增(值不存在会得到1)
incr k2
incrby k2 100

#(整数)值递减
decr k2
decrby k2 100 #浮点数增量

set mf 2.6
incrbyfloat mf 7.3

存储(实现)原理

Redis是KV的数据库,Key-Value一般会用哈希表来存储它, Redis的最外层确实是通过hashtable实现的(这个叫做外层的哈希)。

在Redis里面,这个哈希表怎么实现呢?

看一下C语言的源码(dict.h47行),每个键值对都是一个dictEntry(怪不得叫远程字典服务),通过指针指向key的存储结构和value的存储结构,而且next存储了指向下一个键值对的指针。

1
2
3
4
5
6
7
8
9
10
11
typedef struct redisDb { 
dict *dict; /*所有的键值对*//*The keyspace for this DB*/
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 aPUSH*
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;

以set hello word为例,因为key是字符串,Redis自己实现了一个字符串类型,叫做SDS (Simple Dynamic String简单动态字符串),所以hello指向一个SDS的结构。

value是 world,同样是一个字符串,当 value存储一个字符串的时候, Redis并没有直接使用SDS存储,而是存储在 redisObject中。实际上五种常用的数据类型的任何一种的 value,都是通过 redisObject来存储的,最终 redisObject再通过一个指针指向实际的数据结构,比如字符串或者其他。

SDS定义

1
2
3
4
5
6
struct__attribute__(_ packed __)) sdshdr8{
uint8_t len; /*当前字符数组的长度*/
uint8_t alloc; /*当前字符数组总共分配的内存大小
unsigned char flags; /*当前字符数组的属性、用来标识到底是sdshdr8还是sdshdr16等*/
char buf[]; /*字符串真正的值*/
};

SDS 本质上其实还是字符数组。

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

因为C语言本身没有字符串类型,只能用字符数组char[] 实现。

1、使用字符数组必须先给目标变量分配足够的空间,否则可能会溢出。

2、如果要获取字符长度,必须遍历字符数组,时间复杂度是O(n)。

3、C字符串长度的变更会对字符数组做内存重分配。

4、通过从字符串开始到结尾碰到的第一个”\0”来标记字符串的结束,因此不能保存图片、音频、视频、压缩文件等二进制(bytes)保存的内容,二进制不安全。

SDS的特点:

1、不用担心内存溢出问题,如果需要会对SDS进行扩容。

2、获取字符串长度时间复杂度为O(1),因为定义了len属性。

3、通过“空间预分配”(sdsMakeRoomFor)和“惰性空间释放”,防止多次重分配内存。

4、判断是否结束的标志是len属性,可以包含”\0”(它同样以”\0”结尾是因为这样就可以使用C语言中函数库操作字符串的函数了)。

应用场景

缓存

String类型,缓存热点数据。例如明星出轨、网站首页、报表数据等等。 可以显著提升热点数据的访问速度。

分布式数据共享

String类型,因为Redis是分布式的独立服务,可以在多个应用之间共享例如:分布式Session

1
2
3
4
<dependenecy>
<groupld>org.springframework.session</groupId>
<artifactId>spring-session-data-redis</artifactld>
</dependency>

分布式锁

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

全局ID

INT类型,INCRBY,利用原子性

1
incrby userid 1000

分库分表的场景,一次性拿一段

计数器

INT类型,INCR方法
例如:文章的阅读量,微博点赞数,允许一定的延迟,先写入Redis再定时同步到数据库。

限流

INT类型,INCR方法
以访问者的IP和其他信息作为key,访问一次增加一次计数,超过次数则返回false。

总结一下:利用redis本身的特性,和String内存的存储内容,以及提供的操作方法,我们可以用来达到很多的业务目的。

Hash哈希

存储类型

Hash用来存储多个无序的键值对。最大存储数量2^32-1(40亿左右)

注意:前面说Redis所有的KV本身就是键值对,用dictEntry实现的,叫做外层的哈希,现在讲的是内层的哈希。

Hash的value只能是字符串,不能嵌套其他类型,比如hash或者list。

同样是存储字符串,Hash与String的主要区别

1,把所有相关的值聚集到一个key中,节省内存空间

2、只使用一个key,减少key冲突

3、当需要批量获取值的时候,只需要使用一个命令,减少内存/IO/CPU的消耗

Hash不适合的场景:

1、Field不能单独设置过期时间

2、需要考虑数据量分布的问题(field非常多的时候,无法分布到多个节点)

操作命令

1
2
3
4
5
6
7
8
9
10
hset hl f 6
hset hl e 5
hmset hl a 1 b 2 c 3 d 4
hget hl a
hmget hl a b c d
hkeys hl
hvals hl
hgetall hl
hdel h1 a
hlen hl

存储(实现)原理

Redis的Hash本身也是一个KV的结构,是不是跟外层的哈希一样,用dictEntry 实现呢?
内层的哈希底层可以使用两种数据结构实现:

ziplist:OBJ ENCODING ZIPLIST(压缩列表)

hashtable:OBJENCODINGHT(哈希表)

ziplist压缩列表

ziplist是一个经过特殊编码的,由连续内存块组成的双向链表。

它不存储指向上一个链表节点和指向下一个链表节点的指针,而是存储上一个节点长度和当前节点长度。这样读写可能会慢一些,因为要去算长度,但是可以节省内存,是一种时间换空间的思想。

image-20210824233101343

什么时候使用ziplist存储?

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

1)哈希对象保存的键值对数量 < 512个

2)所有的键值对的健和值的字符串长度都 < 64 byte(一个英文字母一个字节)。

src/redis.conf配置

1
2
hash-max-ziplist-value 64 //ziplist 中最大能存放的值长度
hash-max-ziplist-entries 512 //ziplist中最多能存放的entry节点数量

如果超过这两个阈值的任何一个,存储结构就会转换成hashtable。

总结:字段个数少,字段值小,用ziplist。

hashtable (dict)

在Redis中,hashtable被称为字典(dictionary)。

前面我们知道了,Redis的KV结构是通过一个dictEntry来实现的。在hashtable中,又对dictEntry进行了多层的封装。

image-20210824233438933

注意:dictht后面是NULL说明第二个ht还没用到。dictEntry 后面是NULL说明没有hash到这个地址。dictEntry后面是NULL说明没有发生哈希冲突。

应用场景

存储对象类型的数据

比如对象或者一张表的数据,比String节省了更多key的空间,也更加便于集中管理。

List列表

存储类型

存储有序的字符串(从左到右),元素可以重复。最大存储数量2^32-1(40亿左右)。
image-20210824234120747

操作命令

1
2
3
4
5
6
7
8
9
#元素增减:
lpush queue a
lpush queue b c
rpush queue d e
lpop queue
rpop queue
#取值
lindex queue 0
lrange queue 0 1

image-20210824234433051

存储(实现)原理

在早期的版本中,数据量较小时用ziplist存储(特殊编码的双向链表),达到临界值时转换为linkedlist进行存储,分别对应OBJ_ENCODING_ZIPLIST和OBJ ENCODING LINKEDLIST。

3.2版本之后,统一用quicklist来存储。quicklist存储了一个双向链表,每个节点都是一个ziplist,所以是ziplist和linkedlist的结合体。

quicklist

image-20210824234612188

应用场景

List主要用在存储有序内容的场景。

例如用户的消息列表、网站的公告列表、活动列表、博客的文章列表、评论列表等。

队列/栈

List还可以当做分布式环境的队列/栈使用。

List提供了两个阻塞的弹出操作:BLPOP/BRPOP,可以设置超时时间(单位:秒)。

1
2
blpop queue
brpop queue

BLPOP:BLPOP key1 timeout移出并获取列表的第一个元素,如果列表没有元素会阻塞列表直到等待超时或发现可弹出元素为止。

BRPOP:BRPOP key1 timeout移出并获取列表的最后一个元素,如果列表没有元素会阻塞列表直到等待超时或发现可弹出元素为止。

队列:先进先出,rpush blpop,左头右尾,右边进入队列,左边出队列。

栈:先进后出,rpush brpop

总结一下:

List存储有序的内容,用quicklist实现,本质上是数组+链表。

Hashtable也是数组加链表,只是内部编码结构不一样。

Set集合

存储类型

Set存储String类型的无序集合,最大存储数量2^32-1(40亿左右)。

操作命令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//添加一个或者多个元素 
sadd myset a b c d e f g
//获取所有元素
smembers myset
//统计元素个数
scard myset
//随机获取一个元素
srandmember myset
//随机弹出一个元素
spop myset
从移除一个或者多个元素
srem myset d e f
//查看元素是否存在
sismember myset a

存储(实现)原理

Redis用intset或hashtable存储set。如果元素都是整数类型,就用inset存储。

1
2
3
4
5
typedef struct intset{
uint32_t encoding;//编码类型intl6_t、int32_t、int64_t
uint32_tlength;//长度最大长度:2^32
int8_t contents[];//用来存储成员的动态数组
} intset;

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

如果元素个数超过512个,也会用hashtable存储。跟一个配置有关:

1
set-max-intset-entries 512

set的key没有value,怎么用hashtable存储?value存null就好了。

应用场景

抽奖

随机获取元素:spop myset

点赞、签到、打卡

商品标签

ZSet有序集合

存储类型

sorted set存储有序的元素,每个元素有个score,按照score从小到大排名,score相同时,按照key的ASCII码排序。

image-20210824235845949

数据结构对比

image-20210824235913394

操作命令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//添加元素
zadd myzset l0 java 20 php 30 ruby 40 cpp 50 python
//获取全部元素
zrange myzset 0 l withscores
zrevrange myzset 0 l withscores
//根据分值区间获取元素
zrangebyscore myzset 20 30
//从移除元素也可以根据score rank删除
zrem myzset php cpp
//统计元素个数
zcard myzset
//分值递增
zincrby myzset 5 python
//根据分值统计个数
zcount myzset 20 60
//获取元素rank
zrank myzset python
//获取元素score
zscore myzset python
//也有倒序的rev操作(reverse)

存储(实现)原理

默认使用ziplist编码

在ziplist的内部,按照score排序递增来存储,插入的时候要移动之后的数据。如果元素数量大于等于128个,或者任一member长度大于等于64字节,使用 skiplist+dict存储。

1
2
zset-max-ziplist-entries l28 
zset-max-ziplist-value 64

skiplist (跳表)

先来看一下有序链表:

image-20210825000325463

在这样一个链表中,如果要查找某个数据,那么需要从头开始逐个进行比较直到找到包含数据的那个节点,或者找到第一个比给定数据大的节点为止,时间复杂度为O(n)。同样,当我们要插入新数据的时候,也要经历同样的查找过程,从而确定插入个位置。二分查找法只适用于有序数组,不适用于链表。

假如每相邻两个节点增加一个指针,让指针指向下个节点(或者理解为有三个元素进入了第二层)。

image-20210825000402081

这样所有新增加的指针连成了一个新的链表,但它包含的节点个数只有原来的一半(上图中是7,19,26)。

image-20210825000553029

比如,想查找23,查找的路径是沿着标红的指针所指向的方向进行的:

1、23首先和7比较,再和19比较,比它们都大,继续向后比较。

2、但23和26比较的时候,比26要小,因此回到下面的链表(原链表) ,与19 在第一层的下一个节点22比较。

3、23比22要大,沿下面的指针继续向后和26比较。23比26小,说明待查数据23在原链表中不存在。

在这个查找过程中,由于新增加的指针,我们不再需要与链表中每个节点逐个进行比较了。需要比较的节点数大概只有原来的一半,这就是跳跃表。

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

因为level是随机的,得到的skiplist可能是这样的,有些在第四层,有些在第三层,有些在第二层,有些在第一层。

image-20210825000752075

应用场景

顺序会动态变化的列表,例如百度热榜、微博热搜。

其他数据类型

BitMaps

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

image-20210825001027095

Hyperloglogs

Hyperloglogs:提供了一种不太精确的基数统计方法,用来统计一个集合中不重复的元素个数,比如统计网站的UV,或者应用的日活、月活,存在一定的误差。

Geo

用于存储经纬度

可以增加地址位置信息、获取地址位置信息、计算两个位置的距离、获取指定范围内的地理位置集合等等。

Streams

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

总结

数据结构总结

image-20210825001402299

编码转换总结

image-20210825001505598

应用场景总结

缓存提升热点数据的访问速度

共享数据

数据的存储和共享的问题

全局ID

分布式全局ID的生成方案(分库分表)

分布式锁进程间共享数据的原子操作保证

在线用户统计和计数

队列、栈

跨进程的队列/栈

消息队列

异步解耦的消息机制

服务注册与发现一RPC通信机制的服务协调中心(Dubbo支持Redis)

购物车

新浪/Twitter用户消息时间线

泡学抽奖逻辑(礼物、转发)

点赞、签到、打卡

商品标签

用户(商品)关注(推荐)模型电商产品筛选

排行榜

打赏

请我喝杯咖啡吧~

支付宝
微信