在我们的日常的 Java Web 开发过程中,都是使用数据库来对数据进行存储,提供给系统中的业务进行 CRUD 的一系列操作。可在我们涉及到大数据量和高并发的场景下时,只是使用数据库来处理数据的性能弊端暴露无遗,甚至极其容易就能造成数据库瘫痪,继而导致系统停止服务,从而对服务群体造成严重影响。为了克服以上问题,我们通常使用 Redis 来保证系统的稳定性和可用性。
一、Redis 简介
Redis(全称:Remote Dictionary Server 远程字典服务)是一个开源 Key-Value 数据库,并提供多种语言的 API。
Redis 与其他的 Key-Value 数据库(如 Memcached)相比,有以下几个优势:
- 读写高性能:Redis 能读的速度是 110000 次/s,写的速度是 81000 次/s 。
- 多数据类型:Redis 不仅仅支持 Key-Value 类型的数据,同时还支持 List、Set、Hash、Zset 等数据结构。
- 数据持久化:Redis 可将内存中的数据持久化至硬盘上,对数据的备份和恢复起到良好的作用。
- 原子性操作:Redis 的所有操作都是原子性的。
- 多语言支持:Redis 可以和多种语言进行集成。
......
二、应用场景
Redis 一般用作系统缓存、限流以及保证数据的一致性。
缓存
使用 Redis 作为缓存的读取逻辑如图所示(图源来自:Redis【入门】就这一篇!):
读取过程:
- 用户查询数据时,先去查询 redis 中的数据。
- 如果 redis 中存在该数据,直接返回给用户。
- 如果 redis 中不存在该数据,后台业务查询数据库。
- 将从数据库中查询到的数据写入 redis 中并返回给用户。
缓存穿透
缓存穿透,是指查询一个一定不存在的数据。
场景:一般情况下遭遇黑客攻击时可能会出现这种问题,以高频率的方式去查询一个不存在的数据。这些请求会全部落到数据库上,从而对数据造成极大的压力。
解决方案:通过在 redis 中存放空值对象(即将从数据库中查询所得到的空值对象作为 value 写入 reids 中并为其设置过期时间)可以解决这个问题。
缓存雪崩
缓存雪崩,是指在某一个时间段,缓存集中失效。
场景:大量缓存数据集中过期或者某一个 redis 服务节点宕机,会导致大量的请求落到数据库上,造成服务器压力。
解决方法:
- 对热门数据设置较长的过期时间(也可以不设置过期时间),对冷门数据设置较短的过期时间,可避免因为缓存数据过期而造成的缓存雪崩。
- 通过主从复制对 redis 中的缓存数据进行备份,可以迅速代替宕机的 redis 节点进行工作,在一定程度上可以解决因为 redis 服务节点宕机而造成的缓存雪崩。
缓存击穿
缓存击穿,是指一个 key 非常热点,在不停的扛着大并发,大并发集中对这一个点进行访问,当这个 key 在失效的瞬间,持续的大并发就穿破缓存,直接请求数据库,就像在一个屏障上凿开了一个洞。
场景:访问某些非常热门的数据时,该数据过期了,大量请求落到数据库上造成服务器压力。
解决方案:对这些非常热门的数据不设置过期时间。
三、数据结构
Redis 支持的数据结构有 String、List、Set、Hash、Zset 五种。
String
String(字符串)类型是 Redis 中最为常用的数据类型,主要存储字符串类型的数据。
String 类型最大能储存 512M 的数据。
List
List 存储的是 list 集合数据,集合中允许重复元素。
Set
Set 同 List,但是不允许重复元素。
Hash
Hash 是一个键值对集合。
Hash 存储的是类似于 Map 结构的数据。
Zset(Sorted Set)
Zset 和 Set 一样,不允许重复的成员。
Zset 中的每个元素都都会有一个 score, 元素根据 score 进行排序。
Zset 可用于实现简单的延迟队列。
四、持久化
Redis 提供了两种持久化方式,RDB 和 AOF
RDB(Redis DataBase)
在指定的时间间隔内将内存中的数据集快照写入磁盘,也就是 Snapshot 快照,它恢复时是加载快照文件,将数据写入缓存中。
Redis 会单独创建一个子进程来进行持久化,会先将数据写入到一个临时文件中,待持久化过程都结束了,再用这个临时文件替换上次持久化好的文件。整个过程中,主进程是不进行任何 IO 操作的,这就确保了极高的性能。
如果需要进行大规模数据的恢复,且对于数据恢复的完整性不是非常敏感,那 RDB 方式要比 AOF 方式更加的高效。RDB 的缺点是最后一次持久化后产生的数据可能丢失。
RDB 配置:
# 修改 RDB 文件名称
dbfilename dump.rdb
# 修改 RDB 文件保存路径
dir /redis/data_save
# 修改 RDB 保存策略
save 900 1
save 300 10
save 60 10000
# 当 Redis 无法持久化至磁盘时,关闭Redis的写操作
stop-writes-on-bgsave-error yes
# 进行 rdb 保存时,压缩文件
rdbcompression yes
# 在存储快照后,还可以让Redis使用CRC64算法来进行数据校验,损耗性能,建议关闭
rdbchecksum no
RDB 的优点:
- 使用较少的磁盘空间
- 恢复方式简单,速度较快
RDB 的缺点:
- 数据量过大时,会损耗性能。
- 由于是每隔一段时间进行一次 RDB 持久化操作,服务节点意外宕机的情况下,会丢失最后一次持久化操作之后的所有数据。
AOF(Append Of File)
以日志的形式来记录每个写操作,只能追加文件但不可以修改文件内容,Redis 重启时根据日志文件的内容将写指令从前到后执行一次以完成数据的恢复工作。
AOF 配置:
# 开启 aof 持久化
appendonly yes
# 指定 aof 文件名称
appendfilename "redis_data.aof"
# aof 文件存放路径同rdb
dir /redis/data_save
# 设置 aof 同步频率
# 始终同步,redis的每进行一次操作都会记录到 aof 文件中
# appendsync always
# 每秒同步一次,redis每隔一秒对这一秒内所有的redis操作记录到 aof 文件中
appendsync everysec
# 关闭 aof 同步
# appendsync no
# 配置 aof 重写机制
auto-aof-rewirte-percentage 100
auto-aof-rewrite-min-size 64mb
AOF 的优点:
- 备份机制更稳健,丢失数据概率更低。
- AOF 是日志文件,可以通过改文件找出操作过程中的一些问题并予以解决。
AOF 的缺点:
- 占用更多的磁盘空间。
- 备份和恢复的速度较慢。
- 每次读写都同步的话,有一定的性能压力。
AOF 和 RDB 的选择:
- 对数据不敏感(允许存在数据丢失),优先选择 RDB。
- 对数据敏感的情况下,同时开启 RDB 和 AOF。
- 只是单纯的用作内存和缓存,可以不开启持久化。
五、Redis 主从复制
主从复制,就是主机数据更新后根据配置和策略,自动同步到备机的 master/slaver 机制,Master 以写为主,Slaver 以读为主。
主从关系配置
通过 info replication
命令查看当前 redis 节点的主从复制相关信息。
通过执行 slaveof ip port
命令设置当前 redis 节点为指定 redis 节点的从服务节点。
主从复制过程及原理
过程:
- 当 Slaver 连接上 Master 后,Slaver 向 Master 发送 sync 指令。
- Master 接收到 Slaver 的 sync 指令后,立即进行数据持久化操作,生成 RDB 文件。
- Master 将 RDB 文件发送给 Slaver。
- Slaver 接收到 Master 发送过来的 RDB 文件后,进行全盘加载及复制操作。
- 之后再 Master 中的所有写操作都会立刻发送给 Slaver , Slaver 执行相同的操作来保证主从数据一致。
Slaver 升级为 Master :
当 Master 宕机时,可在 Slaver 节点执行 slaveof no one
命令将当前 Slaver 节点升级为 Master 节点。
Redis 哨兵(Sentinel)模式
能够后台监控 Master 节点是否出现故障,如果故障了通过投票机制将 Slaver 自动升级为 Master。
配置 Sentinel
在 Redis 安装目录下有一个 sentinel.conf 文件,对改文件进行如下修改
启动哨兵
执行 redis-sentinel /myredis/sentinel.conf
当 Master 宕机后,当前 Slaver 会直接替换为 Master, 之前的 Master 重启后会成为当前节点的 Slaver 。
六、Redis 集群
Redis 集群相关知识还未有过实际应用,之后完善。
七、Redis 事务
Redis 通过 MULTI
、EXEC
、DISCARD
、WATCH
等命令来实现事务(transaction)功能。
事务提供了一种将多个命令请求打包,然后一次性、按顺序地执行多个命令的机制,并且在事务执行期间,服务器不会中断事务而去执行其他客户端命令。
MULTI 命令
执行该命令表示开启 redis 事务,之后的所有操作都会进入事务队列。
127.0.0.1:6379>> MULTI
OK
127.0.0.1:6379>> set name Redis;
QUEUED
127.0.0.1:6379>> get name;
QUEUED
127.0.0.1:6379>> set language Java;
QUEUED
127.0.0.1:6379>> get language;
QUEUED
EXEC 命令
发送该命令后,redis 服务器会立即执行 事务队列 中保存的所有命令。
127.0.0.1:6379> EXEC
OK
"Redis"
OK
"Java"
WATCH 命令
WATCH
命令是一个乐观锁,它可以在 EXEC
命令执行前,监视任意数量的数据库键,并在 EXEC
命令执行时,检查被监视的键是否存在被修改过的,如果存在,服务器将拒绝执行事务,并向客户端返回代表事务执行失败的空回复。
127.0.0.1:6379> WATCH name
OK
127.0.0.1:6379> MULTI
OK
127.0.0.1:6379> set name HelloWorld
QUEUED
127.0.0.1:6379> EXEC
(nil)
八、Redis 内存淘汰策略
当 Redis 内存不足时,会采用淘汰策略删除部分缓存。从 Redis4.0 版本之后,Redis 有以下 8 中内存淘汰策略。
- volatile-lru:从设置了过期时间的键集合中淘汰最久没有使用的键
- volatile-lfu:从设置了过期时间的键中淘汰使用频率最少的键
- volatile-random:从设置了过期键的集合中随机淘汰
- volatile-ttl:从设置了过期时间的键中淘汰马上就要过期的键
- allkeys-lru:从所有键中淘汰最久没有使用的键
- allkeys-lfu:从所有键中淘汰使用频率最少的键
- allkeys-random:从所有键中随机淘汰
- noeviction:不会淘汰任何键,但是会报错
LRU(Least Recently Used)
最近最少使用算法:如果一个数据在最近一段时间没有被访问到,那么可以认为在将来它被访问的可能性也很小。因此,当空间满时,最久没有访问的数据最先被置换(淘汰)。
LRU 算法简述:
- 新数据插入到链表头部
- 每当缓存命中(即缓存数据被访问),则将数据移到链表头部
- 当链表满的时候,将链表尾部的数据丢弃
Java 实现 LRU 算法的方式
logback
中的 LRUMessageCache
使用 LinkedHashMap
进行了实现 ,查看 LinkedHashMap
中的 get()
方法也能看到 LRU 的影子。
Redis 中 LRU 的实现
- Redis 操作数据的时候会带上时间戳,当 Redis 内存不足时,会将数据的时间戳与当前时间进行对比,将距离当前时间较为久远的数据进行淘汰。
- Redis 中的 LRU 与常规的 LRU 实现并不相同,常规 LRU 会准确的淘汰掉队头的元素,但是 Redis 的 LRU 并不维护队列,只是根据配置的策略要么从所有的 key 中随机选择 N 个(N 可以配置)要么从所有的设置了过期时间的 key 中选出 N 个键,然后再从这 N 个键中选出最久没有使用的一个 key 进行淘汰。
LFU(Least Frequently Used)
最近最不常用算法:如果一个数据在最近一段时间很少被访问到,那么可以认为在将来它被访问的可能性也很小。因此,当空间满时,最小频率访问的数据最先被淘汰。
参考资料
Redis 集群
Redis 事务
深入剖析 Redis - Redis 集群模式搭建与原理详解
实例解读什么是 Redis 缓存穿透、缓存雪崩和缓存击穿
缓存算法(FIFO 、LRU、LFU 三种算法的区别)
欢迎来到这里!
我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。
注册 关于