分布式
分布式系统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更短的时间标识,异步线程发现快过期时,延长时间同时重新加载数据,更新缓存