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,会从缓存中直接取值。