一、Redis持久化
1、什么是持久化
Redis持久化是指将Redis服务器中的数据保存到磁盘上,以确保在服务器重启或发生故障时仍然能够恢复数据。
Redis是一个内存数据库,数据默认情况下是存储在内存中的。虽然内存具有高速读写的优势,但数据存储在内存中是不持久的,一旦服务器关闭或发生故障,数据就会丢失。
2、Redis持久化的两种方式
为了解决这个问题,Redis提供了持久化机制,将数据保存到磁盘上,以便在重启时能够重新加载并恢复数据。通过持久化,可以保证数据的持久性和可靠性。
-
RDB持久化:RDB持久化是将Redis在某个时间点上的数据状态快照保存到磁盘上的过程。通过配置Redis服务器可以定期或根据条件生成RDB文件,并将其保存到硬盘上。RDB持久化适用于备份、灾难恢复以及在重启时快速加载大数据集。
优点:存储紧凑,节省内存空间。恢复速度非常快。适合全量备份、全量复制的场景,经常用于灾难恢复(对数据的完整性和一致性要求相对较低的场合)。
缺点:容易丢失数据,容易丢失两次快照之间 Redis 服务器中变化的数据。RDB 通过 fork 子进程对内存快照进行全量备份,是一个重量级操作,频繁执行成本高。 -
AOF持久化:AOF(Append-Only File)持久化是将Redis的操作日志以追加的方式写入磁盘的过程。通过记录Redis收到的每个写命令,可以在服务器重启时重新执行这些命令来还原数据集的状态。AOF持久化适用于需要更高的数据安全性和持久性要求的场景。
优点:数据的备份更加完整,丢失数据的概率更低,适合对数据完整性要求高的场景日志文件可读,AOF 可操作性更强,可通过操作日志文件进行修复缺点:AOF 日志记录在长期运行中逐渐庞大,恢复起来非常耗时,需要定期对 AOF 日志进行瘦身处理恢复备份速度比较慢同步写操作频繁会带来性能压力
3、持久化策略
持久化策略包含
-
AOF:默认每秒对数据进行持久化
-
RDB:按条件触发持久化操作,满足任意一个条件 1) 900 1 900秒中修改至少1个键 2) 300 10 300秒中修改至少10个键 3) 60 10000 60秒中修改至少10000个键
4、选择持久化策略
视业务场景而定:
-
允许少量数据丢失,性能要求高,选择RDB
-
只允许很少数据丢失,选择AOF
-
几乎不允许数据丢失,选择RDB + AOF
二、Redis淘汰策略
1、八种策略
- volatile-lru,针对设置了过期时间的key,使用lru算法进行淘汰。
- allkeys-lru,针对所有key使用lru算法进行淘汰。
- volatile-lfu,针对设置了过期时间的key,使用lfu算法进行淘汰。
- allkeys-lfu,针对所有key使用lfu算法进行淘汰。
- volatile-random,从所有设置了过期时间的key中使用随机淘汰的方式进行淘汰。
- allkeys-random,针对所有的key使用随机淘汰机制进行淘汰。
- volatile-ttl,删除生存时间最近的一个键。
- noeviction(默认),不删除键,值返回错误。
2、四种算法
- lru 最近很少的使用的key(根据时间,最不常用的淘汰)
- lfu 最近很少的使用的key (根据计数器,用的次数最少的key淘汰)
- random 随机淘汰
- ttl 快要过期的先淘汰
3、执行原理
- 删除失效主键(消极、积极、主动)
- 淘汰数据的量(为了避免频繁的触发淘汰策略,每次会淘汰掉一批数据,淘汰的数据的大小其实是和置换的大小来确定的,如果置换的数- - 据量大,淘汰的肯定也多。)
- 置换策略(在执行增加数据时,Redis会检查内存使用,如果内存使用超过maxmemory,就会按照置换策略删除一些key)
三、Redis并发问题
1、Redis缓存作用
-
提高读取性能:将经常请求的数据缓存在Redis中,可以大大提高读取的性能。由于Redis是基于内存的,读取速度非常快,无需像传统数据库一样进行磁盘IO操作。
-
减轻数据库压力:通过将部分数据缓存在Redis中,可以减轻对后端数据库的访问压力。当有大量并发请求需要读取同一份数据时,Redis可以直接返回缓存中的数据,而无需每次都去查询数据库。
-
数据结构丰富:Redis支持多种数据结构,如字符串、哈希、列表、集合和有序集合等。这些数据结构的灵活性使得业务开发者可以更方便地在Redis中存储和处理各种数据,提升应用的功能和性能。
-
分布式缓存:Redis支持集群模式和分片模式,可以构建分布式缓存系统。通过将数据分布到不同的Redis节点上,可以提高缓存的容量和并发处理能力,同时也增强了缓存的可靠性和可扩展性。
-
作为消息队列:Redis提供了发布/订阅功能,可以用作简单的消息队列系统。应用可以将消息发布到Redis的频道中,订阅者可以实时接收到发布的消息,实现解耦和异步处理。
总而言之,Redis缓存在提升读取性能、减轻数据库压力、丰富数据结构、构建分布式缓存和作为消息队列等方面发挥着重要的作用。通过合理地使用Redis缓存,可以提高应用的性能、扩展性和可靠性。
2、 Redis基本使用流程
- 先从Redis查询数据
- Redis存在就直接返回
- Redis没有再查询数据库
- 数据库有就保存到Redis中,返回数据
- 数据库没有就返回空
3、并发问题介绍
当缓存系统遇到高并发情况时可能会出现雪崩、击穿和穿透这三个并发问题:
1、雪崩(Cache Avalanche)
原因:
当缓存系统中的大量缓存键同时过期或失效时,会导致大量请求直接访问后端数据库,造成数据库压力剧增,甚至引发系统崩溃。这种情况下,由于缓存无法提供有效数据,所有请求都会打到后端存储上,造成整个系统的性能大幅下降。
解决方法:
- 设置合理的缓存过期时间,将缓存的过期时间尽量分散开,避免同一时间大量缓存同时失效。
- 使用缓存预热策略,提前主动加载数据到缓存中,避免在缓存失效时对后端存储的冲击。
- 使用分布式缓存架构,将缓存部署到多个节点上,提高容错性和可用性。
2、击穿(Cache Miss)
原因:
当某个缓存键失效或不存在时,大量并发请求同时访问该键,导致请求直接访问后端数据库,增加了数据库的负载压力。
解决方法:
- 使用互斥锁(Mutex Lock)或分布式锁等机制,保证只有一个请求可以访问后端数据库,并将结果写入缓存。其他请求等待并从缓存中获取数据。
- 使用缓存穿透解决方案,如将缺失的键设置为空结果或利用布隆过滤器拦截无效的查询请求。
3、穿透(Cache Penetration)
原因:
当大量请求查询一个缓存中不存在的键时,由于缓存中没有对应的数据,每个请求都直接访问后端数据库,导致数据库压力增加。
解决方法:
- 使用布隆过滤器(Bloom Filter)来过滤掉无效的查询请求,减轻对数据库的压力。
- 对于查询结果为空的请求,也将其缓存起来,设置一个较短的过期时间,避免攻击者通过不断发送无效请求来绕过布隆过滤器。
4、JMeter工具的使用
- 添加线程组
- 配置线程数量
- 添加http测试
- 配置http连接
- 添加结果视图
5、 击穿问题
双检锁DCL机制优化方法:
@Override
public Student getStudentById(Long id) {
//获得字符串操作对象
ValueOperations<String, Object> ops = redisTemplate.opsForValue();
//先查询Redis
Student stu = (Student) ops.get(PREFIX + id);
if(stu == null) {
synchronized (this) {
System.out.println("进入了同步锁");
//先查询Redis
stu = (Student) ops.get(PREFIX + id);
//如果Redis缓存存在数据,就直接返回
if (stu != null) {
System.out.println("Redis查到,返回" + stu);
return stu;
}
//如果Redis没有查到数据,就查询MySQL
stu = studentMapper.selectById(id);
//MySQL查到数据,就保存到Redis
if (stu != null) {
System.out.println("MySQL查到,返回" + stu);
ops.set(PREFIX + id, stu);
return stu;
}
//MySQL没有数据,就返回null
System.out.println("MySQL没有数据,就返回null");
}
}else {
System.out.println("没有执行同步锁");
}
return stu;
}
6、穿透问题
布隆过滤器(Bloom Filter):
是一种空间效率高、快速判断元素是否存在的概率性数据结构。它通过利用位数组和多个哈希函数来表示集合中的元素,并判断某个元素是否在集合中。、
原理:
-
初始化:创建一个长度为m的位数组,所有位初始化为0。
-
插入元素:对于要插入的元素,使用k个独立的哈希函数将其映射为m个不同的位,然后将这些位设置为1。
-
查询元素:对于要查询的元素,同样使用k个哈希函数计算哈希值,并检查对应的m个位是否都为1。若其中有任意一个位为0,则可以确定该元素一定不存在于集合中;若所有位都为1,则该元素可能存在于集合中。
特性:
- 空间效率高:相对于存储每个元素的全量信息而言,布隆过滤器只需要存储少量位的状态信息,所需空间较小。
- 查询速度快:由于布隆过滤器的查询只涉及位运算,不需要访问实际元素,所以查询速度非常快。
- 概率性判断:布隆过滤器在查询时可能出现误判,即被判断为存在但实际上不存在的情况。布隆过滤器设置的位数组越大、哈希函数的个数越多,误判概率越小。
使用:
1、导入依赖
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson-spring-boot-starter</artifactId>
<version>3.16.6</version>
</dependency>
2、 创建布隆过滤器
@Configuration
public class RedissonConfig {
@Bean
public RBloomFilter<String> bloomFilter(){
Config config = new Config();
config.setTransportMode(TransportMode.NIO);
SingleServerConfig singleServerConfig = config.useSingleServer();
//可以用"rediss://"来启用SSL连接
singleServerConfig.setAddress("redis://127.0.0.1:6379");
RedissonClient redisson = Redisson.create(config);
//创建布隆过滤器
RBloomFilter<String> bloomFilter = redisson.getBloomFilter("student-filter");
//初始化 参数1 向量长度 参数2 误识别率
bloomFilter.tryInit(10000000L,0.03);
return bloomFilter;
}
}
3、controller中将数据的id保存到过滤器中
@Autowired
private RBloomFilter<String> rBloomFilter;
/**
* 初始化布隆过滤器
* @return
*/
@GetMapping("init-student-filter")
public ResponseResult<String> initStudentFilter(){
List<Student> list = studentService.list();
//将所有id保存到过滤器中
list.forEach(student -> {
rBloomFilter.add(String.valueOf(student.getStuId()));
});
return ResponseResult.ok("ok");
}
4、使用过滤器排除不存在的数据
@Autowired
private RBloomFilter<String> rBloomFilter;
@Override
public Student getStudentById(Long id) {
//获得字符串操作对象
ValueOperations<String, Object> ops = redisTemplate.opsForValue();
//先查询Redis
Student stu = (Student) ops.get(PREFIX + id);
if(stu == null) {
synchronized (this) {
System.out.println("进入了同步锁");
//先查询Redis
stu = (Student) ops.get(PREFIX + id);
//如果Redis缓存存在数据,就直接返回
if (stu != null) {
System.out.println("Redis查到,返回" + stu);
return stu;
}
//使用布隆过滤器判断数据库中是否存在该id
if(rBloomFilter.contains(String.valueOf(id))) {
//如果Redis没有查到数据,就查询MySQL
stu = studentMapper.selectById(id);
//MySQL查到数据,就保存到Redis
if (stu != null) {
System.out.println("MySQL查到,返回" + stu);
ops.set(PREFIX + id, stu);
return stu;
}
}else{
System.out.println("布隆过滤器判断id数据库不存在,直接返回");
}
}
}else {
System.out.println("没有执行同步锁");
}
return stu;
}