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


    分布式


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


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

    分布式系统

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

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

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

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

    单点集中式web应用

    				 web应用容器(端system)
    				------------------------------------
    				|								|
                     |    ---->   Mysql存储			  |
    				|	|							|
    用户 ---访问----> | Web应用						 |
    				|	|						    |
    				|	---->  文件存储				  |
    				|                        		  |
    				-------------------------------------
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

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

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

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

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

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

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

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

    三者拆分的架构方式, 三个服务独立部署,不同的服务器宕机仍然可使用, 且不需要考虑占用过多容量导致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) 文件存储
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    数据库读写分离,反向代理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) 文件存储
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

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

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

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

    软件系统从集中式单机系统,为了解决存储占用容量,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应用的启动类所在位置
    				 							 整个项目/服务的核心开发逻辑
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

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

    创建微服务项目

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

    • IDEA中: File下面创建新项目 New —> New Project ,选择Maven、选择SDK版本,之后直接Next(不使用模板,那是创建module),命名项目,maven坐标尽量简洁,选择项目的位置,之后finish即可
    • 进入项目的初始页面,显示的pom.xml就是父模块的配置文件,在该配置文件中,指定整个项目的资源编码和JDK版本以及公共依赖
    
    <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.0modelVersion>
    
        <groupId>indvi.cfenggroupId>
        <artifactId>CfengMiddleWareartifactId>
        <version>1.0.0version>
        
        
        <properties>
            <java.version>17java.version>
            <project.build.sourceEncoding>UTF-8project.build.sourceEncoding>
            <maven.compiler.source>${java.version}maven.compiler.source>
            <maven.conpiler.target>${java.version}maven.conpiler.target>
        properties>
    
    
    project>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 创建各个子模块,直接在父模块下面开始创建,比如创建子模块api; 直接点击父模块,点击New— Module; 之后还是选择Maven的SDK之后, Next选择模块名称即可; 会自动舒适化生成子模块的pom.xml
    
    <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>CfengMiddleWareartifactId>
            <groupId>indvi.cfenggroupId>
            <version>1.0.0version>
        parent>
        <modelVersion>4.0.0modelVersion>
    
        <artifactId>apiartifactId>
    
        <properties>
            <lombok.version>1.18.20lombok.version>
            <jackson-annotations-version>2.12.6jackson-annotations-version>
        properties>
    
        <dependencies>
            
            <dependency>
                <groupId>org.projectlombokgroupId>
                <artifactId>lombokartifactId>
                <version>${lombok.version}version>
            dependency>
    
            
            <dependency>
                <groupId>com.fasterxml.jackson.coregroupId>
                <artifactId>jackson-annotationsartifactId>
                <version>${jackson-annotations-version}version>
            dependency>
        dependencies>
        
    project>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 同理再创建model模块 【父模块不需要src文件夹,删除,几个子模块中放置代码,父模块进行管理即可】 各个模块包括父模块的groupID都是indvi.cfeng
    
    <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>CfengMiddleWareartifactId>
            <groupId>indvi.cfenggroupId>
            <version>1.0.0version>
        parent>
        <modelVersion>4.0.0modelVersion>
    
        <artifactId>modelartifactId>
    
        <properties>
            <mybatis-plus-spring-boot.version>3.5.2mybatis-plus-spring-boot.version>
            <mybatis-pagehelper.version>4.1.2mybatis-pagehelper.version>
        properties>
    
        <dependencies>
            
            <dependency>
                <groupId>indvi.cfenggroupId>
                <artifactId>apiartifactId>
                <version>${project.parent.version}version>
            dependency>
    
            
            <dependency>
                <groupId>com.baomidougroupId>
                <artifactId>mybatis-plus-boot-starterartifactId>
                <version>${mybatis-plus-spring-boot.version}version>
            dependency>
    
        dependencies>
    project>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 最后创建核心的业务模块server,可以使用依赖管理配置项,配置spring-boot的版本,数据库连接池使用druid,使用starter
    
    <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>CfengMiddleWareartifactId>
            <groupId>indvi.cfenggroupId>
            <version>1.0.0version>
        parent>
        <modelVersion>4.0.0modelVersion>
    
        <artifactId>serverartifactId>
        
    
        <packaging>jarpackaging>
        <properties>
            
            <start-class>cfengMiddware.server.MiddleApplicationstart-class>
            
            <spring-boot.version>2.7.2spring-boot.version>
            <spring-session.version>1.3.5.RELEASEspring-session.version>
            <log4j.version>1.3.8.RELEASElog4j.version>
            <mysql.version>8.0.27mysql.version>
            <druid.version>1.2.8druid.version>
            <guava.version>31.1-jreguava.version>
        properties>
    
        
        <dependencyManagement>
            <dependencies>
                <dependency>
                    <groupId>org.springframework.bootgroupId>
                    <artifactId>spring-boot-dependenciesartifactId>
                    <version>${spring-boot.version}version>
                    <type>pomtype>
                    <scope>importscope>
                dependency>
            dependencies>
        dependencyManagement>
    
        <dependencies>
            
            <dependency>
                <groupId>indvi.cfenggroupId>
                <artifactId>modelartifactId>
                <version>${project.parent.version}version>
            dependency>
    
            
            <dependency>
                <groupId>org.springframework.bootgroupId>
                <artifactId>spring-boot-starter-log4jartifactId>
                <version>${log4j.version}version>
            dependency>
    
    
            
            <dependency>
                <groupId>com.google.guavagroupId>
                <artifactId>guavaartifactId>
                <version>${guava.version}version>
            dependency>
    
            
            <dependency>
                <groupId>mysqlgroupId>
                <artifactId>mysql-connector-javaartifactId>
                <version>${mysql.version}version>
            dependency>
    
            
            <dependency>
                <groupId>com.alibabagroupId>
                <artifactId>druid-spring-boot-starterartifactId>
                <version>${druid.version}version>
            dependency>
    
            <dependency>
                <groupId>org.springframework.bootgroupId>
                <artifactId>spring-boot-starter-webartifactId>
                <version>${spring-boot.version}version>
            dependency>
    
            <dependency>
                <groupId>org.springframework.bootgroupId>
                <artifactId>spring-boot-starter-testartifactId>
                <scope>testscope>
            dependency>
        dependencies>
    
        <build>
            <finalName>cfeng_middleware_${project.parent.version}finalName>
            <plugins>
                <plugin>
                    <groupId>org.springframework.bootgroupId>
                    <artifactId>spring-boot-maven-pluginartifactId>
                    <version>${spring-boot.version}version>
                    <executions>
                        <execution>
                            <goals>
                                <goal>repackagegoal>
                            goals>
                        execution>
                    executions>
                    <configuration>
                        <excludes>
                            <exclude>
                                <groupId>org.projectlombokgroupId>
                                <artifactId>lombokartifactId>
                            exclude>
                        excludes>
                    configuration>
                plugin>
            plugins>
    
            
        build>
    
    project>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112
    • 113
    • 114
    • 115
    • 116
    • 117
    • 118
    • 119

    引入了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);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    日志还需要在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
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32

    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.bootgroupId>
                <artifactId>spring-boot-starter-redisartifactId>
                <version>${redis.version}version>
            dependency>
    
    • 1
    • 2
    • 3
    • 4
    • 5

    而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;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33

    之前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');
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

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

    • SpringBoot整合Mybatis-plus

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

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

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

    同时在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   #控制台打印
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

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

     
            <dependency>
                <groupId>com.baomidougroupId>
                <artifactId>mybatis-plus-generatorartifactId>
                <version>${mybatis-generator.version}version>
            dependency>
            
            <dependency>
                <groupId>org.freemarkergroupId>
                <artifactId>freemarkerartifactId>
                <version>${freemaker.version}version>
            dependency>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

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

    public class CodeGenerator {
    
        /**
         * 

    * 读取控制台内容 *

    */
    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(); } }
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112
    • 113
    • 114
    • 115
    • 116
    • 117
    • 118
    • 119
    • 120
    • 121
    • 122
    • 123
    • 124
    • 125
    • 126
    • 127
    • 128
    • 129
    • 130
    • 131
    • 132
    • 133
    • 134
    • 135
    • 136
    • 137
    • 138
    • 139
    • 140

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

    public interface MidGoodsService extends IService<MidGoods> {
    
    }
    
    • 1
    • 2
    • 3

    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;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51

    可以看到,查询的过程就是先找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更短的时间标识,异步线程发现快过期时,延长时间同时重新加载数据,更新缓存

  • 相关阅读:
    就地初始化与列表初始化
    大众动力总成构建全程数字化的数电票管理平台
    你的Web3域名,价值究竟何在?
    数据在线迁移
    blender从视频中动作捕捉,绑定到人物模型
    c++指针和引用之高难度(一)习题讲解
    前端知识案例101-javascript基础语法-捕获介绍
    区块链 - 为何元宇宙上了时代周刊?
    C++lambda表达式
    Vue项目引入translate.js 国际化自动翻译组件
  • 原文地址:https://blog.csdn.net/a23452/article/details/126822292