SpringBoot集成Redis—缓存穿透解决方案与哨兵模式实战

news/2025/3/31 8:55:09/

目录

1、环境准备

1)pom.xml引入Redis依赖

2) 演示业务场景

2、SpringBoot集成Redis单机模式

1) 通过MyBatis逆向工程生成实体Bean和数据持久层

2) application.yml 中配置redis连接信息

3) 启动redis服务

4)  XinTuProductRedisController类

5) XinTuProductRedisService实现

6)  启动类SpringbootApplication

7)  启动SpringBootCase应用,访问测试

8)  打开Redis 客户端

3、缓存穿透现象

1) 穿透测试

XinTuRedisPenetrateController测试类。 

2 )启动应用程序,浏览器访问测试

3)造成的问题

4)解决方法

4、SpringBoot集成Redis哨兵模式(一主三从三哨兵)

5. 哨兵配置

1)验证主从数据同步

2) 主节点选举


 

以下案例依然在SpringBootCase项目基础上完成。(Redis采用Redis-x64-3.2.100版本)

1、环境准备

1)pom.xml引入Redis依赖

<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

2) 演示业务场景

根据商品总数功能,先从Redis缓存中查找,如果找不到,再从MySQL数据库中查找,然后将数据放到Redis缓存。

2、SpringBoot集成Redis单机模式

1) 通过MyBatis逆向工程生成实体Bean和数据持久层

实体类:

package com.xintu.demo.entity;import java.util.Date;public class TProduct {private Integer id;private Integer categoryId;private String itemType;private String title;private String sellPoint;private String price;private Integer num;private String image;private Integer status;private Integer priority;private String createdUser;private Date createdTime;private String modifiedUser;private Date modifiedTime;public Integer getId() {return id;}public void setId(Integer id) {this.id = id;}public Integer getCategoryId() {return categoryId;}public void setCategoryId(Integer categoryId) {this.categoryId = categoryId;}public String getItemType() {return itemType;}public void setItemType(String itemType) {this.itemType = itemType == null ? null : itemType.trim();}public String getTitle() {return title;}public void setTitle(String title) {this.title = title == null ? null : title.trim();}public String getSellPoint() {return sellPoint;}public void setSellPoint(String sellPoint) {this.sellPoint = sellPoint == null ? null : sellPoint.trim();}public String getPrice() {return price;}public void setPrice(String price) {this.price = price == null ? null : price.trim();}public Integer getNum() {return num;}public void setNum(Integer num) {this.num = num;}public String getImage() {return image;}public void setImage(String image) {this.image = image == null ? null : image.trim();}public Integer getStatus() {return status;}public void setStatus(Integer status) {this.status = status;}public Integer getPriority() {return priority;}public void setPriority(Integer priority) {this.priority = priority;}public String getCreatedUser() {return createdUser;}public void setCreatedUser(String createdUser) {this.createdUser = createdUser == null ? null : createdUser.trim();}public Date getCreatedTime() {return createdTime;}public void setCreatedTime(Date createdTime) {this.createdTime = createdTime;}public String getModifiedUser() {return modifiedUser;}public void setModifiedUser(String modifiedUser) {this.modifiedUser = modifiedUser == null ? null : modifiedUser.trim();}public Date getModifiedTime() {return modifiedTime;}public void setModifiedTime(Date modifiedTime) {this.modifiedTime = modifiedTime;}
}

数据层Mapper: 

package com.xintu.demo.mapper;import com.xintu.demo.entity.TProduct;
import com.xintu.demo.entity.TProductExample;
import java.util.List;import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;@Mapper //方式一:添加@Mapper注解,等同于主类上加@MapperScan("com.demo.demo.mapper")
public interface TProductMapper {long countByExample(TProductExample example);int deleteByExample(TProductExample example);int deleteByPrimaryKey(Integer id);int insert(TProduct record);int insertSelective(TProduct record);List<TProduct> selectByExample(TProductExample example);TProduct selectByPrimaryKey(Integer id);int updateByExampleSelective(@Param("record") TProduct record, @Param("example") TProductExample example);int updateByExample(@Param("record") TProduct record, @Param("example") TProductExample example);int updateByPrimaryKeySelective(TProduct record);int updateByPrimaryKey(TProduct record);
}

2) application.yml 中配置redis连接信息

完整application.yml配置文件如下:

#spring:
#  profiles:
#      active: test #激活对应环境配置,以测试环境为例
server:port: 8888 # 设置内嵌Tomcat端口号servlet:context-path: /springbootcase # 设置项目上下文根路径,这个在请求访问的时候需要用到test:site: 35xintu.com #测试站点user: xintu #测试用户spring:datasource: # mysql相关配置url: jdbc:mysql://localhost:3306/xintu?useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghaiusername: rootpassword: xxx #根据自己的本地配置情况设置devtools:restart:enabled: true  #设置开启热部署additional-paths: src/main/java #重启目录exclude: WEB-INF/** #排除一些不需要自动重启的资源log-condition-evaluation-delta: false #关闭在什么情况下重启的日志记录,需要时可以打开thymeleaf:cache: false #使用Thymeleaf模板引擎,关闭缓存redis: #配置redis连接信息(单机模式)host: 192.168.92.134port: 6379password: #根据自己的本地配置情况设置#在application.yml配置文件中指定映射文件的位置,这个配置只有接口和映射文件不在同一个包的情况下,才需要指定:
mybatis:mapper-locations: classpath:mapper/*.xml

3) 启动redis服务

4)  XinTuProductRedisController类

package com.xintu.demo.controller;import com.xintu.demo.service.XinTuProductRedisService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;import javax.servlet.http.HttpServletRequest;/*** @author XinTu* @classname XinTuProductRedisController* @description TODO* @date 2023年05月05日 5:21*/@RestController
public class XinTuProductRedisController {@Autowiredprivate XinTuProductRedisService productRedisService;@GetMapping(value = "/productredis/allProductNumber")public String allProductNumber(HttpServletRequest request) {Long allProductNumber = productRedisService.allProduct();return "商品数量:" + allProductNumber;}
}

5) XinTuProductRedisService实现

package com.xintu.demo.service;import com.xintu.demo.entity.TProductExample;
import com.xintu.demo.mapper.TProductMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import org.springframework.stereotype.Service;import java.util.concurrent.TimeUnit;/*** @author XinTu* @classname XinTuProductRedisService* @description TODO* @date 2023年05月05日 5:22*/
@Service
public class XinTuProductRedisService {@Autowiredprivate TProductMapper productMapper;// 注入 spring data当中的 RedisTemplate 类@Autowiredprivate RedisTemplate redisTemplate;public Long allProduct() {//设置redisTemplate对象key的序列化方式redisTemplate.setKeySerializer(new StringRedisSerializer());//从redis缓存中获取总商品数Long productCount = (Long) redisTemplate.opsForValue().get("product_count");System.out.println("查询Redis数据库..."+productCount);//判断是否为空if (null == productCount) { //去mysql数据库查询,并存放到redis缓存中System.out.println("查询MySQL数据库...");TProductExample example = new TProductExample();productCount = productMapper.countByExample(example);redisTemplate.opsForValue().set("product_count",productCount, 1, TimeUnit.SECONDS); // 会影响缓存穿透执行时长}return productCount;}}

6)  启动类SpringbootApplication

package com.xintu.demo;import com.xintu.demo.config.XinTuConfigInfo;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.transaction.annotation.EnableTransactionManagement;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;@EnableTransactionManagement //开启事务
@RestController
@SpringBootApplication
public class SpringbootApplication {@Autowiredprivate XinTuConfigInfo configInfo; //测试@ConfigurationProperties@Value("${test.site}")private String site;@Value("${test.user}")private String user;public static void main(String[] args) {SpringApplication.run(SpringbootApplication.class, args);}@GetMapping("/hello")public String hello(@RequestParam(value = "name", defaultValue = "World") String name) {return String.format("欢迎 %s 来到<a href=\"http://www.35xintu.com\">35新途</a>!", name);}@GetMapping("/value")public String testValue() { //测试 @Value 注解return String.format("欢迎 %s 来到<a href=\"http://www.35xintu.com\">%s</a>!" , user,site);}@GetMapping("/config")public String config() { //测试 @ConfigurationProperties 注解System.out.println("hello");return String.format("欢迎 %s 来到<a href=\"http://www.35xintu.com\">%s</a>!" , configInfo.getUser(),configInfo.getSite());}}

7)  启动SpringBootCase应用,访问测试

http://localhost:8888/springbootcase/productredis/allProductNumber 

8)  打开Redis 客户端

启动命令:redis-cli.exe查询命令:get product_count

3、缓存穿透现象

1) 穿透测试

XinTuRedisPenetrateController测试类。 

package com.xintu.demo.controller;import com.xintu.demo.service.XinTuProductRedisService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;import javax.servlet.http.HttpServletRequest;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;/*** @author XinTu* @classname XinTuRedisPenetrateController* @description 模拟缓存穿透* @date 2023年05月05日 6:00*/@RestController
public class XinTuRedisPenetrateController {@Autowiredprivate XinTuProductRedisService productRedisService;@GetMapping(value = "/productredispenetrate/allProductNumber")public String allProductNumber(HttpServletRequest request) {Long allProductNumber = productRedisService.allProduct();//线程池个数,一般建议是CPU内核数 或者 CPU内核数据*2ExecutorService executorService = Executors.newFixedThreadPool(8);for (int i = 0; i < 2000; i++) {executorService.submit(new Runnable() {@Override public void run() {productRedisService.allProduct();}});}return "商品数量:" + productRedisService.allProduct();}
}

2 )启动应用程序,浏览器访问测试

3)造成的问题

多个线程都去查询数据库,这种现象就叫做缓存穿透,如果并发比较大,对数据库的压力过大,有可能造成数据库宕机。

4)解决方法

方案一:加同步锁

修改StudentServiceImpl中的代码

public Long allProduct() {//设置redisTemplate对象key的序列化方式redisTemplate.setKeySerializer(new StringRedisSerializer());//从redis缓存中获取总商品数Long productCount = (Long) redisTemplate.opsForValue().get("product_count");System.out.println("查询Redis数据库..."+productCount);//判断是否为空if (null == productCount) { //去mysql数据库查询,并存放到redis缓存中//设置同步代码块synchronized (this) { //加同步锁productCount = (Long) redisTemplate.opsForValue().get("product_count");if (null == productCount) { // 双重验证System.out.println("查询MySQL数据库...");TProductExample example = new TProductExample();productCount = productMapper.countByExample(example);redisTemplate.opsForValue().set("product_count",productCount, 1, TimeUnit.SECONDS); // 会影响缓存穿透执行时长}}}return productCount;
}

启动应用程序,浏览器访问测试,查看控制台输出只有第一个线程查询数据库,其它线程查询Redis缓存,这样的解决的小问题就是第一批进来的用户会有一个等待,但是这样的影响可以忽略。

① 为什么要做双层验证?

防止线程获取到cpu执行权限的时候,其他线程已经将数据放到Redis中了,所以再次判断;

不能将synchronized范围扩大,因为如果Redis缓存中如果有数据,线程不应该同步,否则影响效率。

② 加同步锁是否是最优方案?

如何是在集群模式下,这种方式依然会有问题。这个时候就需要考虑采用redis分布式锁了,具体方案大家可以自行研究。

4、SpringBoot集成Redis哨兵模式(一主三从三哨兵)

6379是主节点,6380和6381是从节点。

分别修改每个redis.windows.conf和redis.windows-service.conf中的端口号为:6379(主节点保持不变)、6380、6381。

从节点配置文件需要加:

slaveof localhost 6379

从节点整体配置文件:

# 端口配置
port 6380
# 日志文件名
logfile "redis_log_6380.log"
# rdb持久化文件名字
dbfilename "dump6380.rdb"
# 本地ip
bind 127.0.0.1
# 绑定主从关系【该设置说明端口6380的服务为从机,它的主机为:6379】# 从机是否只能读 默认是yes
slave-read-only no

4. 分别启动三台Redis服务器

主节点启动,

从节点启动,

验证主节点,

从节点验证,

5. 哨兵配置

#哨兵模式redis集群配置(哨兵模式)

  redis: #配置redis连接信息(单机模式)host: localhost
#    port: 6379 #f哨兵模式下不要写端口号
#    password: 123456sentinel:  #哨兵模式redis集群配置(哨兵模式)master: mymaster #与哨兵中的sentinel monitor xxx 保持一致nodes: localhost:26379,localhost:26380,localhost:26381

哨兵直接复制Redis文件目录即可。老王这里分为Redis-Sentinel-26379、Redis-Sentinel-26380、Redis-Sentinel-26381。

三个哨兵节点分别增加 sentinel.conf 文件, 文件内容如下:

# 哨兵sentinel实例运行的端口 默认26379
port 26379# 保护模式
protected-mode no# 本地ip
bind 127.0.0.1# 哨兵监听的主服务器 后面的1表示主机挂掉以后进行投票,只需要2票就可以从机变主机
sentinel myid 9c65a6f7aad9e2419a6abce1ce56ff28cb81df34# 设置主机的密码(无密码可以省略)
# sentinel auth-pass mymaster 35xintu# 设置未得到主机响应时间,此处代表5秒未响应视为宕机
sentinel monitor mymaster 127.0.0.1 6380 2# 设置等待主机活动时间,此处代表15秒主机未活动,则重新选举主机
sentinel down-after-milliseconds mymaster 5000# 设置重新选举主机后,同一时间同步数据的从机数量,此处代表重新选举主机后,每次2台从机同步主机数据,直到所有从机同步结束
sentinel failover-timeout mymaster 15000

现在我们启动3个哨兵.

注意,启动redis主备集群时要先启动主,后启动从,哨兵先启动哪个都可以。

启动哨兵命令:redis-server.exe sentinel.conf --sentinel 

分被启动成功之后,就可以进行测试了。

1)验证主从数据同步

客户端连接命令:redis-cli.exe -h 127.0.0.1 -p 6380。

2) 主节点选举

停掉主节点:

验证主节点是否关闭,

哨兵模式中,进行重新选举,

然后看SpringBoot控制台,页切换为了6380. 

此时,说明哨兵模式已经生效,关于主从复制和哨兵机制的原理部分,会在后面的redis相关课程当中给大家详细分析,本篇注重的是SpringBoot集成Redis实战。

以上!可关注,持续输出优质内容!


http://www.ppmy.cn/news/63908.html

相关文章

C语言中链表经典面试题目

&#x1f436;博主主页&#xff1a;ᰔᩚ. 一怀明月ꦿ ❤️‍&#x1f525;专栏系列&#xff1a;线性代数&#xff0c;C初学者入门训练&#xff0c;题解C&#xff0c;C的使用文章&#xff0c;「初学」C &#x1f525;座右铭&#xff1a;“不要等到什么都没有了&#xff0c;才下…

Unity PlayerPrefs、JsonUtility

Unity中有两个常用的数据存储方式&#xff1a;PlayerPrefs和JsonUtility。 PlayerPrefs PlayerPrefs是Unity内置的一种轻量级数据存储方式&#xff0c;可用于存储少量的游戏数据&#xff0c;如分数、解锁状态等。使用PlayerPrefs需要注意以下几点&#xff1a; 存储数据时&am…

【id:115】【20分】D. 向量4(类复合)

文章目录 一、题目描述二、输入与输出1.输入2.输出 三、参考代码四、题解思路 一、题目描述 为向量1题目中实现的CVector类增加成员函数float Average()&#xff0c;计算n维向量的平均值并返回。 定义CStudent类&#xff0c;私有数据成员为&#xff1a; string name; // 姓名…

JavaScript (五) -- JavaScript 事件(事件的绑定方式)

目录 1. JavaScript 事件的概述: 2. 事件的绑定(两种方式): 1. JavaScript 事件的概述: JavaScript事件是指当网页中某个元素被触发时,可以执行一些JS代码来处理这个事件,例如鼠标单击、鼠标移动、键盘按键等。事件通常被认为是浏览器与用户交互的方式之一…

CSS布局基础(精灵图 字体图标 css 三角图标)

精灵图 & 字体图标 & css 三角图标 精灵图使用字体图标下载字体图标使用方式icomoon阿里 iconfontttf 字体 unicodecss 方式js 方式 更新字体图标icomoon阿里 iconfont css三角图标标准三角&#xff08;垂直的两边相等&#xff09;先来个普通盒子&#xff08;当然是五…

深入探究C++中的STL:容器、迭代器与算法全解析

C 基础知识 四 认识STL 上一、 概述1. 起源 Standard Template Library2. 发展历程3. 组成部分与内部实现原理4. 优点和局限性4.1优点4.2局限二、容器1. 定义2. 序列容器2.1 vector2.2 deque2.3 list2.4 forward_list3. 关联容器3.1 set 与 multiset3.2 map 与 multimap4. 无序…

ORA-01555 ORA-22924 快照过旧问题处理

ORA-01555 ORA-22924 快照过旧问题处理 问题描述 使用数据泵导出数据&#xff0c;或在业务功能查询某个表时&#xff0c;可能出现 ORA-01555 ORA-22924 快照过旧的错误&#xff1a; ORA-01555: snapshot too old: rollback segment number with name "" too small…

动画图解常见串行通讯协议:SPI、I²C、UART、红外分析

一、SPI传输 图1&#xff1a;SPI 数据传输 图1.2&#xff1a;SPI数据传输&#xff08;2&#xff09; ​ 图1.3&#xff1a; SPI时序信号 二、IC传输 图1.2.1&#xff1a; I2C总线以及寻址方式 三、UART传输 图1.3.1&#xff1a;PC 上通过UART来调试MCU 图1.3.2&#xff1a;R…