一:Redis的持久化
(1)什么是Redis的持久化
Redis持久化是指将Redis的数据保存到硬盘中,以便在服务器重启或崩溃后能够恢复数据。由于Redis默认将数据保存在内存中,如果不进行持久化操作,重启服务器将导致数据丢失。为了解决这个问题,Redis提供了两种持久化方式:RDB持久化和AOF持久化。
(2)两种持久化策略
①、AOF持久化
AOF持久化是通过将Redis执行的每个写操作追加到一个AOF文件中来实现的。默认每秒对数据进行持久化。
②、RDB持久化
RDB持久化是通过将Redis的数据快照保存到一个RDB文件中来实现的。按条件触发持久化操作,满足任意一个条件。
1) 900 1 900秒中修改1次
2) 300 10 300秒中修改10次
3) 60 10000 60秒中修改10000次
(3)配置方法
可以在redis.conf中配置持久化,如:RDB
启动AOF的配置
appendonly yes 开启AOF
appendfsync everysec 每秒保存
(4)选择持久化策略
我们如何选择RDB和AOF呢?视业务场景而定:
允许少量数据丢失,性能要求高,选择RDB
只允许很少数据丢失,选择AOF
几乎不允许数据丢失,选择RDB + AOF
二:Redis淘汰策略
Redis中的数据太多可能导致内存溢出,Redis会根据情况淘汰一些数据。 Redis的内存上限:64位系统,上限就是内存上限;32位系统,最大是4G
①、设置Redis最大内存
在配置文件redis.conf 中,可以通过参数 maxmemory <bytes> 来设定最大内存,不设定该参数默认是无限制的,但是通常会设定其为物理内存的四分之三。
max-memory 配置0就是无上限(默认)
②、设置内存淘汰方式
当现有内存大于 maxmemory 时,便会触发redis主动淘汰内存方式,通过设置 maxmemory-policy ,有如下几种淘汰方式:
- Noeviction(默认):当内存不足时,Redis不会删除任何键,而是直接返回错误信息。
Volatile-ttl:Redis会优先淘汰剩余过期时间较短的键,以释放内存空间。
alkeys-lru (推荐l):使用LRU算法淘汰比较少使用的键 LRU算法:Least Recently Used 最近最少使用算法,淘汰长期不用的缓存
LFU算法:Least Frequently Used 频率最少使用算法,淘汰使用频率少的缓存
volatile-lru: 在过期的键中淘汰较少使用的
allkeys-random: 加入键的时候如果过限,在所有键中随机淘汰
volatile-random: 加入键的时候如果过限,在过期键中随机淘汰
allkeys-lfu:从所有键中逐渐使用频率最少的键
volatile-lfu:从所有配置了过期时间的键中驱逐使用频率最少的键
三:Redis并发问题
(1)Redis缓存作用
Redis缓存在提高应用程序性能、减轻后端负载和网络压力、提供临时数据存储和实现数据共享等方面起着重要作用。它可以帮助提升应用程序的可伸缩性、性能和用户体验。作用有:
提升系统的性能
Redis基于内存,IO效率远高于MySQL数据库
减少数据库的压力
Redis处理很多请求,减少MySQL的请求量,避免MySQL压力过大导致宕机
提供临时数据存储
Redis缓存还可以用于临时数据的存储,如会话数据、访问令牌、临时计数器等。这些数据通常不需要永久保存,并且频繁读写访问。使用Redis缓存可以方便地对这些临时数据进行快速、高效的存取,而无需频繁访问持久化存储。
缓解网络压力
通过将数据存储在与应用程序相同的服务器中,Redis缓存可以减少对远程资源的访问,从而减轻网络传输压力。这对于分布式系统、微服务架构或跨地域应用程序尤为重要,因为网络延迟和带宽限制可能成为性能瓶颈。
(2)Redis使用的流程
(4)为什么要使用缓存
把经常查询的数据,很少修改的数据存放到缓存中,减少访问数据库,降低数据库压力并且缓存一般都是内存,访问速度比较快。
(5)并发问题介绍
并发问题,大量并发量访问服务器,可能导致问题:
问题 | 原因 | 解决方案 |
---|---|---|
雪崩 | 1. Redis热点数据同时过期,大量请求全部打到MySQL,MySQL宕机 2. 单个Redis服务出现问题或重启 | 1. 将热点数据过期时间设置为随机值,避免同时过期 2. 配置Redis集群,解决单点故障问题 |
击穿 | 大量并发请求访问Redis同一个数据,还没有向Redis保存,有大量线程同时访问,导致MySQL压力过大 | 通过上锁(双检锁)实现线程同步执行 |
穿透 | 大量请求访问MySQL没有的数据,Redis缓存无法命中,导致数据库压力过大 | 1. 在Redis中保存空对象,给空对象设置过期时间 2. 使用布隆过滤器筛选掉不存在的数据 |
①击穿问题
缓存击穿是指,对于一些设置了过期时间的key,如果这些可能会在某些时间点被超高并发地访问,是一种非常热点的数据。如果这个在大量请求同时进来前正好失效,那么所有对这个的数据查询都落到,我们称为缓存击穿。
解决方法:
- 设置热点数据永远不过期。
- 加分布式锁,让未获取到分布式锁的线程自旋操作,缓解数据库的压力。
线程并发的案例
@Service
public class StudentServiceImpl extends ServiceImpl<StudentMapper, Student> implements StudentService {
public static final String PREFIX = "Student-";
@Autowired
private StudentMapper studentMapper;
@Autowired
private RedisTemplate<String,Object> redisTemplate;
@Override
public Student getStudentById(Long id) {
//获得字符串操作对象
ValueOperations<String, Object> ops = redisTemplate.opsForValue();
//先查询Redis
Student 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");
return null;
}
}
@RestController
public class StudentController {
@Autowired
private StudentService studentService;
@GetMapping("/student/{id}")
public ResponseResult<Student> getStudentById(@PathVariable Long id){
return ResponseResult.ok(studentService.getStudentById(id));
}
}
JMeter工具的使用
1)添加线程组
2) 配置线程数量
3) 添加http测试
4) 配置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;
}
②穿透问题
缓存穿透是指,指查询一个一定不存在的数据,由于缓存是不命中,将去查询数据库,但是数据库也无此记录,我们没有将这次查询的写入缓存,这将导致这个不存在的数据每次请求都要到存储层去查询,失去了缓存的意义。
解决方法:
- 接口层增加校验,如用户鉴权校验,或者对做基础校验,的直接拦截;
- 从缓存取不到的数据,在数据库中也没有取到,这时也可以将存为null,缓存有效时间可以设置短点,如30秒(设置太长会导致正常情况也没法使用),这样可以防止攻击用户反复用同一个id暴力攻击。
- 使用布隆过滤器
例子:
解决方案1 : 保存空对象设置过期时间
@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;
}else {
//MySQL没有数据,在Redis保存空对象,设置过期时间
System.out.println("MySQL没有数据");
Student student = new Student();
ops.set(PREFIX + id, student,5, TimeUnit.SECONDS);
}
}
}else {
System.out.println("没有执行同步锁");
}
return stu;
}
解决方法2:使用布隆过滤器
布隆过滤器
布隆过滤器(Bloom Filter)是1970年由布隆提出的。它实际上是一个很长的二进制向量和一系列随机映射函数。布隆过滤器可以用于检索一个元素是否在一个集合中。它的优点是空间效率和查询时间都比一般的算法要好的多,缺点是有一定的误识别率和删除困难。
布隆过滤器的优点:
- 时间复杂度低,增加和查询元素的时间复杂为O(N),(N为哈希函数的个数,通常情况比较小)
- 保密性强,布隆过滤器不存储元素本身
- 存储空间小,如果允许存在一定的误判,布隆过滤器是非常节省空间的(相比其他数据结构如Set集合)
布隆过滤器的缺点:
- 有点一定的误判率,但是可以通过调整参数来降低
- 无法获取元素本身
- 很难删除元素
判断不存在的数据一定不存在,判断存在的数据可能不存在。
使用布隆过滤器:
1) 将数据保存到布隆过滤器中
2) 使用布隆过滤器进行判断
Redission
Redission是一个基于Redis的分布式Java对象与服务的开源框架。它是Redisson项目的一部分,提供了丰富的分布式Java对象、集合、锁、信号量、闭锁、消息队列和分布式服务等功能。Redission旨在简化分布式系统的开发,并提供高性能的分布式数据结构和分布式服务的支持。
基于Redis工具包,提供大量功能,包含:
布隆过滤器
分布式锁
分布式原子类
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) 将数据的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;
}
四、总结
Redis是一个基于内存的数据存储系统,由于内存有限,当内存不足时,Redis需要选择一些数据进行淘汰,以释放出空闲内存空间。
Redis持久化并不一定是必需的,取决于业务需求和数据的重要性。在某些应用场景下,可以权衡性能和数据安全,选择不进行持久化。
Redis在处理并发问题时需要考虑竞态条件、脏读、丢失更新、死锁和数据竞争等情况,并采取相应的措施来确保数据的一致性和完整性