Redis 系列05--Redis 常见问题

Redis 系列05--Redis 常见问题,第1张

上一篇:Redis 系列04--Spring Data Redis_fengxianaa的博客-CSDN博客

1. 代码准备

增加依赖



  org.springframework.boot
  spring-boot-starter-jdbc



  mysql
  mysql-connector-java
  5.1.46

修改 application.properties ,增加数据库配置

#数据库
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/数据库名
spring.datasource.username=
spring.datasource.password=

数据库中有一条数据

修改 User 类

public class User {

    public int id;
    public String name;
    public int age;

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    @Override
    public String toString() {
        return "User{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }
}

新增 UserService 类

@Service
public class UserService {

    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    @Autowired
    private JdbcTemplate jdbcTemplate;

    public User getUser(int id){
        System.out.println("先从缓存中获取......");
        String s = stringRedisTemplate.opsForValue().get(id+"");
        if(StringUtils.hasText(s)){
            return JSONObject.parseObject(s, User.class);
        }

        System.out.println("缓存中没有再查询数据库......");
        User user = jdbcTemplate.queryForObject("select * from user where id= ?",
                new Object[]{id}, new BeanPropertyRowMapper<>(User.class));

        System.out.println("放到缓存中......");
        //注意设置缓存的时候,一定要设置超市时间
        stringRedisTemplate.opsForValue().set(id + "", JSONObject.toJSONString(user), 30, TimeUnit.SECONDS);

        return user;
    }
}

新建 UserController

@RestController
public class UserController {
    
    @Autowired
    private UserService userService;
    
    @GetMapping("/get")
    public Object getUser(@RequestParam int id){
        return userService.getUser(id);
    }
}

浏览器访问结果

后台输出:

再次访问,就不会查询数据库了

2. 数据库、redis数据不一致问题

有了缓存,查询数据确实方便,但是修改数据,除了更新数据库,也要更新 redis 缓存

一般采用两种方案:

  1. 修改数据后,在把数据放到缓存中
  2. 直接删除缓存

第一种需要 先查询-->修改-->更新缓存,比较麻烦,而且如果这个数据不经常使用,平白占用内存

所以一般都是使用第二种,在修改数据的时候,直接删除缓存

无论哪种方案,都逃不过:数据库、redis数据不一致问题

  1. 先更新数据库,再删缓存

如果代码按照上图中的 1、2、3、4 执行,就会造成数据库是新数据,redis 是老数据

        2. 先删缓存,再更新数据库

如果代码按照上图中的 1、2、3、4 执行,还是会造成数据库是新数据,redis 是老数据

以上两种方案都无法解决数据的高一致性

解决方案

延时双删:修改数据的线程,删除缓存-->更新数据库-->延时20毫秒再删缓存

这样就能保证数据库和redis的实时一致性

如果不睡眠20毫秒,可能会产生以下情况

如果你的代码效率高,机器性能足够好,就可能产生上图的情况,第三步3和第4步瞬间完成,

之所以 睡眠20毫秒,就是为了,让 “再次删除缓存” 这个 *** 作在最后执行

当然这个20毫秒不是固定值,睡眠 30、40、50 都可以

代码
public void updateAge(int id, int age){
    //删除缓存
    stringRedisTemplate.delete(id+"");
    //更新数据库
    jdbcTemplate.update("update user set age=? where id=? ", age, id);

    try {
        Thread.sleep(20);
    } catch (InterruptedException e) {
    }
    //再删除缓存
    stringRedisTemplate.delete(id+"");
}

其他方案

保证对某条数据的查询、修改 *** 作同步执行

对数据id进行路由,把同一条数据的 *** 作都放到同一个队列中,用其他线程一个个处理

3. 缓存穿透

缓存穿透:用户查询的数据在 缓存 和 数据库中都不存在,对这些数据的请求会直达数据库,给数据库造成很大的压力,若黑客利用此漏洞进行攻击可能压垮数据库。

解决方案
  • 布隆过滤器,
    • 原理:对已有数据的key进行hash,然后放到布隆过滤器中,在请求到达 缓存 和 数据库之前,先用布隆过滤器判断这个 key 是否存在,不存在就直接返回
      • 举一个极端的例子,假设你生成的数据 id 从 1 开始且有序,
        • 从内存中划出一块空间,设置其中的每一位,(8位 = 1byte,1024byte = 1kb)
          id是 1 的数据存在,就设置第 1 位=1
          id是 2 的数据不存在,就设置第 2 位=0,依次类推
          这样当请求过来时,判断对应 位 上的数据是否为 1,来进行过滤
      • redis 的 bitmap 可以做这个事情(之后再说)
  • 设置空对象
    • 如果数据库查不到这条数据,那么就在 缓存中设置一个NULL
    • 问题:
      • 设置NULL,虽然有过期时间,但会占用一些空间
      • 可能造成缓存和数据库的数据短期内不一致,假如过期时间30秒,设置id=100的为null,此时其他线程增加了这个数据,那么30秒内 缓存和数据库 不一致
        这时可以在新增数据成功后尝试删除缓存
代码:
public User getUser(int id){
    System.out.println("先从缓存中获取......");
    String s = stringRedisTemplate.opsForValue().get(id+"");
    if(StringUtils.hasText(s)){
        //如果从redis中查到null,返回:没有这个用户
        if ("null".equals(s)) {
             User user = new User();
             user.setName("没有这个用户");
             return user;
        }
        return JSONObject.parseObject(s, User.class);
    }

    System.out.println("缓存中没有再查询数据库......");
    //这里跟之前不一样,换成了query方法
    List users = jdbcTemplate.query("select * from user where id= ?",
            new Object[]{id}, new BeanPropertyRowMapper<>(User.class));
    if(CollectionUtils.isEmpty(users)){
        //没有从数据库中查到数据,就设置一个null
        stringRedisTemplate.opsForValue().set(id + "", "null", 30, TimeUnit.SECONDS);
        User user = new User();
        user.setName("没有这个用户");
        return user;
    }else{
        System.out.println("放到缓存中......");
        stringRedisTemplate.opsForValue().set(id + "", JSONObject.toJSONString(users.get(0)), 30, TimeUnit.SECONDS);
    }

    return users.get(0);
}

结果:

4. 缓存击穿

其实就是热点数据问题,如果一个 key 是热点数据,就在它刚刚过期时,这时候大量请求来访问这个 key,因为没有缓存,所以这些请求都直达数据库。

解决方案
  • 加锁
    • 上图,同时两个线程访问一个key,而且都未命中缓存,所以开始加分布式锁
      • 线程 1 加锁成功,就从数据库中获取数据,然后写入缓存,最后释放锁
      • 线程 2 加锁失败,就休眠一定时间,然后查询缓存,查不到就重新申请加锁
    • 缺点:线程等待,影响一些性能
    代码:
public User getUser(int id){
    String s = stringRedisTemplate.opsForValue().get(id+"");
    if(StringUtils.hasText(s)){
        return JSONObject.parseObject(s, User.class);
    }
    //1. 未命中,加分布式锁
    Boolean lock = stringRedisTemplate.opsForValue().setIfAbsent(id + "_lock", "lock", 60, TimeUnit.SECONDS);
    if(lock){
        //2. 加锁成功,查询数据库,并写入缓存
        List users = jdbcTemplate.query("select * from user where id= ?",
                new Object[]{id}, new BeanPropertyRowMapper<>(User.class));
        stringRedisTemplate.opsForValue().set(id + "", JSONObject.toJSONString(users.get(0)), 30, TimeUnit.SECONDS);
        //3. 释放锁,解锁的代码要写到finally中
        stringRedisTemplate.delete(id + "_lock");
        return users.get(0);
    }else{
        //4. 加锁失败,休眠20毫秒,继续查询缓存
        try {
            Thread.sleep(20);
        } catch (InterruptedException e) {}

        return getUser(id);
    }
}
  • 逻辑过期
    • 最大特点是,设置缓存时,不设置过期时间,而是在缓存中加一个字段(expiretime),记录这条数据的过期时间,比如:
    • 每次从缓存中拿出时,如果 当前时间 > expire,那么就重新查询数据库,更新缓存
      • 缺点:实现相对复杂,无法保证数据是最新的
     代码:
public User getUser(int id){
    //1. 从缓存获取数据
    String s = stringRedisTemplate.opsForValue().get(id+"");
    User user = null;
    if(StringUtils.hasText(s)){
        user = JSONObject.parseObject(s, User.class);
    }

    if(user == null || System.currentTimeMillis() >= user.getExpire()){
        //2. 如果已经过期,加分布式锁
        Boolean lock = stringRedisTemplate.opsForValue().setIfAbsent(id + "_lock", "lock", 60, TimeUnit.SECONDS);
        if(lock){
            //3. 新建线程,查询数据库,并写入缓存
            new Thread(() -> {
                List users = jdbcTemplate.query("select * from user where id= ?",
                        new Object[]{id}, new BeanPropertyRowMapper<>(User.class));
                users.get(0).setExpire(System.currentTimeMillis() + 60000);
                //注意:不设置过期时间
                stringRedisTemplate.opsForValue().set(id + "", JSONObject.toJSONString(users.get(0)));
                //4. 释放锁,解锁的代码要写到finally中
                stringRedisTemplate.delete(id + "_lock");
            }).start();
        }
    }
    return user;
}

5. 缓存崩溃

也叫缓存雪崩,由于 redis 宕机 或者 同时间大量的key过期,导致大量请求到达后端数据库,从而导致数据库崩溃,整个系统崩溃

解决方案
  • 事前
    • redis集群
    • 多机房部署,避免一起宕机
    • 多套redis集群,之间做数据同步
  • 事中
    • 对源服务访问限流
    • 对redis服务访问限流
    • 多级缓存
  • 事后
    • redis 利用备份数据做数据恢复

欢迎分享,转载请注明来源:内存溢出

原文地址:https://54852.com/langs/735810.html

(0)
打赏 微信扫一扫微信扫一扫 支付宝扫一扫支付宝扫一扫
上一篇 2022-04-27
下一篇2022-04-27

发表评论

登录后才能评论

评论列表(0条)

    保存