Redis

2022-02-20  本文已影响0人  zhemehao819

一、Redis基础

1. 概述

NoSQL(NoSQL = Not Only SQL),意思是不仅仅是SQL,泛指非关系型的数据库。

1.1 CAP理论

CAP理论图的核心是:一个分布式系统不可能同时很好的满足一致性,可用性和分区容错性这三个需求,最多只能同时较好的满足两个。因此,根据CAP原理将NoSQL数据库分成了满足CA原则、满足CP原则和满足AP原则三大类:

CA: 传统Oracle数据库
AP: 大多数网站架构的选择
CP: Redis、Mongodb

1.3 Redis特点

2. Redis五大数据类型

操作指令参考: http://redisdoc.com/
所有的key都为String类型,讨论数据类型是说的value的类型

2.1 字符串(String)

//=====================基本操作=======================//
//设置String
set key value
mset key1 value1 key2 value2...
//设置生命周期
setex key seconds value 

//得到String
get key 
mget key1 key2...

//删除String
del key

//向字符串的后面追加字符,如果有就补在后面,如果没有就新建
append key value

//=================String作为数值的操作===================//
//增长指令,只有当value为数字时才能增长
incr key  
incrby key increment  
incrbyfloat key increment 

//减少指令,有当value为数字时才能减少
decr key  
decrby key increment

指定生命周期

//设置数据的生命周期,单位 秒
setex key seconds value
//设置数据的生命周期,单位 毫秒
psetex key milliseconds value

2.2 列表(List)

//=====================基本操作=======================//
//添加修改数据,lpush为从左边添加,rpush为从右边添加
lpush key value1 value2 value3...
rpush key value1 value2 value3...

//查看数据, 从左边开始向右查看. 如果不知道list有多少个元素,end的值可以为-1,代表倒数第一个元素
//lpush先进的元素放在最后,rpush先进的元素放在最前面
lrange key start end
//得到长度
llen key
//取出对应索引的元素
lindex key index

//获取并移除元素(从list左边或者右边移除)
lpop key
rpop key


//规定时间内获取并移除数据,b=block,给定一个时间,如果在指定时间内放入了元素,就移除
blpop key1 key2... timeout
brpop key1 key2... timeout

//移除指定元素 count:移除的个数 value:移除的值。 移除多个相同元素时,从左边开始移除
lrem key count value

2.3 集合(Set)

//添加元素
sadd key member1 member2...

//查看元素
smembers key

//移除元素
srem key member

//查看元素个数
scard key

//查看某个元素是否存在
sismember key member
//从set中任意选出count个元素
srandmember key count

//从set中任意选出count个元素并移除
spop key count

//求两个集合的交集、并集、差集
sinter key1 key2...
sunion key1 key2...
sdiff key1 key2...

//求两个set的交集、并集、差集,并放入另一个set中
sinterstore destination key1 key2...
sunionstore destination key1 key2...
sdiffstore destination key1 key2...

//求指定元素从原集合放入目标集合中
smove source destination key

2.4 哈希(Hash)

//插入(如果已存在同名的field,会被覆盖)
hset key field value
hmset key field1 value1 field2 value2...
//插入(如果已存在同名的field,不会被覆盖)
hsetnx key field value

//取出
hget key field
hgetall key

//删除
hdel key field1 field2...

//获取field数量
hlen key

//查看是否存在
hexists key field

//获取哈希表中所有的字段名或字段值 
hkeys key
hvals key

//设置指定字段的数值数据增加指定范围的值 
hincrby key field increment 
hdecrby key field increment

2.5 有序集合Zset(sorted set)

//插入元素, 需要指定score(用于排序)
zadd key score1 member1 score2 member2

//查看元素(score升序), 当末尾添加withscore时,会将元素的score一起打印出来
zrange key start end (withscore)
//查看元素(score降序), 当末尾添加withscore时,会将元素的score一起打印出来
zrevrange key start end (withscore)

//移除元素
zrem key member1 member2...

//按条件获取数据, 其中offset为索引开始位置,count为获取的数目
zrangebyscore key min max [withscore] [limit offset count]
zrevrangebyscore key max min [withscore] [limit offset count]

//按条件移除元素
zremrangebyrank key start end
zremrangebysocre key min max
//按照从大到小的顺序移除count个值
zpopmax key [count]
//按照从小到大的顺序移除count个值
zpopmin key [count]

//获得元素个数
zcard key

//获得元素在范围内的个数
zcount min max

//求交集、并集并放入destination中, 其中numkey1为要去交集或并集集合的数目
zinterstore destination numkeys key1 key2...
zunionstore destination numkeys key1 key2...
//查看某个元素的索引(排名)
zrank key member
zrevrank key member

//查看某个元素索引的值
zscore key member
//增加某个元素索引的值
zincrby key increment member

3. Redis持久化

3.1 简介

什么是持久化

利用永久性存储介质将数据进行保存,在特定的时间将保存的数据进行恢复的工作机制称为持久化。

为什么要持久化

防止数据的意外丢失,确保数据安全性

持久化过程保存什么

3.2 RDB

RDB的触发有两种方式:

自动: 默认的RDB快照触发机制(可以在redis.conf中的SNAPSHOTTING中查看)

1分钟内改动了1万次
5分钟内改动了10万次
15分钟内改动了1次

默认的名字为:dump.rdb

手动: 命令save或者是bgsave
如果出现某个数据非常重要,无法等到自动备份,这时候我们可以通过这
个命令来进行手动备份

save:save时只管保存,其它全部阻塞
bgsave:Redis会在后台异步进行快照操作

如何恢复RDB

将备份文件 (dump.rdb) 移动到启动Redis服务的目录即可

优缺点

优势

劣势

停止RDB

使用下面的命令:
CONFIG SET save ""

小结

3.3 AOF

AOF概念

AOF写数据三种策略

AOF功能开启:

appendonly yes|no
appendfsync always|everysec|no

AOF自动重写

//触发重写的最小大小
auto-aof-rewrite-min-size size 
//触发重写须达到的最小百分比
auto-aof-rewrite-percentage percent
//当前.aof的文件大小
aof_current_size 
//基础文件大小
aof_base_size

4. Redis事务

基于特定条件的事务执行

分布式锁

注意:上述解决方案是一种设计概念,依赖规范保障,具有风险性

分布式锁加强

二、Redis应用

1. Redis分布式锁

商品超卖问题:

在多线程情况下会出现并发问题导致商品超卖问题。(多个线程同时获取到stock数量)

1)解决多线程并发问题

加锁解决多线程并发问题:

上述代码,在分布式环境情况下,每个环境的jvm不同(不同进程),synchronized锁不是同一把锁,此时还是会有并发问题。

2)解决多台服务部署应用导致并非问题

使用redis分布式锁解决,使用setnx + 超时时间:

其中:

上述代码还是有问题:
假设有3个客户端来请求这段代码,其中客户端A需要15秒钟才执行完,在执行第10秒的时候因为到了过期时间自动释放锁,此时客户端B拿到锁进来了,假设执行了5秒中B还正在执行中,此时客户端A执行到第43行去删除锁,此时删除的客户端拿的锁。那么,客户端C自然就可以再次拿锁进入。。。依次循环等于锁一直处于失效状态。

3)解决锁的误释放问题

问题的根本原因是自己设置的锁被别人释放掉了,可以设置一个客户端id来表示自己的锁。

4)释放锁的原子操作问题

当某一客户端a执行到46行时,卡了一下导致到了过期时间,客户端释放了锁,此时客户端B设置了锁进入程序,此时客户端A恢复,此时客户端执行47行删除的锁其实是客户端B的锁。

使用看门狗+锁续命来解决:弄一个监控线程轮训主线程是否还在执行,如果还在执行就给锁续命增加过期时间。

使用redisson框架来解决:

Redisson分布式锁实现原理:

Redlock实现原理(解决主从切换时锁失效的情况):

其实,分布式锁是和高并发是违背的,上述代码等同于串行执行。----分段加锁处理。

2. 缓存数据库双写不一致问题

情况1:

情况2:

解决方案:使用分布式锁,使得每个线程操作串行执行。

3. redis为什么这么快

1)内存数据库
2)单线程
3)底层的数据结构
4)全局维护了一个hash表。


三、Redis五大数据类型应用场景

1. String应用场景

2. Hash应用场景

3. List应用场景

比如:微信公共号的消息流。

4. Set应用场景

集合操作可实现共同关注模型、推荐模型(可能认识的人)。

5. Zset应用场景

四、Redis 底层的数据结构

Redis 底层的数据结构一共有 6 种,和数据类型对应关系如下图:

1.SDS

字符串在 Redis 中是很常用的,键值对中的键是字符串,值有时也是字符串。

Redis 使用 C 语言实现的,但是它没有直接使用 C 语言的 char* 字符数组来实现字符串,而是自己封装了一个名为简单动态字符串(simple dynamic string,SDS) 的数据结构来表示字符串。

1.1 C 语言字符串的缺陷

C 语言的字符串其实就是一个字符数组,即数组中每个元素是字符串中的一个字符。

比如,下图就是字符串“xiaolin”的 char* 字符数组的结构:

在 C 语言里,对字符串操作时,char * 指针只是指向字符数组的起始位置,而字符数组的结尾位置就用“\0”表示,意思是指字符串的结束

因此,C 语言标准库中字符串的操作函数,就通过判断字符是不是“\0”,如果不是说明字符串还没结束,可以继续操作,如果是则说明字符串结束了,停止操作。

举个例子,C 语言获取字符串长度的函数 strlen,就是通过字符数组中的每一个字符,并进行计数,等遇到字符为“\0”后,就会停止遍历,然后返回已经统计到的字符个数,即为字符串长度。下图显示了 strlen 函数的执行流程:

很明显,C 语言获取字符串长度操作的时间复杂度是 O(N)(*****这是一个可以改进的地方*)

C 语言的字符串用 “\0” 字符作为结尾标记有个缺陷。假设有个字符串中有个 “\0” 字符,这时在操作这个字符串时就会提早结束,比如 “xiao\0lin” 字符串,计算字符串长度的时候则会是 4。

还有,除了字符串中不能 “\0” 字符外,用 char* 字符串中的字符必须符合某种编码(比如ASCII)。

这些限制使得 C 语言的字符串只能保存文本数据,不能保存像图片、音频、视频文化这样的二进制数据(*****这也是一个可以改进的地方*****)

C 语言标准库中字符串的操作函数是很不安全的,对程序员很不友好,稍微一不注意,就会导致缓冲区溢出。

举个例子,strcat 函数是可以将两个字符串拼接在一起的。

c //将 src 字符串拼接到 dest 字符串后面 char *strcat(char dest, const char src);

C 语言的字符串是不会记录自身的缓冲区大小的,所以 strcat 函数假定程序员在执行这个函数时,已经为 dest 分配了足够多的内存,可以容纳 src 字符串中的所有内容,而一旦这个假定不成立,就会发生缓冲区溢出将可能会造成程序运行终止,(这是一个可以改进的地方)。

而且,strcat 函数和 strlen 函数类似,时间复杂度也很高,也都需要先通过遍历字符串才能得到目标字符串的末尾。然后对于 strcat 函数来说,还要再遍历源字符串才能完成追加,对字符串的操作效率不高。

通过以上的分析,可以得知 C 语言的字符串 不足之处以及可以改进的地方:

Redis 实现的 SDS 的结构就把上面这些问题解决了,接下来我们一起看看 Redis 是如何解决的。

1.2 SDS 结构设计

SDS 的数据结构:

结构中的每个成员变量分别介绍下:

总的来说,Redis 的 SDS 结构在原本字符数组之上,增加了三个元数据:len、alloc、flags,用来解决 C 语言字符串的缺陷。

1.3 节省内存空间

SDS 结构中有个 flags 成员变量,表示的是 SDS 类型。

Redos 一共设计了 5 种类型,分别是 sdshdr5、sdshdr8、sdshdr16、sdshdr32 和 sdshdr64。

这 5 种类型的主要区别就在于,它们数据结构中的 len 和 alloc 成员变量的数据类型不同之所以 SDS 设计不同类型的结构体,是为了能灵活保存不同大小的字符串,从而有效节省内存空间

2. 压缩列表

压缩列表是 Redis 数据类型为 list 和 hash 的底层实现之一。

压缩列表结构设计

压缩列表是 Redis 为了节约内存而开发的,它是由连续内存块组成的顺序型数据结构,有点类似于数组。不作过多讲解。

在有序集合zset的实现里面,跳跃表和压缩列表都有用到。
根据上面都描述,压缩列表的使用场景一般是短字符串,且元素个数不能太多。所以在zset里面有几个配置:
zset-max-ziplist-entries: 默认值128, 当元素个数小于这个值都时候使用压缩列表。否则使用跳跃表。
zset-max-ziplist-value:默认值64,当每个元素的字符串长度小于这个值的时候使用压缩列表,否则使用跳跃表。
满足这两个条件中的任意一个条件,就会转换到跳跃表。而转换到跳跃表之后,即便把元素删除,也不好回退到压缩列表。

3. 跳跃表

什么是跳跃表 ?

对于一个单链表来说, 即便链表中存储的数据是有序的, 我们想要随机查找一个数据,那么也只能从头到尾遍历链表节点, 这样查找的效率就会很低,是件复杂度为 O(n)

如果我们想要提高其查找效率, 可以考虑在链表上建索引的方式。每2个节点提取一个节点到上一级,我们把抽出来的那一级叫作索引。

image

可以看出, 加了一层索引以后, 查找一个节点需要遍历的节点数少了, 也就是说查找效率提升了,同理可以再加一层索引。

从图中我们可以看出,查找效率又有提升。在例子中我们的数据很少,当有大量的数据时,我们可以增加多级索引,其查找效率可以得到明显提升。

像这种链表加多级索引的结构,就是跳跃表!

Redis使用跳跃表作为有序集合键的底层实现之一,如果一个有序集合包含的元素数量比较多,又或者有序集合中元素的成员是比较长的字符串时, Redis就会使用跳跃表来作为有序集合健的底层实现。

这里我们需要思考一个问题——为什么元素数量比较多或者成员是比较长的字符串的时候Redis要使用跳跃表来实现?
从上面我们可以知道,跳跃表在链表的基础上增加了多级索引以提升查找的效率,但其是一个空间换时间的方案,必然会带来一个问题——索引是占内存的。原始链表中存储的有可能是很大的对象,而索引结点只需要存储关键值值和几个指针,并不需要存储对象,因此当节点本身比较大或者元素数量比较多的时候,其优势必然会被放大,而缺点则可以忽略

Redis中跳跃表的实现

Redis的跳跃表由zskiplistNode和skiplist两个结构定义,其中 zskiplistNode结构用于表示跳跃表节点,而 zskiplist结构则用于保存跳跃表节点的相关信息,比如节点的数量,以及指向表头节点和表尾节点的指针等

结构右方的是四个 zskiplistNode结构,该结构包含以下属性:

五、Redis常见问题

1. 缓存穿透、缓存击穿、缓存雪崩 如何解决?

缓存穿透

当缓存与DB中都不存在该数据时,如果此时发起恶意大量请求,会导致这个不存在的数据每次请求都会查询DB,导致DB压力过大,直接挂掉。

解决方案:
(1)对空值缓存,并设置较短的过期时间。
(2)设置访问白名单,使用布隆过滤器,分别通过多个哈希函数生成多个哈希值,然后将这些哈希值存到一个足够大的bitmap中,此时一个一定不存在的数据就会被这个bitmap拦截,从而减少了数据库的查询压力。
(3)业务过滤。

缓存击穿

某个热点key对应的数据存在,但在redis中过期,此时若有大量并发请求过来,这些请求发现缓存过期一般都会从后端DB加载数据并回设到缓存,这个时候大并发的请求可能会瞬间把后端DB压垮。

解决方案:
(1)设置热点数据永远不过期。
(2)使用分布式锁。

缓存雪崩

缓存中大批量的数据集中过期,从而导致查询数据量巨大,引起数据库压力过大甚至down机。

解决方案:
(1)将缓存失效时间分散开
(2)如果缓存数据库是分布式部署,将热点数据均匀分布在不同缓存中。
(3)设置热点数据永远不过期。

2. Redis为什么这么快?

(1)内存存储,没有磁盘IO上的开销
(2)单线程实现,避免了多个线程之间线程切换和锁资源争用的开销
(3)优化的数据结构
(4)非阻塞IO,Redis使用多路复用IO技术,在poll,epool,kqueue选择最优IO实现

上一篇下一篇

猜你喜欢

热点阅读