Redis详细讲解(二)

文章介绍了Redis的持久化机制,包括RDB快照和AOF日志,以及如何配置和选择持久化策略。此外,讨论了Redis的内存淘汰策略,如LRU和TTL,以应对内存限制。文章还详细阐述了并发环境下可能出现的雪崩、击穿和穿透问题,并给出了相应的解决方案,如设置随机过期时间、使用锁和布隆过滤器来防止这些问题的发生。
摘要由CSDN通过智能技术生成

一、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:不淘汰任何数据,当内存满时,新的写入操作会报错。

  1. volatile-lru:淘汰设置了过期时间的数据中,最近最少使用的数据。这种策略适用于缓存数据,可以保证缓存中的数据都是最近使用过的。
  2. volatile-ttl:淘汰设置了过期时间的数据中,距离过期时间最近的数据。这种策略适用于一些临时性的数据,可以保证数据不会过期。
  3. volatile-random:淘汰设置了过期时间的数据中,随机选择一个数据进行淘汰。
  4. allkeys-lru:淘汰所有数据中,最近最少使用的数据。这种策略适用于缓存数据和持久化数据混合使用的情况。
  5. 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;
}

运行测试

 

此时查询不存在的数据直接返回了空,同样解决了穿透问题。 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值