中间件Redis(五)--缓存穿透实战 springboot+redis

Redis缓存穿透实战

1. 什么是缓存穿透

使用redis查询数据的正常流程:

  • 前端访问数据, 首先会在缓存redis中查询,如果查询到数据, 则将数据返回给用户,流程结束
  • 如果在数据库中没有查询到数据,则前往数据库中查询,如果此时能查到数据, 则将数据返回给用户,同时将数据塞到缓存中, 流程结束
  • 如果在数据库中也没有查到数据,则返回null,流程结束

造成缓存穿透的原因

如果前端频繁的发起访问请求,恶意的提供不存在的key, 则数数据库中查询到的数据永远为Null,由于null数据是不会存入缓存中的, 因此每次访问请求时将查询数据库,如果此时有恶意攻击,发起“洪流“式的查询,则很有可能对的数据库造成极大的压力,甚至压垮数据库

缓存穿透: 永远越过了缓存而直接永远地访问数据库

缓存穿透的解决方案:

  • 当从数据库中的查询不到数据的时候,则将null存入缓存, 并给key设置一个TTL,设置过期时间,这样可以降低数据查询的频率

2. 缓存雪崩和缓存击穿

(1)缓存雪崩:指的是在某个时间点,缓存中的Key集体发生过期失效致使大量查询数据库的请求都落在了DB(数据库)上,导致数据库负载过高,压力暴增,甚至有可能“压垮”数据库。这种问题产生的原因其实主要是因为大量的Key在某个时间点或者某个时间段过期失效导致的。所以为了更好地避免这种问题的发生,一般的做法是为这些Key设置不同的、随机的TTL(过期失效时间),从而错开缓存中 Key的失效时间点,可以在某种程度上减少数据库的查询压力。
(2)缓存击穿:指缓存中某个频繁被访问的Key(可以称为“热点Key”),在不停地扛着前端的高并发请求,当这个Key突然在某个瞬间过期失效时,持续的高并发访问请求就“穿破”缓存,直接请求数据库,导致数据库压力在某一瞬间暴增。这种现象就像是“在一张薄膜上凿出了一个洞”。这个问题产生的原因主要是热点的Key过期失效了,而在实际情况中,既然这个Key可以被当作“热点”频繁访问,那么就应该设置这个Key永不过期,这样前端的高并发请求将几乎永远不会落在数据库上。

3. 缓存穿透实战

使用Springboot+ Redis+Mybatis进行测试redis缓存击穿

将书单商品加到数据库中, 如果有该商品,则将商品存入缓存,从缓存中取数据, 如果查询不存在的数据, 则将将key设置的值设置为null, 同时设置一个TTL, 设置过期时间, 防止redis缓存击穿

  • 从pom.xml引入依赖
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-jdbc</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>1.3.2</version>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
  • 创建书单实体类
package com.redis.test.demo.entity;

import com.fasterxml.jackson.annotation.JsonFormat;

import java.util.Date;

public class Item {
    private Integer id;

    private String code;

    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private Date createTime;

    public Integer getId() {
        return id;
    }

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

    public String getCode() {
        return code;
    }

    public void setCode(String code) {
        this.code = code == null ? null : code.trim();
    }

    public Date getCreateTime() {
        return createTime;
    }

    public void setCreateTime(Date createTime) {
        this.createTime = createTime;
    }
}
  • 生成Mapper接口
package com.redis.test.demo.mapper;

import com.redis.test.demo.entity.Item;
import io.lettuce.core.dynamic.annotation.Param;

public interface ItemMapper {
    // 根据编码查询
    Item selectByCode(@Param("code") String code);

}
  • 实现防止缓存击穿service层代码
package com.redis.test.demo.service;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.redis.test.demo.entity.Item;
import com.redis.test.demo.mapper.ItemMapper;
import org.apache.logging.log4j.util.Strings;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.stereotype.Service;

import javax.annotation.Resource;
import java.util.concurrent.TimeUnit;

@Service()
public class cachePassService {
    private static final Logger log = LoggerFactory.getLogger(cachePassService.class);
    // 定义缓存中key的命名前缀
    private static final String key = "item1:";
    //定义Mapper
    @Resource
    private ItemMapper itemMapper;
    //定义redis操作组件
    @Resource
    private RedisTemplate redisTemplate;
    // 定义json序列化和反序列框架
    @Resource
    private ObjectMapper objectMapper;

    public Object getItemInfo(String code) throws Exception {
        Item item = null;
        // 定义一个真正的key
        final String realKey = key + code;
        log.info("key值Wie{}", realKey);
        // 定义的redisTemplate数据操作框架
        ValueOperations valueOperations = redisTemplate.opsForValue();
        if (redisTemplate.hasKey(realKey)) {
            log.info("---------从缓存中取出数据------");
            Object res = valueOperations.get(realKey);
            if (res != null && !Strings.isEmpty(res.toString())) {
                item = objectMapper.readValue(res.toString(), Item.class);
            }
        } else {
            // 缓存中没有该商品
            log.info("----------从数据库中取出数据----------");
            item = itemMapper.selectByCode(code);
            if (item != null) {
                log.info("------------将数据库中的数据写入缓存---------");
                // 将数据库中查到的数据进行序列号写入缓存中
                valueOperations.set(realKey, objectMapper.writeValueAsString(item));
            } else {
                // 设置失效时间,并存入空
                log.info("查询数据");
                valueOperations.set(realKey, "", 30, TimeUnit.MINUTES); // minutes
            }
        }
        return item;
    }

    public Item selectByCode(String code) {
        return itemMapper.selectByCode(code);
    }
}
  • 实现Controller
package com.redis.test.demo.controller;

import com.redis.test.demo.entity.Item;
import com.redis.test.demo.service.cachePassService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.annotation.Resource;
import java.util.HashMap;
import java.util.Map;

@RestController
public class cachePassController {
    private static final Logger log = LoggerFactory.getLogger(cachePassController.class);

    @Resource
    private cachePassService cacheService;

    @RequestMapping("info/{code}")
    public Map<String, Object> getItem(@PathVariable String code) {
        //定义接口返回的数据格式
        Map<String, Object> resMap = new HashMap<>();
        resMap.put("code", 0);
        resMap.put("msg", "成功");
        try {
            resMap.put("data", cacheService.getItemInfo(code));
        } catch (Exception e) {
            resMap.put("code", -1);
            resMap.put("msg", "失败");
        }
        return resMap;
    }

    @RequestMapping("getUser/{code}")
    public Item hello(@PathVariable String code) {
        return cacheService.selectByCode(code);
    }
}

  • 启动类记得加MapperScan
package com.redis.test.demo;

import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
@MapperScan("com.redis.test.demo.mapper")
public class DemoApplication {

    public static void main(String[] args) {
        SpringApplication.run(DemoApplication.class, args);
    }
}

  • Mapper.xml文件内容
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.redis.test.demo.mapper.ItemMapper">
  <resultMap id="BaseResultMap" type="com.redis.test.demo.entity.Item">
    <id column="id" jdbcType="INTEGER" property="id" />
    <result column="code" jdbcType="VARCHAR" property="code" />
    <result column="create_time" jdbcType="TIMESTAMP" property="createTime" />
  </resultMap>
  <sql id="Base_Column_List">
    id, code, create_time
  </sql>
  <select id="selectByCode" resultType="com.redis.test.demo.entity.Item">
    select
    *
    from item
    where code=#{code}
  </select>
</mapper>
  • 配置文件application.properties设置redis端口并连接数据库,配置的mybatis
server.port=8081
spring.redis.port=6379
spring.redis.host=127.0.0.1
spring.datasource.url=jdbc:mysql://localhost:3306/test?useUnicode=true&characterEncoding=utf-8&useSSL=true&serverTimezone=UTC
spring.datasource.username=root
spring.datasource.password=123456
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
mybatis.mapper-locations=classpath:mappers/*Mapper.xml
mybatis.type-aliases-package=com.redis.test.demo.entity
  • 最后项目结构
    在这里插入图片描述

启动项目进行访问:

http://localhost:8081/info/book_10010

可以看到如果数据库中没有book_10010,则会返回一个null, 并存在缓存中

如果在数据库中有book_11010, 首次的访问,会从数据库中取值, 并存到缓存中,再次查询book_11010,会从缓存中直接取值。

最后附上demo的项目git地址: Redis缓存击穿实战Demo
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值