目录
4)AOF持久化策略[always,everysec,no]
简介
Redis是一个NoSQL(Not Only SQL)数据库,也是一个基于内存的key-value结构数据库,常用作缓存使用,除了用作缓存之外的应用场景还有任务队列、消息队列、分布式锁。
一、数据类型
Redis数据类型:String、hash、set、zset(sorted set)、list
二、Redis常用命令
更多命令可以参考Redis中文网:http://www.redis.net.cn。下面我们主要研究比较常用的命令。命令的大小写是不分的。
1、字符串String操作命令
SETEX命令可以用来保存验证码等一段时间后就会过期的数据。
SETNX可以用来实现分布式锁。
2、哈希hash操作命令
可以简单将key理解为一个类,field对应类中的属性,而value就对应不同属性的取值。
3、列表list操作命令
4、集合set操作命令
其中SDIFF的参数顺序会影响结果sdiff key1 key2就是从key1中将与key2重复的元素删掉; sdiff key2 key1则相反,是从key2中将与key1重复的元素删掉。
5、有序集合zset(sorted set)操作命令
其中ZRANGE命令中要是带上withscores,则在返回指定区间内成员的同时会返回这些成员对应的分数。
6、通用命令
三、在Java中操作Redis
1、使用Jedis操作Redis
例:
2、使用Spring Data Redis操作Redis
1)相关配置
使用之前要在application.yml中进行Redis的相关配置。如果设置了密码,可以将密码那行的注释去掉。此外,Redis默认提供16个数据库,database:0表示使用第0号数据库,这16个数据库之间数据不共通。在命令行中则可以通过:select number的方式切换到第number号数据库。
2)关于RedisTemplate的序列化及反序列化
RedisTemplate把Key,Value经过序列化存到Redis,序列化过的内容无法直接识别,默认使用的是JDK序列化机制。例如执行下面的方法:
结果为:
- 序列化:将对象转化为可传输的字节序列的过程
- 反序列化:和上面相反
序列化的目的:为了对象可以跨平台存储以及通过网络传输
序列化只是一种拆装组装对象的规则,常见序列化方法有:JDK(默认,不支持跨语言)、Json、XML、Hession、Kryo(不支持跨语言)、Thrift等。
如果想设置序列化方式,需要创建一个配置类RedisConfig,现以将Key以及HashKey的序列化方式改为String类型为例:
这时候再设置Key-Value时key就会以正常显示了,例如执行下面的方法:
结果为:
因为Value没有进行序列化设置的更改,因此Value依然使用JDK序列化机制。需要注意的是,除了RedisTemplate,还有一个StringRedisTemplate类。这个类把Key以及Value直接作为String处理,可读性更好,但由于StringRedisTemplate的参数只能为String类型,因此用处没有RedisTemplate广泛。
3)数据类型操作
1、操作String类型数据
首先,redisTemplate类中有个无参的构造方法,如下:
@Override
public ValueOperations<K, V> opsForValue() {
if (valueOps == null) {
valueOps = new DefaultValueOperations<>(this);
}
return valueOps;
}
redisTemplate.opsForValue()的方法都定义在ValueOperations<K, V>中,一共有16个方法,16个方法见:
2、操作Hash类型数据
首先,redisTemplate类中有个无参的构造方法,如下:
@Override
public <HK, HV> HashOperations<K, HK, HV> opsForHash() {
return new DefaultHashOperations<>(this);
}
redisTemplate.opsForHash()的方法都定义在HashOperations<K,HK,HV>中,有14个方法,具体使用见:
3、操作list类型数据
首先,redisTemplate类中有个无参的构造方法,如下:
@Override
public ListOperations<K, V> opsForList() {
if (listOps == null) {
listOps = new DefaultListOperations<>(this);
}
return listOps;
}
redisTemplate.opsForList()的方法都定义在ListOperations<K,V>中,有22个方法,具体使用见:
4、操作set类型数据
首先,redisTemplate类中有个无参的构造方法,如下:
@Override
public SetOperations<K, V> opsForSet() {
if (setOps == null) {
setOps = new DefaultSetOperations<>(this);
}
return setOps;
}
redisTemplate.opsForSet()的方法都定义在SetOperations<K,V>中,有24个方法,具体使用见:
5、操作zset类型数据
同理,redisTemplate.opsForZSet()有16个方法,具体使用见:
6、通用操作
四、持久化
Redis持久化是指在指定的时间间隔内将内存中的数据集快照(snapshotting)写入磁盘,恢复时是将快照文件读入内存。持久化有两种方式:RDB(Redis DataBase)以及AOF(Append Of File)
1、RDB
1)备份是如何进行的
- redis会单独创建一个子进程(使用fork函数)来进行持久化,会先将数据写入到一个临时文件中,待持久化过程都结束了,再用这个临时文件替换上次持久化好的文件。整个过程中,主进程是不进行任何IO操作的,这就确保了极高的性能
- 如果需要进行大规模数据的恢复,且对于数据恢复的完整性不是非常敏感,那RDB方式要比AOF方式更加的高效
- RDB的缺点是最后一次持久化后的数据可能丢失
注:fork函数的作用是复制一个与当前进程一样的进程。新进程的所有数据(变量、环境变量、程序计数器等) 数值都和原进程一致,但是是一个全新的进程,并作为原进程的子进程。
2)持久化触发方式
那么问题来了,Redis的持久化是怎么触发的呢?总不可能是随机触发的吧。答案就是,Redis的持久化触发方式有两种,分别是:自动触发和手动触发。
自动触发:
通过在Redis的配置文件redis.conf中进行设置
图中的save是用来配置触发 Redis的 RDB 持久化条件,也就是什么时候将内存中的数据保存到硬盘。比如"save m n"。表示m秒内数据集存在n次修改时,自动触发bgsave。 默认如下配置: save 900 1 :表示900秒钟内至少1个键被更改则进行快照。 save 300 10 :表示300秒内至少10个键被更改则进行快照。 save 60 10000 :表示60秒内至少10000个键被更改则进行快照。
手动触发:
通过执行save或者bgsave命令来触发RDB 持久化条件。
1、save
该命令会阻塞当前Redis服务器,执行save命令期间,Redis不能处理其他命令,直到RDB过程完成为止。显然该命令对于内存比较大的实例会造成长时间阻塞,这是致命的缺陷,为了解决此问题,Redis提供了第二种方式。
2、bgsave
执行该命令时,Redis会在后台异步进行快照操作,快照同时还可以响应客户端请求。具体操作是Redis进程执行fork操作创建子进程,RDB持久化过程由子进程负责,完成后自动结束。阻塞只发生在fork阶段,一般时间很短。基本上 Redis 内部所有的RDB操作都是采用 bgsave 命令。
ps:执行执行 flushall 命令,也会产生dump.rdb文件,但里面是空的.
3)数据恢复
将备份文件 (dump.rdb) 移动到 redis 安装目录并启动服务即可,redis就会自动加载文件数据至内存了。Redis 服务器在载入 RDB 文件期间,会一直处于阻塞状态,直到载入工作完成为止。
4)RDB的优缺点
优点:
RDB可以最大化Redis的性能。
- 父进程在保存RDB文件时唯一要做的就是fork出一个子进程,然后这个子进程就会处理接下来的所有保存工作,父进程无需执行任何磁盘I/O操作。同时这个也是一个缺点,如果数据集比较大的时候,fork可以能比较耗时,造成服务器在一段时间内停止处理客户端的请求。
- RDB 在恢复大数据集时,速度比 AOF 的恢复速度要快。
缺点:
- 如果数据集比较大的时候,fork可以能比较耗时,造成服务器在一段时间内停止处理客户端的请求。
- RDB快照是一次全量备份,存储的是内存数据的二进制序列化形式,存储上非常紧凑。当进行快照持久化时,会开启一个子进程专门负责快照持久化,子进程会拥有父进程的内存数据,父进程修改内存子进程不会反应出来,所以在快照持久化期间修改的数据不会被保存,可能丢失数据。一旦Redis异常退出,就会丢失最后一次快照以后更改的所有数据。
这个时候我们就需要根据具体的应用场景,通过组合设置自动快照条件的方式来将可能发生的数据损失控制在能够接受范围。如果数据相对来说比较重要,希望将损失降到最小,则可以使用AOF方式进行持久化。
2、AOF
默认情况下Redis没有开启AOF方式的持久化。
1)如何开启AOF
# 可以通过修改redis.conf配置文件中的appendonly参数开启
appendonly yes
# AOF文件的保存位置和RDB文件的位置相同,都是通过dir参数设置的。
dir ./
# 默认的文件名是appendonly.aof,可以通过appendfilename参数修改
appendfilename appendonly.aof
2)备份是如何进行的
以日志的形式来记录每个写操作(增量保存),将 Redis 执行过的所有写指令记录下来 (读操作不记录),AOF文件的保存路径,同RDB的路径一致, 只许追加文件但不可以改写文件,redis 启动之初会读取该文件重新构建数据,换言之,redis 重启的话就根据日志文件的内容将写指令从前到后执行一次以完成数据的恢复工作。
3)持久化过程
(1)客户端的请求写命令会被append追加到AOF缓冲区内;
(2)AOF缓冲区根据AOF持久化策略[always,everysec,no]将操作sync同步到磁盘的AOF文件中;
(3)AOF文件大小超过重写策略或手动重写时,会对AOF文件rewrite重写,压缩AOF文件容量;
(4)Redis服务重启时,会重新load加载AOF文件中的写操作达到数据恢复的目的;
4)AOF持久化策略[always,everysec,no]
appendfsync always
始终同步,每次Redis的写入都会立刻记入日志;性能较差但数据完整性比较好
appendfsync everysec
每秒同步,每秒记入日志一次,如果宕机,本秒的数据可能丢失。
appendfsync no
redis不主动进行同步,把同步时机交给操作系统,只有在操作系统需要时再刷新数据。
5)Rewrite 压缩
Rewrite 压缩是什么 AOF 采用文件追加方式,文件会越来越大为避免出现此种情况,新增了重写机制,当 AOF 文件的大小超过所设定的阈值时,Redis 就会启动 AOF 文件的内容压缩,只保留可以恢复数据的最小指令集,可以使用命令 bgrewriteaof。
重写原理,如何实现重写 AOF 文件持续增长而过大时,会 fork 出一条子进程来将文件重写 (也是先写临时文件最后再 rename),redis4.0 版本后的重写,是指把 rdb 的快照,以二进制的形式附在新的 aof 头部,作为已有的历史数据,替换掉原来的流水账操作。
no-appendfsync-on-rewrite:(默认参数为no) 当我们同时执行主进程的写操作和子进程的重写操作时,两者都会操作磁盘,而重写往往会涉及到大量的磁盘操作,这样就会造成主进程在写aof
文件的时候出现阻塞的情形。 为了解决这个问题,no-appendfsync-on-rewrite
参数出场了。
- 如果该参数设置为
no
,是最安全的方式,不会丢失数据,但是要忍受阻塞的问题; - 如果设置为
yes
,这就相当于将appendfsync
设置为no
,这说明并没有执行磁盘操作,只是写入了缓冲区。因此这样并不会造成阻塞(因为没有竞争磁盘),但是如果这个时候redis
挂掉,就会丢失数据。丢失多少数据呢?在linux
的操作系统的默认设置下,最多会丢失30s的数据。
因此,如果应用系统无法忍受延迟,而可以容忍少量的数据丢失,则设置为yes
;如果应用系统无法忍受数据丢失,则设置为no
。
触发机制,何时重写 Redis 会记录上次重写时的 AOF 大小,默认配置是当 AOF 文件大小是上次 rewrite 后大小的一倍且文件大于 64M 时触发。重写虽然可以节约大量磁盘空间,减少恢复时间。但是每次重写还是有一定的负担的,因此设定 Redis 要满足一定条件才会进行重写。
auto-aof-rewrite-percentage:设置重写的基准值,文件达到 100% 时开始重写(文件是原来重写后文件的 2 倍时触发)。
auto-aof-rewrite-min-size:设置重写的基准值,最小文件 64MB。达到这个值开始重写。
系统载入时或者上次重写完毕时,Redis 会记录此时 AOF 大小,设为 base_size,如果 Redis 的 AOF 当前大小 >= base_size +base_size*100% (默认) 且当前大小 >=64mb (默认) 的情况下,Redis 会对 AOF 进行重写。例如:文件达到 70MB 开始重写,降到 50MB,下次什么时候开始重写?100MB。
重写流程 - -------- bgrewriteaof 触发重写,判断是否当前有 bgsave 或 bgrewriteaof 在运行,如果有,则等待该命令结束后再继续执行;
- 主进程 fork 出子进程执行重写操作,保证主进程不会阻塞;
- 子进程遍历 redis 内存中数据到临时文件,客户端的写请求同时写入 aof_buf 缓冲区和 aof_rewrite_buf 重写缓冲区,保证原 AOF 文件完整以及新 AOF 文件生成期间的新的数据修改动作不会丢失;
- 子进程写完新的 AOF 文件后,向主进程发信号,父进程更新统计信息。主进程把 aof_rewrite_buf 中的数据写入到新的 AOF 文件;
- 使用新的 AOF 文件覆盖旧的 AOF 文件,完成 AOF 重写。
6)AOF优点和缺点
优点
- 备份机制更稳健,丢失数据概率更低。
- 可读的日志文本,通过操作AOF稳健,可以处理误操作。
缺点
-
比起RDB占用更多的磁盘空间。
-
恢复备份速度要慢。
-
每次读写都同步的话,有一定的性能压力。
-
存在个别Bug,造成恢复不能。
3、总结
官方推荐两个都启用,AOF和RDB同时开启,系统默认取AOF的数据(数据不会存在丢失);
如果对数据不敏感,可以选单独用RDB;
不建议单独用 AOF,因为可能会出现Bug;
如果只是做纯内存缓存,可以都不用。
五、Redis应用中可能存在的问题
1、缓存穿透
1)概念
由图可看出,缓存穿透指的是客户端一直查询redis和数据库中均不存在的数据,由于redis中不存在对应数据,因此每次请求都会被压到数据库。如果请求过多则可能压垮数据库。
2)解决办法
- 简单粗暴,我们直接将空对象缓存起来,即是指在数据库中也找不到对应数据的情况下,对key进行set (key,null)。但这种方法可能会导致两种问题: 1、value为null 仍占用内存空间,空值做了缓存,意味着缓存中存了更多的键,需要更多的内存空间。一般来说,我们会给这类数据设置一个较短的过期时间,让其自动剔除。 2、缓存与数据库数据不一致,例如过期时间设置为5分钟,如果此时数据库添加了这个数据,那此段时间就会出现缓存和数据库数据的不一致,此时可以利用消息系统或者其他方式清除掉缓存层中的空对象。
- 使用布隆过滤器(布隆挡在前面!),在访问缓存层和存储层之前,将存在的key用布隆过滤器提前保存起来,做第一层拦截,当收到一个对key请求时先用布隆过滤器验证是key否存在,如果存在在进入缓存层、存储层。可以使用bitmap做布隆过滤器。这种方法适用于数据命中不高、数据相对固定、实时性低的应用场景,代码维护较为复杂,但是缓存空间占用少。 布隆过滤器实际上是一个很长的二进制向量和一系列随机映射函数。布隆过滤器可以用于检索一个元素是否在一个集合中。它的优点是空间效率和查询时间都远远超过一般的算法,缺点是有一定的误识别率和删除困难。布隆过滤器拦截的算法描述: 初始状态时,BloomFilter是一个长度为m的位数组,每一位都置为0。 添加元素x时,x使用k个hash函数得到k个hash值,对m取余,对应的bit位设置为1。 判断y是否属于这个集合时,对y使用k个哈希函数得到k个哈希值,对m取余,所有对应的位置若为1,则认为y属于该集合(哈希冲突,可能存在误判),否则就认为y不属于该集合。可以通过增加哈希函数和增加二进制位数组的长度来降低错报率。
2、缓存击穿
1)概念
key 对应的数据存在,但在 redis 中过期,此时若有大量并发请求过来,这些请求发现缓存过期一般都会从后端数据库加载数据并回设到缓存,这个时候大并发的请求可能会瞬间把后端数据库压垮。
缓存击穿发生的原因:redis 某个 key 过期了,大量访问使用这个 key(热门 key)。
2)解决办法
- 利用分布式互斥锁。只允许一个线程重建缓存,其他线程等待重建缓存的线程执行完,重新从缓存获取数据即可。本方案思路比较简单,但是存在一定的隐患,如果在查询数据库 + 和 重建缓存(key失效后进行了大量的计算)时间过长,也可能会存在死锁和线程池阻塞的风险,高并发情景下吞吐量会大大降低!
- 加长热门数据的key,并实时调整 key 的过期时长。但可能导致数据不一致问题。
3、缓存雪崩
1)概念
缓存雪崩和缓存击穿有些相似,也是key 对应的数据存在,也是对应数据在 redis 中过期,也是大并发的请求瞬间把后端数据库压垮。但缓存雪崩与缓存击穿的区别在于缓存雪崩针对很多 key 缓存,缓存击穿则是针对某一个 key 。
2)解决方法
- 构建多级缓存架构,nginx 缓存 + redis 缓存 + 其他缓存(ehcache 等)。例如本地进程作为一级缓存,redis作为二级缓存,不同级别的缓存设置的超时时间不同,即使某级缓存过期了,也有其他级别缓存兜底。
- 使用锁或队列,用加锁或者队列的方式来保证不会有大量的线程对数据库一次性进行读写,从而避免失效时大量的并发请求落到底层存储系统上,该方法不适用高并发情况。
- 使用令牌桶或漏桶限流算法。通过限流算法限制访问数据的线程量。
- 将缓存失效时间分散开。比如可以在原有的失效时间基础上增加一个随机值,比如 1-5 分钟随机,这样每一个缓存的过期时间的重复率就会降低,就很难引发集体失效的事件。
六、Redis IO 多线程
IO 多线程其实指客户端交互部分的网络 IO 交互处理模块多线程,而非执行命令多线程。Redis6 执行命令依然是单线程。
另外,多线程 IO 默认也是不开启的,需要再配置文件中配置:
io-threads-do-reads yes
io-threads 4
参考链接:
https://blog.csdn.net/m0_43424329/article/details/124364120;