redis常用功能及其原理

单线程的 redis 为什么这么快

  • redis 中所有数据都在内存中,所有操作都是在内存中进行的,自然会快

  • 正因为使用的是单线程,所以不需要考虑加锁问题,也不会有 cpu 时间片的切换导致的性能损耗,cpu 的使用率非常高。而且现代 cpu 性能是很高的,一般 cpu io 达到瓶颈的场景很少,所以单线程够用了。

  • 采用了 IO 多路复用技术,让单个线程可以高效地处理多个连接请求

  • redis 中的数据结构是专门设计的,针对性能做了不少优化。比如 ziplist、渐进式 rehash 等

  • <其他原因>

要注意的是:这里说的单线程,指的是使用单线程处理客户端的请求。不代表redis中所有操作都是单线程的,比如 bg (background的意思) 开头的命令,会 fork 子进程来处理,不会阻塞主线程!

当然,使用单线程也有它的缺点,比如一旦出现一个非常耗时的任务,会阻塞整个 redis!还有一点时单线程无法充分发挥多核 cpu 的性能。

基础数据结构

redis所有的数据结构都是以唯一的 key 字符串作为名称,通过唯一 key 来获取 value,不同类型数据结构的差异在于 value 的类型

string

string 是 redis 中最简单也是最常用的数据结构。

redis 的字符串是动态字符串,它的内部结构是一个字符数组,类似于 java 中的 ArrayList,采用冗余分配空间的方式来减少内存的频繁分配。

redis 给当前字符串分配的实际空间 capacity 一般高于实际字符串长度 len,当字符串长度小于 1MB 时,扩容都是加倍现有的空间。如果长度超过 1MB,扩容时一次只会扩 1MB 的空间。字符串最大长度是 512 MB !

list

redis 里的列表类似于 java 里的 LinkedList,它是链表而不是数组。

所以,redis 里 list 的插入和删除操作非常快,但是索引定位很慢。

使用时可以通过 rpush 来添加数据,通过 lpop 来取数据。

快速列表 quicklist

redis 底层存储的不是简单的 linkedlist,而是quicklist,称为快速列表

普通的链表需要的指针空间很大,会浪费空间,还会加重内存的碎片化,所以 redis 使用了 ziplist压缩列表(ziplist 是一块连续的内存空间,元素之间没有冗余空间),将多个 ziplist 使用双向指针串起来组成链表,既满足快速的插入删除,又不浪费太多空间。

redis 的 list 可以用来做异步队列,把不是特别紧急的任务放到 list 中,然后另外一个线程从 list 取出任务进行后续处理。

hash

redis 里的 hash 类似于 java 中的 HashMap,内部存储很多键值对,而且也是由"数组+链表"来实现的。

当然,也不是完全一样,比如 redis 的 hash 的值只能是字符串,而且 rehash 方式不一样。

java 的 HashMap 在 rehash 时,会一次性全部 rehash,比较耗时。但是 redis 是单线程的,如果一个 hash 中内容很多,也一次性全部 rehash 的话,会阻塞服务,其他操作无法进行。所以 redis 为了追求高性能且不阻塞服务,采用了渐进式 rehash 策略

渐进式 rehash

rehash 时,保留新旧两个 hash 结构,查询时会同时查询两个 hash 结构,在后续的定时任务以及 hash 操作指令中,逐渐地将旧 hash 的内容迁移到新 hash 结构中,迁移完成后,就会使用新的 hash 结构了

set

redis 里的 hash 类似于 java 中的 HashSet,存储的值无序且唯一。

zset 有序列表

set 是无序的,而 zset 是一个 有序的 set,存储的值唯一且有序

zset 给每个 value 设置一个 score,表示这个 value 的排序权重。

> zadd books 9.0 "think in java"
(integer) 1
> zadd books 8/0 "redis深度历险"
(integer) 1
> zrange books 0 -1  # 按 score 正序查询
1) "redis深度历险"
2) "think in java"

跳跃列表

zset 的排序功能在内部是通过跳跃列表实现的。

既然要能够排序,那插入数据的时候,就需要快速定位到需要插入的位置,当然不可能逐一比较,这太慢了。我们可能会想到用二分查找,但二分查找的对象必须是数组。而 zset 是链表。为了解决这个问题,跳跃列表使用了一种层级机制

首先,所有元素都在最底层,也就是图中的 L0 层,然后在 L0 中随机(一半左右元素)选出几个代表,放到 L1 层 (是身兼数职),然后再从 L1 层的元素中选出代表放到 L2 层,以此类推,最大是 L31 层。

插入数据时,先从顶层开始定位,比较 score 的大小,然后一级级往下定位,直到找出插入的位置,是不是很像二分查找?

持久化

虽说redis操作的是内存,但是一般redis中缓存的数据量非常地大,如果由于机器宕机导致好不容易缓存好的大量数据丢失,那后果是很严重的,所以持久化功能必不可少。(当然,有些项目可能并不需要持久化,那也可以关闭)

redis的持久化主要有三种方案:RDB、AOF、以及混合持久化(RDB+AOF)

RDB (shapshot)

redis 定时自动将内存的数据快照保存到二进制文件中,默认是 dump.rdb。文件名和路径可以修改。在 redis 重启时

相关配置

# 配置RDB快照文件的名字
dbfilename "dump.rdb"

# 配置文件路径,默认当前路径
dir ./

# 定时配置
# 900秒内有1个key被更新了,就保存快照文件
# 300秒内有10个key被更新了,就保存快照文件
# 60秒内有10000个key被更新了,就保存快照文件
save 900 1
save 300 10
save 60 10000

上面的3个定时配置,相互之间是"或"的关系,只要满足其中一条,就会保存快照文件。

如果在设定的时间内没有达到要求,则前面的数量会被忽略。比如60秒内只有6000个数据更新了,那后面的60秒的数量会重新从0开始计算,而不是6000!

手动保存快照文件

手动保存快照文件可以通过 save 或者 bgsave 命令

save 命令会阻塞 redis,而 bgsave 是后台执行的,不会阻塞 redis。

redis根据配置自动保存快照文件,用的也是 bgsave

优缺点

优点:快照文件是二进制的,当redis重启时,通过快照文件来恢复内存速度很快

缺点:快照文件保存间隔比较长,所以会丢失的数据也会比较多

AOF (Append-Only file)

AOF持久化会将每一条对内存进行修改的指令都记录进 aof 文件中,安全性比较高,所以当同时开启 RDB 和 AOF 时,redis在恢复数据的时候,优先用的是 aof 文件来恢复!

相关配置

# 开启aof功能,默认是没开启的,需要修改为yes
appendonly yes

# aof文件名
appendfilename "appendonly.aof"

# redis将数据fsync到磁盘文件的间隔,有三种选项:always、everysec、no。
# 一般使用 everysec,它兼顾了速度和安全性。
appendfsync everysec

那么 "appendfsync everysec" 是什么意思呢?

首先,AOF文件是在磁盘上的,而 redis 的数据是在内存中的,redis 在将指令写入 aof 文件中的时候,会先将内容写到内核为文件分配的一个内核缓存中,剩下的问题就是如何从内核缓存中写到磁盘文件上,这里调用的是 Linux 的 glibc 提供的 fsync 函数。fsync 函数可以将指定的内容强行从内核缓存刷到磁盘!

上面 appendfsync 配置的就是调用 fsync 函数的频率!always 表示没次对 aof 日志进行写操作,都调用 fsync 函数。everysec 表示每秒调用一次。no 表示从不调用,由操作系统自己来决定何时同步到磁盘(这显然不安全)

fsync是一个磁盘IO操作,它很慢,所以不可能没条指令到调用一次 fsync(如果每条指令都调用一次,意味着每次对内存的修改,都要去访问一次磁盘,那 redis 也就没有高性能可言了)。所以一般都是用 everysec ,也就是每秒进行一次 fsync 操作。

这里其实可以看出,可能会有一秒的数据会丢失的。因为有可能才把数据写到内核缓存,还没来得及调用 fsync时,redis 就宕机了,那这部分数据是会丢失的。但这种方案基本已经是最优的了,是在数据安全性和性能之间做的一个折中。

AOF重写

aof 文件中,可能会有很多重复的数据,比如我们用 incr 命令对一个 key 自增了 10000 遍,那文件中会有 10000条对应的记录,但实际上我们只要知道现在值是多少就行了,无需关注过程。所以针对这个问题,redis 提供给了 aof 文件重写的功能。

AOF重写原理:通过 bgrewriteaof 指令对 AOF 日志进行瘦身。重新开一个子进程对内存进行遍历,转换成 redis 的操作指令,序列化到 一个新的 aof 文件中。序列化完成后再将操作期间发生的增量 AOF 日志追加到新的日志文件中,然后用新文件替代旧的 AOF 日志文件。

有两个相关的配置如下:

# aof文件至少要打到64mb,才会自动重写
auto-aof-rewrite-min-size 64mb 

# aof文件自上一次重写后文件大小增长了100%,才重写。这里的数字表示百分比
auto-aof-rewrite-percentage 100

aof 文件重写时,是启动子进程来执行的。所以不会影响主线程的运行。命令:bgrewriteaof。

优缺点

优点:AOF 相比 RDB 来说,数据安全性要高很多,即使丢失数据,也只会丢失1秒以内的数据

缺点:AOF文件比较大,在恢复数据的时候,相比RDB来说,会慢很多。

混合持久化

为了解决 RDB 方式丢失数据多,AOF 方式重启慢的问题,redis 设计了混合持久化的方式。

什么是混合持久化

如果开启混合持久化 ,AOF在重写时,将会重写这一刻之前的内存做RDB快照处理,并且RDB快照内容和增量的aof修改内存数据的命令存在一起,都写入新的AOF文件。新的文件一开始不叫appendonly.aof,等到重写完新的AOF文件才会进行改名,原子地覆盖原有AOF文件,完成新旧两个文件的替换。所以 aof 文件中,既有 RDB 快照,又有增量的数据命令。

这里的混合持久化,指的不是同时开启 RDB 以及 AOF 的功能。因为如果只是这两个同时开启的话,在重启时,redis 会使用  aof 文件进行数据恢复,实际上使用的只是 AOF 功能!

相关配置

# 开启混合持久化
aof-use-rdb-preamble yes

当使用了混合持久化,在 redis 重启时,会可以先加载 RDB 的内容,然后再重放增量 AOF 日志就可以完全替代之前的 AOF 全量文件重放,因此重启效率大幅提升。

推荐使用混合持久化,综合了 RDB 和 AOF 的优势!

架构种类

单机

单机没啥好讲的,比较简单。它的问题就是 redis 宕机后,缓存就彻底不可用了,对于大型系统来说是致命的。当然,对于小的系统,其实关系也不大,完全可以就用单机。毕竟系统越复杂,出问题的概率也越大,维护成本也越高。

主从

主从架构中,可以为 master 配置一个或多个 slave 节点。

有一点要说明:主从架构中如果 master 挂了,那 redis 也就不可访问了,slave 是不会自动升级为 master 的!需要手动修改 slave 的配置然后重启,并且需要改 java 代码中配置的 redis 地址!所以其实主从架构很少用。

相关配置

# 从 masterip:masterport 复制数据
replicaof <masterip> <masterport>

# slave 节点只读(默认就是yes)
replica-read-only yes

主从复制原理

一、全量复制

此图片来源于网络!

主从同步,同步的是 rdb 数据!而且就算 master 没有开启 rdb,也不影响,因为用的根本不是那个 rdb 文件,而是 master 为了这次同步,专门生成的快照文件。

流程说明:

  1. slave发起 psync 命令同步数据

  2. master 收到 psync 命令执行 bgsave 生成最小 rdb 快照数据,并且对最近数据进行缓存

  3. master 发送 rdb 数据到 slave

  4. master 发送【最近的缓存数据】到 slave

  5. slave 刷新本地的 rdb 数据

  6. slave 上根据 rdb 数据和缓存的最近数据,生成完整的 rdb 数据并放入内存。

二、增量复制

slave 一旦经历过全量同步后,后面的同步都是走增量同步了。

此图片来源于网络!

 流程说明:

  1. slave 和 master 连接断开后,重新连接到 master 时,发送 psync(offset) 命令。offset 是 slave中最新那条数据的偏移量

  2. master 收到 offset 之后,会判断这个 offset 是否在 master 的最近缓存范围内,如果在的话,master 会把 offset 之后的数据返回给 slave (例如 master 的最近缓存范围是 500 - 2000, offset 是 1000 ,那么会返回 1001 -2000)

  3. 如果 offset 不在最近缓存范围内,则会触发一次全量同步!

哨兵

哨兵是一种高可用架构

需要注意的是,哨兵不会提供 redis 服务,提供服务还是靠的 master 和 slave。哨兵是用来监控 redis 节点的。

架构图

哨兵通常会配置为集群(配置文件中配置需要监听的 master 节点)。哨兵集群会监听主从架构中各个节点,当有半数以上的哨兵发现 master 节点挂了,会触发选举,从 slave 节点中选举出一个,改为 master,同时会修改此 slave 节点的配置文件。同时会修改原 master 节点的配置文件,降级为 slave。其实它做的是,就是主从架构中,当 master 挂了之后,人工做的修改节点配置然后重启的事情。有了哨兵,就不需要人为介入了,仿佛什么事也没发生。

相关配置

# 哨兵监听的 master ip+port 。后面的数字表示有多少个 sentinel 任务 master 失效时,
# 才真的算 master 失效(值一般为:sentinel总数/2 + 1,所以如果有3个哨兵,这里就是 2)
sentinel  monitor  mymaster  <masterip> <masterport>  2

客户端连接时,是连接哨兵,然后哨兵会找出 master 节点,返回给客户端,客户端将它放到内存中缓存起来。当 master 节点挂掉,哨兵重新选举新的 master,在新的 master 还没被选举出来之前,客户端内存中的 master 还是旧的,此时访问 redis 会失败(内部选举是需要一定时间的)。哨兵选举出新的 master 之后,会更新客户端内存中的 master,下次客户端再访问时就能成功了。

局限性

只有一个主节点对外提供服务,很难支撑比较大的系统,内存也是不够。

哨兵是用来监控主从节点的,对于节点的水平扩容,并没有帮助。

在进行主从切换时,会有访问瞬断的情况,在瞬断期间无法访问 redis。

集群(核心)

redis 集群是作者自己提供的集群化方案,相比哨兵模式来说,强大很多。

redis 集群能很方便地支持集群的水平扩容,而且很大程度了解决了哨兵模式的访问瞬断问题(没有完全解决)

架构图

架构说明

redis 集群由多个小的主从结构组成,各个主从结构之间数据是独立的

各个节点之间相互发送心跳来保持通信,这样可以及时发现失联的 master,以便及时进行主从切换。

数据分片原理

redis 集群一共有 16384 个槽,近乎平均分配个集群中各个主从结构。

客户端发起请求时,redis 对 key 做 CRC16 算法,然后对 16384 取模,得到槽位 slot

每个 master 有分配给它的槽位段,所以根据计算出来的槽位,就找到对应的 master,再进行操作。所以说 redis 集群中,数据是分片存储的,各个主从结构中的数据不一样!

节点之间的通信 (gossip 协议)

每个节点都会频繁给其他节点发送 ping,带上自己的状态和维护的集群元数据。

gossip 也有自己的通信接口,默认是主节点的 端口+10000,比如 master 端口是 6379,那gossip端口就是 16379。所以搭建集群的时候,要注意防火墙中要打开 gossip 的通信端口!

何时进行主从切换

相关配置参数 cluster-node-timeout(默认15秒,可以根据实际需求修改)

节点之间通信时,发现某个 master 失联,并且失联时间已经达到 cluster-node-timeout 时,就认为 master 挂了,然后进行主从切换。

有时候网络会有短暂的波动,很久就恢复,这种情况就不需要去进行主从切换。所以通过此参数,可以排除是网络波动。

选举原理

  1. 节点之间通过 gossip 协议通信时,slave 如果发现 master 节点挂了,会将自己记录的集群 currentEpoch (集群经历的选举周期次数)加 1,然后广播发送 FAILOVER_AUTH_REQUEST 给集群中的其他节点

  2. 其他节点收到此消息,只有 master 节点会响应,判断请求是否合法,然后发送 FAILOVER_AUTH_ACK,在每个选举周期 epoch 中只会发送一次 ack,也就是说一个主从架构中的多个 slave 都发了消息的话,master 只会响应第一个收到的 slave,先到先得

  3. slave 收集其他 master 发送的 FAILOVER_AUTH_ACK,一旦收到超过半数的 master 返回的 ack,就升级为新的 master,再广播通知集群中其他节点,原先的 master 节点恢复后,会降级为 slave。(所以说,集群里至少需要3个主节点,挂了一个 master,还能有 2 个 master 来发送 ack)

slave 节点在发现 master 失联时,不是立即发送的,而是会等一会,以免其他 master 还没发现这个 master 失联(会拒绝且不发送 ack)

某个 master 挂了,在重新选举出新的 master 之前,这个主从架构分配到的槽位所包含的 key,操作会有影响,其他的槽位还是可以正常使用,这也就是前面提到的访问瞬断问题,使用 redis 集群时,访问瞬断现象只会发生在某一个主从架构中,比起哨兵模式的整个 redis 访问瞬断,会好很多!

集群中 master 数量一般使用奇数个,以 5 和 6 为例,挂了一个 master 后,都是收到 3 个 master 的 ack,就可以重新进行主从切换,所以使用 5 个 master,可以节省一些资源!

集群是否完整提供服务

当负责一个插槽的 master 挂了,并且它的 slave 也全都挂了,那集群还允许使用吗?

这是通过配置项 cluster-require-full-coverage 来控制的。如果配置为 no,则一个主从全下线时,集群中其他主从仍然可以正常提供服务。如果配置为 yes,则整个 redis 不能提供服务! 

水平扩容

集群模式相比哨兵模式好的很重要一点就是方便扩容。

在现有的 redis 集群中添加新的 master ,使用的是 add-node 命令

# add-node 后面跟的是需要新增的节点的ip+port,master 后面的是集群中人任意一个存活着的节点
./redis-cli -a <password> --cluster add-node 127.0.0.1:6380  127.0.0.1:6379

刚添加进去的节点,会被认为是 master,然后里面是没有数据的,因为它现在还没有被分配槽位!所以 key 是定位不到这个 master 的。其他 master 的数据也不可能主动的往这上面迁移。需要主动执行 reshard 命令!

# reshard命令用于迁移槽位,需要在集群中可用的master节点上执行此命令
# 执行时会让我们选择需要分配的槽位,以及需要分配在哪个 master 节点上
./redis-cli -a <password> --cluster reshard 127.0.0.1:6380

在新加的 master 节点上,再增加个 slave 节点的话,也是先执行 add-node 命令。新节点刚加进去的时候,它也是 master,这时候需要进到它的客户端中,并执行 replicate 命令,指定它想要加入的 master 节点的 ID,它就变为 slave 节点了

删除节点

删除节点是通过 del-node 命令操作的。但是要注意,删除节点之前,需要先 reshard 一把,把这个 master 拥有的槽位分配给别的 master,再删除此节点。毕竟删除节点可以,但不能丢失数据对吧!

数据不均匀问题

由于对 key 分配槽位,是通过 hash 算法来实现的,那么这必然有个问题,就是某些业务的数据量特别大,比如 user,它可能有几千万条数据,而有的 key 的记录可能就很少,所以会造成数据的严重不均衡,某的 master 压力特别的,有的又特别空闲。方案主要有垂直扩容和水平扩容。

  • 垂直扩容:对于某些特定的 key 所处的节点加大内存空间

  • 水平扩容:对 key 进行一定的设计,使得他们被分配到不同的 master 上

分布式锁

互联网应用中,有很多并发的场景,比如秒杀活动,假设只有1000件库存,但是有几十万的人同时在抢,如果仅仅通过数据库,从查询库存并校验,然后经历一些列操作最终扣减库存,需要花费不少时间,这期间可能几百上千的请求就过来了,很容易就超卖了。

还有简单的场景是定时器,单机部署的时候并不会有问题,但是往往我们的系统会有多个实例,通过 nginx 或者 kubernetes 等进行负载均衡,那如果每个实例到点时都执行定时器,对于那些不支持幂等性的业务来说,那影响也是很大的。

上述的场景,是不能用 java 的多线程(比如 synchronized 关键字)来解决的,因为 java 多线程适用于单实例,对于多实例场景无能为力。所以需要用 redis 的分布式锁来解决。

setnx + expire

以多实例下执行定时器为例。

redis 的 setnx 命令,只有在 key 不存在时才能插入成功,当插入成功时,我们才执行定时器,这样可以保证只有一个实例上的定时器可以执行成功。定时器执行结束后释放锁(在 finally 中释放)

当时光 setnx 有个最大的问题,如果 setnx 成功了,但是执行过程中机器宕机了,那么锁会无法释放,导致"死锁",那是致命的。

所以需要给锁加一个超时时间,例如10秒钟,这样就是服务器宕机了,也能保证锁会自动释放,后续还能使用。但是呢,还有问题,就是 setnx 成功了,还没等超时时间设置成功,服务器就宕机了(虽然这概率极其低,但也是存在的不是么),所以 redis 提供了 setnx + expire 的原子操作,使得超时时间必然能设置成功! 

# java 中对 setnx + expire 的原子操作的封装
Boolean setIfAbsent(K key, V value, long expireTime, TimeUnit timeUnit)

这里超时时间的设置,需要根据具体场景了设定。但其实问题还是有的,这个超时时间设置多少都不会太完美(异常情况太多,可能突然服务器非常卡,导致本该几秒内就结束的操作,花了1分钟还没跑完!),如果在设定的超时时间内,代码还没执行完成,这个时候却把锁释放了,对定期器来讲或许影响还不大,但如果是扣减库存那种场景,却是致命的,会导致超卖。所以有个方案是对锁进行续命。

锁续命

锁续命指的是 setnx expire 成功后,可以额外开启一个线程监听这个锁的状态,比如超时时间30秒,那子线程中可以每隔10秒去校验一次锁还在不在,如果还在,那么将锁的超时时间重新设为 30秒。如果锁已经不在了,那么子线程就销毁。

这个思路还是比较巧妙的,可以解决我们能想到的几乎所有问题。但一般我们不会在代码中直接写锁续命的功能,因为一看这流程就很复杂,而且要写到没有 bug,恐怕不大容易!这个时候就可以使用市面上已经有的开源框架:redisson。

最彻底的解决方案:redisson

redisson 的分布式锁,实现了一套完整的 setnx + expire + 锁续命的功能,且经历了大厂业务的考验,底层代码十分健壮,是最彻底的解决方案。

使用起来很简单,代码如下:

public void execute () {
	String key = "redis_key";
    // 获取锁对象
	RLock rlock = redisson.getLock(key);
	try {
		// 加锁,实现锁续命
		rlock.lock(); 
	} finally {
		// 解锁
		rlock.unlock(); 
	}
}

redisson 默认设了超时时间为30秒,然后每隔10秒监测一次,并实现了锁续命的功能。

内部大量运用了 lua 脚本,可以保证原子性。

应用问题

缓存穿透

缓存穿透是指缓存和数据库中都没有的数据

查询一个不存在的数据 ,一般查询不到时不会往 redis 里存,所以黑客可能利用这种情况 ,造几个一定不存在的数据,发送大量的请求,每个请求都能穿透缓存层,直接访问到后端数据库,把数据库搞垮

解决方案

  1. 根据不存在的 key 查询不到数据,也保存到缓存中去,值为null,下次再根据这个 key 查询时,就能在缓存层面返回空。可以设置一个过期时间,毕竟这是垃圾数据,不应该一直保留在缓存中。

  2. 布隆过滤器

    布隆过滤器用于判断一个 key 存不存在。它是一个大型的位数组(数组里的值只有0和1)和几个不一样的 hash 函数,且它的 hash 函数散列值算的很均匀。

    布隆过滤器用几个 hash 函数对 key 做 hash 运算然后取模,算出来是几,就将数组的第几个元素设置为 1,每次 hash 运算都会进行设置。那么判断一个 key 就简单了,只要对需要判断的 key 做相同的 hash 运算,只要有一个函数算出来的位置上是 0,那就说明这个 key 值一定不存在!

    布隆过滤器的特点:布隆过滤器返回不存在,那一定是不存在。布隆过滤器返回存在,那 key 可能存在,也可能不存在(不存在的可能性比较小)。

    所以我们使用布隆过滤器,可以过滤掉大量的非法请求。黑客可能造了大量不同的不存在的 key,如果都存在缓存中,那么需要的空间是很大的,使用布隆过滤器可以很好地进行过滤!

缓存击穿

缓存击穿是指:缓存中没有但是数据库中存在

比如突然来了某个热点,此时缓存中没有,但是来了大量数据,都直接访问数据库

解决方案

  1. 设置热点数据永不过期

  2. 限流,降级等。或者使用分布式锁也行,加锁失败后sleep会重新再查询

​​​缓存雪崩

缓存雪崩是指由于缓存层不可用导致整个系统雪崩,无法正常提供服务。

导致缓存雪崩的场景比较多,这里大概列几点

  1. redis 挂掉了,请求全部打到后端数据库,导致数据库崩溃,最终所有服务都无法接收请求

  2. 请求量太大,当前 redis 的 QPS 不足以支持这个大访问量,多余的请求超时,可能就访问后端了,导致后端崩溃

  3. 大量 key 同时失效(比如大促活动时,运维人员事先在 redis 中初始化好 key),导致短时间内请求全部放到到后端,导致后端崩溃

  4. 其他场景

解决缓存雪崩主要有下面几个方法(场景比较多,对应的解决方法也比较多,不止这里类列出的):

  1. 尽量保证缓存服务的高可用,比如使用哨兵或集群。当然,最好是集群。

  2. 依赖其他组件对请求限流并降级,有条件的可以多加几层限流,减少 redis 层的压力。

  3. 对于大量 key 同时失效的问题,可以对 key 设置随机的超时时间,这样缓存失效的时间就分散开来了

bigkey的危害

对于那种 value 特别大的 key,我们称之为 bigkey。比如用一个 hash 存储几千万个数据,那它显然就是 bigkey,bigkey有很多危害,平常使用时要尽量避免!

  • 导致 redis 阻塞

    redis 是单线程的,操作一个 bigkey,会导致其他操作长时间阻塞

  • 导致网络拥塞

    网络带宽是有限的,如果频繁访问一个 bigkey,那就算 redis 性能跟得上,但是 value 过大,再加上请求过多,那很可能会超出网络带宽,性能瓶颈就卡在带宽上了

  • 过期删除导致阻塞

    如果对一个 bigkey 设置了过期时间,那到期自动删除时,如果没有设置异步删除,那也可能导致 redis 阻塞。手动删除 bigkey 时,建议使用 scan 方式。

​​​​​lua脚本

是一种脚本语言,允许开发者使用lua语言编写脚本,一起传到 redis 里执行。

主要有以下几点好处:

1、原子操作:一个脚本中可能包含多条命令,但是lua脚本可以保证这些命令要么都成功,要么都失败,失败后会回滚。所以这也显得 redis 的事务功能比较鸡肋,一般要用事务的话,就直接用 lua 脚本了。

2、减少网络开销:本来每个操作都要有一次网络请求,现在可以多次操作只用一次网络请求。

lua 脚本固然厉害,但是平时使用 redis 的过程中,也基本不会去用的。因为 redis 是单线程的,一旦 lua 脚本使用不当,会导致其他对内存的操作全部卡住!

Springboot 接入

在 springboot 中使用 redis 非常简单,只要引入相应的 maven 坐标,配置 ip、端口、密码,然后就能通过 RedisTemplate 或者 StringRedisTemplate 来操作了。

注意:最好不要同时使用 RedisTemplate 和 StringRedisTemplate!因为两者的序列化方式不同,它们的数据是不通的。比如用 RedisTemplate 设置的数据,通过 StringRedisTemplate 来取的话,取出来的会是 null!

# maven 坐标
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

# application.yml 中配置 redis, 还有其他参数,根据实际情况配置
spring:
  redis:
    host: 127.0.0.1
    port: 6379
    password: myPassword
    database: 0

# java 代码
@SpringBootTest
public class RedisTest {
    @Autowired
    private RedisTemplate redisTemplate;

    @Test
    public void testTemplate(){
        redisTemplate.opsForValue().set("name", "xujingyi");
        System.out.println(redisTemplate.opsForValue().get("name"));
    }
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值