什么叫緩存穿透?
模擬一個(gè)場景:
前端用戶發(fā)送請求獲取數(shù)據(jù),后端首先會在緩存Redis中查詢,如果能查到數(shù)據(jù),則直接返回.如果緩存中查不到數(shù)據(jù),則要去數(shù)據(jù)庫查詢,如果數(shù)據(jù)庫有,將數(shù)據(jù)保存到Redis緩存中并且返回用戶數(shù)據(jù).如果數(shù)據(jù)庫沒有則返回null;
這個(gè)緩存穿透的問題就是這個(gè)返回的null上面,如果客戶端惡意頻繁的發(fā)起Redis不存在的Key,且數(shù)據(jù)庫中也不存在的數(shù)據(jù),返回永遠(yuǎn)是null.當(dāng)洪流式的請求過來,給數(shù)據(jù)庫造成極大壓力,甚至壓垮數(shù)據(jù)庫.它永遠(yuǎn)越過Redis緩存而直接訪問數(shù)據(jù)庫,這個(gè)過程就是緩存穿透.
其實(shí)是個(gè)設(shè)計(jì)上的缺陷.
緩存穿透解決方案
業(yè)界比較成熟的一種解決方案:當(dāng)越過緩存,且數(shù)據(jù)庫沒有該數(shù)據(jù)返回客戶端null并且存到Redis,數(shù)據(jù)是為"",看實(shí)際情況并給這個(gè)Key設(shè)置過期時(shí)間.這種方案一定程度上減少數(shù)據(jù)庫頻繁查詢的壓力.
實(shí)戰(zhàn)過程
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緩存穿透實(shí)戰(zhàn)', '2019-03-17 17:21:16');
項(xiàng)目整體結(jié)構(gòu)
依賴
<?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 單機(jī)環(huán)境配置
##將docker腳本部署的redis服務(wù)映射為宿主機(jī)ip
##生產(chǎn)環(huán)境推薦使用阿里云高可用redis服務(wù)并設(shè)置密碼
host: 127.0.0.1
port: 6379
password:
database: 0
ssl: false
##redis 集群環(huán)境配置
#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
數(shù)據(jù)庫映射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>
<!--根據(jù)商品編碼查詢-->
<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:";
/**
* 獲取商品詳情-如果緩存有,則從緩存中獲取;如果沒有,則從數(shù)據(jù)庫查詢,并將查詢結(jié)果塞入緩存中
* @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("---獲取商品詳情-緩存中不存在該商品-從數(shù)據(jù)庫中查詢---商品編號為:{} ",itemCode);
//從數(shù)據(jù)庫中獲取該商品詳情
item=itemMapper.selectByCode(itemCode);
if (item!=null){
valueOperations.set(key,objectMapper.writeValueAsString(item));
}else{
//過期失效時(shí)間TTL設(shè)置為30分鐘-當(dāng)然實(shí)際情況要根據(jù)實(shí)際業(yè)務(wù)決定
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;
/**
* 緩存穿透實(shí)戰(zhàn)
* @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
查看日志輸出
用個(gè)數(shù)據(jù)庫不存在的
localhost/cache/pass/item/info?itemCode=book_10012
后端的處理是將不存在的key存到redis并指定過期時(shí)間
其他典型問題介紹
緩存雪崩:指的的某個(gè)時(shí)間點(diǎn),緩存中的Key集體發(fā)生過期失效,導(dǎo)致大量查詢的請求落到數(shù)據(jù)庫上,導(dǎo)致數(shù)據(jù)庫負(fù)載過高,壓力暴增的現(xiàn)象
解決方案:設(shè)置錯(cuò)開不同的過期時(shí)間
緩存擊穿:指緩存中某個(gè)頻繁被訪問的Key(熱點(diǎn)Key),突然過期時(shí)間到了失效了,持續(xù)的高并發(fā)訪問瞬間就像擊破緩存一樣瞬間到達(dá)數(shù)據(jù)庫。
解決辦法:設(shè)置熱點(diǎn)Key永不過期文章來源:http://www.zghlxwxcb.cn/news/detail-837572.html
緩存預(yù)熱:一般指應(yīng)用啟動前,提前加載數(shù)據(jù)到緩存中文章來源地址http://www.zghlxwxcb.cn/news/detail-837572.html
到了這里,關(guān)于Redis緩存預(yù)熱-緩存穿透-緩存雪崩-緩存擊穿的文章就介紹完了。如果您還想了解更多內(nèi)容,請?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!