当前位置: 首页 > news >正文

【分布式】分布式系统、Redis中间件 、Cache穿透、击穿、雪崩

分布式

内容管理

    • 分布式系统
      • 单点集中式web应用
      • 应用与文件服务和数据库单独拆分
      • 引入缓存、集群,改善系统整体性能
      • 数据库读写分离,反向代理CDN加速
      • 分布式文件系统和分布式数据库
    • 分布式中间件
    • 微服务项目
      • 微服务项目规范
      • 创建微服务项目
    • Redis --- 缓存中间件
      • Redis 微服务
        • List --- 排名、排行榜、近期访问
        • Set ---- 缓存不重复数据 【自动剔除重复】、解决重复提交、剔除重复ID
        • Sorted Set --- 排行榜(充值、积分、成绩)
        • Hash --- 缓存对象,减少key数量
        • Key失效 ---- 数据库查询的数据放入缓存设置TTL, 在TTL内查询直接从缓存读取
      • 缓存穿透
        • 解决方案: Null也缓存
        • 缓存穿透演示 ----- Goods系统
      • 缓存雪崩
        • 解决方案:TTL后加随机数,均匀失效
      • 缓存击穿
        • 解决方法: 热点Key不设置过期时间、 互斥锁


分布式系统introduce、 使用SpringBoot搭建规范的微服务项目,Redis击穿、雪崩、穿透


Cfeng同时再进行多条线路的进行: 架构师应试、项目完善(cfeng.net)、高并发(JUC、多线程)、高性能(分库分表,性能优化)、分布式(cloud、微服务、分布式中间件),当前的内容属于分布式专题,相关的代码会上传到gitee

分布式系统

分布式 系统 — 高吞吐、强扩展、高并发、低延迟、灵活部署;

  • 分布式系统强大, system内部至少由多台计算机组成(性能更大), 一个统一的“机器中心”, 由一组独立的计算机组成【区别与之前的一台】
  • 但是,用户感知 该机器中心为一个单个系统,不能感知到计算机集群的存在

最简单的: 程序A、B运行在两台计算机上面,协作完成一个功能,理论上说,这就组成了分布式系统,A、B程序可以相同,也可以不同,如果相同(比如Redis)就组成了集群 redis集群主从复制,哨兵选举(ping pong)

分布式系统之前,软件系统基本都是集中式的,单机系统【软件、硬件高度耦合】,但是随着访问量和业务量的上升,应用逐渐从集中式(单体)转化为分布式

单点集中式web应用

				 web应用容器(端system)
				------------------------------------
				|								|
                 |    ---->   Mysql存储			  |
				|	|							|
用户 ---访问----> | Web应用						 |
				|	|						    |
				|	---->  文件存储				  |
				|                        		  |
				-------------------------------------

单点集中式Web应用架构作为后台管理应用为主: CRM或者OA都可以, 特点就是 项目的数据库(Mysql、redis、mongoDB)和 应用项目的war包 部署在同一台服务器; 同时文件的上传存储也是上传到本台服务器

单点集中式项目 适合小型项目,发布便捷、运维工作量小,但是一旦服务器挂了, 不管是应用、还是存储都是over了

cfeng目前的项目都是单点集中式,但是引入minIO之后将逐步文件存储分离; (其实是因为非盈利的流量小,不必要开很多台服务器)

应用与文件服务和数据库单独拆分

随着应用的运行,上传到服务器的文件和数据库的数据量会急剧扩大,大量占据服务器的容量,影响应用的性能

为了解决文件和数据库数据量逐步扩大占据了服务器的容量, 所以将数据库、web应用和文件存储服务单独拆分为独立的服务, 避免了存储的瓶颈

				 web应用容器(端系统)    -------->   DB容器(host)    Mysql存储
				-----------------       |
				|				| _____|
用户 ---访问----> |      Web应用	  |      |
				-------------------      ------- >    文件服务容器(host) 文件存储

三者拆分的架构方式, 三个服务独立部署,不同的服务器宕机仍然可使用, 且不需要考虑占用过多容量导致web应用的效率降低; 不同的服务器宕机之后,其他的仍然可以使用

比如minio文件服务器单独部署一台服务器,DB单独占据一台服务器

引入缓存、集群,改善系统整体性能

文件、DB拆分之后解决了文件占用存储容量导致web服务容量少的问题,但是当并发量变大,还是存在问题

请求并发量增加之后, 单台Web服务器(Tomcat)不足以支撑应用, 引用缓存和集群可以解决问题:

  • 引入Cache: 将大量用户的读请求引导到缓存(redis),写操作进入数据库【读写分离】,性能优化: 将数据库一部分或者系统经常访问的数据放入缓存中,减少数据库的访问的压力,提高并发性能
  • 引入集群: 减少单台服务器的压力。 可以部署多个Tomcat服务器减少单台服务器的压力, 如Nginx + Lvs; 多个应用服务器负载均衡,减少单机的负载压力, Session使用Redis管理)
					     redis(hosts) 集群
					       |
                         	 |
                        	  web应用容器  host(集群 nginx)
					     web应用容器(host)
					 web应用容器 (host)      -------->   DB容器(host)    Mysql存储
					-----------------       |
					|				| _____|
用户 -负载均衡-访问--> |      Web应用	  |       |
					-------------------      ------- >    文件服务容器(host) 文件存储

数据库读写分离,反向代理CDN加速

互联网system中,用户的读请求数量往往大于写请求,但是读写会相互竞争,这个时候写操作会受到影响,数据库出现存储瓶颈【春节高峰12306访问】,因此一般情况下会像redis集群一样读写分离,主写从读

除此之外,为了加速网站的访问速度,加速静态资源的访问,需要将系统大部分静态资源放到CDN中, 加入反向代理的配置,减少访问网站直接去服务器读取静态数据

DB读写分离将有效提高数据库的存储性能, CDN和反向代理加速加速系统访问速度

					  		  		 redis(hosts) 集群
					    		  		 |
                         					 |
                        					  web应用容器  host(集群 nginx)
					    				 web应用容器(host)
							 		web应用容器 (host)       ------->   DB容器(host 主)    Mysql存储
									-----------------       |     -->   DB容器(host 从)     mysql从  
								-->|				| _____|
用户 -CDN加速 --反向代理-负载均衡-访问--> |      Web应用   |       |
								 -->|------------------      ------- >    文件服务容器(host) 文件存储

分布式文件系统和分布式数据库

统计检测发现,系统对于某些表的请求量最大,为了进一步减少数据库压力,需要分库分表, 根据业务拆分数据库

					  		  		 redis(hosts) 集群
					    		  		 |
                         					 |
                        					  web应用容器  host(集群 nginx)
					    				 web应用容器(host)
							 		web应用容器 (host)       ------->   DB容器(host 主)    分布式数据库
									-----------------       |                             读写分离,分库分表
								-->|				| _____|
用户 -CDN加速 --反向代理-负载均衡-访问--> |      Web应用   |       |
								 -->|------------------      ------- >    文件服务容器(host) 文件存储

软件系统从集中式单机系统,为了解决存储占用容量,web的处理能力,数据库访问速度,加载速度、业务DB压力,不断升级为集群的分布式数据库和分布式文件系统; 高吞吐、高并发、低延迟的特点 ------ 产生分布式系统

  • 内聚性和透明性 : 分布式系统建立在网络之上(网络的传输访问); 高度的内聚,透明
  • 可扩展性质: 分布式系统可以随着业务增长动态扩展系统组件,提高系统整体的处理能力 ----- 优化系统性能,升级硬件(垂直) ; 增加计算单元(服务器)— 扩展系统规模 水平扩展
  • 可用可靠性: 可靠性 ---- 给定周期内系统无故障运行的平均事件,可用性 ---- 量化的指标是给定周期内系统无故障运行的总时间 (可靠 为平均; 可用 为 总)
  • 高性能: 不管单机系统还是分布式系统,都重视性能, 常见: 高并发 ( 单位时间处理任务越多越好)、 低延迟(每个任务的平均处理时间最少),分布式系统就是利用更多机器实现更强大计算存储能力 — 高性能
  • 一致性: 分布式为了提高可用可靠性,一般都会引入冗余(副本),为例保证各节点状态一直,必须一致性,多个节点在给定时间内操作或者存储的数据只有一份

分布式系统也是存在很多隐患的:

  • 网络不可靠: 分布式系统中节点本质通过网络通信,网络可能不可靠,出现网络延时、丢包、消息丢失
  • 节点故障不可避免: 分布式系统节点数目增加,出现故障概率变高,可用可靠性质,故障发生要保证系统可用,所以需要将该节点负责的计算和存储服务转移到其他节点

分布式中间件

分布式中间件 是一种 独立的基础系统软件、服务程序; 处于操作系统软件和用户的应用软件之间,具有独立性,作为独立的软件系统

比如redis/rabittMQ、Zookeeper、Elasticsearch、Nginx等都是中间件, 可以实现缓存、消息队列、分布式锁、全文搜索、负载均衡等功能; 高吞吐、并发、低延迟、负载均衡等要求让中间件也开始变为分布式;eg: 基于Redis的分布式缓存、基于RabitMQ的分布式消息中间件、基于ZooKeeper的分布式锁、基于Elasticsearch的全文搜索

  • Redis: 基于内存存储的内存数据库,主要就是作为缓存使用
  • Redission: 架设在redis基础上的java驻内存数据网络 In-Memory Data Gird, 可以说Redission为Redis的升级版,分布式的工具 【协调分布式多机多线程并发系统】,Redission能够更好处理分布式锁
  • RabbitMQ: 消息中间件,实现消息的异步分发、模块解耦、接口限流; 处理高并发的业务场景,【接口限流,降低压力,异步分发降低响应时间】
  • Zookeeper: 分布式的应用程序协调服务【注册中心】,Dubbo的服务者消费者,注册服务,订阅服务; 配置维护、分布式同步、 分布式独享锁🔒、选举、队列

微服务项目

SpringBoot — “微框架”,快速开发扩展性强、微小项目 【其能够很好编写微服务项目,而不是解决SSM的相关的短板】

SpringBoot的起步依赖和自动配置解决了SSM的配置难,xml文件复杂的短板 ,其能够开撕搭建企业级项目并且可以快速整合第三方框架、内嵌容器,打包jar即可部署到服务器,并且内置Actuator监控,Cfeng使用时倾向于使用Spring家族的其他产品: spring JDBC、Spring Data、 Spring Security

微服务项目规范

微服务的开发需要规范化,才有利于团队协作以及后期的维护

主要的规范----- 基于Maven构建多模块, 每个模块各司其职,负责应用的不同的功能,每个模块采用层级依赖的方式,最终构成聚合型的Maven项目 【Cfeng.net最开始没有涉及为微服务形式,后期扩展繁杂】

                  |----------子模块:api: 面向接口服务的配置,比如待发布的Dubbo服务接口配置在该模块
                  |                        整个项目中所有模块公用的依赖配置,可以层级式传递依赖
                  |           
父模块  -----------| --------  子模块: model: 面向ORM(对象实体映射) 数据库的访问配置
				 |
				 |___________ 子模块: server: 用于打包可执行的jar、war的执行组件
				 							 Spring Boot应用的启动类所在位置
				 							 整个项目/服务的核心开发逻辑

父模块聚合多个字模块,包括api、model、server等多个模块,server依赖model,model依赖api,形成聚合的maven项目

创建微服务项目

之前Cfeng都是创建的单模块项目,这里演示创建多模块的微服务项目

  • IDEA中: File下面创建新项目 New —> New Project ,选择Maven、选择SDK版本,之后直接Next(不使用模板,那是创建module),命名项目,maven坐标尽量简洁,选择项目的位置,之后finish即可
  • 进入项目的初始页面,显示的pom.xml就是父模块的配置文件,在该配置文件中,指定整个项目的资源编码和JDK版本以及公共依赖
<?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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>indvi.cfeng</groupId>
    <artifactId>CfengMiddleWare</artifactId>
    <version>1.0.0</version>
    
    <!--定义整个项目的编、还有版本等信息.....-->
    <properties>
        <java.version>17</java.version>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <maven.compiler.source>${java.version}</maven.compiler.source>
        <maven.conpiler.target>${java.version}</maven.conpiler.target>
    </properties>


</project>
  • 创建各个子模块,直接在父模块下面开始创建,比如创建子模块api; 直接点击父模块,点击New— Module; 之后还是选择Maven的SDK之后, Next选择模块名称即可; 会自动舒适化生成子模块的pom.xml
<?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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>CfengMiddleWare</artifactId>
        <groupId>indvi.cfeng</groupId>
        <version>1.0.0</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>api</artifactId>

    <properties>
        <lombok.version>1.18.20</lombok.version>
        <jackson-annotations-version>2.12.6</jackson-annotations-version>
    </properties>

    <dependencies>
        <!-- lombok依赖-->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>${lombok.version}</version>
        </dependency>

        <!-- jackson依赖-->
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-annotations</artifactId>
            <version>${jackson-annotations-version}</version>
        </dependency>
    </dependencies>
    
</project>
  • 同理再创建model模块 【父模块不需要src文件夹,删除,几个子模块中放置代码,父模块进行管理即可】 各个模块包括父模块的groupID都是indvi.cfeng
<?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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>CfengMiddleWare</artifactId>
        <groupId>indvi.cfeng</groupId>
        <version>1.0.0</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>model</artifactId>

    <properties>
        <mybatis-plus-spring-boot.version>3.5.2</mybatis-plus-spring-boot.version>
        <mybatis-pagehelper.version>4.1.2</mybatis-pagehelper.version>
    </properties>

    <dependencies>
        <!-- model模块依赖api模块 ,将api的相关的lombok和jackson都引入了的-->
        <dependency>
            <groupId>indvi.cfeng</groupId>
            <artifactId>api</artifactId>
            <version>${project.parent.version}</version>
        </dependency>

        <!-- mybatis-plus依赖  model模块主要就是处理数据 -->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>${mybatis-plus-spring-boot.version}</version>
        </dependency>

    </dependencies>
</project>
  • 最后创建核心的业务模块server,可以使用依赖管理配置项,配置spring-boot的版本,数据库连接池使用druid,使用starter
<?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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>CfengMiddleWare</artifactId>
        <groupId>indvi.cfeng</groupId>
        <version>1.0.0</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>server</artifactId>
    <!-- 该模块就是处理项目的核心的业务-->

    <packaging>jar</packaging>
    <properties>
        <!-- 指定boot项目的起始类的位置 -->
        <start-class>cfengMiddware.server.MiddleApplication</start-class>
        <!-- server为主模块,引入springBoot的相关依赖starter-->
        <spring-boot.version>2.7.2</spring-boot.version>
        <spring-session.version>1.3.5.RELEASE</spring-session.version>
        <log4j.version>1.3.8.RELEASE</log4j.version>
        <mysql.version>8.0.27</mysql.version>
        <druid.version>1.2.8</druid.version>
        <guava.version>31.1-jre</guava.version>
    </properties>

    <!-- springboot依赖管理  子starter就可以不写版本号,之前springbootCLI方式创建的是parent,定义版本号-->
    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-dependencies</artifactId>
                <version>${spring-boot.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <dependencies>
        <!--model依赖-->
        <dependency>
            <groupId>indvi.cfeng</groupId>
            <artifactId>model</artifactId>
            <version>${project.parent.version}</version>
        </dependency>

        <!--日志-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-log4j</artifactId>
            <version>${log4j.version}</version>
        </dependency>


        <!-- guava-->
        <dependency>
            <groupId>com.google.guava</groupId>
            <artifactId>guava</artifactId>
            <version>${guava.version}</version>
        </dependency>

        <!-- Mysql-->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>${mysql.version}</version>
        </dependency>

        <!-- druid-->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid-spring-boot-starter</artifactId>
            <version>${druid.version}</version>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
            <version>${spring-boot.version}</version>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <finalName>cfeng_middleware_${project.parent.version}</finalName>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <version>${spring-boot.version}</version>
                <executions>
                    <execution>
                        <goals>
                            <goal>repackage</goal>
                        </goals>
                    </execution>
                </executions>
                <configuration>
                    <excludes>
                        <exclude>
                            <groupId>org.projectlombok</groupId>
                            <artifactId>lombok</artifactId>
                        </exclude>
                    </excludes>
                </configuration>
            </plugin>
        </plugins>

        <!-- resource默认就是src下,不需要配置-->
    </build>

</project>

引入了model依赖,因此还包括mybatis、lombok等依赖

  • 在server的src下面创建主启动类,其位置在server的配置文件中指定
@SpringBootApplication
public class MiddleApplication extends SpringBootServletInitializer {

    @Override
    protected SpringApplicationBuilder configure(SpringApplicationBuilder builder) {
        return super.configure(builder);
    }

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

日志还需要在resources下面配置log4j.properties配置文件

#log4j.rootLogger=CONSOLE,info,error,DEBUG
log4j.rootLogger=info,error,CONSOLE,DEBUG
log4j.appender.CONSOLE=org.apache.log4j.ConsoleAppender     
log4j.appender.CONSOLE.layout=org.apache.log4j.PatternLayout     
log4j.appender.CONSOLE.layout.ConversionPattern=%d{yyyy-MM-dd-HH-mm} [%t] [%c] [%p] - %m%n     
log4j.logger.info=info
log4j.appender.info=org.apache.log4j.DailyRollingFileAppender
log4j.appender.info.layout=org.apache.log4j.PatternLayout     
log4j.appender.info.layout.ConversionPattern=%d{yyyy-MM-dd-HH-mm} [%t] [%c] [%p] - %m%n  
log4j.appender.info.datePattern='.'yyyy-MM-dd
log4j.appender.info.Threshold = info   
log4j.appender.info.append=true   
log4j.appender.info.File=/home/admin/pms-api-services/logs/info/api_services_info
#log4j.appender.info.File=/Users/dddd/Documents/testspace/pms-api-services/logs/info/api_services_info
log4j.logger.error=error  
log4j.appender.error=org.apache.log4j.DailyRollingFileAppender
log4j.appender.error.layout=org.apache.log4j.PatternLayout     
log4j.appender.error.layout.ConversionPattern=%d{yyyy-MM-dd-HH-mm} [%t] [%c] [%p] - %m%n  
log4j.appender.error.datePattern='.'yyyy-MM-dd
log4j.appender.error.Threshold = error   
log4j.appender.error.append=true   
log4j.appender.error.File=/home/admin/pms-api-services/logs/error/api_services_error
#log4j.appender.error.File=/Users/dddd/Documents/testspace/pms-api-services/logs/error/api_services_error
log4j.logger.DEBUG=DEBUG
log4j.appender.DEBUG=org.apache.log4j.DailyRollingFileAppender
log4j.appender.DEBUG.layout=org.apache.log4j.PatternLayout     
log4j.appender.DEBUG.layout.ConversionPattern=%d{yyyy-MM-dd-HH-mm} [%t] [%c] [%p] - %m%n  
log4j.appender.DEBUG.datePattern='.'yyyy-MM-dd
log4j.appender.DEBUG.Threshold = DEBUG   
log4j.appender.DEBUG.append=true   
log4j.appender.DEBUG.File=/home/admin/pms-api-services/logs/debug/api_services_debug
#log4j.appender.DEBUG.File=/Users/dddd/Documents/testspace/pms-api-services/logs/debug/api_services_debug

mybatis-plus引入只需要配置数据源,指定type为druid即可,因为其余对象已经由SpringBoot自动配置了,将@MapperScan放在主启动类上面扫描Mapper所在位置即可

Redis — 缓存中间件

之前Cfeng分享过Redis,包括在LInux上面的基本操作和各种基本的数据结构、常用命令,以及使用Jedis客户端,整合使用RedisTemplate或者Repository方式 ,整合Spring-Data-Redis框架,替换lettuce为jedis,【当然最佳的为Redission】

所以接下来的重点就是结合 具体实际分析Redis以及其相关问题比如雪崩、穿透、击穿

单体架构的热门应用是撑不住巨大的用户流量的,所以新型的架构比如 面向SOA系统架构、分库分表应用架构、微服务/分布式系统架构, 基于分布式中间件架构 层出不穷

巨大流量分析用户的读请求 远远多于用户的写请求, 频繁的读请求在高并发的情况下会增加数据库压力,导致服务器整体的压力上升 -------- 响应慢,卡住 (Cfeng的网站没有CDN加速,也挺慢的目前)

解决频繁读请求造成的数据库压力上升的一个方案 — 缓存组件Redis,将频繁读取的数据放入缓存,减少IO,降低整体压力【Redis基于内存,多路IO复用】 Redis的QPS可达到100000+, 现阶段大部分分布式架构应用的分布式缓存都出现了Redis的影响

  • 热点数据的存储和展示 : 大部分用户频繁访问的数据,比如微博热搜,采用传统的数据库读写会增加数据库的压力,影响性能
  • 最近访问数据: 用户最近访问(访问历史) 采用日期字段标记,传统方式就会频繁在数据记录表中查询比较,耗时,而Redis的List可以作为最近访问的足迹的数据结构,性能更好
  • 并发访问: 高并发访问某些数据,Redis可以先将这些数据装载进入缓存,每次请求直接从缓存中读取,不需要数据库IO
  • 排名: “”排行榜“功能 — 可以直接使用Redis的Sorted Set 实现用户排名,避免传统的Order Group, 还有过期时间等的应用

Redis还可以在消息队列、分布式锁、Session集群等多方面发挥作用

Redis缓存的key的名称最好有意义,一般分割采用:, 比如spring:session , redis:order:no:1001

Redis 微服务

Cfeng之前使用的数据库都是Spring-Data下面的产品,但是直接添加Data-redis,如果使用jedis还需要单独引入Jedis,所以可以直接引入redis-starter, 其下包含Data-redis和Jedis以及Spring-boot的相关依赖

  <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-redis</artifactId>
            <version>${redis.version}</version>
        </dependency>

而yml的配置和之前是相同的,配置相关的host和port和password等

最后书写配置类,配置RedisTemplate和StringRedisTemplate, 在进行Redis的业务操作之前,一定要记得将模板对象组件代码加入项目

@Configuration
@RequiredArgsConstructor
public class CommonConfig {

    //自动配置的链接工厂,SpringBoot可以自动注入port等属性,不用像之前那样子手动,但Jedis pool还是需要显性
    private final RedisConnectionFactory redisConnectionFactory;

    /**
     * 注入可以直接引入,或者放在参数中,也可以自动DI
     */
    @Bean
    public RedisTemplate<String,Object> redisTemplate() {
        RedisTemplate<String,Object> redisTemplate = new RedisTemplate<>();
        //配置链接工厂,序列化策略
        redisTemplate.setConnectionFactory(redisConnectionFactory);
        redisTemplate.setKeySerializer(new StringRedisSerializer());
        redisTemplate.setValueSerializer(new Jackson2JsonRedisSerializer(Object.class));
        //hash
        redisTemplate.setHashKeySerializer(new StringRedisSerializer());
        redisTemplate.afterPropertiesSet();
        return redisTemplate;
    }

    /**
     * 缓存操作组件StringRedisTemplate
     */
    @Bean
    public StringRedisTemplate stringRedisTemplate() {
        StringRedisTemplate stringRedisTemplate = new StringRedisTemplate();
        stringRedisTemplate.setConnectionFactory(redisConnectionFactory);
        return stringRedisTemplate;
    }
}

之前Cfeng一直使用的是RedisTemplate,这里就都 配置上,设置序列化策略后其实想用就用,StringRedisTemplate就是将Value的数据类型都是String,序列化都是String,底层源码就是都设置为String方式序列化

Redis相关数据结构如String、Hash、Set、List、 Sorted Set等不多介绍,详见之前的blog, 这里演示实际生产中的使用场景

String作为最简单的数据结构,可以直接用于生成验证码的过期时间即可,之前Cfeng使用Kapcha还自定义了验证码对象,手动设置值放入Session中,虽然Session也是在Redis中,但总感觉麻烦了一些

List — 排名、排行榜、近期访问

其实List表为一种线性表,可以选择leftPush和 RightPop, 这就一个顺序表,可以选择作为实际场景的排名等数据的处理

访问记录对象直接插入该List,设置过期时间即可, 应用场景:缓存排名、排行榜、近期访问

Set ---- 缓存不重复数据 【自动剔除重复】、解决重复提交、剔除重复ID

Set用于存储相同类型不重复数据,底层的数据结构为哈希表【散列表,内容对应位置】 — 添加、删除、查找操作复杂度均为O(1)

应用场景: 缓存不重复的数据、解决重复提交、剔除重复ID

Sorted Set — 排行榜(充值、积分、成绩)

SortedSet和Set一样不重复,但是通过底层的Score就可以既不重复又有序

可以用于各种排行榜数据的缓存【放入缓存时放入标识字段和排序字段即可】 – 不需要通过数据库内部的Order,提升性能

Hash — 缓存对象,减少key数量

Hash可以缓存对象,所以在实际场景中如果缓存的对象具有共享,比如直接都是一种对象,那么就缓存一个list key即可,数据放入list即可,这样可以减少redis的缓存中整体的数量

Key失效 ---- 数据库查询的数据放入缓存设置TTL, 在TTL内查询直接从缓存读取

Key失效最主要就是设置数据库查询的数据放入缓存的时间间隔TTL,不可能一直存在与Cache中,所以需要设置,在TTL时间内都是直接从Cache中获取,数据库压力小,前台的速度也快一点

还可以将数据压入缓存队列,设置TTL,TTL时触发监听事件,处理业务逻辑 【不单单是删除cache的数据】

缓存穿透

Redis作为缓存可以大大提升效率(查询数据方面可以直接从缓存中获取,降低查询数据库的频率), 但是还是存在一些问题: 缓存穿透、缓存击穿和缓存雪崩

在这里插入图片描述

前端用户获取数据,后台会先在Redis中进行查询,如果有数据就会直接返回结果,over; 但是没有就会在数据库中查询,查询到之后会更新缓存同时返回结果,over;没有查询到数据就会返回空,over

缓存穿透的原因 在第三个流程 ---- 数据库没有查询到数据,直接返回Null给前台,over, 如果前台频繁请求不存在的数据,数据库永远查询为Null, 但是Null没有存入缓存,所以每次请求都会查询数据库

若前台恶意攻击,发起洪流式查询,会给数据库造成很大压力,压垮数据库

这就是缓存穿透 — 前台请求的值一直不存在于cache,而永远越过Cache直接访问数据库

缓存穿透发生的场景:

  • 原来数据存在,由于某些原因(误删等),在缓存和数据库层面都删除了,前端依然存在
  • 恶意攻击行为,利用不存在Key或者恶意尝试大量不存在的业务数据请求

解决方案: Null也缓存

典型解决方案就是改造第三个流程, 直接返回NULL -----> 将NULL塞入缓存中同时返回给前台结果, 这样就可以一定程度上解决缓存穿透,重复查询时会直接从Cache中读取

缓存穿透演示 ----- Goods系统

这里直接采取查询商品Goods来演示缓存的问题和解决的方案

首先在数据库中创建表Goods表

USE  db_middleware;

DROP TABLE IF EXISTS `mid_goods`;
CREATE TABLE `mid_goods` (
  `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 `mid_goods` VALUES ('1', 'book_10010', '分布式中间件', '2022-09-11 17:21:16');

之后在 model模块创建相关的实体和mapper文件, 其中xml文件放在model的resources中

  • SpringBoot整合Mybatis-plus

首先server模块 依赖 model模块,并且微服务项目所有的子模块的resources会整合到一起,所以model模块的resources可以直接看作server的在一起; 整个项目的yml配置在server模块下面

在server的pom中指定了启动类位置,启动类上面指定mapper位置

@MapperScan("cfengMiddware.model.mapper")  //model的依赖是加入了的,所以可以扫描到其位置

同时在server的pom中进行配置, 这里的configuration对应的就是之前mybatis.xml的配置

#mybatis-plus配置, 最终整合之后都是一个resources
mybatis-plus:
  mapper-locations: classpath:mappers/*.xml
  check-config-location: true
  type-aliases-package: CfengMiddleWare.model.entity
  configuration:
    cache-enabled: true   #允许缓存
    default-statement-timeout: 3000   #超时时间   自动配置直接放在yml中即可
    map-underscore-to-camel-case: true #驼峰命名
    use-generated-keys: true   #允许执行玩SQL插入语句后返回主键
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl   #控制台打印

model模块中引入逆向工程的依赖

 <!-- mybatis plus 代码生成器依赖-->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-generator</artifactId>
            <version>${mybatis-generator.version}</version>
        </dependency>
        <!-- 代码生成器模板 -->
        <dependency>
            <groupId>org.freemarker</groupId>
            <artifactId>freemarker</artifactId>
            <version>${freemaker.version}</version>
        </dependency>

同时创建CodeGenerator逆向生成工具类

public class CodeGenerator {

    /**
     * <p>
     * 读取控制台内容
     * </p>
     */
    public static String scanner(String tip) {
        Scanner scanner = new Scanner(System.in);
        StringBuilder help = new StringBuilder();
        help.append("请输入" + tip + ":");
        System.out.println(help.toString());
        if (scanner.hasNext()) {
            String ipt = scanner.next();
            if (StringUtils.isNotBlank(ipt)) {
                return ipt;
            }
        }
        throw new MybatisPlusException("请输入正确的" + tip + "!");
    }

    public static void main(String[] args) {
        // 代码生成器
        AutoGenerator mpg = new AutoGenerator();

        // 全局配置
        GlobalConfig gc = new GlobalConfig();
        String projectPath = System.getProperty("user.dir");
        gc.setOutputDir(projectPath + "/src/main/java");
        gc.setAuthor("cfeng");
        gc.setOpen(false);
        // service 命名方式
        gc.setServiceName("%sService");
        // service impl 命名方式
        gc.setServiceImplName("%sServiceImpl");
        // 自定义文件命名,注意 %s 会自动填充表实体属性!
        gc.setMapperName("%sMapper");
        gc.setXmlName("%sMapper");
        gc.setFileOverride(true);
        gc.setActiveRecord(true);
        // XML 二级缓存
        gc.setEnableCache(false);
        // XML ResultMap
        gc.setBaseResultMap(true);
        // XML columList
        gc.setBaseColumnList(false);
        // gc.setSwagger2(true); 实体属性 Swagger2 注解
        mpg.setGlobalConfig(gc);

        // 数据源配置
        DataSourceConfig dsc = new DataSourceConfig();
        dsc.setUrl("jdbc:mysql://localhost:3306/db_middleware?useUnicode=true&characterEncoding=utf-8&useSSL=true&servertimezone=GMT%2B8");
        // dsc.setSchemaName("public");
        dsc.setDriverName("com.mysql.cj.jdbc.Driver");
        dsc.setUsername("cfeng");
        dsc.setPassword("a1234567890b");
        mpg.setDataSource(dsc);

        // 包配置
        PackageConfig pc = new PackageConfig();
        //pc.setModuleName(scanner("模块名"));
        pc.setParent("CfengMiddleWare.model");
        pc.setEntity("entity");
        pc.setMapper("mapper");
        //service在server模块
//        pc.setService("cfengMiddleware.server.service");  //先只生成model
//        pc.setServiceImpl("cfengMiddleware.server.service.impl");
//        pc.setController("cfengMiddleware.server.controller");
        mpg.setPackageInfo(pc);

        // 自定义配置
        InjectionConfig cfg = new InjectionConfig() {
            @Override
            public void initMap() {
                // to do nothing
            }
        };

        // 如果模板引擎是 freemarker
        String templatePath = "/templates/mapper.xml.ftl";
        // 如果模板引擎是 velocity
        // String templatePath = "/templates/mapper.xml.vm";

        // 自定义输出配置
        List<FileOutConfig> focList = new ArrayList<>();
        // 自定义配置会被优先输出
        focList.add(new FileOutConfig(templatePath) {
            @Override
            public String outputFile(TableInfo tableInfo) {
                // 自定义输出文件名 , 如果你 Entity 设置了前后缀、此处注意 xml 的名称会跟着发生变化!!
                String moduleName = pc.getModuleName()==null?"":pc.getModuleName();
                return projectPath + "/src/main/resources/mapper/" + moduleName
                        + "/" + tableInfo.getEntityName() + "Mapper" + StringPool.DOT_XML;
            }
        });
        /*
        cfg.setFileCreate(new IFileCreate() {
            @Override
            public boolean isCreate(ConfigBuilder configBuilder, FileType fileType, String filePath) {
                // 判断自定义文件夹是否需要创建
                checkDir("调用默认方法创建的目录");
                return false;
            }
        });
        */
        cfg.setFileOutConfigList(focList);
        mpg.setCfg(cfg);

        // 配置模板
        TemplateConfig templateConfig = new TemplateConfig();

        // 配置自定义输出模板
        //指定自定义模板路径,注意不要带上.ftl/.vm, 会根据使用的模板引擎自动识别
        // templateConfig.setEntity("templates/entity2.java");
        // templateConfig.setService();
        // templateConfig.setController();

        templateConfig.setXml(null);
        mpg.setTemplate(templateConfig);

        // 策略配置
        StrategyConfig strategy = new StrategyConfig();
        strategy.setNaming(NamingStrategy.underline_to_camel);
        strategy.setColumnNaming(NamingStrategy.underline_to_camel);
        //strategy.setSuperEntityClass("cn.com.bluemoon.demo.entity");
        strategy.setEntityLombokModel(true);
        strategy.setRestControllerStyle(true);
        // 公共父类
        //strategy.setSuperControllerClass("cn.com.bluemoon.demo.controller");
        // 写于父类中的公共字段
        //strategy.setSuperEntityColumns("id");
        strategy.setInclude(scanner("表名,多个英文逗号分割").split(","));
        strategy.setControllerMappingHyphenStyle(true);
        strategy.setTablePrefix(pc.getModuleName() + "_");
        mpg.setStrategy(strategy);
        mpg.setTemplateEngine(new FreemarkerTemplateEngine());
        mpg.execute();
    }

}

之后就逆向生成了Mybatis-plus的基本的mapper和相关的service,因为全自动框架,所以基本的IService,ServiceImpl包含了基本的CRUD,而JPA只能生成相关的Reposiroty,相比规范的dao和service,controller结构有差别

public interface MidGoodsService extends IService<MidGoods> {

}

Model模块为entity和mapper所在的包,server为service和controller所在位置

@Service
@Slf4j
@RequiredArgsConstructor
public class MidGoodsServiceImpl extends ServiceImpl<MidGoodsMapper, MidGoods> implements MidGoodsService {

    private final  MidGoodsMapper midGoodsMapper;

    private final ObjectMapper objectMapper;

    //redis模板
    private final RedisTemplate redisTemplate;

    //redis存储的命名的前缀
    private static final String keyPerfix = "goods:";

    /**
     * 获取商品,优先从Cache中获取,Cache中没有,再从数据库中查询,并且塞入Cache中
     * 为了解决缓存穿透,当数据库中也不存在数据时,将null放入缓存中,设置过期时间
     */
    @Override
    public MidGoods getGoodsInfo(String goodsCode)  throws Exception{
        //定义商品对象
        MidGoods goods = null;
        //缓存中的key
        final String key = keyPerfix + goodsCode;
        //Redis的操作组件
        ValueOperations valueOperations = redisTemplate.opsForValue();
        //业务逻辑
        if(redisTemplate.hasKey(key)) {
            log.info("获取商品:Cache中存在,商品编号:{}",goodsCode);
            //缓存中获取
            Object res = valueOperations.get(key);
            //找到就进行解析返回
            if(res != null && !Strings.isNullOrEmpty(res.toString())) {
                goods = objectMapper.convertValue(res,MidGoods.class); //JSON格式序列化
            }
        } else {
            //在数据库中查询
            log.info("该商品不在Cache中");
            goods = midGoodsMapper.selectByCode(goodsCode);
            if(goods != null) {
                //数据库中找到该商品,写入缓存,局部性原理
                valueOperations.set(key,objectMapper.writeValueAsString(goods));
            } else {
                //缓存穿透发生的情况,为了避免,这里也将其写入缓存中,当然需要设置过期的时间,这里假设30min
                valueOperations.set(key,"",30L, TimeUnit.MINUTES); //没有查询到,空字符串null
            }
        }
        return goods;
    }
}

可以看到,查询的过程就是先找Cache,没有再查找数据库,查询到之后会将其写入缓存,如果不窜在,缓存穿透情况,那么缓存null值,设置过期时间,这样如果用户恶意访问,也只会从Cache中访问

当然这是正常情况下的解决方案,前台同时需要对用户输入的数据进行校验,比如查询年龄,输入1000就是不可能的,要保证合理的数据才进行查询,同时后台缓存null即可【一般设置缓存null值时间为5分钟

缓存雪崩

在使用缓存时,一般会设置过期时间,保持缓存和数据库的一致性,同时减少冷缓存占用过多的内存, 但是当 大量热点缓存采用相同的时效,导致某个时刻,缓存Key集体失效,大量请求全部转发到数据库,导致数据库压力骤增,甚至宕机,形成一系列连锁反应 — 缓存雪崩 Cache Avalanche

缓存雪崩的场景:

  • 大量热点数据同时过期
  • 缓存服务故障

解决方案:TTL后加随机数,均匀失效

一般的解决方法就是设置不同的过期时间,随机TTL,比如可以在整体30分钟后加上随机数1-5分钟,这样就是均匀失效,错开缓存Key的失效时间点,减少数据库的查询压力

雪崩发生时,服务熔断、限流、降级; 构建高可用集群(防止Cache故障),采用双Key策略,主Key设置过期时间,被Key不设置过期时间,主Key失效时,返回备用Key

缓存击穿

缓存雪崩时大量热点Key同时失效导致的,而如果单个热点Key,在不停的扛着高并发,在这个Key的失效瞬间,持续的高并发请求会击穿缓存,直接请求数据库,导致数据库压力暴增, 就像在薄膜上凿开一个洞 ---- 缓存击穿

多点同时失效就是雪崩(没有一个无辜),单点就是击穿,【缓存击穿是缓存雪崩的子集)

产生的原因就是热点Key过期,并且这个key的访问非常频繁

解决方法: 热点Key不设置过期时间、 互斥锁

热点数据不设置过期时间,永不过期,这样前端的高并发请求永远都不会落在数据库上面,但是还是有丢丢问题,那就是内存的容量有限

还可以使用互斥锁 Mutex Key, 只让一个线程构建缓存,其他线程等待构建缓存执行完毕之后,在从缓存中获取数据,单机通过synchronized或者lock,分布式环境使用分布式锁, 提前使用互斥锁,在value内部设置设置比Redis更短的时间标识,异步线程发现快过期时,延长时间同时重新加载数据,更新缓存

相关文章:

  • Rust基础语法
  • 电子知识学习网站
  • 全站最简单 “数据滚动可视化大屏” 【JS基础拿来即用】
  • Vue项目实战——【基于 Vue3.x + Vant UI】实现一个多功能记账本(开发导航栏及公共部分)
  • ScalableViT网络模型
  • Nginx配置流数据转发指导
  • 【单细胞高级绘图】10.KEGG富集结果的圆圈图
  • 怎样在应用中实现自助报表功能?
  • 生成指定位数的随机验证码
  • 线性布局和相对布局
  • 高级数据结构——红黑树
  • Python语言学习:Python语言学习之数据类型/变量/字符串/操作符/转义符的简介、案例应用之详细攻略
  • CentOS下安装及配置MySQL
  • 开发者说论文|人工智能为设备磨损“把脉”:依托飞桨开展的铁谱图像智能故障诊断研究...
  • 学习笔记4--自动驾驶汽车感知系统
  • 分享的文章《人生如棋》
  • “Material Design”设计规范在 ComponentOne For WinForm 的全新尝试!
  • 30天自制操作系统-2
  • Android优雅地处理按钮重复点击
  • Cookie 在前端中的实践
  • gf框架之分页模块(五) - 自定义分页
  • Java IO学习笔记一
  • Java到底能干嘛?
  • Making An Indicator With Pure CSS
  • mongo索引构建
  • open-falcon 开发笔记(一):从零开始搭建虚拟服务器和监测环境
  • PAT A1050
  • PyCharm搭建GO开发环境(GO语言学习第1课)
  • python3 使用 asyncio 代替线程
  • Python爬虫--- 1.3 BS4库的解析器
  • SpiderData 2019年2月16日 DApp数据排行榜
  • 简析gRPC client 连接管理
  • 延迟脚本的方式
  • 原生 js 实现移动端 Touch 滑动反弹
  • d²y/dx²; 偏导数问题 请问f1 f2是什么意思
  • 通过调用文摘列表API获取文摘
  • ​Base64转换成图片,android studio build乱码,找不到okio.ByteString接腾讯人脸识别
  • #HarmonyOS:基础语法
  • (2009.11版)《网络管理员考试 考前冲刺预测卷及考点解析》复习重点
  • (39)STM32——FLASH闪存
  • (4)事件处理——(7)简单事件(Simple events)
  • (C#)一个最简单的链表类
  • (pojstep1.1.1)poj 1298(直叙式模拟)
  • (zt)基于Facebook和Flash平台的应用架构解析
  • (附表设计)不是我吹!超级全面的权限系统设计方案面世了
  • (附源码)ssm户外用品商城 毕业设计 112346
  • (三)uboot源码分析
  • (实战)静默dbca安装创建数据库 --参数说明+举例
  • (转)es进行聚合操作时提示Fielddata is disabled on text fields by default
  • *** 2003
  • .NET中GET与SET的用法
  • .Net中wcf服务生成及调用
  • .skip() 和 .only() 的使用
  • /etc/X11/xorg.conf 文件被误改后进不了图形化界面
  • /usr/lib/mysql/plugin权限_给数据库增加密码策略遇到的权限问题