什么叫缓存穿透?
模拟一个场景:
前端用户发送请求获取数据,后端首先会在缓存Redis中查询,如果能查到数据,则直接返回.如果缓存中查不到数据,则要去数据库查询,如果数据库有,将数据保存到Redis缓存中并且返回用户数据.如果数据库没有则返回null;
这个缓存穿透的问题就是这个返回的null上面,如果客户端恶意频繁的发起Redis不存在的Key,且数据库中也不存在的数据,返回永远是null.当洪流式的请求过来,给数据库造成极大压力,甚至压垮数据库.它永远越过Redis缓存而直接访问数据库,这个过程就是缓存穿透.
其实是个设计上的缺陷.
缓存穿透解决方案
业界比较成熟的一种解决方案:当越过缓存,且数据库没有该数据返回客户端null并且存到Redis,数据是为"",看实际情况并给这个Key设置过期时间.这种方案一定程度上减少数据库频繁查询的压力.
实战过程
CREATE TABLE `item` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`code` varchar(255) DEFAULT NULL COMMENT '商品编号',
`name` varchar(255) CHARACTER SET utf8mb4 DEFAULT NULL COMMENT '商品名称',
`create_time` datetime DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8 COMMENT='商品信息表';
INSERT INTO `item` VALUES ('1', 'book_10010', 'Redis缓存穿透实战', '2019-03-17 17:21:16');
项目整体结构
依赖
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.2</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.example</groupId>
<artifactId>redis1</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>redis1</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</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>2.3.0</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter-test</artifactId>
<version>3.0.3</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
</project>
启动类
@SpringBootApplication
@MapperScan({"com.example.redis1.mapper"})
public class Redis1Application {
public static void main(String[] args) {
SpringApplication.run(Redis1Application.class, args);
}
}
application.yml
server:
port: 80
spring:
application:
name: redis-test
redis:
##redis 单机环境配置
##将docker脚本部署的redis服务映射为宿主机ip
##生产环境推荐使用阿里云高可用redis服务并设置密码
host: 127.0.0.1
port: 6379
password:
database: 0
ssl: false
##redis 集群环境配置
#cluster:
# nodes: 127.0.0.1:7001,127.0.0.1:7002,127.0.0.1:7003
# commandTimeout: 5000
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://1111111:3306/redis-test?useSSL=false&useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&serverTimezone=GMT%2B8&useCursorFetch=true
username: xxxx
password: xxxxxxxx
mybatis:
mapper-locations: classpath:mappers/*Mapper.xml # 指定mapper文件位置
type-aliases-package: com.example.redis1.pojo
configuration:
map-underscore-to-camel-case: true
logging:
level:
com.example.redis1.mapper: debug
数据库映射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.example.redis1.mapper.ItemMapper" >
<resultMap id="BaseResultMap" type="com.example.redis1.pojo.Item" >
<id column="id" property="id" jdbcType="INTEGER" />
<result column="code" property="code" jdbcType="VARCHAR" />
<result column="name" property="name" jdbcType="VARCHAR" />
<result column="create_time" property="createTime" jdbcType="TIMESTAMP" />
</resultMap>
<sql id="Base_Column_List" >
id, code, name, create_time
</sql>
<select id="selectByPrimaryKey" resultMap="BaseResultMap" parameterType="java.lang.Integer" >
select
<include refid="Base_Column_List" />
from item
where id = #{id,jdbcType=INTEGER}
</select>
<delete id="deleteByPrimaryKey" parameterType="java.lang.Integer" >
delete from item
where id = #{id,jdbcType=INTEGER}
</delete>
<insert id="insert" parameterType="item" >
insert into item (id, code, name,
create_time)
values (#{id,jdbcType=INTEGER}, #{code,jdbcType=VARCHAR}, #{name,jdbcType=VARCHAR},
#{createTime,jdbcType=TIMESTAMP})
</insert>
<insert id="insertSelective" parameterType="item" >
insert into item
<trim prefix="(" suffix=")" suffixOverrides="," >
<if test="id != null" >
id,
</if>
<if test="code != null" >
code,
</if>
<if test="name != null" >
name,
</if>
<if test="createTime != null" >
create_time,
</if>
</trim>
<trim prefix="values (" suffix=")" suffixOverrides="," >
<if test="id != null" >
#{id,jdbcType=INTEGER},
</if>
<if test="code != null" >
#{code,jdbcType=VARCHAR},
</if>
<if test="name != null" >
#{name,jdbcType=VARCHAR},
</if>
<if test="createTime != null" >
#{createTime,jdbcType=TIMESTAMP},
</if>
</trim>
</insert>
<update id="updateByPrimaryKeySelective" parameterType="item" >
update item
<set >
<if test="code != null" >
code = #{code,jdbcType=VARCHAR},
</if>
<if test="name != null" >
name = #{name,jdbcType=VARCHAR},
</if>
<if test="createTime != null" >
create_time = #{createTime,jdbcType=TIMESTAMP},
</if>
</set>
where id = #{id,jdbcType=INTEGER}
</update>
<update id="updateByPrimaryKey" parameterType="item" >
update item
set code = #{code,jdbcType=VARCHAR},
name = #{name,jdbcType=VARCHAR},
create_time = #{createTime,jdbcType=TIMESTAMP}
where id = #{id,jdbcType=INTEGER}
</update>
<!--根据商品编码查询-->
<select id="selectByCode" resultType="item">
select
<include refid="Base_Column_List" />
from item
where code = #{code}
</select>
</mapper>
pojo
package com.example.redis1.pojo;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.Data;
import java.util.Date;
@Data
public class Item {
private Integer id;
private String code;
private String name;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss",timezone = "GMT+8")
private Date createTime;
}
mapper
package com.example.redis1.mapper;
import com.example.redis1.pojo.Item;
import org.apache.ibatis.annotations.Param;
public interface ItemMapper {
int deleteByPrimaryKey(Integer id);
int insert(Item record);
int insertSelective(Item record);
Item selectByPrimaryKey(Integer id);
int updateByPrimaryKeySelective(Item record);
int updateByPrimaryKey(Item record);
Item selectByCode(@Param("code") String code);
}
service
package com.example.redis1.service;
import com.example.redis1.mapper.ItemMapper;
import com.example.redis1.pojo.Item;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.stereotype.Service;
import java.util.concurrent.TimeUnit;
/**
* 缓存穿透service
* Created by Administrator on 2019/3/17.
*/
@Service
public class CachePassService {
private static final Logger log= LoggerFactory.getLogger(CachePassService.class);
@Autowired
private ItemMapper itemMapper;
@Autowired
private RedisTemplate redisTemplate;
@Autowired
private ObjectMapper objectMapper;
private static final String keyPrefix="item:";
/**
* 获取商品详情-如果缓存有,则从缓存中获取;如果没有,则从数据库查询,并将查询结果塞入缓存中
* @param itemCode
* @return
* @throws Exception
*/
public Item getItemInfo(String itemCode) throws Exception{
Item item=null;
final String key=keyPrefix+itemCode;
ValueOperations valueOperations=redisTemplate.opsForValue();
if (redisTemplate.hasKey(key)){
log.info("---获取商品详情-缓存中存在该商品---商品编号为:{} ",itemCode);
//从缓存中查询该商品详情
Object res=valueOperations.get(key);
if (res!=null&&!(res.equals(""))){
item=objectMapper.readValue(res.toString(),Item.class);
}
}else{
log.info("---获取商品详情-缓存中不存在该商品-从数据库中查询---商品编号为:{} ",itemCode);
//从数据库中获取该商品详情
item=itemMapper.selectByCode(itemCode);
if (item!=null){
valueOperations.set(key,objectMapper.writeValueAsString(item));
}else{
//过期失效时间TTL设置为30分钟-当然实际情况要根据实际业务决定
valueOperations.set(key,"",30L, TimeUnit.MINUTES);
}
}
return item;
}
}
controller
package com.example.redis1.controller;
import com.example.redis1.service.CachePassService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.util.HashMap;
import java.util.Map;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* 缓存穿透实战
* @Author:debug (SteadyJack)
* @Date: 2019/3/17 18:33
**/
@RestController
public class CachePassController {
private static final Logger log= LoggerFactory.getLogger(CachePassController.class);
private static final String prefix="cache/pass";
@Autowired
private CachePassService cachePassService;
/**
* 获取热销商品信息
* @param itemCode
* @return
*/
@RequestMapping(value = prefix+"/item/info",method = RequestMethod.GET)
public Map<String,Object> getItem(@RequestParam String itemCode){
Map<String,Object> resMap=new HashMap<>();
resMap.put("code",0);
resMap.put("msg","成功");
try {
resMap.put("data",cachePassService.getItemInfo(itemCode));
}catch (Exception e){
resMap.put("code",-1);
resMap.put("msg","失败"+e.getMessage());
}
return resMap;
}
}
第一次访问
localhost/cache/pass/item/info?itemCode=book_10010
查看日志输出
用个数据库不存在的
localhost/cache/pass/item/info?itemCode=book_10012
后端的处理是将不存在的key存到redis并指定过期时间
其他典型问题介绍
缓存雪崩:指的的某个时间点,缓存中的Key集体发生过期失效,导致大量查询的请求落到数据库上,导致数据库负载过高,压力暴增的现象
解决方案:设置错开不同的过期时间
缓存击穿:指缓存中某个频繁被访问的Key(热点Key),突然过期时间到了失效了,持续的高并发访问瞬间就像击破缓存一样瞬间到达数据库。
解决办法:设置热点Key永不过期
缓存预热:一般指应用启动前,提前加载数据到缓存中