欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

【Redis】Redis持久化、集群方案、缓存穿透、缓存击穿、缓存雪崩和分布式锁

程序员文章站 2022-05-04 12:51:19
...

Redis其他知识点

一、Redis持久化方式

1.1 什么是Redis持久化

由于redis的值放在内存中,为防止突然断电等特殊情况的发生,需要对数据进行持久化备份。即将内存数据保存到硬盘。

1.2 Redis 持久化存储方式

1.2.1 RDB持久化

RDB 是以二进制文件,是在某个时间点将数据写入一个临时文件,持久化结束后,用这个临时文件替换上次持久化的文件,达到数据恢复。

**优点:**使用单独子进程来进行持久化,主进程不会进行任何 IO 操作,保证了 redis 的高性能

**缺点:**RDB 是间隔一段时间进行持久化,如果持久化之间 redis 发生故障,会发生数据丢失。所以这种 方式更适合数据要求不严谨的时候

这里说的这个执行数据写入到临时文件的时间点是可以通过配置来自己确定的,通过配置redis 在 n 秒 内如果超过 m 个 key 被修改这执行一次 RDB 操作。这个操作就类似于在这个时间点来保存一次 Redis 的所有数据,一次快照数据。所有这个持久化方法也通常叫做 snapshots。

RDB持久化模式是默认开启的

1.2.2 AOF持久化

Append-Only File,将“操作 + 数据”以格式化指令的方式追加到操作日志文件的尾部,在 append 操作 返回后(已经写入到文件或者将要写入),才进行实际的数据变更,“日志文件”保存了历史所有的操作过 程;当 server 需要数据恢复时,可以直接 replay 此日志文件,即可还原所有的操作过程。AOF 相对可靠,AOF 文件内容是字符串,非常容易阅读和解析。

**优点:**可以保持更高的数据完整性,如果设置追加 file 的时间是 1s,如果 redis 发生故障,最多会丢失 1s 的数据;且如果日志写入不完整支持 redis-check-aof 来进行日志修复;AOF 文件没被 rewrite 之前 (文件过大时会对命令进行合并重写),可以删除其中的某些命令(比如误操作的 flushall)。

**缺点:**AOF 文件比 RDB 文件大,且恢复速度慢。

我们可以简单的认为 AOF 就是日志文件,此文件只会记录“变更操作”(例如:set/del 等),如果 server 中持续的大量变更操作,将会导致 AOF 文件非常的庞大,意味着 server 失效后,数据恢复的过程将会 很长;事实上,一条数据经过多次变更,将会产生多条 AOF 记录,其实只要保存当前的状态,历史的 操作记录是可以抛弃的;因为 AOF 持久化模式还伴生了“AOF rewrite”。

AOF 的特性决定了它相对比较安全,如果你期望数据更少的丢失,那么可以采用 AOF 模式。如果 AOF 文件正在被写入时突然 server 失效,有可能导致文件的最后一次记录是不完整,你可以通过手工或者 程序的方式去检测并修正不完整的记录,以便通过 aof 文件恢复能够正常;同时需要提醒,如果你的 redis 持久化手段中有 aof,那么在 server 故障失效后再次启动前,需要检测 aof 文件的完整性。

AOF 模式默认是关闭的。

1.2.3 AOF与RDB区别

RDB:

RDB是在某个时间点将数据写入一个临时文件,持久化结束后,用这个临时文件替换上次持久化的文 件,达到数据恢复。

优点:使用单独子进程来进行持久化,主进程不会进行任何IO操作,保证了redis的高性能

缺点:RDB是间隔一段时间进行持久化,如果持久化之间redis发生故障,会发生数据丢失。所以这种方 式更适合数据要求不严谨的时候

AOF:

Append-only file,将“操作 + 数据”以格式化指令的方式追加到操作日志文件的尾部,在append操作返 回后(已经写入到文件或者即将写入),才进行实际的数据变更,“日志文件”保存了历史所有的操作过程; 当server需要数据恢复时,可以直接replay此日志文件,即可还原所有的操作过程。AOF相对可靠,它 和mysql中bin.log、apache.log、zookeeper中txn-log简直异曲同工。AOF文件内容是字符串,非常容易阅读和解析。

**优点:**可以保持更高的数据完整性,如果设置追加file的时间是1s,如果redis发生故障,最多会丢失1s 的数据;且如果日志写入不完整支持redis-check-aof来进行日志修复;AOF文件没被rewrite之前(文 件过大时会对命令进行合并重写),可以删除其中的某些命令(比如误操作的flushall)。

**缺点:**AOF文件比RDB文件大,且恢复速度慢。

二、Redis主从复制

持久化保证了即使redis服务重启也不会丢失数据,但是当redis服务器的硬盘损坏了可能会导致数据丢失,通过redis的主从复制机制就可以避免这种单点故障(单台服务器的故障)。

主redis中的数据和从上的数据保持实时同步,当主redis写入数据时通过主从复制机制复制到两个从服务上。

主从复制不会阻塞master,在同步数据时,master 可以继续处理client 请求。

主机master配置:无需配置

一般选用:一主两从或一主一从的配置。

【Redis】Redis持久化、集群方案、缓存穿透、缓存击穿、缓存雪崩和分布式锁

复制的过程原理

  • 当从库和主库建立MS(master slaver)关系后,会向主数据库发送SYNC命令;

  • 主库接收到SYNC命令后会开始在后台保存快照(RDB持久化过程),并将期间接收到的写命令缓存起来;

  • 快照完成后,主Redis会将快照文件和所有缓存的写命令发送给从Redis;

  • 从Redis接收到后,会载入快照文件并且执行收到的缓存命令;

  • 主Redis每当接收到写命令时就会将命令发送从Redis,保证数据的一致;【内部完成,所以不支持 客户端在从机人为写数据。】

当主Redis宕机时

从数据库(从机)中执行SLAVEOF NO ONE命令,断开主从关系并且提升为主库继续服务[把 一个从做为主机,这个时候新主机[之前的从机]就具备写入的能力]。当主服务器修好后,重新启动后,执行SLAVEOF命令,将其设置为从库[老主机设置为从机]。

但是这样有一个弊端,所有过程均需要手动执行,过程复杂,容易出错。

是否有更好的方案?

三、Redis哨兵模式

**哨兵模式:**给集群分配一个站岗的。

哨兵的作用就是对Redis系统的运行情况监控,它是一个独立进程,它的功能:

  • 1.监控主数据库和从数据库是否运行正常;

  • 2.主数据出现故障后自动将从数据库转化为主数据库;

如果主机宕,开启选举工作,选择一个从做主机。

环境准备:一主两从,启动任一从机时,启动哨兵模式。

虽然哨兵(sentinel) 释出为一个单独的可执行文件 redis-sentinel ,但实际上它只是一个运行在特殊模式下的 Redis 服务器,你可以在启动一个普通 Redis 服务器时通过给定 --sentinel 选项来启动哨兵 (sentinel)。

【Redis】Redis持久化、集群方案、缓存穿透、缓存击穿、缓存雪崩和分布式锁
哨兵主要是用来监听主服务器的,所以一般把哨兵部署在从服务器上监听。

总结

主从集群:主机有写入权限。从机没有,只有可读。

意外宕机方案:

手动恢复:人为重启服务器,主机宕,把从机设置为主机。

自动恢复:使用哨兵监控。自动切换主从。

四、Redis集群方案

4.1 redis-cluster架构图

架构细节:

(1) 所有的redis节点彼此互联(PING-PONG机制),内部使用二进制协议优化传输速度和带宽。

(2) 节点的fail是通过集群中超过半数的节点检测有效时整个集群才生效。

(3) 客户端与redis节点直连,不需要中间proxy层.客户端不需要连接集群所有节点,连接集群中任何一个可用节点即可。

(4) redis-cluster把所有的物理节点映射到[0-16383]slot上,cluster 负责维护node<->slot<->value。

Redis 集群中内置了 16384 个哈希槽,当需要在 Redis 集群中放置一个 key-value 时,redis 先对 key 使用 crc16 算法算出一个结果,然后把结果对 16384 求余数,这样每个 key 都会对应一个编号在 0- 16383 之间的哈希槽,redis 会根据节点数量大致均等的将哈希槽映射到不同的节点。

示例如下:

【Redis】Redis持久化、集群方案、缓存穿透、缓存击穿、缓存雪崩和分布式锁

4.2 redis-cluster投票:容错

【Redis】Redis持久化、集群方案、缓存穿透、缓存击穿、缓存雪崩和分布式锁

心跳机制

(1) 集群中所有master参与投票,如果半数以上master节点与其中一个master节点通信超过(clusternode-timeout),认为该master节点挂掉。

(2) 什么时候整个集群不可用(cluster_state:fail)?

  • 如果集群任意master挂掉,且当前master没有slave,则集群进入fail状态。也可以理解成集群的[0- 16383]slot映射不完全时进入fail状态。
  • 如果集群超过半数以上master挂掉,无论是否有slave,集群进入fail状态。

4.3 集群搭建步骤

4.3.1 安装redis

4.3.2 创建集群目录

[aaa@qq.com redis]# mkdir redis-cluster

4.3.3 在集群目录下创建节点目录

【Redis】Redis持久化、集群方案、缓存穿透、缓存击穿、缓存雪崩和分布式锁

搭建集群最少也得需要3台主机,如果每台主机再配置一台从机的话,则最少需要6台机器。 设计端口 如下:创建6个redis实例,需要端口号7001~7006。

[aaa@qq.com myapps]# cp redis/ redis-cluster/7001 -r
[aaa@qq.com myapps]# cd redis-cluster/7001
[aaa@qq.com 7001]# ll
drwxr-xr-x. 2 root root 4096 7月 1 10:22 bin
-rw-r--r--. 1 root root 3446 7月 1 10:22 dump.rdb
-rw-r--r--. 1 root root 41404 7月 1 10:22 redis.conf

4.3.4 如果存在持久化文件,则删除

[aaa@qq.com 7001]# rm -rf appendonly.aof dump.rdb

4.3.5 修改redis.conf配置文件,打开Cluster-enable yes

说明:cluster-enable 表示:是否支持集群,默认其实也是打开,保险起见检查一遍

【Redis】Redis持久化、集群方案、缓存穿透、缓存击穿、缓存雪崩和分布式锁

4.3.6 修改端口

【Redis】Redis持久化、集群方案、缓存穿透、缓存击穿、缓存雪崩和分布式锁

4.3.7 复制出7002-7006机器

[aaa@qq.com redis-cluster]# cp 7001/ 7002 -r
[aaa@qq.com redis-cluster]# cp 7001/ 7003 -r
[aaa@qq.com redis-cluster]# cp 7001/ 7004 -r
[aaa@qq.com redis-cluster]# cp 7001/ 7005 -r
[aaa@qq.com redis-cluster]# cp 7001/ 7006 -r
[aaa@qq.com redis-cluster]# ll
total 28
drwxr-xr-x. 3 root root 4096 Jun 2 00:02 7001
drwxr-xr-x. 3 root root 4096 Jun 2 00:02 7002
drwxr-xr-x. 3 root root 4096 Jun 2 00:02 7003
drwxr-xr-x. 3 root root 4096 Jun 2 00:03 7004
drwxr-xr-x. 3 root root 4096 Jun 2 00:03 7005
drwxr-xr-x. 3 root root 4096 Jun 2 00:03 7006
-rwxr-xr-x. 1 root root 3600 Jun 1 23:52 redis-trib.rb

4.3.8 修改7002-7006机器的端口

4.3.9 启动7001-7006这六台机器,写一个启动脚本:自定义shel脚本

[aaa@qq.com redis-cluster]# vi startall.sh

内容:

cd 7001 
./bin/redis-server ./redis.conf 
cd .. 
cd 7002 
./bin/redis-server ./redis.conf 
cd .. 
cd 7003 
./bin/redis-server ./redis.conf 
cd .. 
cd 7004 
./bin/redis-server ./redis.conf 
cd .. 
cd 7005
./bin/redis-server ./redis.conf 
cd .. 
cd 7006 
./bin/redis-server ./redis.conf 
cd ..

4.3.10 修改start-all.sh文件的权限

[aaa@qq.com redis-cluster]# chmod u+x startall.sh

4.3.11 启动所有的实例

[aaa@qq.com redis-cluster]# ./startall.sh

4.3.12 创建集群(关闭防火墙)

注意:在任意一台上运行,不要在每台机器上都运行,一台就够了 redis 5.0.5中使用redis-cli --cluster替 代redis-trib.rb,命令如下:

redis-cli --cluster create ip:port ip:port --cluster-replicas 1
[aaa@qq.com redis_cluster]# cd /home/admin/myapps/redis-cluster/7001/bin
[aaa@qq.com bin]# ./redis-cli --cluster create 192.168.122.130:7001 192.168.122.130:7002 192.168.122.130:7003 192.168.122.130:7004 192.168.122.130:7005 192.168.122.130:7006 --cluster-replicas 1
\>>> Creating cluster
Connecting to node 127.0.0.1:7001: OK
Connecting to node 127.0.0.1:7002: OK
Connecting to node 127.0.0.1:7003: OK
Connecting to node 127.0.0.1:7004: OK
Connecting to node 127.0.0.1:7005: OK
Connecting to node 127.0.0.1:7006: OK
\>>> Performing hash slots allocation on 6 nodes...
Using 3 masters:
127.0.0.1:7001
127.0.0.1:7002
127.0.0.1:7003
Adding replica 127.0.0.1:7004 to 127.0.0.1:7001
Adding replica 127.0.0.1:7005 to 127.0.0.1:7002
Adding replica 127.0.0.1:7006 to 127.0.0.1:7003
[OK] All 16384 slots covered.

4.3.13 连接集群

命令:-c:指定是集群连接

[aaa@qq.com 7001]# ./bin/redis-cli -h 127.0.0.1 -p 7001 -c
[aaa@qq.com 7001]# ./bin/redis-cli -h 127.0.0.1 -p 7001 -c
127.0.0.1:7001> set username java123
-> Redirected to slot [14315] located at 127.0.0.1:7003
OK

4.3.14 查看集群信息

127.0.0.1:7003> cluster info
cluster_state:ok
cluster_slots_assigned:16384
cluster_slots_ok:16384
cluster_slots_pfail:0
cluster_slots_fail:0
cluster_known_nodes:6
cluster_size:3
cluster_current_epoch:6
cluster_my_epoch:3
cluster_stats_messages_sent:1186
cluster_stats_messages_received:1186

4.3.15 查看集群中节点信息

127.0.0.1:7003> cluster nodes
713218b88321e5067fd8ad25c3bf7db88c878ccf 127.0.0.1:7003 myself,master - 0 0 3
connected 10923-16383
e7fb45e74f828b53ccd8b335f3ed587aa115b903 127.0.0.1:7001 master - 0 1498877677276
1 connected 0-5460
b1183545245b3a710a95d669d7bbcbb5e09896a0 127.0.0.1:7006 slave
713218b88321e5067fd8ad25c3bf7db88c878ccf 0 1498877679294 3 connected
8879c2ed9c141de70cb7d5fcb7d690ed8a200792 127.0.0.1:7005 slave
4a312b6fc90bfee187d43588ead99d83b407c892 0 1498877678285 5 connected
4a312b6fc90bfee187d43588ead99d83b407c892 127.0.0.1:7002 master - 0 1498877674248
2 connected 5461-10922
4f8c7455574e2f0aab1e2bb341eae319ac065039 127.0.0.1:7004 slave
e7fb45e74f828b53ccd8b335f3ed587aa115b903 0 1498877680308 4 connected

五、Redis的缓存穿透,缓存击穿,缓存雪崩问题

5.1 缓存的概念

什么是缓存?

广义的缓存就是在第一次加载某些可能会复用数据的时候,在加载数据的同时,将数据放到一个指定的地点做保存。再下次加载的时候,从这个指定地点去取数据。这里加缓存是有一个前提的,就是从这个地方取数据,比从数据源取数据要快的多。

java狭义一些的缓存,主要是指三大类

  • 虚拟机缓存(ehcache,JBoss Cache)
  • 分布式缓存(redis,memcache)
  • 数据库缓存正常来说,速度由上到下依次减慢

5.2 缓存雪崩

5.2.1 缓存雪崩产生的原因

缓存雪崩通俗简单的理解就是:由于原有缓存失效(或者数据未加载到缓存中),新缓存未到期间(缓 存正常从Redis中获取,如下图)所有原本应该访问缓存的请求都去查询数据库了,而对数据库CPU和 内存造成巨大压力,严重的会造成数据库宕机,造成系统的崩溃。

【Redis】Redis持久化、集群方案、缓存穿透、缓存击穿、缓存雪崩和分布式锁

缓存失效的时候如下图:

【Redis】Redis持久化、集群方案、缓存穿透、缓存击穿、缓存雪崩和分布式锁

5.2.2 解决方案

5.2.2.1:加锁排队

在缓存失效后,通过加锁或者队列来控制读数据库写缓存的线程数量。比如对某个key只允许一个线 程查询数据和写缓存,其他线程等待。虽然能够在一定的程度上缓解了数据库的压力但是与此同时又降 低了系统的吞吐量。

注意:加锁排队只是为了减轻数据库的压力,并没有提高系统吞吐量。假设在高并发下,缓存重建期间 key是锁着的,这是过来1000个请求999个都在阻塞的。同样会导致用户等待超时,这是个治标不治本 的方法。

5.2.2.2:设置key过期时间

分析用户的行为,不同的key,设置不同的过期时间,让缓存失效的时间点尽量均匀。

5.3 缓存穿透

缓存穿透是指用户查询数据,在数据库没有,自然在缓存中也不会有。这样就导致用户查询的时候, 在缓存中找不到,每次都要去数据库再查询一遍,然后返回空。这样请求就绕过缓存直接查数据库,这 也是经常提的缓存命中率问题。

5.3.1 解决方案

5.3.1.1 存放默认值

如果查询数据库也为空,直接设置一个默认值存放到缓存,这样第二次到缓冲中获取就有值了,而不 会继续访问数据库,这种办法最简单粗暴。

5.3.1.2 存放空结果

把空结果,也给缓存起来,这样下次同样的请求就可以直接返回空了,既可以避免当查询的值为空时 引起的缓存穿透。同时也可以单独设置个缓存区域存储空值,对要查询的key进行预先校验,然后再放 行给后面的正常缓存处理逻辑。

5.4 缓存击穿

对于一些设置了过期时间的key,如果这些key可能会在某些时间点被超高并发地访问,是一种非常“热 点”的数据。这个时候,需要考虑一个问题:缓存被“击穿”的问题,这个和缓存雪崩的区别在于这里针对某一key缓存,前者则是很多key。

5.4.1 热点key

某个key访问非常频繁,当key失效的时候有大量线程来构建缓存,导致负载增加,系统崩溃。

5.4.2 解决办法

5.4.2.1使用锁

单机用synchronized,lock等,分布式用分布式锁。

5.4.2.2 缓存过期时间不设置,而是设置在key对应的value里

如果检测到存的时间超过过期时间则异步更新缓存。

六、Redisd的分布式锁

6.1 使用分布式锁要满足的几个条件

  • 系统是一个分布式系统(关键是分布式,单机的可以使用ReentrantLock或者synchronized代码块 来实现)
  • 共享资源(各个系统访问同一个资源,资源的载体可能是传统关系型数据库或者NoSQL)
  • 同步访问(即有很多个进程同时访问同一个共享资源。)

6.2 什么是分布式锁?

  • 线程锁:主要用来给方法、代码块加锁。当某个方法或代码使用锁,在同一时刻仅有一个线程执行该方法或该代码段。线程锁只在同一JVM中有效果,因为线程锁的实现在根本上是依靠线程之间共享内存实现的,比如synchronized是共享对象头,显示锁Lock是共享某个变量(state)。
  • 进程锁:为了控制同一操作系统中多个进程访问某个共享资源,因为进程具有独立性,各个进程无法访 问其他进程的资源,因此无法通过synchronized等线程锁实现进程锁。
  • 分布式锁:当多个进程不在同一个系统中,用分布式锁控制多个进程对资源的访问。

6.3 应用的场景

线程间并发问题和进程间并发问题都是可以通过分布式锁解决的,但是强烈不建议这样做!因为采用分布式锁解决这些小问题是非常消耗资源的!分布式锁应该用来解决分布式情况下的多进程并发问题才是最合适的。

有这样一个情境,线程A和线程B都共享某个变量X。

如果是单机情况下(单JVM),线程之间共享内存,只要使用线程锁就可以解决并发问题。

如果是分布式情况下(多JVM),线程A和线程B很可能不是在同一JVM中,这样线程锁就无法起到作用 了,这时候就要用到分布式锁来解决。

6.4 使用redis的setNX命令实现分布式锁

分布式锁可以基于很多种方式实现,比如zookeeper、redis…。不管哪种方式,他的基本原理是不 变的:用一个状态值表示锁,对锁的占用和释放通过状态值来标识。

【Redis】Redis持久化、集群方案、缓存穿透、缓存击穿、缓存雪崩和分布式锁

6.4.1 实现的原理

Redis为单进程单线程模式,采用队列模式将并发访问变成串行访问,且多客户端对Redis的连接不存在竞争关系。redis的SETNX命令可以方便的实现分布式锁。

6.4.2 基本命令解析

6.4.2.1 setNX(SET if Not eXists)
SETNX key value

将 key 的值设为 value ,当且仅当 key 不存在。

若给定的 key 已经存在,则 SETNX 不做任何动作。

SETNX 是『SET if Not eXists』(如果不存在,则 SET)的简写

返回值:

设置成功,返回 1 。

设置失败,返回 0 。

例子:

redis> EXISTS job # job 不存在
(integer) 0
redis> SETNX job "programmer" # job 设置成功
(integer) 1
redis> SETNX job "code-farmer" # 尝试覆盖 job ,失败
(integer) 0
redis> GET job # 没有被覆盖
"programmer"

所以我们使用执行下面的命令SETNX可以用作加锁原语(locking primitive)。比如说,要对关键字(key) foo 加锁,客户端可以尝试以下方式:

SETNX lock.foo <current Unix time + lock timeout + 1>

如果 SETNX返回 1 ,说明客户端已经获得了锁,SETNX将键 lock.foo 的值设置为锁的超时时间(当前 时间 + 锁的有效时间)。 之后客户端可以通过 DEL lock.foo 来释放锁。

如果 SETNX返回 0 ,说明 key 已经被其他客户端上锁了。如果锁是非阻塞(non blocking lock)的,我 们可以选择返回调用,或者进入一个重试循环,直到成功获得锁或重试超时(timeout)。

6.4.2.2 getSET

先获取key对应的value值。若不存在则返回nil,然后将旧的value更新为新的value。

GETSET key value

将给定 key 的值设为 value ,并返回 key 的旧值(old value)。

当 key 存在但不是字符串类型时,返回一个错误。

返回值:

返回给定 key 的旧值[之前的值]。

当 key 没有旧值时,也即是, key 不存在时,返回 nil。

6.4.2.3 总结
  • 同一时刻只能有一个进程获取到锁。setnx
  • 释放锁:锁信息必须是会过期超时的,不能让一个线程长期占有一个锁而导致死锁; (最简单的方式就是del, 如果在删除之前死锁了。)

可以通过 DEL lock.foo 来释放锁。

如果 SETNX返回 0 ,说明 key 已经被其他客户端上锁了。如果锁是非阻塞(non blocking lock)的,我 们可以选择返回调用,或者进入一个重试循环,直到成功获得锁或重试超时(timeout)。