一、Redis的持久化
redis支持持久化,由于缓存在内存中,为了避免关闭程序或者断电后的数据丢失,redis将数据保存到磁盘中。
1.持久化策略
redis提供了两种持久化的方式,分别是RDB(Redis DataBase)和AOF(Append Only File)。
RDB:就是在不同的时间点,将redis存储的数据生成快照并存储到磁盘等介质上。
AOF:就是将redis执行过的所有写指令记录下来,在下次redis重新启动时,只要把这些写指令从前到后再重复执行一遍,就可以实现数据恢复了。
两种策略可以同时使用,也可以都不使用。
2.配置方法
可以在redis.conf文件中配置。
RDB
当redis操作满足任意一个条件后,将触发持久化操作
-
900 1 900秒中修改1次
-
300 10 300秒中修改10次
-
60 10000 60秒中修改10000次
AOF
appendonly yes 开启AOF
appendfsync always redis针对每个写入命令均会主动调用fsync刷磁盘;
appendfsync everysec 每秒调一次fsync刷盘。
appendfsync no redis不主动调用fsync,何时刷盘由OS来调度;
3.持久化策略的选择
我们通常会按照业务场景来选择持久化策略
-
允许少量数据丢失,性能要求高,选择RDB
-
只允许很少数据丢失,选择AOF
-
几乎不允许数据丢失,选择RDB + AOF
二、Redis淘汰策略
为了保证内存的使用效率和性能,需要采用一些淘汰策略来管理内存中的数据。Redis 支持多种淘汰策略。noeviction:不淘汰任何数据,当内存满时,新的写入操作会报错。
- volatile-lru:淘汰设置了过期时间的数据中,最近最少使用的数据。这种策略适用于缓存数据,可以保证缓存中的数据都是最近使用过的。
- volatile-ttl:淘汰设置了过期时间的数据中,距离过期时间最近的数据。这种策略适用于一些临时性的数据,可以保证数据不会过期。
- volatile-random:淘汰设置了过期时间的数据中,随机选择一个数据进行淘汰。
- allkeys-lru:淘汰所有数据中,最近最少使用的数据。这种策略适用于缓存数据和持久化数据混合使用的情况。
- allkeys-random:淘汰所有数据中,随机选择一个数据进行淘汰。
maxmemory-policy 设置具体的淘汰算法
三.并发问题介绍
并发问题,大量并发量访问服务器,可能导致雪崩,击穿,穿透问题。
问题 | 原因 | 解决方案 |
---|---|---|
雪崩 | 1. Redis热点数据同时过期,大量请求全部打到MySQL,MySQL宕机 2. 单个Redis服务出现问题或重启 | 1. 将热点数据过期时间设置为随机值,避免同时过期 2. 配置Redis集群,解决单点故障问题 |
击穿 | 大量并发请求访问Redis同一个数据,还没有向Redis保存,有大量线程同时访问,导致MySQL压力过大 | 通过上锁(双检锁)实现线程同步执行 |
穿透 | 大量请求访问MySQL没有的数据,Redis缓存无法命中,导致数据库压力过大 | 1. 在Redis中保存空对象,给空对象设置过期时间 2. 使用布隆过滤器筛选掉不存在的数据 |
1.Apache JMeter
这里我们使用Apache JMeter工具来模拟并发问题。
1)添加线程组
2)修改线程数
3)添加请求
3)配置请求信息
2.击穿问题示例
按照上一篇的编程式缓存作为例子
@Override public Student getStudentById(Long studentId) { //获得字符串操作对象 ValueOperations<String, Object> ops = redisTemplate.opsForValue(); //先查询Redis Student stu = (Student) ops.get(PREFIX + studentId); //如果Redis缓存存在数据,就直接返回 if (stu != null) { System.out.println("Redis查到,返回" + stu); return stu; } //如果Redis没有查到数据,就查询MySQL System.out.println("Redis未查到数据,开始查询数据库"); stu= studentMapper.selectById(studentId); if (stu != null) { System.out.println("MySQL查询到数据,返回" + stu); //保存到Redis ops.set(PREFIX + studentId, stu); return stu; } System.out.println("MySQL没查到数据,直接返回null"); return null; }
运行压力测试,结果如下
这里出现redis无缓存时,大量访问数据库,给数据库带来了较大压力。
3.解决缓存击穿问题
解决的方法就是给代码上锁
public synchronized Student getStudentById(Long studentId)
可以直接使用同步方法,但这样其效率会很低。对上锁方法优化,可以使用同步代码块与双检索。
@Override public Student getStudentById(Long studentId) { //获得字符串操作对象 ValueOperations<String, Object> ops = redisTemplate.opsForValue(); Student stu; //先查询Redis stu = (Student) ops.get(PREFIX + studentId); //如果Redis缓存存在数据,就直接返回 if (stu != null) { System.out.println("Redis查到,返回" + stu + " 未进锁"); return stu; } synchronized (lock) { stu = (Student) ops.get(PREFIX + studentId); if (stu != null) { System.out.println("Redis查到,返回" + stu); return stu; } //如果Redis没有查到数据,就查询MySQL System.out.println("Redis未查到数据,开始查询数据库"); //MySQL查到数据,同时保存到Redis stu= studentMapper.selectById(studentId); if (stu != null) { System.out.println("MySQL查询到数据,返回" + stu); //保存到Redis ops.set(PREFIX + studentId, stu); return stu; } System.out.println("MySQL未查到数据,返回null"); return null; } }
运行效果
可以看到,除去第一次查询数据库以外,其他都是在缓存中查到返回 。
这样就可以在解决击穿问题的同时提升效率。
4.穿透问题示例
在上面的情况下可以避免击穿问题,那么在查询一个数据库中不存在的数据又会如何呢
再次运行测试查询一个不存在的数据
可以看到,这种情况下会出现反复向数据库查询一个不存在的值。
5.解决穿透问题
1)在查询到一个数据库不存在的值时,向redis中存入一个空对象,并设置有效时间即可。
@Override public Student getStudentById(Long studentId) { //获得字符串操作对象 ValueOperations<String, Object> ops = redisTemplate.opsForValue(); Student stu; //先查询Redis stu = (Student) ops.get(PREFIX + studentId); //如果Redis缓存存在数据,就直接返回 if (stu != null) { System.out.println("Redis查到,返回" + stu + " 未进锁"); return stu; } synchronized (lock) { stu = (Student) ops.get(PREFIX + studentId); if (stu != null) { System.out.println("Redis查到,返回" + stu); return stu; } //如果Redis没有查到数据,就查询MySQL System.out.println("Redis未查到数据,开始查询数据库"); //MySQL查到数据,同时保存到Redis stu= studentMapper.selectById(studentId); if (stu != null) { System.out.println("MySQL查询到数据,返回" + stu); //保存到Redis ops.set(PREFIX + studentId, stu); return stu; } else { //MySQL没有查询到数据,向Redis保存一个空数据 System.out.println("MySQL未查到数据,返回null"); stu = new Student(); ops.set(PREFIX + studentId, stu, 5, TimeUnit.SECONDS); return null; } } }
此时再运行测试
这样在查询到数据库不存在的数据时,即可将空对象存入redis,后续查询回直接返回一个空对象,不再去查询数据库。
2) 使用布隆过滤器,过滤掉不存在的数据
布隆过滤器(Bloom Filter)其实是基于bitmap的一种应用, 1970 年由布隆提出。它由一个很长的二进制比特数组和一系列哈希函数构成,用于高效地检索数据是否存在。通俗的说可以把布隆过滤器理解为一个集合,我们可以往里面添加值,并且能判断某个值是否在里面。当布隆过滤器告诉我们某个值存在时,其实这个值只是有可能存在;可是它说某个值不存在时,那这个值就真的不存在。
首先引入依赖
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson-spring-boot-starter</artifactId>
<version>3.16.6</version>
</dependency>
创建布隆过滤器
@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;
}
}
初始化布隆过滤器,查询数据库,将数据的id存入过滤器
@GetMapping("/init-student-filter") public ResponseResult<String> inisStudentFilter(){ List<Student> list = studentService.list(); list.forEach(student -> { rBloomFilter.add(String.valueOf(student.getStudentId())); }); return ResponseResult.ok("ok"); }
使用过滤器排除不存在的数据
@Override public Student getStudentById(Long studentId) { //获得字符串操作对象 ValueOperations<String, Object> ops = redisTemplate.opsForValue(); //先查询Redis Student stu = (Student) ops.get(PREFIX + studentId); //如果Redis缓存存在数据,就直接返回 if (stu != null) { System.out.println("Redis查到,返回" + stu + " 未进锁"); return stu; } synchronized (lock) { stu = (Student) ops.get(PREFIX + studentId); if (stu != null) { System.out.println("Redis查到,返回" + stu); return stu; } //如果Redis没有查到数据,就查询MySQL System.out.println("Redis未查到数据,开始查询数据库"); if ( rBloomFilter.contains(String.valueOf(studentId)) ) { //布隆过滤器判断id数据库存在,查询数据库 stu= studentMapper.selectById(studentId); if (stu != null) { System.out.println("MySQL查询到数据,返回" + stu); //保存到Redis ops.set(PREFIX + studentId, stu); return stu; } } else { System.out.println("布隆过滤器判断id数据库不存在,直接返回"); } } return stu; }
运行测试
此时查询不存在的数据直接返回了空,同样解决了穿透问题。