1 项目开始
1.1 工具清单
01.常用信息1
a.ORM框架1
xbatis
tkMapper
MyBatis
MilvusPlus
Mybatis-MP
mybatis-flex
MyBatis-Plus
MyBatis-Plus-Join
b.ORM框架2
APIJSON
easy-aop:AOP
pagehelper:分页
jimmer:jvm的ORM框架
easy-trans:数据翻译框架
easy-data-scope:数据权限
Alibaba Druid:数据库连接池
django-ninja:python的ORM框架
Whodb:告别手写SQL自然语言聊天查询
DuckDB:1个开源的列式关系数据库管理系统
MindsDB:一种将机器学习引入现有的SQL数据库的工具
Apache ShardingSphere:分库分表、读写分离(主从数据库)
c.数据转换
iotdb
easy-es
easykafka
mongo-plus
ModelMapper
MapStruct
MapStructPlus
d.ES系列
Kibana:客户端
LogiKM:Kafka监控
Loadgen:压测工具
Logstash:收集日志
Elasticsearch:Easysearch、RediSearch、ManticoreSearch、OpenSearch、Meilisearch
elasticsearch-head:可视化界面ES head插件
APO + ClickHouse
alloy + loki + grafana
kafka + filebeat + clickhouse
CloudflareWorkers + ReactRouter + KV、R2对象存储、D1数据库、日志
e.线程池
hippo4j:动态线程池,有无依赖中间件实现动静线程池,默认实现Nacos和Apollo的版本
asyncTool:线程编排并行框架,代替CompletableFuture
dynamic-tp:动态线程池,默认实现依赖Nacos或Apollo
f.迁移数据
DRD
DTS
DataX
canal
Flink
yugong
Maxwell
Databus
SeaTunnel
CloudCanal
02.常用信息2
a.office
Aspose
Apache POI
EasyExcel/FastExcel
tika:word转html
iText:生成PDF、将XML、Html转为PDF文件
vue-office:支持多种Office文件预览的Vue组件库
b.reprot
echarts:图表
JimuReport:积木报表
DataV:Vue大屏数据展示组件库
c.flow
Jflow
Airflow
FlowLong
Warm-Flow
Flowable
Activiti7
d.flow-club
LiteFlow
logicflow:vue3组件
juejin-wuyang/memberclub
e.json
Gson
TSON
Jackson
org.json
Fastjson
Fastjson2
APIJSON:实时零代码、全功能、强安全ORM库
f.log
SLF4J
Log4j2
Logback
g.http
OKHttp
httpclient
RestTemplate
h.规则引擎
URule
Drools
Aviator
EasyRules
mermaid-flow
03.常用信息3
a.认证
MaxKey
Sa-Token
JustAuth
xxl-sso单点
b.验证码
kaptcha
cn.hutool.captcha
cloud.tianai.captcha.tianai-captcha
https://gitee.com/anji-plus/captcha
c.短信
sms4j
Aliyun SMS SDK
Huawei Cloud SMS
Tencent Cloud SMS SDK
d.支付
码支付
Adyen
jeepay
daxpay
e.脚手架
pig4
guns
ruoyi
jeecg
learun
MISBoot
TinyEngine
SpringBlade
f.BI工具
BIRT
Redash
CBoard
Grafana
Superset
DataEase
Metabase
润乾报表
g.Git
Gogs
Gitea
GitLab
OneDev
GitBucket
Gitbli
Forgejo
Github桌面端
Gitbulter
TortoiseGit
04.常用信息4
a.AI
easyAi
SpringAI
deepseek4j
langchat
ChatGPT-MP
Langchain4j
spring-ai-alibaba
智谱AI
腾讯AI Lab
百度飞桨
华为ModelArts
阿里云机器学习平台PAI
b.任务调度
disjob
quartz
xxl-job
cron-job
SnailJob
Elastic-Job
karatttt/k-job
aizuda/snail-job
Quartz Cluster
celery:异步队列
c.Web框架
Guice:Google开源
Solon:轻量级Java框架
MuYun:轻量级Java框架
Spring:最热门Java框架
Javalin:轻量级Java框架
Quarkus:云原生Java框架
SpringBlade:Sword (基于 React、Ant Design)、Saber (基于 Vue、Element-UI)
d.web语言
Groovy:动态语言
Kotlin:静态语言
Liquor:动态编译器工具
Aviator:规则引擎
e.同类产品
zeal:文档
Maven:mvnd
SDKMAN:JDK版本管理
arthas:jarboot、jmap、jvisualvm
Jpom、drone、TeamCity、Goploy
f.周边产品
pf4j:插件框架,动态加载
JAXB:返回xml格式
nmap4j:端口信息
Drools:业务规则管理系统
seraph:支持接口幂等和消息队列重复消费问题
ip2region:准确率99.9%的离线IP地址定位库
sensitive-word:敏感词过滤工具
Concept Plugin 2:插件化,动态类加载
g.API管理
Apifox
Apidog
Apipost
Hoppscotch
Bruno
Postman
Proxyman
YApi
Insomnia
Reqable
Postcat
Torna
reqresapp
h.Docs管理
ShowDoc
SpringDoc
magic-api
Swagger:API-Helper
i.项目管理
Plane:简单易用、开源免费的特点
Asana:流行的项目管理工具,功能全面,但有时会显得过于复杂
Monday:强大的项目管理平台,但价格较高,对小型团队不太友好
Linear:以简洁著称的项目管理工具,界面友好,但定制性相对较弱
JIRA:Atlassian开发的知名项目管理工具,功能丰富但学习曲线较陡
05.工具类
a.Apacha
commons-io
commons-cli
commons-dbcp
commons-text
commons-lang
commons-lang3
commons-codec
commons-dbutils
commons-logging
commons-digester
commons-beanutils
commons-fileupload
commons-collections4
b.Common
Guava
xxl-tool
spring自带工具类
mybatis自带工具类
Lombok:AutoValue、Immutables、Kotlin、SuperBuilder(Lombok1.18版引入)
Hutool:5.8.35(2024-12-25),收购前的最后1个版本,从5.x.x升到到了6.0.0后,出现了各种包名不存在的错误
c.Excel
SpreadJS
handsontable
fortune-sheet
x-spreadsheet
Luckysheet过时,univer替代
kkFileViewL:SpringBoot框架,在线预览,doc、docx、xls、xlsx、ppt、pptx、pdf、txt、zip、rar
d.Redis
Jedis:支持大部分Redis命令
Lettuce:支持同步、异步和响应式API,支持集群和哨兵模式。相比Jedis,它是基于Netty实现的,适合高并发场景
Spring Data Redis:基于Jedis和Lettuce实现,提供了模板(RedisTemplate)、缓存、发布订阅等功能。它可以与Spring生态系统无缝集成,并支持注解式缓存。
RedisTemplate(Spring Data Redis内置工具类):能够支持对各种数据类型的操作,包括字符串、哈希、列表、集合等,可以通过Spring配置灵活地支持各种序列化和反序列化方式。
-----------------------------------------------------------------------------------------------------
Redsync:Redsync是基于Redisson的一个小型工具库,用于实现Redis的分布式锁,支持多实例之间的锁同步。
Redisson:分布式锁、集合、队列、布隆过滤器、计数器、任务调度、限流器等,同时支持同步、异步和反应式接口
RedisQueue:基于Redis的队列实现,支持简单的消息队列
Redis Delayed Queue:基于Redis的队列实现,支持简单的延迟队列
-----------------------------------------------------------------------------------------------------
RediSearch:Redis官方的一个模块,提供全文搜索和结构化数据查询的功能。它支持复杂的查询操作和自动索引,适合于实现搜索功能。
Redis Bloom:Redis官方提供的一个模块,支持布隆过滤器(Bloom Filter)、计数最小堆(Cuckoo Filter)等数据结构,可以帮助实现去重和快速的集合判断等。使用场景:适合需要实现高效去重、黑名单过滤、推荐系统等场景。
RedisGraph:一个图数据库模块,为Redis提供了图数据结构的支持,能够执行高效的图查询操作。适合社交网络、推荐系统等需要存储和查询复杂图数据的场景。
Series:RedisTimeSeries是一个Redis模块,用于存储和查询时间序列数据,支持自动采样、数据压缩和聚合查询等。
e.Redis
Redisson:通过Redis的原子操作和数据结构,实现高效的分布式限流
Bucket4j:基于令牌桶算法的Java限流库,支持分布式限流(结合Redis)
Resilience4j:一个轻量级的容错库,支持限流、熔断、重试等功能
Guava RateLimiter:Google的Guava库提供了简单易用的限流器
Spring Cloud Gateway:用于微服务架构中的API网关,内置限流功能
f.其他工具
请求调用:io.github.burukeyou/uniapi-http
图床工具:io.gitee.wangfugui-ma/github-spring-boot-starter
图床工具:io.gitee.wangfugui-ma/minio-spring-boot-starter
TCC框架:https://github.com/changmingxie/tcc-transaction
Autowired:io.github.burukeyou/>spring-smart-di-all
工具类/组件:xw-fast
阿里巴巴COLA架构:shiyindaxiaojie/eden-demo-cola
微信开发Java的SDK:https://github.com/binarywang/WxJava
g.json格式化
在前后端分离的项目中,使用 JSON 作为数据交换格式时,数字类型的数据在传输过程中会保持其原有的数值类型,
而不会自动转换为字符串(文本)格式。JSON 支持多种数据类型,包括数字、字符串、布尔值、数组和对象等。
例如,以下是一个 JSON 对象,其中包含不同类型的数据:
{
"name": "Alice",
"age": 30,
"isStudent": false,
"grades": [85, 90, 92]
}
在这个 JSON 对象中:
"age": 30 是一个数字类型。
"grades": [85, 90, 92] 是一个数组,其中的元素也是数字类型。
当前端和后端通过 HTTP 请求或响应传输 JSON 数据时,这些数字类型的数据会保持为数字类型。
只有在解析或处理 JSON 数据时,才可能因为编程语言的特性而导致类型转换。
例如,在 JavaScript 中,JSON 数据会被解析为 JavaScript 对象,其中数字仍然是数字类型。
h.Spring开发工程师
高性能场景:
Quarkus:原生编译,毫秒级启动
Micronaut:编译时依赖注入,更低的内存占用
Vert.x:响应式编程模型,更高的并发性能
-----------------------------------------------------------------------------------------------------
简单场景:
Javalin:极简的Web框架
Spark Java:函数式的路由定义
原生Java:有时候最简单的方案就是最好的
06.代码生成器
a.大狼狗
https://github.com/moshowgame/SpringBootCodeGenerator
b.MyBatis-Plus Generator
地址:https://github.com/baomidou/generator
特点:MyBatis-Plus配套的代码生成器、支持自定义模板、支持多种数据库、使用简单,配置灵活
c.JHipster
地址:https://github.com/jhipster/generator-jhipster
特点:完整的应用程序生成器、支持Spring Boot + Angular/React、包含前后端完整解决方案、支持微服务架构
d.Renren-Generator
地址:https://github.com/renrenio/renren-generator
特点:轻量级代码生成器、提供可视化界面、支持自定义模板、主要面向单体应用
e.EasyCode
地址:https://github.com/makejavas/EasyCode
特点:IDEA插件形式、支持多种模板引擎、操作简单直观、高度可定制
f.Spring Initializr
地址:https://github.com/spring-io/initializr
特点:Spring官方项目生成器、支持各种Spring组件、可以生成基础项目结构、提供Web界面
g.Jeecg-Boot
地址:https://github.com/jeecgboot/jeecg-boot
特点:低代码开发平台、代码生成器是其核心功能之一、支持前后端分离、提供完整的开发框架
h.orange-admin
地址:https://gitee.com/orangeform/orange-admin
特点:可完整支持多应用、多租户、多渠道、工作流、在线表单、自定义数据同步、自定义Job、多表关联、跨服务多表关联、框架技术栈自由组合
i.Mockaroo
地址:https://www.mockaroo.com/
特点:一款线数据生成工具,提供超过125种数据类型,包括姓名、地址、日期、图片等。用户可以自定义数据结构,生成符合实际需求的测试数据
j.JSON-Generator
地址:https://json-generator.com/
特点:主要专注于生成 JSON 格式的测试数据
k.SQL-Data Generator
地址:https://sqldatagenerator.com/generator
特点:专门为生成用于 SQL 数据库测试数据而设计的工具
l.Generatedata
地址:https://generatedata.com/generator
特点:一款开源的在线测试数据生成器,它支持30多种数据类型,包括姓名、电子邮件、地址、电话号码等,并支持多种导出格式
m.chartdb
地址:https://github.com/chartdb/chartdb
特点:免安装免密码,免费开源+智能生成,数据库架构图的懒人福音,开发者必备
07.Spring系列
a.核心模块
spring-core:Spring的核心模块,包含基础的工具类和通用的Spring组件,是Spring其他模块的依赖。
spring-context:包含应用上下文和依赖注入的实现。
spring-beans:提供BeanFactory功能,用于管理Spring Bean的生命周期和依赖注入。
spring-expression:支持Spring Expression Language(SpEL),用于表达式解析。
spring-retry:Spring的扩展模块,重试框架
-----------------------------------------------------------------------------------------------------
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context-support</artifactId>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-beans</artifactId>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-expression</artifactId>
</dependency>
-----------------------------------------------------------------------------------------------------
<!-- Spring-Retry重试框架 -->
<dependency>
<groupId>org.springframework.retry</groupId>
<artifactId>spring-retry</artifactId>
</dependency>
<!-- also need to add Spring AOP into our project-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aspects</artifactId>
</dependency>
b.Spring Boot 模块
spring-boot-starter:基础启动器,包含所有Spring Boot项目的基础依赖
spring-boot-starter-web:用于Web应用开发,包含MVC、RESTful API支持、Tomcat嵌入式容器等
spring-boot-starter-data-jpa:提供Spring Data JPA和Hibernate集成,用于数据库持久化
spring-boot-starter-aop:用于AOP支持,包含AspectJ等相关依赖
spring-boot-starter-security:包含Spring Security依赖,用于安全认证和授权
spring-boot-starter-validation:自定义验证注解
-----------------------------------------------------------------------------------------------------
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
c.Spring Data 模块
spring-data-commons:Spring Data的基础模块,提供通用的数据访问抽象。
spring-data-jpa:用于与JPA兼容的数据访问,包括与Hibernate等ORM框架集成。
spring-data-redis:与Redis集成的模块,提供对Redis数据存储的支持。
spring-data-mongodb:提供MongoDB的数据访问支持。
-----------------------------------------------------------------------------------------------------
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-commons</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-mongodb</artifactId>
</dependency>
d.Spring Security 和 OAuth
spring-security-core:Spring Security的核心模块,用于身份验证、授权。
spring-security-web:用于保护Web应用程序,包含过滤器、会话管理、CSRF防护等。
spring-security-config:用于安全配置,支持基于Java和XML的配置。
spring-security-oauth2-client:提供OAuth2.0客户端支持,用于与OAuth服务器集成。
-----------------------------------------------------------------------------------------------------
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-core</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-config</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-oauth2-client</artifactId>
</dependency>
e.Spring Cloud 模块
spring-cloud-starter-netflix-eureka-client:提供Eureka客户端支持,用于服务注册和发现。
spring-cloud-starter-openfeign:支持声明式HTTP客户端,方便服务之间的通信。
spring-cloud-starter-gateway:Spring Cloud Gateway,用于微服务网关管理。
spring-cloud-config-client:支持分布式配置管理,通过Spring Cloud Config服务来动态加载配置。
-----------------------------------------------------------------------------------------------------
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-config-client</artifactId>
</dependency>
f.消息队列和异步支持
spring-amqp:与RabbitMQ集成的模块,提供消息传输和消息监听支持。
spring-kafka:与Apache Kafka集成,用于发布和消费Kafka消息。
spring-messaging:包含消息发送和异步任务处理的工具,支持WebSocket等协议。
spring-websocket:提供WebSocket支持,适合实时通信应用。
-----------------------------------------------------------------------------------------------------
<dependency>
<groupId>org.springframework.amqp</groupId>
<artifactId>spring-amqp</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.kafka</groupId>
<artifactId>spring-kafka</artifactId>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-messaging</artifactId>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-websocket</artifactId>
</dependency>
g.测试相关模块
spring-boot-starter-test:Spring Boot的测试启动器,包含JUnit、Mockito、Hamcrest等常用测试依赖。
spring-test:Spring的核心测试模块,提供MockMvc、事务回滚等测试工具。
-----------------------------------------------------------------------------------------------------
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<scope>test</scope>
</dependency>
h.国际化与其他支持模块
spring-oxm:提供对象-XML映射,适合XML数据转换。
spring-jms:支持JMS API,用于消息传递和异步通信。
spring-batch:批处理框架,支持大数据批处理任务。
-----------------------------------------------------------------------------------------------------
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-oxm</artifactId>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jms</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.batch</groupId>
<artifactId>spring-batch-core</artifactId>
</dependency>
1.2 版本迭代
01.阿里开源
a.工具类
arthas,阿尔萨斯
fastjson
easyexcel
transmittable
b.数据库
canal,可那哦,基于MySQL数据库增量日志解析,提供增量数据订阅和消费
Druid,拽的
c.微服务
nacos
dubbo
rocketmq
Sentinel,森特闹
spring-cloud-alibaba
02.后端版本
a.JDK
JDK(Java Development Kit):JDK 21
b.Spring
Spring Framework:版本 6.0.14
Spring MVC:使用 Spring Framework 同步更新,最新为 6.0.14
Spring Boot:版本 3.3.5
Spring Cloud:2023.0.x(Leyton)
Spring Security:版本 6.3
c.ORM
MyBatis:版本 3.5.x
MyBatis-Plus:版本 3.5.3.1
d.SQL
MySQL:社区版 8.1.x
Redis:版本 8.1.x
KeyDB:redis分支,KeyDB在100%兼容redis API的情况下将redis改造成多线程
Kafka:版本 4.0.0
Oracle:Oracle Database 23c
MongoDB:版本 8.0.3
RabbitMQ:版本 3.12.x
Elasticsearch:版本 8.10
e.other
Python:版本 3.11.x
Golang:版本 1.21
csharp:版本 12,一门既有 Python 的开发效率,又有 C/C++/Rust 性能的编程语言
03.SQL特性
a.Redis
Stream(5.x)
多线程IO(6.x)
RediSearch
Redis for AI
增强版的Redis查询引擎,向量数据库,生成式AI应用中检索增强生成(RAG)
b.MongoDB
AtlasSearch
c.RabbitMQ
Stream(3.9)
d.Elasticsearch
Easy-Es
e.Kafka
4.0正式发布,首个默认KRaft模式运行,移除单独维护Zookeeper降低复杂性
04.Spring Boot 2.x与3.x
a.Java 17 的要求
Spring Boot 3.x 最低要求 Java 17,而 2.x 版本最高支持 Java 8
这意味着 Spring Boot 3.x 利用了 Java 17 中的新特性,如区域性 API、封闭类和记录等功能
b.Jakarta EE 9 的整合
Spring Boot 3.x 将原有的 Java EE API 转换为 Jakarta EE 9 API
所有的命名空间从 javax.* 改为了 jakarta.*,例如:
javax.persistence.* 变为 jakarta.persistence.*
javax.servlet.* 变为 jakarta.servlet.*
这一变化意味着在迁移到 Spring Boot 3.x 时,必须对项目中的所有依赖项进行相应的更新
c.对实例化和配置的支持改进
Spring Boot 3.x 引入了更优化的实例化和配置机制,改进了对配置的支持。比如:
支持新的注解方式和默认配置,为开发者提供更好的开发体验
采用了新的 @Configuration 和 @Bean 注解机制,更直观地支持条件化配置
d.Spring Framework 6.x 的基础
Spring Boot 3.x 构建在 Spring Framework 6.x 之上
其中包含了大量的性能优化、安全改进和新特性的支持
如对响应式编程的支持、原生支持 AOT 编译(Ahead-of-Time Compilation)、更好的 Kotlin 支持等
e.AOT编译的支持
在 Spring Boot 3.x 中,引入了 AOT 编译特性,可以在构建时将应用程序编译成更小的二进制文件
这对于创建云原生应用程序,特别是 Docker 容器中的应用程序非常有优势。这大幅降低了启动时间以及内存占用
f.原生支持的增强
Spring Boot 3.x 对 GraalVM 原生映像的支持进行增强
使开发者能够更方便地创建快速启动的微服务和无服务器架构的光滑体验
g.日志管理改善
Spring Boot 3.x 在对日志的使用方面进行了改进,引入了更灵活的日志格式配置
改进了对 Log4j2 和 SLF4J 的支持。此外,新的安全审计日志特性让开发者可以更方便地进行日志审计与监控
h.支持新版本的库和依赖
Spring Boot 3.x 更新了很多主要的库和依赖版本,比如 Spring Data、Spring Security
这些更新不仅增强了功能,还修复了一些已知的安全和性能问题
同时,用户可以获取API的新特性,提升开发效率
i.集成测试的改进
在 Spring Boot 3.x 中,提供了更好的集成测试支持,改进了启动特性,简化了测试应用程序的环境设置
此外,新的 @SpringBootTest 注解提供了更灵活的配置选项,帮助开发者更好地进行集成测试
j.小型项目的适配
Spring Boot 3.x 在小型项目的适配中也有改进,提供了更轻量化的环境设置和配置,支持微服务结构
从而使得小型项目的设置更加快速高效
k.新的 Actuator 功能
Spring Boot 3.x 中的 Actuator 模块获得了许多新特性,包括:
更加详细的运行时监控和健康检查API
拥有更多的自定义信息暴露能力,允许开发者监控应用程序的状态、指标和配置
l.更新的 Spring Cloud 兼容性
与 Spring Boot 3.x 一同发布的 Spring Cloud 也相应更新,提供了对新特性的支持
比如服务发现、配置管理、负载均衡等,使开发者能够更好地进行分布式系统的设计与实现
m.Spring Boot 3.x for 与 2.x 迁移的开发者需要注意以下几点
所有 javax.* 依赖需要转换为 jakarta.*
确保所有依赖项和库兼容 Java 17
检查配置文件和注解使用,特别是在 Bean 生命周期和 AOT 编译方面的更改
1.3 语言特性
00.总结
a.虚假的“工程严谨性”
在技术演进的浪潮中,Java曾以“跨平台、高可靠”的口号统治企业级开发生态二十余年。
然而站在2023年的节点回望,这片被Spring框架裹挟的疆域,正显露出难以掩盖的疲态。
从语法设计到工程实践,从框架哲学到运维成本,Java生态暴露出结构性缺陷,
而Spring全家桶则像一剂甜蜜的毒药,让开发者沉溺于虚假的“工程严谨性”中无法自拔。
b.Java语法设计的局限性
反射机制:虽然提供了灵活性,但也破坏了封装性,可能导致安全和维护问题。
样板代码:Java的getter/setter方法导致了大量样板代码,尽管工具如Lombok可以缓解,但这反映了语言设计的不足。
设计模式的复杂性:Java在实现某些设计模式时显得繁琐,尤其是在函数式编程和事件处理方面。
c.Spring框架的复杂性
依赖注入的复杂性:Spring的DI机制虽然强大,但配置复杂,容易导致维护困难。
AOP的隐患:面向切面编程虽然可以分离关注点,但也可能导致代码难以理解和调试。
数据库抽象的局限性:Spring Data JPA等工具在某些情况下可能导致性能问题,如N+1查询。
d.异常处理的挑战
Checked Exception的困扰:强制异常处理可能导致代码臃肿,且异常传播路径不清晰。
全局异常处理的复杂性:在微服务架构中,异常处理变得更加复杂,可能导致事务管理问题。
e.微服务时代的挑战
Spring Cloud的局限性:在微服务架构中,Spring Cloud的某些设计可能导致对底层细节的控制不足。
自动装配的风险:Spring Boot的自动装配在某些情况下可能导致兼容性问题。
f.新兴技术的挑战与机遇
Kotlin和Go的崛起:这些语言在某些方面提供了更好的性能和开发体验,挑战了Java的传统地位。
新框架的出现:如Micronaut和Quarkus,通过更高效的DI和原生编译,提供了更好的性能和启动时间。
g.重构与未来展望
去Spring化战略:通过采用更轻量级的工具和框架,可以减少复杂性和提高性能。
思想解放:开发者需要在技术选型上更加灵活,不应盲目追随传统框架和模式。
01.语法设计的先天残疾与后天畸形
Java诞生之初的“稳健”承诺,实则是表达力匮乏的遮羞布。
其引以为傲的反射机制,彻底撕碎了面向对象封装性的庄严宣言。
开发者前脚用private修饰字段,后脚就能通过Field.setAccessible(true)暴力破解——这种精神分裂式的设计,
堪比在保险库大门贴封条的同时,将钥匙插在锁孔上。更讽刺的是,这种机制被奉为“框架灵活性”的基石,
Spring正是借此实现Bean注入等核心功能,最终让权限管控沦为皇帝的新衣。
---------------------------------------------------------------------------------------------------------
在基础语法层面,Java对简洁性的践踏堪称行为艺术。其他语言用属性访问器简化字段操作,
Java却用getXxx()和setXxx()制造出全球最大规模的样板代码垃圾场。
当Kotlin的data class用一行代码实现POJO时,Java社区不得不用Lombok在编译阶段生成字节码,
这种脱裤子放屁的“创新”,本质是语言缺陷催生的畸形产物。
---------------------------------------------------------------------------------------------------------
设计模式的过度滥用,更是暴露语言表达力的严重不足。观察者模式需要先定义接口再实现匿名内部类,
而C#一个委托就能优雅解决;函数式编程直到Java 8才勉强补课,但受限于final变量约束和Checked Exception的拖累,
Lambda表达式始终戴着镣铐跳舞。当Python开发者用列表推导式处理数据流时,
Java程序员还在为Stream.peek()的副作用争论不休,这种代际差距早已超出框架优化的能力范围。
02.Spring框架:精密架构下的系统性陷阱
Spring最初以“轻量级容器”的形象挑战EJB霸权,却在二十年间异化为比EJB更恐怖的庞然大物。
其依赖注入(DI)机制看似解耦了组件依赖,实则将对象生命周期管理推入配置地狱。
@Autowired注解修饰接口的伪装下,是@Primary、@Qualifier、条件化Bean定义组成的迷宫。
某电商平台在切换支付渠道时,需要同时修改Java注解、YAML配置及POM文件中的Profile定义
——这种“分散式配置”带来的维护成本,远超直接调用工厂方法的原始方案。
---------------------------------------------------------------------------------------------------------
面向切面编程(AOP)则是Spring埋下的定时炸弹。把日志、事务等横切关注点抽象为切面的代价,
是业务逻辑被切割成散落在多个代理层的碎片。某金融机构的核心转账服务,
因@Transactional与@Async注解冲突,导致事务提交与异步线程的时序错乱,最终引发账户余额连环异常。
工程师耗费三周排查才发现,动态代理生成的子类破坏了this引用的原始行为——这种深藏在字节码增强层的隐患,
让问题定位变成一场面向源码的考古发掘。
---------------------------------------------------------------------------------------------------------
而Spring Data JPA对数据库访问的抽象,更像是场掩耳盗铃的表演。
方法名推导生成SQL语句的魔法,在遭遇N+1查询问题时瞬间破功;乐观锁机制与JVM缓存的碰撞,
曾导致某票务系统出现超卖事故。当开发者试图通过@Query注解手动优化时,他们实际上在亲手拆解框架宣称的“自动化优势”,
这种自我否定的行为模式,暴露出过度抽象带来的现实悖论。
03.异常处理:架构师的自欺欺人指南
Java的异常处理机制,堪称软件工程史上最成功的集体幻觉。Checked Exception强迫开发者用try-catch块处理本
应属于业务逻辑的流程控制,这种设计在JDBC API中达到荒诞的巅峰:每个数据库操作都被包裹在嵌套异常块中,
核心业务代码反而沦为配角。更恶劣的是,这种机制催生出throws Exception的摆烂式写法,让异常传播路径的
追踪变成不可能的任务。
---------------------------------------------------------------------------------------------------------
Spring对此的“改良”方案,是将异常转换为响应状态码。但@ControllerAdvice全局异常处理器在微服务场景下,
却成为分布式事务的噩梦。某跨境支付系统曾因Feign客户端未能透传异常信息,导致部分服务误判事务状态,
引发跨多国系统的数据回滚故障。这种框架层面的“贴心设计”,反而在复杂系统中制造出更多不可控因素。
04.微服务时代的皇帝新衣
当Spring Cloud试图将单体架构的编程模型强加给微服务体系时,其设计理念的割裂感暴露无遗。
声明式的@FeignClient接口虽然简化了HTTP调用,却让开发者丧失对超时、重试、熔断等关键参数的控制权。
某社交平台在“黑色星期五”遭遇的雪崩事故,直接源于Ribbon负载均衡器的默认配置与服务端容量不匹配——
这些被框架隐藏的底层细节,最终以生产事故的形式向开发者索债。
---------------------------------------------------------------------------------------------------------
而Spring Boot约定优于配置(Convention Over Configuration)的哲学,在云原生环境下演变为灾难的导火索。
自动装配机制在容器化部署时频繁引发类路径冲突,某物流公司曾因spring-boot-starter-data-redis与Lettuce客户端
版本不兼容,导致全球仓库系统缓存失效达6小时。这种“开箱即用”的便捷性,实则以牺牲环境控制力为代价,
与Kubernetes倡导的显式声明理念背道而驰。
05.突围者的启示与生态困局
Kotlin的崛起,给了Java生态一记响亮的耳光。这个完全兼容JVM的语言,用扩展函数消除了工具类的存在价值,
用空安全设计避免了NullPointerException的肆虐,更用协程实现了对线程池的降维打击。
某头部电商将订单核心模块改用Kotlin后,代码量减少40%,且借助inline class特性使内存占用下降15%。
这些数据无情地揭示:Java的语言缺陷绝非不可逾越的天堑,而是社区固步自封的后果。
---------------------------------------------------------------------------------------------------------
在云原生战场,Go语言则撕开了Java生态的防线。其协程模型在连接密集型场景的性能表现,
让Netty等Java异步框架相形见绌;编译为单一二进制文件的特性,更是将Spring Boot应用的臃肿镜像打入冷宫。
某证券交易系统用Go重构后,订单处理延迟从50毫秒降至8毫秒,这种代际差已超出JVM调优的弥补范围。
---------------------------------------------------------------------------------------------------------
即便在Java生态内部,觉醒者也正在反抗Spring霸权。Micronaut框架通过编译期DI取代运行时反射,
使应用启动时间缩短至1/10;Quarkus借助GraalVM实现原生编译,将内存占用压缩到传统Spring应用的1/5。
某工业物联网平台改用Vert.x后,不仅吞吐量提升8倍,GC停顿时间更从200ms降至不足10ms——这些实践印证了一个残酷事实:
脱离Spring的Java应用,反而更能释放JVM的原始性能。
06.重构之路:打破思维钢印
Java生态的救赎,必须从承认Spring框架的过渡性质开始。在Serverless架构渐成主流的今天,冷启动速度直接决定商业成本,
而Spring的层层抽象恰恰与此需求背道而驰。明智的团队已开始实施“去Spring化”战略:
---------------------------------------------------------------------------------------------------------
用Handlebars代替Thymeleaf实现模板引擎,消除XML配置依赖
采用JDBI或JOOQ替代Hibernate,重获SQL控制权
使用Guice进行轻量化依赖注入,避免自动装配的魔法
某金融科技公司通过上述改造,将API网关的镜像体积从380MB缩减至45MB,这在全球数百个边缘计算节点的部署中,每年节省超百万美元云开支。
---------------------------------------------------------------------------------------------------------
在语言层面,Project Loom的虚拟线程虽试图挽回并发处理的颜面,但其兼容性包袱导致性能提升有限;
GraalVM原生编译尽管突破启动速度瓶颈,却与反射机制天然对立,这让Spring生态陷入两难境地。
这些技术困局折射出更深层的生态危机:Java的辉煌建立在二十年前的技术假设之上,
当云原生、异构计算、边缘智能等新范式崛起时,其核心架构已成为转型的枷锁。
07.废墟上的重生
Java与Spring的霸权终将落幕,这不是语言的失败,而是技术演进的必然。
正如C#通过开源跨平台实现涅槃,Python借AI浪潮重获新生,Java生态需要一场彻底的思想解放运动。
开发者应当清醒认识到:框架不是信仰,设计模式不是圣经,真正的工程能力体现在对技术选型的冷峻判断,
而非对陈旧教条的盲目追随。当我们在Kubernetes集群中部署着由Go编写的事件驱动函数,
用Rust实现的高性能算法引擎,与遗留的Java服务进行交互时,
一个后Spring时代的技术图景已悄然展开——那里没有银弹,但充满可能。
2 计算机基础
2.1 git
01.分类1
git init:初始化一个新的Git仓库
git clone [url]:克隆一个远程仓库
git add [file]:添加文件到暂存区
git commit -m "[message]":提交暂存区的内容
git status:查看工作目录状态
git log:查看提交历史
02.分类2
git branch:列出分支,创建新分支
git checkout [branch]:切换分支
git merge [branch]:合并分支
git pull:从远程仓库拉取并合并代码
git push:推送代码到远程仓库
git remote -v:查看远程仓库信息
2.2 linux1
00.总结
a.Linux
硬盘使用情况:du
内存使用且情况:free
CPU使用情况:top
网络使用情况:netstat
b.Java程序问题分析
jps:查看本机java进程信息
jstack:打印线程的栈信息,制作 线程dump文件
jmap:打印内存映射信息,制作 堆dump文件
jstat:性能监控工具
jhat:内存分析工具,用于解析堆dump文件并以适合人阅读的方式展示出来
jconsole:简易的JVM可视化工具
jvisualvm:功能更强大的JVM可视化工具
javap:查看字节码
01.查看/创建/更新/删除/移动/重命名
a.查看,文件
more 文件名 --浏览过多文件
less 文件名 --浏览过多文件
-----------------------------------------------------------------------------------------------------
cat [选项] 文件名 --将文件内容打印到终端
-----------------------------------------------------------------------------------------------------
head [选项] 文件名 --显示文件头部
head -n 30 ka* --显示文件头部:前30行,默认10行
tail [选项] 文件名 --显示文件尾部
tail -n 30 ka* --显示文件尾部:前30行,默认10行
-----------------------------------------------------------------------------------------------------
grep 关键字 查找范围 --查询字符串
grep oracle /ect/passwd
-----------------------------------------------------------------------------------------------------
ls -ltr --等同与ll
ls -ltr XHTODKY* --等同与ll,查询*gz
ls -ltr | grep XHTODKY --等同与ll,查询*gz
find . -type f -name 'XHTODKY*' --查看以XHTODKY开头的文件
-----------------------------------------------------------------------------------------------------
ls | grep install 管道符
b.创建/更新,文件
a.参数
touch 文件名 用于修改文件或者目录的时间属性,包括存取时间和更改时间
若文件不存在,系统会建立一个新的文件
-------------------------------------------------------------------------------------------------
-a 改变档案的读取时间记录
-m 改变档案的修改时间记录
b.示例
touch newfile --若文件不存在,系统会建立一个新的文件
touch newfile1 newfile2 newfile3 --若文件不存在,系统会建立一个新的文件
-------------------------------------------------------------------------------------------------
touch testfile --将testfile时间修改为【当前系统时间】
ls -l testfile --查看文件的时间属性
-------------------------------------------------------------------------------------------------
echo "write" > job1.md --创建job1.md文件,并将write写入job1.md
c.删除,文件/目录
rm [选项] 目标文件 用来删除文件,也可删除多个文件或目录,以及将某个目录及其下的所有文件及子目录均删除
对于链接文件,只是断开了链接,原文件保持不变
-----------------------------------------------------------------------------------------------------
rm -rf /usr/local/hue
rm -rf /usr/local/hue/atp.txt
-----------------------------------------------------------------------------------------------------
-f 在没有确认删除提示的下删除文件并忽略不存在的文件和参数
-i 删除文件前提示确认
-I 在删除三个以上的文件或递归删除文件之前提示确认
-r, -recursive 以递归方式删除目录及其内容
-d, --dir 不使用 -r/-R/-recursive 删除空目录,rm -dir 等同于 rmdir
-v, --verbose 显示正在进行的步骤
d.复制,文件及目录(经测试,linux操作cp命令速度,比拖出win10操作要快)
cp [选项] 源文件 目标文件
-a 保留链接、文件属性,复制目录时可递归的复制目录
-i 如果目标文件或目录已经存在,则对用户进行提示,可以用字母y确认,其他字母都是否认
-r 复制目承,实现将源目承下的文件和子目录一起复制到目标目录中
e.移动/重命名,文件及目录
mv [选项] 源文件 目标文件
02.grep/sed/管道符
a.grep查找
a.查找文件
ls | grep install --查找文件:文件名
ls | grep install.sh --查找文件:文件名
-------------------------------------------------------
ls | grep 'install.sh' --查找文件:字符串
ls | grep 'stall.sh' --查找文件:字符串
-------------------------------------------------------
grep 关键字 查找范围 --查询字符串
grep oracle /ect/passwd
b.查找文件内容
demo.txt有如下内容:
name
passwd
pick
-------------------------------------------------------
grep 'a' demo.txt --查找单字符串:demo.txt中【'a'单个字符串】
grep 'a' demo* --查找单字符串:前缀为demo中【'a'单个字符串】
grep 'a' *txt --查找单字符串:后缀为txt中【'a'单个字符串】
-------------------------------------------------------
grep -e 'name' -e passwd demo.txt --查找多字符串:demo.txt中【'name' 'passwd'】
c.其他
grep -i 'name' demo.txt --默认grep区分大小写,通过-i开启忽略大小写
grep -i "\<name\>" demo.txt --精确匹配,默认会返回nickname,严格搜索name
grep -n 'name' demo.txt --开启行号
grep -n 'name' demo.txt | sort --开启行号+对结果排序
b.sed命令
sed -i 's/KA1/KA2/' ka14.txt --默认,匹配第一个
sed -i '22,$s/$/|+|/' ka14.txt --
c.管道符
命令格式:cmd1 | cmd2 | cmd3 | ... | cmd(n)
作用:将一个命令的执行结果作为另一个命令输入来执行
03.系统管理的命令有哪些?
ps:显示当前运行的进程。ps aux显示所有进程。
top:实时显示进程动态。
kill:终止进程。kill -9 PID强制终止。
df:显示磁盘空间使用情况。df -h以易读格式显示。
du:显示目录或文件的磁盘使用情况。
free:显示内存和交换空间的使用情况。
chmod:更改文件或目录的权限。
chown:更改文件或目录的所有者和所属组。
04.网络管理的命令有哪些?
ping:检查与远程服务器的连接。
wget:从网络上下载文件。
ifconfig:显示网络接口的配置信息。
netstat:显示网络连接、路由表和网络接口信息。
05.压缩和解压的命令有哪些?
tar:打包或解包.tar文件。tar cvf archive.tar files打包,tar xvf archive.tar解包。
gzip / gunzip:压缩或解压.gz文件。
zip / unzip:压缩或解压.zip文件。
06.查找文件的命令有哪些?
find:在目录树中查找文件。find /directory/ -name filename。
2.3 linux2
01.查看ip
a.Windows
ipconfig --查看IPV4
----------------------------------------------------------------
netstat -ano --查看全部端口
netstat -aon|findstr 135 --查看某个端口
taskkill -f -pid 135 --删除某个端口
----------------------------------------------------------------
tasklist|findstr 12088 --根据pid查看应用程序
b.Centos7
yum install net-tools -y --安装net-tools
----------------------------------------------------------------
ip a / ip addr / ip address --查看IPV4
ifconfig -a --查看IPV4
hostname -I --查看IPV4
----------------------------------------------------------------
netstat -nap --查看正常使用的端口以及关联的进程
netstat -lnpt | grep 8080 --查看8080端口
netstat -ntulp | grep 8080 --查看8080端口
----------------------------------------------------------------
lsof -i 8080 --查看8080端口
ps [pid] --查看8080端口对应的pid进程
kill -9 [pid] --关闭8080端口对应的pid进程
c.ps进程
ps aux | grep java
kill -9 [进程id]
----------------------------------------------------------------
kill -s 9 pid --常用1:停止【进程pid】,-s 9 强制终止
kill -s 9 `pgrep firefox` --常用2:停止【进程名】
02.ps进程
a.参数
-e 显示所有进程
-f 全部列出,通常和其他选项联用
-------------------------------------------------------------------------------------------------
-a 显示一个终端的所有进程,除了会话引线
-u username显示该用户下的所有进程,且显示各个命令的详细路径
-x 显示没有控制终端的进程,同时显示各个命令的具体路径
-aux 显示所有包含其他使用者的进程
b.查看
$ top --说明:动态实时视图
$ pstree -aup --说明:以树状图展现
$ ps aux | less --说明:ps查看进程信息,并通过less分页显示
-------------------------------------------------------------------------------------------------
$ ps -ef
UID PID PPID C STIME TTY TIME CMD
smx 1822 1 0 11:38 ? 00:00:49 gnome-terminal
smx 1823 1822 0 11:38 ? 00:00:00 gnome-pty-helper
-------------------------------------------------------------------------------------------------
$ ps -aux
USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
smx 1822 0.1 0.8 58484 18152 ? Sl 11:38 0:49 gnome-terminal
smx 1823 0.0 0.0 1988 712 ? S 11:38 0:00 gnome-pty-helper
-------------------------------------------------------------------------------------------------
$ ps -aux | grep firefox
USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
smx 1822 0.1 0.8 58484 18152 ? Sl 11:38 0:49 firefox-3.6.18/firefox-bin
smx 1823 0.0 0.0 1988 712 ? S 11:38 0:00 firefox
c.杀死
$ kill -s 9 pid --常用1:停止【进程pid】,-s 9 强制终止
$ kill -s 9 `pgrep firefox` --常用2:停止【进程名】
03.nohup后台运行
a.后台运行
nohup --加在一个命令的最前面,表示不挂断的运行命令
& --加载一个命令的最后面,表示这个命令放在后台执行
-----------------------------------------------------------------------------------------------------
nohup /root/runoob.sh & --示例
b.查看后台运行
a.区别
ps:适用于查看瞬时进程的动态,可以查看其他终端的进程
jobs:只能查看当前终端后台执行的任务,无法查看其他终端的进程
b.jobs
只有在当前命令行中使用nohup和&时,job可以显示进程
如果将【写到命令行内容,写道.sh脚本】中,执行脚本,jobs无法显示进程
停止jobs
c.ps
ps -ef
ps -ef | less
ps -ef | grep java
-------------------------------------------------------------------------------------------------
ps -aux
ps -aux | less
ps -aux | grep java
-------------------------------------------------------------------------------------------------
pstree -aup
c.关闭后台运行
a.前台进程
Ctrl + Z --终止
b.后台进程
kill -s 9 pid --常用1:停止【进程pid】,-s 9 强制终止
kill -s 9 `pgrep firefox` --常用2:停止【进程名】
2.4 nginx
00.正反代理
a.正向代理(对外表现为“客户端”)
a.流程
内网client -> 访问 -> 外网server
b.场景
访问谷歌服务器,由代理去谷歌取到返回数据,然后再返回给自己
c.用途
翻墙
缓存,加速访问资源
对客户端访问授权,上网进行认证
记录用户访问记录,上网行为管理
d.总结
正向代理中,proxy和client同属一个LAN,对server透明;
正向代理即是客户端代理, 代理客户端, 服务端不知道实际发起请求的客户端;
b.反向代理(对外表现为“服务端”)
a.流程
外网server -> 访问 -> 内网client
b.场景
代理服务器接受网络上的连接请求,然后将请求转发给内部网络上的服务器,
并将从服务器上得到的结果返回给internet上请求连接的客户端,此时代理服务器对外就表现为一个服务器
c.用途
负载均衡
保证内网安全:通常将“反向代理”设置为“公网访问地址”
d.总结
反向代理中,proxy和server同属一个LAN,对client透明;
反向代理即是服务端代理, 代理服务端, 客户端不知道实际提供服务的服务端;
01.什么是Nginx?
Nginx ,是一个 Web 服务器和反向代理服务器,用于 HTTP、HTTPS、SMTP、POP3 和 IMAP 协议。
目前使用的最多的 Web 服务器或者代理服务器,像淘宝、新浪、网易、迅雷等都在使用。
Nginx 的主要功能如下:
作为 http server (代替 Apache ,对 PHP 需要 FastCGI 处理器支持)
反向代理服务器
实现负载均衡
虚拟主机
02.Nginx常用命令
启动 nginx
停止 nginx -s stop 或 nginx -s quit
重载配置 ./sbin/nginx -s reload(平滑重启) 或 service nginx reload
重载指定配置文件 .nginx -c /usr/local/nginx/conf/nginx.conf
查看 nginx 版本 nginx -v
检查配置文件是否正确 nginx -t
显示帮助信息 nginx -h
03.Nginx有哪些优点?
跨平台、配置简单
非阻塞、高并发连接:处理 2-3 万并发连接数,官方监测能支持 5 万并发。
内存消耗小:开启 10 个 Nginx 才占 150M 内存
成本低廉,且开源
稳定性高,宕机的概率非常小
04.Nginx是如何实现高并发的?
事件驱动架构:Nginx 使用了基于事件驱动的异步非阻塞模型。这意味着它能够使用少量工作进程处理大量并发连接,每个进程都可以处理多个连接,而不是为每个连接分配一个进程或线程。
多进程模型:Nginx 使用多进程模型,包括一个主进程和多个工作进程。主进程负责管理和调度,工作进程处理实际的请求。这种方式能够充分利用多核 CPU 的性能,提高并发处理能力。
异步 I/O:Nginx 采用异步 I/O 操作,可以在 I/O 操作(如网络读取或写入)时不阻塞工作进程。这样,一个工作进程可以在等待 I/O 操作完成时继续处理其他请求,提高资源利用率。
高效的内存管理:Nginx 在内存管理上非常高效,使用预分配内存池来减少内存碎片和分配/释放内存的开销,从而提高性能。
事件通知机制:Nginx 使用了高效的事件通知机制(如 epoll, kqueue),能够快速地知道哪些连接有事件发生,从而及时处理,提高响应速度。
05.为什么Nginx采用单线程?
Nginx 采用单线程加异步非阻塞的事件驱动模型,是为了简化编程模型,提高资源利用率,减少上下文切换,提升内存使用效率,并增强系统的可靠性和稳定性。
06.什么是动态资源、静态资源分离?
动态资源、静态资源分离,是让动态网站里的动态网页根据一定规则把不变的资源和经常变的资源区分开来,动静资源做好了拆分以后我们就可以根据静态资源的特点将其做缓存操作,这就是网站静态化处理的核心思路。
动态资源、静态资源分离简单的概括是:动态文件与静态文件的分离。
将静态资源放到 Nginx 中,动态资源转发到 Tomcat 服务器中去。
07.Nginx有哪些负载均衡策略?
1.轮询(默认)round_robin
2.IP 哈希 ip_hash
3.最少连接 least_conn
4.权重(Weight)
2.5 network
01.OSI七层模型、OSI五层协议、TCP/IP四层模型
a.OSI七层架构
应用层 为应用程序提供服务
表示层 数据格式转化、数据加密
会话层 建立、管理和维护会话
传输层 建立、管理和维护户端到端的连接 数据如何在网络上进行传输
网络层 IP选址及路由选择 不同的协议对网络进行逻辑划分
数据链路层 提供介质访问和链路管理 交换机、网桥、中继器
物理层 物理层 网线
b.OSI五层协议
应用层(dns,http) DNS解析成IP并发送http请求
传输层(tcp,udp) 建立tcp连接(三次握手)
网络层(IP,ARP) IP寻址,为数据在结点之间传输创建逻辑链路
数据链路层(PPP) 封装成帧,在通信实体间建立数据链路链接
物理层(物理介质) 通过双绞线,电磁波等各种介质来传输比特流
02.HTTP
a.版本
HTTP1.0 定义了三种请求方法:GET, POST 和 HEAD方法。
HTTP1.1 新增了六种请求方法:OPTIONS、PUT、PATCH、DELETE、TRACE 和 CONNECT 方法。
PING不是UDP,而是ICMP协议
b.HTTP协议的特点?
HTTP允许传输任意类型的数据。传输的类型由Content-Type加以标记。
无状态。对于客户端每次发送的请求,服务器都认为是一个新的请求,上一次会话和下一次会话之间没有联系。
支持客户端/服务器模式。
c.HTTP报文格式
HTTP请求由请求行、请求头部、空行和请求体四个部分组成。
HTTP响应也由四个部分组成,分别是:状态行、响应头、空行和响应体。
d.HTTP状态码有哪些?
1xx 服务器收到请求,需要请求者继续执行操作
2xx 请求正常处理完毕
3xx 重定向,需要进一步操作已完成请求
4xx 客户端错误,服务器无法处理请求
5xx 服务器处理请出错
e.HTTPS与HTTP的区别?
HTTP是超文本传输协议,信息是明文传输;HTTPS则是具有安全性的ssl加密传输协议。
HTTP和HTTPS用的端口不一样,HTTP端口是80,HTTPS是443。
HTTPS协议需要到CA机构申请证书,一般需要一定的费用。
HTTP运行在TCP协议之上;HTTPS运行在SSL协议之上,SSL运行在TCP协议之上。
f.浏览器中输入URL返回页面过程?
解析域名,找到主机 IP。
浏览器利用 IP 直接与网站主机通信,三次握手,建立 TCP 连接。浏览器会以一个随机端口向服务端的 web 程序 80 端口发起 TCP 的连接。
建立 TCP 连接后,浏览器向主机发起一个HTTP请求。
服务器响应请求,返回响应数据。
浏览器解析响应内容,进行渲染,呈现给用户。
03.TCP、UDP
a.TCP有哪些特点?
TCP是面向连接的运输层协议
每一条TCP连接只能有两个端点
TCP提供可靠交付的服务
TCP提供全双工通信
面向字节流
b.TCP和UDP的区别?
TCP面向连接;UDP是无连接的,即发送数据之前不需要建立连接。
TCP提供可靠的服务;UDP不保证可靠交付。
TCP面向字节流,把数据看成一连串无结构的字节流;UDP是面向报文的。
TCP有拥塞控制;UDP没有拥塞控制,因此网络出现拥塞不会使源主机的发送速率降低(对实时应用很有用,如实时视频会议等)。
c.为什么要三次握手?
如果不是三次握手,只有两次
如果客户端发出请求连接时,报文延时了,于是客户端重新发送了一次连接请求消息
后来收到了确认,建立了连接,然后完成了数据传输,关闭了连接
此时,服务器收到了那个迟到的请求消息,此时他应该是个废物了
但是如果只有两次握手,服务器收到请求就响应建立了连接了
但是如果是三次,客户端不会再次确认,服务器也就随后知道了这消息有问题,不会建立连接
d.两次握手可以吗?
如果客户端发出请求连接时,报文延时了,于是客户端重新发送了一次连接请求消息
后来收到了确认,建立了连接,然后完成了数据传输,关闭了连接
此时,服务器收到了那个迟到的请求消息,此时他应该是个废物了
但是如果只有两次握手,服务器收到请求就响应建立了连接了
但是如果是三次,客户端不会再次确认,服务器也就随后知道了这消息有问题,不会建立连接
e.为什么是四次挥手?
连接建立以后就可以进行数据通信传输了
通信结束后,需要断开连接,断开连接需要四次交互,常被称为四次挥手
最初状态均为ESTABLISHED,客户端与服务器相互进行数据传送
下图假设客户端无数据发送,请求断开连接
-------------------------------------------------------------------------------------------------
但是在关闭连接时,当Server端收到Client端发出的连接释放报文时,很可能并不会立即关闭SOCKET,
这时Server端才能发送连接释放报文,之后两边才会真正的断开连接。故需要四次挥手。
04.DNS
a.DNS服务器分类
根DNS服务器
顶级DNS服务器
权威DNS服务器
b.DNS域名称的五个类别
根域:最高级别,单个句点
顶级域:国家/地区,.com.
第二层域:个人或组织,qq.com.
第二层域的子域:网站名,www.qq.com.
主机名:特定计算机,h1.www.qq.com.
c.DNS域名解析过程
第一步:浏览器缓存
第二步:系统缓存(Hosts文件)
第三步:路由器缓存,以上三步均为客户端的DNS缓存
第四步:ISP缓存
第五步:根域名服务器
第六步:顶级域名服务器
第七步:主域名服务器
第八步:保存结果至缓存
d.浏览器中输入URL返回页面过程?
解析域名,找到主机 IP。
浏览器利用 IP 直接与网站主机通信,三次握手,建立 TCP 连接。浏览器会以一个随机端口向服务端的 web 程序 80 端口发起 TCP
建立 TCP 连接后,浏览器向主机发起一个HTTP请求。
服务器响应请求,返回响应数据。
浏览器解析响应内容,进行渲染,呈现给用户。
e.DHCP
动态主机设置协议(DHCP)是一种使网络管理员能够集中管理和自动分配IP网络地址的通信协议
DHCP不论是否“自动分配IP”,使用时,“只要填写正确即可”,“注意一点,采用手动配置的IP,易造成填写错误、不规范”
例如,DHCP默认分配(192.168.5.10-192.168.5.254),则可以手动“192.168.5.8或192.168.5.255”,不冲突即可
05.CDN
a.定义
CDN(Content Delivery Network)是一种分布式网络架构,用于加速互联网内容的分发。
b.优点
因为网络传输有距离限制,部署杭州的服务器,不同地区的用户访问得到响应的时长是不一样的,杭州的用户来访问肯定比美国的用户来访问快多了。所以就弄了个CDN来加快内容的分发。
它通过在全球多个地理位置部署服务器,当用户请求内容时,CDN会根据用户的地理位置,将请求转发到最近的缓存服务器上。这样可以减少数据传输的延迟,提高用户访问速度,同时减轻源服务器的负载。
c.场景
CDN通常用于加速静态内容(如图片、视频、静态页面等)的访问,提高网站的性能和用户体验。
06.GET 和 POST 是 HTTP 协议中最常用的两种请求方法,它们的区别如下:
a.数据传输方式
GET:通过 URL 传递数据,数据附加在查询字符串中,格式为 ?key1=value1&key2=value2。
POST:通过请求体传递数据,数据包含在 HTTP 请求的主体部分。
b.数据长度限制
GET:有长度限制(一般为 2048 字符),不适合传输大量数据。
POST:没有严格的长度限制,可以传输较大的数据。
c.安全性
GET:不安全,数据在 URL 中可见,易被缓存和记录。
POST:相对安全,数据在请求体中,不易被看到,适合传输敏感信息。
d.总结
GET:适用于获取数据,参数通过 URL 传递,长度有限,安全性低。
POST:适用于提交数据,参数在请求体中,安全性较高,适合传输大数据。
3 项目问答
3.1 项目背景:规模
01.说明
a.三要素
项目介绍、岗位职责、业绩、技术亮点
b.团队规模
整体开发团队30人左右,我们java这边的话就是分了两个小组,每个小组5个人
c.其他
运维、测试、前端、产品、移动端、大数据、UI设计师
02.流程
a.项目背景
两三句话简单的说一下这个项目的大概情况
是自研、外包、迭代升级、二次开发,为谁开发、项目用来做什么、实际价值、靠什么盈利等等,
简单介绍即可,项目背景不要长篇大论,没人想听
b.项目业务模块
单体项目:项目采用单体架构,核心模块有,A模块、B模块、C模块
微服务项目:项目使用微服务技术架构,拆分了有A服务、B服务、C服务
c.技术选型
项目采用前后端分离模式开发和部署,我主要完成后端编码工作,并维护好前后端接口文档
主要用到的技术栈是:SpringCloud、SpringBoot、MybatisPlus、SpringSecurity、Redis、XXL-JOB、Elasticsearch、MongoDB、Kafka
d.岗位职责
我在项目中主要承担核心开发的角色,包括项目研讨、需求分析、设计合理的技术方案以及编码实现
还有就是配合测试完成月度测试任务,以及上线时加班到凌晨,确保项目上线过程中不出问题
我们项目虽然分模块开发,但是我作为核心开发,大部分模块我写过,像X服务、YY服务、ZZ服务,这些都是我主要写的
有的时候我也会帮助新同事过度一下,还有指导一些实习生,做一些边角料的工作,比如基本的增删改查,接口文档编写等等
03.示例
a.项目背景
校园优选是基于X公司原有的校园派送服务进行业务升级,定制化开发的一套面向大学生的团购优选平台
项目依托于公司在全省高校经营的快递驿站或合作的校园驿站、便利超市、水吧等为基础,面向校园大学生群体精细化运营
打造垂直化的电商、社交平台。目前项目一期已上线,覆盖高校30余所,注册大学生用户超5W+,日均订单3000+
b.项目平台
公司运营平台:仓储、配送调度、供应链、结算、订单管理、营销中心
派送终端:团购发起、物资清单分拣、收益结算、售后处理
用户终端:搜索、购物车、订单、用户中心、活动
c.技术架构
项目基于微服务技术架构,主要以Alibaba系列微服务组件进行构建,围绕业务拆分需求,每一块业务对应一个微服务
例如:会员服务、系统服务、商品服务、营销服务、搜索服务、购物车服务、订单服务、支付服务、认证服务、物流服务
项目中用到的其他技术栈也挺多的,比如:SpringBoot、MybatisPlus、Redis、RabbitMQ、Elasticsearch、OSS、MySQL、Nginx等等
d.岗位职责
我在这个主要承担Java后端开发,算是一个核心人员吧,因为我钢刚入职的时候,这个项目才刚开始,所以我也算老员工了
有的时候也会帮助新同事快速适应这个项目的技术栈。到项目后期,我也会指导一下实习生,帮忙写一写接口文档和使用手册
这个项目怎么说呢,我们对于工作划分没有那么细致,每一个业务模块我都有所参与
我比较熟悉的模块比如:权限管理模块、商品模块、订单模块、购物车模块、搜索模块是我主要负责完成的
这就是我最近这个项目的大概情况
3.2 技术架构:4步
01.错误示例
我们项目用到了SpringCloud Alibaba、Nacos、OpenFeign、Sentinel、SkyWalking、 Seata等等这些技术
02.介绍
我们项目基于微服务技术架构
围绕业务拆分了十个核心的业务微服务,分别是:会员服务、系统服务、商品服务、营销服务、搜索服务、购物车服务、订单服务、支付服务、认证服务、物流服务
项目前后端分离开发和部署,前端分了平台运营管理、会员小程序端、团长端三个终端
微服务相关的组件我们用到了:Nacos、OpenFeign、Sentinel、SkyWalking、Seata等微服务组件
03.链路
当前端请求后端时,首先经过网关层。网关这边我们设置了两层网关
首先是使用Ngix搭建入口网关,我们采用动态静态的方式搭建的,保障Nginx的高可用
我们把前端项目和前端的静态资源部署在Nginx,当请求过来后,直接从Nginx服务器返回
如果是动态请求,Nginx服务器会转发给Gateway网关微服务,网关这边我们设计了灰度发布、登录状态校验等全局过滤器
Gateway网关微服务会从Nacos中拉取服务列表,按照配置的路由规则转发给后面的业务微服务,默认规则是轮询
04.中间件
另外在业务设计的时候,也会用到一些中间件或者第三序服务,比如:阿里云的OSS对象存储、基于Redisson实现的分布式锁
使用Redis作为缓存组件、RabbitMQ消息队列满足一些非强一致性和服务解耦的场景
使用Elasticsearch设计搜索服务并且基于ELK技术栈搭建日志收集平台等等
而像业务微服务中的开发,还是以Spring Boot、MybatisPlus、MySQL、Redis这些基本的开发技术作为主要实现
05.其他
日常工作中也用到了PostMan、Knife4J、JMeter等开发工具
别的话也没什么了,还有就是交付的时候,我们使用Jenkins做了整合,实现自动构建Docker镜像、自动部署
我们是提前编写好Jenkins脚本,需要的时候只需要在Jenkins Web页面中操作,一键部署项目
3.3 工程结构:4步
01.前后端分离
我们项目采用前后端分离设计开发,我们后端工程只编写后端代码。使用Maven管理整个项目
首先,在父工程pom文件中统一管理依赖,锁定依赖的版本号,避免版本号冲突的问题,并且聚合所有的子工程
然后创建service工程,工程类型为pom格式,也是用来聚合所有的业务微服务,并统一管理了所有业务微服务的通用依赖
例如:web场景启动器、MySQL、Nacos、Sentinel、OpenFeign等等。这样每一个业务微服务只需要继承父工程后,就自动拥有了这些依赖,不用重复导入了
02.公共模块
在实际开发过程中,总有一些工具类需要在多个工程中重复使用,所以我们又创建了common工程,用来存放一些封装的工具类,
比如:字符串处理相关、RabbitMQ、Redis等相关的工具类。
03.外部调用模块
一个service-client工程,用来保存每一个业务微服务对外提供服务的Feign接口,一个业务微服务相关的Feign接口,在一个service-client的子工程中
因为微服务调用的时候,一些实体类需要在双方微服务之间传递,所以创建了model工程,统一管理了所有的VO、实体类、枚举类等等
04.网关模块
网关微服务,请求到后端后,先由网关微服务处理,网关微服务根据预先配置的路由规则,路由到业务微服务中对请求进行处理
3.4 开发环境:gogs
01.分类
Gogs
开发环境、测试环境和线上生产环境
02.分类
VPN连接
wiki平台
3.5 数据库表:评审
01.评审
数据表设计完成后,需要开会评审,需要项目经理审核
这些数据表其实也是随着业务需求不断改变的,我们项目是以月度为单位下发需求,有的时候需求变动了,多少会改一些数据库字段、新增一两张数据表
02.围绕业务设计
首先我们是拆分出来有哪些业务模块,优先梳理出核心业务,围绕核心业务进行设计数据表,这部分工作主要是产品经理完成的,他会给出初版的需求文档
然后我围绕我负责的几个模块,分析相关的业务流程,每一个流程的业务涉及到哪些实体,整个业务流程围绕这几个实体是怎么运转的
当我把这些想明白后,我直接使用powerdesigner绘制物理数据模型,就是ER图
评审的时候,就是围绕ER图进行讨论,审核通过,就导出数据库的建表语句进行开发
03.设计表
权限:管理员表、角色表、权限表、管理员角色中间表、角色权限中间表、后台用户登录日志表
订单:购物车表、订单配送表、订单表、订单项信息表、订单操作日志、记录表、订单退货申请表、退货原因表表、订单设置表、支付信息表、退款信息表
商品:商品属性表、属性分组表、三级分类表、商品评价表、商品评价回复表、sku表、sku属性表、sku详情表、sku图片表、sku海报表、sku的库存历史记录
3.6 前端部署:app+pc
01.部署
PC端:nginx
App端:打包成应用
02.部署nginx
将静态资源更新到nginx的html目录下,然后修改nginx配置文件,这样前端请求域名根路径下的静态资源时
配置关于后端的api访问路由,凡是以app路径开头的资源,全都转发到后端的网关微服务上,转发的策略用的是轮询
3.7 后端部署:架构
01.问题
你们项目部署了几台服务器?可以说公司自己搭建的机房,也可以说买的云服务器
是你部署的吗?部署环境是你搭建的吗?这种问题很变态,我是Java
服务状态你们是怎么监控的?
面对并发压力,你们是怎么扩容的?
01.前置网关层
Nginx高可用方案:部署两台Nginx服务器,使用KeepAlive实现主备模式。主服务器绑定VIP作为项目入口,一旦主服务器故障,从服务器自动接管VIP,确保服务的高可用性
静态资源管理:前端编译后的静态资源部署在Nginx服务器上,并使用CDN加速
请求转发与灰度发布:Nginx将请求转发至Gateway微服务网关,通过调整路由权重实现灰度发布
02.微服务部署
服务实例管理:Gateway网关和业务微服务通常部署3个以上的实例,根据需求动态调整实例数量
自动化部署:使用Jenkins脚本实现微服务实例的自动化部署
03.数据存储与消息中间件
独立部署与高可用:MySQL、Redis、Kafka、MongoDB、Elasticsearch等组件独立部署,并配置主备或集群模式以确保高可用性
04.运维与监控
运维部署:线上生产环境由专门的运维团队负责,使用刀架式服务器部署应用系统和大数据计算系统
服务监控:通过SkyWalking监控微服务状态,包括JVM参数、GC情况、内存和CPU利用率,以及微服务调用链路的异常和超时
日志管理:使用ELK(Elasticsearch、Logstash、Kibana)实现服务内部日志的集中管理和检索
05.问题排查与优化
资源监控与扩容:通过SkyWalking监控服务资源利用率,动态增加服务实例以分担压力
问题分析:若问题持续,检查服务实例状态、网络状况、请求响应时间等。分析项目日志和JVM GC内存快照,排查业务代码问题,如SQL查询慢、远程调用慢、内存泄露等
3.8 后端部署:elk日志
01.ELK组件介绍
Elasticsearch:开源分布式搜索引擎,负责日志的索引和存储,支持分布式、自动分片、RESTful接口等特性
Logstash:日志收集、过滤、转发工具,采用C/S架构,负责将各类日志统一收集并转发给Elasticsearch
Kibana:提供日志分析的Web界面,帮助汇总、分析和搜索数据日志
FileBeat:轻量级日志收集工具,适合在各服务器上搜集日志并传输给Logstash
Kafka:用于异步收集日志,增强系统的可靠性和性能
02.ELK在项目中的应用
日志收集:每个微服务项目中依赖Logstash,日志发送到Logstash Server端
配置管理:在Logstash Server端配置Elasticsearch相关信息,如主机地址和索引名称
日志转发:业务微服务的logback配置文件中添加Logstash Appender,将日志发送到Logstash Server端
日志存储与检索:Logstash将日志输出到Elasticsearch,Kibana配置Discover界面用于快速检索日志
03.错误日志处理
网络问题:对于微服务调用相关的错误日志,如网络分区或超时,记录并使用降级方法处理。频繁超时则反馈给运维进行处理
业务逻辑问题:对于业务逻辑异常,如参数校验、空指针问题,立即处理并测试审核后快速发布替换
3.9 后端部署:jenkins的ci流程
01.DevOps
CI:持续集成可以帮助开发者更加方便地将代码更改合并到主分支,给测试组测试
CD:持续部署(交付)可以自动把已验证的代码发布到企业自己的存储库
02.工作步骤
拉取git仓库代码
通过maven构建项目
通过SonarQube做代码质量检测
通过Docker制作自定义镜像
通过自定义镜像推送到Harbor
通过Publish Over SSH通知目标服务器
03.搭建K8S+Docker+Jenkins+Prometheus
我们是使用Jenkins实现项目的可持续集成,在Jenkins中,我们整合了Git、SSH、Maven、Docker等工具,然后编写了一波构建的脚本,当需要进行构建或者发布时,只需要在Jenkins Web页面中,执行提前写好的任务即可
在我之前的公司,是内网部署了Jenkins服务器、GitLab、MySQL、本地镜像私服。一切行为都是在公司内部网络中执行的
每一个微服务中,都提前编写了DockerFile,写好了编译的细节,比如:基于JDK1.8构建镜像,设置JVM内存初始参数,运行java -jar命令等
日常开发我们将代码提交到开发分支,每次到发版或者部署测试环境时,组长会将开发分支的代码合并到预发布分支或者测试分支,然后在Jenkins web页面中,执行构建脚本,自动部署开发环境或者测试环境
04.Jenkins的执行流程很简单
1.首先是创建一个简单的流水线工作,然后编写每一个步骤
2.从gitlab中拉取分支的代码,执行maven 命令,编译完毕后,同时把docker镜像也构建出来了
3.将构建好的镜像推送到公司内部搭建的镜像私服
4.我们会提前配好目标服务器的SSH公私钥,这样在Jenkins中直接就可以通过SSH远程连接的方式,在目标服务器中执行部署操作:先停掉并删除旧的容器和镜像,再拉取要部署的镜像,启动最新镜像就可以了
当然,我们在Jenkins不止只写了一个脚本,我们还写了服务降级的脚本和服务版本回退的脚本。需要用到的时候,直接一键执行就可以了
3.10 前后交互:knife4j、postman
01.Swagger与Knife4j
使用Swagger和Knife4j快速生成后端接口文档。Knife4j整合了Swagger和OpenApi,提供了增强功能
注解支持:通过Swagger注解标注类、方法、属性,快速生成在线文档
页面美化与在线调试:Knife4j美化了文档页面,并支持在线调试和导出离线文档
02.postman
开发环境启用:在开发环境中启用Knife4j,前端可以访问接口文档并导出离线版进行对接
Postman使用:本地测试和前端沟通时,使用Postman进行接口测试。Postman提供专业的测试功能,支持团队协作
接口维护:在Postman中创建团队,维护项目的每一个接口,方便前后端直接调用和测试
4 在线问答论坛系统
4.1 zset
01.排行榜
a.定义
使用ZSet实现一个简单的排行榜系统,通过分数对玩家进行排名
b.原理
使用ZSet的分数排序特性,添加玩家及其分数,获取排名,更新分数
c.常用API
ZADD:添加玩家及其分数
ZREVRANGE:获取排名
ZINCRBY:更新玩家分数
d.使用步骤
1.使用ZADD命令添加玩家及其分数
2.使用ZREVRANGE命令获取排名
3.使用ZINCRBY命令更新玩家分数
e.场景示例
# 添加玩家及其分数
ZADD leaderboard 100 "player1"
ZADD leaderboard 200 "player2"
ZADD leaderboard 150 "player3"
# 获取排名
ZREVRANGE leaderboard 0 -1 WITHSCORES
# 更新玩家分数
ZINCRBY leaderboard 50 "player1"
# 获取某个元素的排名
ZRANK leaderboard "player1"
02.本周热议
a.定义
使用ZSet实现本周热议的文章排行榜,通过评论数量进行排序
b.原理
使用ZSet的分数排序特性,缓存热评文章,评论数量排行
c.常用 API
ZADD:添加文章及其评论数量
ZREVRANGE:展示排行榜
ZUNIONSTORE:并集运算
d.使用步骤
1.项目启动前,获取近7天文章
2.初始化文章的总评论量,使用ZADD
3.对文章做并集运算,使用ZUNIONSTORE
4.根据评论量从大到小展示,使用ZREVRANGE
e.场景示例
# 初始化操作
ZADD weekly_articles 10 "article1"
ZADD weekly_articles 20 "article2"
ZADD weekly_articles 15 "article3"
# 并集运算
ZUNIONSTORE total_comments 2 weekly_articles other_articles
# 展示排行榜
ZREVRANGE total_comments 0 -1 WITHSCORES
03.分值计算:计算前7天内的热点文章
a.定义
通过 Redis 的有序集合(ZSet),可以实现对前7天内的文章进行分值计算和排序,以确定热点文章
分值计算基于文章的阅读、点赞、评论和收藏等指标
b.原理
a.定时任务
每天凌晨执行任务,查询前7天的文章
b.分值计算
根据阅读、点赞、评论和收藏的权重计算每篇文章的分值
c.排序和存储
将计算出的分值用于排序,并将结果存入Redis的ZSet中
c.常用API
a.ZADD
添加元素到有序集合中,并设置分值
b.ZREVRANGE
按分值从高到低获取有序集合中的元素
c.ZREM
删除有序集合中的元素
d.使用步骤
a.定时任务
每天凌晨执行,查询前7天的文章
b.查询文章
从数据库获取过去7天的文章数据
c.计算分值
根据权重计算每篇文章的分值
d.排序和存储
使用 ZADD 将文章及其分值存入 Redis 的 ZSet
e.获取热点文章
使用 ZREVRANGE 获取分值最高的前30篇文章
e.场景示例
import redis
from datetime import datetime, timedelta
# 连接 Redis
r = redis.Redis()
# 定时任务:每天凌晨执行
def daily_task():
# 查询前7天的文章
articles = query_articles_from_db()
# 计算分值并存入 Redis
for article in articles:
score = calculate_score(article)
r.zadd("hot_articles", {article['id']: score})
# 获取分值最高的前30篇文章
top_articles = r.zrevrange("hot_articles", 0, 29, withscores=True)
print("Top 30 Hot Articles:", top_articles)
# 查询数据库中的文章
def query_articles_from_db():
# 模拟数据库查询
return [
{'id': 'article1', 'reads': 100, 'likes': 20, 'comments': 10, 'favorites': 5},
{'id': 'article2', 'reads': 150, 'likes': 30, 'comments': 15, 'favorites': 10},
# 更多文章...
]
# 计算文章分值
def calculate_score(article):
reads_weight = 1
likes_weight = 3
comments_weight = 5
favorites_weight = 8
score = (article['reads'] * reads_weight +
article['likes'] * likes_weight +
article['comments'] * comments_weight +
article['favorites'] * favorites_weight)
return score
# 执行定时任务
daily_task()
f.说明
a.定时任务
使用调度工具(如 cron 或 APScheduler)每天凌晨执行 daily_task 函数
b.分值计算
根据业务需求调整权重,确保分值计算符合实际情况
c.Redis 存储
使用 ZSet 存储文章及其分值,确保快速排序和检索
4.2 kaptcha
01.使用Kaptcha生成验证码
a.步骤
步骤1:kaptchaConfig.java :配置类,【配置验证码】
步骤2,AuthController.java :控制层,【生成验证码】
步骤3:reg.ftl :模板引擎,【使用验证码】
步骤4:从session中获取KAPTCHA_SESSION_KEY,即正确的验证码【text】
b.代码
@ResponseBody
@PostMapping("/register")
public Result doRegister(User user, String repass, String vercode) {
// 使用ValidationUtil工具类,校验【输入是否错误】
ValidationUtil.ValidResult validResult = ValidationUtil.validateBean(user);
if(validResult.hasErrors()) {
return Result.fail(validResult.getErrors());
}
// 校验【密码是否一致】
if(!user.getPassword().equals(repass)) {
return Result.fail("两次输入密码不相同");
}
// 校验【验证码是否正确】:从session中获取KAPTCHA_SESSION_KEY,即正确的验证码【text】
String kaptcha_session_key = (String) req.getSession().getAttribute(KAPTCHA_SESSION_KEY);
System.out.println(kaptcha_session_key);
if(vercode == null || !vercode.equalsIgnoreCase(kaptcha_session_key)) {
return Result.fail("验证码输入不正确");
}
// 完成注册
Result result = userService.register(user);
// 如果校验成功,则完成注册,跳转/login页面
return result.action("/login");
}
4.3 rabbitmq
01.通过使用RabbitMQ保证ES全文搜索能实时更新,并即时反馈给用户
a.步骤1
配置RabbitMQ,使用direct交换机,然后 绑定 队列 与 交换机
b.步骤2
使用RabbitMQ监控队列es_queue,
增加/删除帖子后,立即调用 ES中的增加/删除方法,从而保证 ES中的数据 与 MySQL数据 一致
02.分发策略
a.direct
消息中的路由键(RoutingKey),基于完全匹配、单播的模式
b.fanout
把所有发送到fanout交换器的消息路由到所有绑定该交换器的队列中,fanout 类型转发消息是最快的
c.topic
通过模式匹配的方式对消息进行路由,将路由键和某个模式进行匹配,此时队列需要绑定到一个模式上
d.headers
不依赖于路由键进行匹配,是根据发送消息内容中的headers属性进行匹配
4.4 freemarker
01.封装与自定义 Freemarker 标签,并利用其 macro 宏来快速完成页面开发
步骤1:配置标签,实现TemplateDirectiveModel接口,重写 excute 方法
步骤2:开发标签
步骤3:注册标签
02.实现方式
a.方式一
实现TemplateDirectiveModel接口,重写 excute 方法
b.方式二
采用 mblog 项目对该 TemplateDirectiveModel 接口进行封装
实现 TemplateDirectiveModel 接口较为复杂,故我们可以直接使用 mblog 项目中已经封装好的类:
org.myslayers.common.templates.DirectiveHandler、TemplateDirective、TemplateModelUtils;
其中,我们只需要重写 TemplateDirective 类中的 getName()和 excute(DirectiveHandler handler)
本次使用 PostsTemplate、TimeAgoMethod 进行开发使用;
最后,使用 FreemarkerConfig 类在 Springboot 中对 PostsTemplate、TimeAgoMethod 进行标签的声明
<timeAgo></timeAgo>、<details></details>。
03.具体实现
a.配置标签
DirectiveHandler.java:配置类,【配置标签】
TemplateDirective.java :配置类,【配置标签】
TemplateModelUtils.java :配置类,【配置标签】
b.开发标签
TimeAgoMethod.java :工具类,【开发标签】 重写 TemplateDirective 类中的 getName()和 excute(DirectiveHandler handler)
PostsTemplate.java :工具类,【开发标签】 重写 TemplateDirective 类中的 getName()和 excute(DirectiveHandler handler)
c.注册标签
FreemarkerConfig.java :配置类,【注册标签】
/**
* Freemarker配置类
*/
@Configuration
public class FreemarkerConfig {
@Autowired
private freemarker.template.Configuration configuration;
@Autowired
TimeAgoMethod timeAgoMethod;
@Autowired
PostsTemplate postsTemplate;
@Autowired
HotsTemplate hotsTemplate;
/**
* 注册为“timeAgo”函数:快速实现日期转换
* 注册为“posts”函数:快速实现分页
*/
@PostConstruct
public void setUp() {
configuration.setSharedVariable("timeAgo", timeAgoMethod);
configuration.setSharedVariable("details", postsTemplate);
}
}
4.5 shiro+session
01.Shiro与Session结合来完成用户登录接口的开发
a.步骤
步骤1:集成 Shiro 环境
步骤2,配置过滤器链
步骤3:拦截 login 对登录的用户信息,使用Shiro 对 session 进行账号密码验证
AuthController
生成Token:根据UsernamePasswordToken参数可知,会对username、password进行token生成
使用Token:使用该token进行登录
AccountRealm.java :过滤器,【重写父类 AuthorizingRealm 方法】
1.获取Token
2.根据token获取username、password,并进行login登录,返回AccountProfile账户信息
3.通过profile、token.getCredentials()、getName(),获取AuthenticationInfo子接口对象(SimpleAuthenticationInfo)
步骤4:对登录前后页面,使用shiro-tags进行shiro标签的使用
b.具体实现
a.步骤1
集成 Shiro 环境
b.步骤2,过滤器链
ShiroConfig.java :配置类,【安全管理器、拦截器链】
-------------------------------------------------------------------------------------------------
/**
* 拦截器链
*/
@Bean
public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager) {
ShiroFilterFactoryBean filterFactoryBean = new ShiroFilterFactoryBean();
// 配置安全管理器
filterFactoryBean.setSecurityManager(securityManager);
// 配置登录的url
filterFactoryBean.setLoginUrl("/login");
// 配置登录成功的url
filterFactoryBean.setSuccessUrl("/user/center");
// 配置未授权跳转页面
filterFactoryBean.setUnauthorizedUrl("/error/403");
// 配置过滤链定义图
Map<String, String> hashMap = new LinkedHashMap<>();
hashMap.put("/login", "anon");
filterFactoryBean.setFilterChainDefinitionMap(hashMap);
return filterFactoryBean;
}
c.步骤3:对登录的用户信息,使用Shiro进行账号密码验证
AuthController.java :控制层,【用户登录】
@Controller
public class AuthController extends BaseController {
/**
* 登录:Shiro校验
*/
@ResponseBody
@PostMapping("/login")
public Result doLogin(String email, String password) {
/**
* 使用hutool的StrUtil工具类,【isEmpty】字符串是否为空、【isBlank】字符串是否为空白
*/
if (StrUtil.isEmpty(email) || StrUtil.isBlank(password)) {
return Result.fail("邮箱或密码不能为空");
}
/**
* 使用Shiro框架,生成token后进行登录
*/
try {
// 生成Token:根据UsernamePasswordToken参数可知,会对username、password进行token生成
UsernamePasswordToken token = new UsernamePasswordToken(email, SecureUtil.md5(password));
// 使用Token:使用该token进行登录
SecurityUtils.getSubject().login(token);
} catch (AuthenticationException e) {
// 使用Shiro框架中封装好的常见错误进行【异常处理】
if (e instanceof UnknownAccountException) {
return Result.fail("用户不存在");
} else if (e instanceof LockedAccountException) {
return Result.fail("用户被禁用");
} else if (e instanceof IncorrectCredentialsException) {
return Result.fail("密码错误");
} else {
return Result.fail("用户认证失败");
}
}
/**
* 如果登录成功,跳转/根页面
*/
return Result.success().action("/");
}
}
-------------------------------------------------------------------------------------------------
AccountProfile.java :实体类
AccountRealm.java :过滤器,【重写父类 AuthorizingRealm 方法】
/**
* AccountRealm:重写父类AuthorizingRealm方法
*/
@Component
public class AccountRealm extends AuthorizingRealm {
@Autowired
UserService userService;
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
return null;
}
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token)
throws AuthenticationException {
// 1.获取Token
UsernamePasswordToken usernamePasswordToken = (UsernamePasswordToken) token;
// 2.根据token获取username、password,并进行login登录,返回AccountProfile账户信息
AccountProfile profile = userService.login(usernamePasswordToken.getUsername(), String.valueOf(usernamePasswordToken.getPassword()));
// 3.通过profile、token.getCredentials()、getName(),获取AuthenticationInfo子接口对象(SimpleAuthenticationInfo)
SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(profile, token.getCredentials(), getName());
return info;
}
}
-------------------------------------------------------------------------------------------------
UserServiceImpl.java :业务层实现,ccountRealm 根据 token 获取 username、password,
并进行 login 登录,返回 AccountProfile 账户信息
d.步骤4:对登录前后页面,使用shiro-tags进行shiro标签的使用
个人用户的【登录】:shiro-freemarker-tags 标签
-------------------------------------------------------------------------------------------------
FreemarkerConfig.java :配置类,【注册标签,将 shiro-freemarker-tags 注册到 Freemarker 配置类】
@PostConstruct
public void setUp() {
configuration.setSharedVariable("timeAgo", timeAgoMethod);
configuration.setSharedVariable("details", postsTemplate);
configuration.setSharedVariable("hots", hotsTemplate);
configuration.setSharedVariable("shiro", new ShiroTags()); //shiro-freemarker-tags标签 -> 声明为shiro标签
}
5 在线权限管理系统
5.1 权限管理
01.Jwt
a.什么情况会跨域
同一协议, 如http或https;同一IP地址, 如127.0.0.1;同一端口, 如8080;
以上三个条件中有一个条件不同就会产生跨域问题。
b.使用jwt根本原因
【前后端分离后产生的跨域问题sessionid丢失,cookies无法写入】
c.jwt加盐加密
a.概念
JWT,全写JSON Web Token, 是开放的行业标准RFC7591,用来实现端到端安全验证.
通过一些算法对加密字符串和JSON对象之间进行加解密。
b.组成
头部:一个json字符串,包含当前令牌名称,使用 加密算法 进行加密处理
载荷:一个json字符串,包含一些自定义的信息,
签名:由头部信息使用base64加密之后 + 载荷使用base64加密之后的部分 + 在加上当前的密钥
将这三部分用连接成一个完整的字符串,构成了最终的jwt
c.原因
JWT加密JSON,保存在客户端,不需要在服务端保存会话信息。可以应用在前后端分离的用户验证上,
后端对前端输入的用户信息进行加密产生一个令牌字符串,前端再次请求时附加此字符串,后端再使用算法解密。
d.优点
可扩展性好
应用程序分布式部署的情况下,session需要做多机数据共享,通常可以存在数据库或者redis里面。而jwt不需要。
无状态
jwt不在服务端存储任何状态。RESTful API的原则之一是无状态,发出请求时,总会返回带有参数的响应,不会产生附加影响。用户的认证状态引入这种附加影响,这破坏了这一原则。另外jwt的载荷中可以存储一些常用信息,用于交换信息,有效地使用 JWT,可以降低服务器查询数据库的次数。
e.缺点
安全性
由于jwt的payload是使用base64编码的,并没有加密,因此jwt中不能存储敏感数据。
而session的信息是存在服务端的,相对来说更安全。
一次性
无法废弃 通过上面jwt的验证机制可以看出来,一旦签发一个jwt,在到期之前就会始终有效,无法中途废弃。
传统的cookie续签方案一般都是框架自带的,session有效期30分钟,30分钟内如果有访问,有效期被刷新至30分钟。
一样的道理,要改变jwt的有效时间,就要签发新的jwt。
最简单的一种方式是每次请求刷新jwt,即每个http请求都返回一个新的jwt。
这个方法不仅暴力不优雅,而且每次请求都要做jwt的加密解密,会带来性能问题。
另一种方法是在redis中单独为每个jwt设置过期时间,每次访问时刷新jwt的过期时间。
02.SpringSecurity
a.概念
认证:根据声明者所特有的识别信息,确认声明者的身份
鉴权:对于一个声明者所声明的身份权利,对其所声明的真实性进行鉴别确认的过程
授权:资源所有者委派执行者,赋予执行者指定范围的资源操作权限,以便执行者代理执行对资源的相关操作
权限控制:对可执行的各种操作组合配置为权限列表,然后根据执行者权限,若其操作在权限范围内,则允许执行
b.SpringSecurity过滤器顺序
验证码认证过滤器(Redis数据库)(如果验证成功,将生成的 jwt 放置到请求头中;否则不放 jwt 到请求头)
-> 用户名密码过滤器 (MySQL数据库)
-> AuthenticationManageri认证管理器(JWT合法/异常/过期)
(AuthenticationSuccessHandler 登录成功处理器 将生成的 jwt 放置到请求头中)
(AuthenticationFailureHandler 登录失败处理器)
c.认证步骤
a.步骤1:身份认证主要分为两次,
a.第0次
Kaptcha Redis数据库
b.第1次
登录认证/授权:用户名、密码和验证码完成登录 MySQL数据库
c.第2次
token认证:请求头携带Jwt进行身份认证 JWT合法/异常/过期
b.步骤2:登录验证
a.一是实体类
public class UserDetailAccount implements UserDetails {
// 获取当前用户对象的用户名
private String username;
// 获取当前用户对象的密码
private String password;
// 当前账户是否可用
private Boolean enabled;
// 当前账户是否未过期
private Boolean accountNonExpired;
// 当前账户是否未锁定
private Boolean accountNonLocked;
// 当前账户是否未过期
private Boolean credentialsNonExpired;
// 获取当前用户对象所具有的角色信息
private final Collection<? extends GrantedAuthority> authorities;
}
b.二是UserDetailServiceImpl
Service public class UserDetailServiceImpl implements UserDetailsService {
校验用户是否存在、获取用户权限信息(角色、菜单权限),返回一个 UserDetails 对象
/**
* 校验用户是否存在、获取用户权限信息(角色、菜单权限),返回一个 UserDetails 对象
*/
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// 校验用户是否存在
User user = userService.getByUsername(username);
if (user == null) {
throw new UsernameNotFoundException("用户名不存在!");
}
// 获取用户权限信息(角色编码code、菜单权限perms)
List<GrantedAuthority> authority = getAuthority(user.getId());
// 返回 AccountUser 对象
UserDetailAccount accountUser = new UserDetailAccount(user.getId(),
user.getUsername(), user.getPassword(), authority);
return accountUser;
}
/**
* 获取用户权限信息(角色编码code、菜单权限perms)
*/
public List<GrantedAuthority> getAuthority(Integer userId) {
String authority = userService.getUserAuthorityInfo(userId);
return AuthorityUtils.commaSeparatedStringToAuthorityList(authority);
}
/**
* 校验用户是否存在、获取用户权限信息(角色、菜单权限),返回一个 UserDetails 对象
*/
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// 校验用户是否存在
User user = userService.getByUsername(username);
if (user == null) {
throw new UsernameNotFoundException("用户名不存在!");
}
// 获取用户权限信息(角色编码code、菜单权限perms)
List<GrantedAuthority> authority = getAuthority(user.getId());
// 返回 AccountUser 对象
UserDetailAccount accountUser = new UserDetailAccount(user.getId(),
user.getUsername(), user.getPassword(), authority);
return accountUser;
}
/**
* 获取用户权限信息(角色编码code、菜单权限perms)
*/
public List<GrantedAuthority> getAuthority(Integer userId) {
String authority = userService.getUserAuthorityInfo(userId);
return AuthorityUtils.commaSeparatedStringToAuthorityList(authority);
}
}
c.三是SecurityConfig配置类
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
// 身份认证:基于数据库的认证
@Autowired
UserDetailServiceImpl userDetailServiceImpl;
/**
* 身份认证:基于数据库的认证
*/
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailServiceImpl);
}
}
d.四是表:用户ID获取用户关联的菜单的id
<select id="getNavMenuIds" resultType="java.lang.Long">
SELECT DISTINCT rm.menu_id
FROM sys_user_role ur
LEFT JOIN `sys_role_menu` rm ON rm.role_id = ur.role_id
WHERE ur.user_id = #{userId};</select>
c.步骤3:token认证
a.引入jwt依赖
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
b.application-win.yml
myslayers:
jwt:
secret: f4e2e52034348f86b67cde581c0f9eb5
expire: 2468
header: authorization
c.生成和校验 jwt 的工具类:
生成 jwt token
获取 jwt 信息
判断 token 是否过期,true代表过期;false代表有效
d.JWT 过滤器:获取请求里的 token,验证 token 是否合法/异常/过期,
并将 UsernamePasswordAuthenticationToken 填充到 SecurityContextHolder
@Override
protected void doFilterInternal(HttpServletRequest req, HttpServletResponse resp, FilterChain chain)
throws IOException, ServletException {
// 获取请求里的 token,验证 token 是否合法/异常/过期
String jwt = req.getHeader(jwtUtils.getHeader());
if (StrUtil.isBlankOrUndefined(jwt)) {
chain.doFilter(req, resp);
return;
}
Claims claim = jwtUtils.getClaimByToken(jwt);
if (claim == null) {
throw new JwtException("TOKEN 异常");
}
if (jwtUtils.isTokenExpired(claim)) {
throw new JwtException("TOKEN 已过期");
}
// 返回一个 UsernamePasswordAuthenticationToken 对象,并将其填充到 SecurityContextHolder
String username = claim.getSubject();
User user = userService.getByUsername(username);
UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken
(username, null, userDetailService.getAuthority(user.getId()));
SecurityContextHolder.getContext().setAuthentication(token);
// 过滤器:放行
chain.doFilter(req, resp);
}
---------------------------------------------------------------------------------------------
JWT 鉴权失败处理器:403状态,JWT 鉴权失败处理器:权限不足!
JWT 认证失败处理器:401状态,JWT 认证失败处理器:请先登录!
JWT 登出成功处理器:JWT:将请求头的 jwt 置空
e.过滤器顺序
验证码认证过滤器(Redis数据库)(如果验证成功,将生成的 jwt 放置到请求头中;否则不放 jwt 到请求头)
-> 用户名密码过滤器 (MySQL数据库)
-> AuthenticationManageri认证管理器(JWT合法/异常/过期)
(AuthenticationSuccessHandler 登录成功处理器 将生成的 jwt 放置到请求头中)
(AuthenticationFailureHandler 登录失败处理器)
d.步骤4:访问授权
a.问题1:我们是在哪里赋予用户权限的?
有两个地方:1、用户登录,调用调用UserDetailsService.loadUserByUsername()方法时候可以返回用户的权限信息。
2、接口调用进行身份认证过滤器时候JWTAuthenticationFilter(JWT 过滤器),需要返回用户权限信息
b.问题2:在哪里决定什么接口需要什么权限?
Security内置的权限注解:@PreAuthorize:方法执行前进行权限检查
@PostAuthorize:方法执行后进行权限检查
@Secured:类似于 @PreAuthorize
---------------------------------------------------------------------------------------------
比如需要Admin角色权限:@PreAuthorize("hasRole('admin')")
比如需要添加管理员的操作权限:@PreAuthorize("hasAuthority('sys:user:save')")
c.使用注解
比如需要Admin角色权限:@PreAuthorize("hasRole('admin')")
比如需要添加管理员的操作权限:@PreAuthorize("hasAuthority('sys:user:save')")
5.2 菜单导航
00.思路
a.步骤1
后端,从Controller层 获取 当前用户ID 关联的 菜单id
b.步骤2
前端,全局前置守卫,对Vuex、axios、router处理,获取当前路由列表,
然后从后端获取的 nav权限 把【新路由】添加到【旧路由数组】中
同时,将 权限列表、菜单列表、路由规则 放到Vuex中
c.步骤3
前端,利用 vuex 的计算属性 computed 动态获取 菜单列表
01.步骤1:Controller层获取nav
@RestController
@RequestMapping("/sys/menu")
public class MenuController extends BaseController {
@GetMapping("/list")
@PreAuthorize("hasAuthority('sys:menu:list')")
public Result list() {
// 查看 menu 菜单信息(全部)
List<Menu> menus = menuService.getAllMenuInfo();
return Result.success("获取/sys/menu/list成功!", menus);
}
@GetMapping("/nav")
public Result nav(Principal principal) {
// 1.获取 authority 权限信息:根据 userId 获取 角色编码code、菜单权限perms
User user = userService.getByUsername(principal.getName());
String authorityInfo = userService.getUserAuthorityInfo(user.getId());
String[] authorityInfoArray = StringUtils.tokenizeToStringArray(authorityInfo, ",");
// 2.获取 Menu 菜单信息(用户)
List<MenuDto> navs = menuService.getUserMenuInfo();
return Result.success("获取/sys/menu/nav成功!",
MapUtil.builder()
.put("authoritys", authorityInfoArray)
.put("nav", navs)
.map()
);
}
@GetMapping("/list")
@PreAuthorize("hasAuthority('sys:role:list')")
public Result list(String name) {
// 查看 role 角色信息(全部)
Page<Role> pageData = roleService.page(getPage(),
new QueryWrapper<Role>().like(StrUtil.isNotBlank(name), "name", name)
);
// 获取 role 对应的 menus
pageData.getRecords().forEach(u -> {
List<Menu> menus = menuService.listMenusByRoleId(u.getId().longValue());
u.setMenus(menus);
});
return Result.success("获取/sys/role/list", pageData);
}
}
02.步骤2:Service层获取nav
public interface MenuService extends IService<Menu> {
List<MenuDto> getUserMenuInfo();
List<Menu> getAllMenuInfo();
List<Menu> listMenusByRoleId(Long roleId);
}
public class MenuServiceImpl extends ServiceImpl<MenuMapper, Menu> implements MenuService {
@Autowired
UserService userService;
@Autowired
UserMapper userMapper;
/**
* 获取 Menu 菜单信息(用户)
*/
@Override
public List<MenuDto> getUserMenuInfo() {
// 获取 User 信息
String username = (String) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
User user = userService.getByUsername(username);
// 获取 User 对应的 menu 菜单信息
List<Menu> menus = this.listByIds(userMapper.getNavMenuIds(user.getId()));
// 将 menu 从 List 转换为 树状结构的 List
List<Menu> menuTree = buildTreeMenu(menus);
// 实体转DTO
return convert(menuTree);
}
/**
* 获取 Menu 菜单信息(全部)
*/
@Override
public List<Menu> getAllMenuInfo() {
// 获取 全部 menu 菜单信息
List<Menu> sysMenus = this.list(new QueryWrapper<Menu>().orderByAsc("sorted"));
// 将 menu 从 List 转换为 树状结构的 List
return buildTreeMenu(sysMenus);
}
03.步骤3:前端路由
a.侧边菜单 - 页面渲染
export default {
name: 'SideMenu',
/* 动态加载【菜单列表】:此写法【menuList: this.$store.state.menus.menuList】在使用store之前加载,从而造成无法加载该数据,故使用【计算属性computed 从store动态获取】 */
computed: {
menuList: {
get() {
return this.$store.state.menus.menuList
},
set(val) {
this.$store.state.menus.menuList = val
},
},
},
methods: {
selectMenu(item) {
this.$store.commit('ADD_TAB', item)
},
},
}
</script>
b.侧边菜单 - 路由规则
Vue.use(VueRouter)
const routes = [
{
path: '/login',
name: 'Login',
component: Login,
},
{
path: '/',
name: 'Index',
component: Index,
children: [
{
path: '/user/center',
name: 'userCenter',
component: () => import('@/views/user/Center'),
},
// {
// path: '/sys/users',
// name: 'SysUser',
// component: () => import("@/views/sys/User")
// },
// {
// path: '/sys/roles',
// name: 'SysRole',
// component: () => import("@/views/sys/Role")
// },
// {
// path: '/sys/menus',
// name: 'SysMenu',
// component: () => import("@/views/sys/Menu")
// }
],
},
]
c.侧边菜单 - 状态管理
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
export default {
state: {
menuList: [],
permList: [],
hasRoutes: false,
},
mutations: {
SET_MENU_LIST: (state, menus) => {
state.menuList = menus
},
SET_PER_LIST: (state, perms) => {
state.permList = perms
},
SET_HAS_ROUTES: (state, hasRoutes) => {
state.hasRoutes = hasRoutes
},
RESET_ROUTE_STATE: (state) => {
state.menuList = []
state.permList = []
state.hasRoutes = false
},
},
}
d.侧边菜单 - 全局前置守卫
`/utils/guard.js` :全局前置守卫(路由规则),根据【用户权限】动态获取【菜单列表】
// ? 内容:全局前置守卫(路由规则),根据【用户权限】动态获取【菜单列表】
import axios from '@/utils/axios'
import store from '@/store'
import router from '@/router'
// 全局前置守卫
router.beforeEach((to, from, next) => {
let hasRoute = store.state.menus.hasRoutes
let token = store.state.token
if (to.path == '/login') {
next()
} else if (!token) {
next({ path: '/login' })
} else if (token && !hasRoute) {
initMenu(router, store)
store.commit('SET_HAS_ROUTES', true)
next()
}
next()
})
// 初始【路由列表】
export const initMenu = (router, store) => {
if (store.state.menus.menuList.length > 0) {
return null
}
axios
.get('/sys/menu/nav', {
headers: { authorization: localStorage.getItem('token') },
})
.then((res) => {
if (res) {
// 获取【菜单列表】
store.commit('SET_MENU_LIST', res.data.data.nav)
// 获取【权限列表】
store.commit('SET_PER_LIST', res.data.data.authoritys)
// 绑定【动态路由】
formatRoutes(res)
}
})
}
// 绑定【动态路由】
export const formatRoutes = (res) => {
// 1.获取当前路由列表
let nowRoutes = router.options.routes
// 2.遍历【res.data.data.nav】,并依次将其加入路由列表
res.data.data.nav.forEach((menu) => {
if (menu.children) {
menu.children.forEach((e) => {
// 【处理:格式化路由】
let route = menuToRoute(e)
// 把【新路由】添加到【旧路由数组】中
if (route) {
nowRoutes[1].children.push(route)
}
})
}
})
router.addRoutes(nowRoutes)
}
// 处理【格式化路由】
export const menuToRoute = (menu) => {
if (menu.component) {
let route = {
path: menu.path,
name: menu.name,
component: () => import('@/views/' + menu.component + '.vue'),
meta: {
icon: menu.icon,
title: menu.title,
},
}
return route
}
return null
}
5.3 redis验证码
01.验证码(先存后发)
a.定义
使用 Redis Hash 存储验证码及其相关信息,如生成时间和验证码类型
b.原理
通过哈希结构将验证码及附加信息结构化地存储在一个键下,便于管理和检索
c.常用API
HSET:设置哈希表中的字段值
HGET:获取哈希表中的字段值
d.使用步骤
选择哈希键:选择一个合适的键来存储哈希数据,例如 captcha:user123
定义字段:在哈希中定义存储验证码及附加信息的字段,例如 code、created_at、type
存储数据:使用 HSET 命令将验证码及其相关信息存储到 Redis 哈希中
e.场景示例
# 存储验证码及附加信息
HSET captcha:user123 code 4567
HSET captcha:user123 created_at 1687995600
HSET captcha:user123 type "email_verification"
# 获取验证码
HGET captcha:user123 code
f.基本流程
1.前端:访问登录页面
2.后端:第1步,生成【key】+【code】,并将该kv键值存到redis,【code经base64输出codeBase64 Image】
第2步,发送【key】、【codeBase64 Image】
3.前端:发送【用户名】、【密码】、【key】、【codelnput】
4.后端:用【key】获取【code】,然后【code比较codelnput】
5.前端:将【比较结果】进行显示,【验证码不正确/正确】
02.实现步骤
a.步骤1
前端,访问登录页面
b.步骤2
后端,利用redis数据库 生成 [key] + [code验证码],并将该kv键值存到redis,然后【code经base64输出codeBase64Image】
后端,发送给前端 key,codeBase64Image,如果验证成功,将生成的 jwt 放置到请求头中;否则不放 jwt 到请求头
@GetMapping("/captcha")
public Result captcha() throws IOException {
// 1.生成 key、code
String key = UUID.randomUUID().toString();
String code = producer.createText();
// Redis:利用哈希表,将【key】-【code】存储到【Const.CAPTCHA_KEY】中
redisUtil.hset(Const.CAPTCHA_KEY, key, code, 120);
// 2.通过 IO 输出 codeImag
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
BufferedImage image = producer.createImage(code);
ImageIO.write(image, "jpg", outputStream);
String str = "data:image/jpeg;base64,";
BASE64Encoder encoder = new BASE64Encoder();
String codeBase64Image = str + encoder.encode(outputStream.toByteArray());
return Result.success(
"获取/captcha成功!",
MapUtil.builder()
.put("key", key)
.put("code", code)
.put("codeBase64Image", codeBase64Image)
.build()
);
}
/**
* 登录成功处理器
*/
@Override
public void onAuthenticationSuccess(
HttpServletRequest req, HttpServletResponse resp, Authentication authentication)
throws IOException, ServletException {
// 编码:设置字符编码
resp.setContentType("application/json;charset=UTF-8");
// JWT:将生成的 jwt 放置到请求头中
String jwt = jwtUtils.generateToken(authentication.getName());
resp.setHeader(jwtUtils.getHeader(), jwt);
// 输出:使用 out 将 result 以 str 的形式 进行输出
ServletOutputStream out = resp.getOutputStream();
Result result = Result.success("登录成功处理器:获取 JWT 成功!");
out.write(JSONUtil.toJsonStr(result).getBytes("UTF-8"));
out.flush();
out.close();
}
/**
* 登录失败处理器
*/
@Override
public void onAuthenticationFailure(HttpServletRequest req, HttpServletResponse resp,
AuthenticationException exception) throws IOException, ServletException {
// 编码:设置字符编码
resp.setContentType("application/json;charset=UTF-8");
// 输出:使用 out 将 result 以 str 的形式 进行输出
ServletOutputStream out = resp.getOutputStream();
Result result = Result.fail("登录失败处理器:用户名/密码/验证码 有误!");
out.write(JSONUtil.toJsonStr(result).getBytes("UTF-8"));
out.flush();
out.close();
}
c.步骤3
前端,发送表单信息,包括用户名、密码、key,codeInput,并获取后台请求头中的jwt
ruleLoginForm: {
username: 'admin',
password: '123456',
code: '',
key: '',
codeBase64Image: '',
},
this.$axios.post('/doLogin?' + qs.stringify(this.ruleLoginForm)).then((res) => {
// 1.Post请求:获取jwt并存放到vuex
const jwt = res.headers['authorization']
this.$store.commit('SET_TOKEN', jwt)
// 2.Get请求:获取userInfo并存放到vuex
this.$axios.get('/sys/user/info').then((res) => {
this.$store.commit('SET_USERINFO', res.data.data)
// 3.跳转主页
this.$router.push('/')
})
})
d.步骤4
后端,用key获取code,然后 code 比较 codeInput
@Override
protected void doFilterInternal(HttpServletRequest req, HttpServletResponse resp, FilterChain filterChain)
throws ServletException, IOException {
// 匹配 /doLogin 时,请求方式必须为 POST,而 post 无法识别
if ("/doLogin".equals(req.getRequestURI()) && req.getMethod().equals("POST")) {
try {
// 校验:验证码的正确性
String key = req.getParameter("key");
String code = req.getParameter("code");
if (StringUtils.isBlank(code) || StringUtils.isBlank(key)) {
throw new CaptchaException("验证码错误!");
}
if (!code.equals(redisUtil.hget(Const.CAPTCHA_KEY, key))) {
throw new CaptchaException("验证码错误!");
}
// Redis:删除当前使用的验证码,【Const.CAPTCHA_KEY】对应的键值对【key】-【code】
redisUtil.hdel(Const.CAPTCHA_KEY, key);
} catch (CaptchaException e) {
// 异常:交给认证失败处理器
loginFailureHandler.onAuthenticationFailure(req, resp, e);
}
}
// 过滤器:放行
filterChain.doFilter(req, resp);
}
后端,将 比较结果 返回给前端
e.过滤器链配置
同时,我们需要 将 验证码认证过滤器(CaptchaFilter)放在 SpringSecurity 的 UsernamePasswordAuthenticationFilter 过滤器前
顺序:验证码认证过滤器(如果验证成功,将生成的 jwt 放置到请求头中;否则不放 jwt 到请求头)
-> 用户名密码过滤器
-> AuthenticationManageri认证管理器
(AuthenticationSuccessHandler 登录成功处理器 将生成的 jwt 放置到请求头中)
(AuthenticationFailureHandler 登录失败处理器)
6 源点数字-智能化运营平台
6.1 第三方登录
00.描述
在第三方登录中,将完整的组织结构数据接入rbac模块,主要包括三方表、职位表、用户、角色、菜单、部门
通过http请求和fastjson工具进行数据处理,使用stream流基于内存计算来区分新增、更新和删除数据
这样每张表都会有三类数据,然后通过手动事务进行工厂方法的数据批量操作,并记录日志以确保数据的一致性和完整性
01.设计过程
a.说明
将晋钢E家的数据引入ERP系统中的RBAC模块
具体来说,就是将一套完整的组织结构数据接入RBAC模块涉及的19张数据表中
这些核心表包括:职位表、角色表、三方表、用户表、菜单表、部门表
b.以同步部门信息为例
a.第1步
将Ejia的地址、端口和参数封装成一个HTTP请求,并使用Hutool工具包中的post()方法发送该请求
将返回的Ejia部门信息存储在一个String类型的变量中。接着,使用Alibaba团队开发的Fastjson工具类
对返回的数据进行筛选和清理,并将其格式化为符合ERP系统需求的信息
b.第2步
将格式化后的Ejia数据处理为符合ERP系统中RABC部门表的树形结构数据
然后,使用Stream流将所有Department对象转换为DepartmentTreeVo对象
并按ID存储到一个Map<String, DepartmentTreeVo>中
最后,使用Stream过滤出父级部门并将其子部门添加到对应的parent部门
变成List<DepartmentTreeVo> 数据
c.第3步
根据ERP系统中部门表的规则(如id与parent_id的关系,OrgCode格式为A01、A02A01、A02A01A02等)
将得到的List<DepartmentTreeVo>数据处理为符合ERP规范的List<SysDepart>部门数据
d.第4步
将List<SysDepart>中的两种数据(Ejia和ERP)进行基于内存的数据区分,分为待新增、待更新(共同数据)和待删除这三类数据。具体内容如下:
待新增:Ejia中存在,但ERP中不存在的部门
待更新:Ejia和ERP中共同存在,但信息不同的部门
待删除:ERP中存在,但Ejia中不存在的部门
每一类数据都会涉及与部门相关联的表,
如sys_depart、sys_user、sys_user_depart、sys_depart_role、sys_depart_permission、sys_depart_role_permission、sys_depart_role_user。
e.第5步
将区分好的三类数据(待新增、待更新、待删除)进行批量操作
使用手动事务DefaultTransactionDefinition
然后使用普通工厂实现方法handleBatchOperationSysDepart进行集中对数据库的批量UPDATE操作
对上述提到的表(如sys_depart、sys_user、sys_user_depart、...)进行异常抛出处理
使得手动事务能够生效,即在发生异常时能够正常回滚数据,确保数据的一致性和完整性
f.第6步
处理结束后,对该定时任务的行为进行日志记录
将数据库每个表的具体操作情况(如原始记录数、新增记录数、更新记录数、删除记录数、同步后最终记录数)打印到日志中
方便后续的监控和数据排查
6.2 单体架构的项目改造
00.描述
单体架构的项目改造,对原jeecgboot项目进行精简,移除冗余模块(quartz、justauth、oss、tenant...)
同时移除接口(登录/登出、用户管理、角色管理和菜单管理),仅实现sysbaseapiimpl类以便独立为代码生成器
然后,与去除jeecg标识的ydszboot项目共享redis,实现token验证一致性
针对两者端口不一致,使用vite配置不同的路由和端口转发规则,以绕过加密的jar包实现改造目的
同时,在nginx/caddy中也配置相应的端口转发规则
01.设计过程
a.说明
该项目计划提供一系列关键功能,包括服务注册与配置、分布式调度任务、权限校验、服务监控和链路追踪等
这些功能将为后续旧项目的迁入提供有效支持,确保系统的稳定性和可扩展性
b.单体架构
a.阶段1:代码生成器,已完成
通过移除Quartz、JustAuth、OSS、Tenant、Third模块,显著减轻了原项目的体积
最终目标是仅保留代码生成器功能,不包括登录/登出、用户管理、角色管理和菜单管理
仅实现SysBaseApiImpl类,该模块主要用于集成,不能单独启动
b.阶段2:共用一个Token功能,已完成
由于代码生成器现在作为一个独立项目启动,其端口号与原ERP项目不同,导致存在跨域问题
因此,我们希望两个项目能够共用一套验证逻辑,即Token的一致性。目前,这一功能已经实现
c.阶段3:接入代码生成器到ERP,已完成
针对上一阶段中提到的端口不同导致的跨域问题
有两种稳妥的解决方案:前端使用Vite代理和后端使用网关
在前期的单体架构中,我们选择使用Vite代理
需要注意的是,在部署dist到Nginx时,也需要配置对应的转发规则,共有4条
1)处理所有以/ydszboot/online开头的请求
2)处理除/online外的所有/ydszboot请求
3)将/jmreport重定向到完整的报表服务URL
4)处理文件上传请求
6.3 单体架构的项目扩展1
00.描述
单体架构的项目扩展1:通过yml配置不同密钥来暴露API,实现对不同模块服务的资源保护
结合类和方法注解,实现细粒度的权限控制,以便在远程服务调用中应用权限规则
接口被划分为不同的权限组:A组(权限a、b、c)、B组(其他权限),并支持扩展到C组
以增强单体架构对接第三方服务的灵活性
01.设计过程
a.说明
一个基于 YAML 配置(不使用数据库)实现的权限接口设计方案说明,该方案支持4种类型(例如类似于 yunzhijia、dingding、wechat 等)
并通过类注解和方法注解实现细粒度的权限控制。假设我们的需求是将各个接口按“组”划分,例如
A组:对应权限 a、b、c
B组:对应其他权限
C组:对应另外一些权限
D组:如果需要,可扩展为第四种类型
b.YAML配置设计
不依赖数据库,所有认证信息都在 YAML 文件中静态配置
例如将配置信息放在 application.yml 或单独的配置文件(例如 access-config.yml)中
-----------------------------------------------------------------------------------------------------
access:
strategies:
# yunzhijia 类型,对应 A组,权限 a、b、c
yunzhijia:
token: "token_yzj_1234" # 该 token 作为认证凭证
group: "A"
permissions:
- "a"
- "b"
- "c"
# dingding 类型,对应 B组
dingding:
token: "token_dd_5678"
group: "B"
permissions:
- "read"
- "write"
# wechat 类型,对应 C组
wechat:
token: "token_wc_9012"
group: "C"
permissions:
- "query"
- "update"
# 其他类型,可扩展为第四种(如其他接口系统)
other:
token: "token_other_3456"
group: "D"
permissions:
- "exec"
-----------------------------------------------------------------------------------------------------
说明:
每个策略(strategy)通过 token 唯一标识,并有所属 group 及其可用的 permissions
这里的 token 可以是生成的密钥或约定字符串,在请求过程中的携带方式(例如通过 HTTP Header)用于认证
c.注解设计
a.类级别注解:@AccessType
用于标识当前 Controller 对应的认证组,如 A、B、C 等。
-------------------------------------------------------------------------------------------------
@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.TYPE })
public @interface AccessType {
/**
* 指定需要的认证组,比如 "A", "B", "C" 或者 "D"
*/
String value();
}
b.方法级别注解:@AccessPermission
用于标注具体方法需要的权限。
-------------------------------------------------------------------------------------------------
@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.METHOD })
public @interface AccessPermission {
/**
* 指定调用该方法需要的权限,如 "a", "b", "c", "read", "write" 等
*/
String value();
}
d.配置属性类
创建一个配置属性类(例如 AccessProperties),利用 Spring Boot 的 @ConfigurationProperties 注解读取上面的 YAML 配置,使配置信息自动加载到内存供后续使用
-----------------------------------------------------------------------------------------------------
@ConfigurationProperties(prefix = "access")
public class AccessProperties {
private Map<String, Strategy> strategies = new HashMap<>();
public static class Strategy {
private String token;
private String group;
private List<String> permissions = new ArrayList<>();
// getter setter ...
}
// getter、setter...
}
e.拦截器或 AOP 切面的设计
设计一个拦截器或 AOP 切面(例如 AuthInterceptor 或 AccessCheckAspect),在每次请求时完成以下工作:
1.提取请求的 token
从请求头(例如 Authorization 或自定义 Header 如 X-Access-Token)中获取 token
2.从配置中查找对应的策略
根据 token 在加载的 AccessProperties 中查找对应的策略信息,获得该 token 的所属组及权限列表
3.获取控制器和方法上的权限注解
如果 Controller 上存在 @AccessType 注解,标识该接口要求的认证组
如果方法上存在 @AccessPermission 注解,则对应具体的权限要求
4.验证匹配
检查配置中 token 对应的组是否与 @AccessType 指定相符
检查配置中 token 配置的权限列表中是否包含方法上要求的权限
5.放行或拒绝
验证通过则允许请求继续,验证失败则返回一个错误响应(例如返回 HTTP 401/403)
f.使用示例
在 Controller 中使用注解:
@AccessType("A") // 表示当前 Controller 归属于A组(yunzhijia类型)
@RestController
@RequestMapping("/demo")
public class DemoController {
@AccessPermission("a") // 表示调用该方法需要权限 "a"
@GetMapping("/getData")
public Result getData() {
// 业务逻辑...
return Result.success();
}
@AccessPermission("b") // 另一权限示例
@PostMapping("/updateData")
public Result updateData() {
// 更新逻辑...
return Result.success();
}
}
-----------------------------------------------------------------------------------------------------
当客户端请求接口时,需要在请求头中带上相应的 token(例如 "token_yzj_1234"),拦截器(或 AOP 切面)
会进行验证,只有当 token 对应的 group 为 "A",且权限数组中包含所请求的方法权限(比如 "a" 或 "b")时才能通过认证
g.总结
静态配置:所有 access token、所属组及权限通过 YAML 文件定义,动态性通过配置文件管理(可与 Nacos 等配置中心结合,实现实时刷新)
注解方式:通过 @AccessType 和 @AccessPermission 注解,将权限要求直接标注在 Controller 和接口方法上,方便实现细粒度控制
拦截器/AOP 实现:在请求拦截时解析 token 并与注解信息进行校验,保证每个请求只能由符合权限的 token 访问
这种方案兼顾轻量、安全和配置集中管理,能够满足类似于 yunzhijia、dingding、wechat 类型的认证需求,同时还可以根据实际业务对不同组(A组:a、b、c;B组;C组等)进行模块化的权限管理
6.4 单体架构的项目扩展2
00.描述
单体架构的项目扩展2:接入jflow国产全开源工作流引擎;使用mapstruct-plus替代beanutil进行对象拷贝
接入hippo4j动态可观测线程池框架;接入asynctool解决任意的多线程并行、串行、阻塞、依赖、回调的并发问题
接入arthas在线诊断工具以实现不重启应用即可进行问题排查、性能分析和代码跟踪等操作
01.设计过程
6.5 单体架构的项目扩展3
00.描述
单体架构的项目扩展2:支持旧项目的灵活迁入。
通过使用maven-shade-plugin生成约150MB的独立可依赖jar包,并将其集成到各个小型服务中
从而继承starter定义的springboot环境、sql环境和开源服务utils等特性
在打包部署时,使用spring-boot-maven-plugin生成可执行的完整jar包
项目可以采用单模块或多模块的方式注册到主应用,通过独立维护starter的发布方式,实现了小型服务开发的简洁与高效
01.设计过程
a.maven-shade-plugin
a.优点
打包所有依赖:可以将所有依赖项打包到一个单独的JAR文件中,生成一个“胖JAR”(fat JAR),便于分发和部署
避免类冲突:提供了重定位(relocation)功能,可以重命名包路径,避免依赖冲突
灵活性:可以通过配置来选择性地包含或排除特定的依赖项和资源文件
b.缺点
打包体积大:生成的JAR文件通常较大,因为包含了所有依赖项
启动速度:启动速度可能会稍慢,因为需要解压和加载所有依赖项
复杂配置:配置相对复杂,尤其是在处理依赖冲突和重定位时
b.spring-boot-maven-plugin
a.优点
简化配置:专为Spring Boot应用设计,配置简单,开箱即用
内嵌容器:支持内嵌的Web容器(如Tomcat、Jetty),便于开发和部署
分层打包:支持分层打包(layered JARs),可以优化Docker镜像的构建和部署
自动化:自动处理Spring Boot应用的依赖和启动类,减少手动配置
b.缺点
依赖Spring Boot: 主要适用于Spring Boot应用,不适用于其他类型的Java应用
灵活性较低: 相对于maven-shade-plugin,灵活性稍差,主要是为了简化Spring Boot应用的打包和部署
6.6 微服务架构项目改造
00.描述
微服务架构项目改造,nacos支持嵌入到springboot子模块中,简化了部署和管理
gateway实现了http简易版本、nacos服务发现+redis动态刷新网关版本
各服务均支持seata分布式事务、xxl-job分布式任务和redisson分布式锁
同时引入sentinel实现熔断降级、网关限流和QPS限制,并使用skywalking实现分布式追踪
主子服务通过直接依赖maven-shade-plugin制作的独立jar包,可以放在不同项目中,仍能实现微服务远程rpc调用能力
01.设计过程
a.设计1
nacos支持嵌入到springboot子模块中,简化了部署和管理
b.设计2
gateway实现了http简易版本、nacos服务发现+redis动态刷新网关版本
c.设计3
各服务均支持seata分布式事务、xxl-job分布式任务和redisson分布式锁
同时引入sentinel实现熔断降级、网关限流和QPS限制,并使用skywalking实现分布式追踪
d.设计4
主子服务通过直接依赖maven-shade-plugin制作的独立Jar包,可以放在不同项目中,仍能实现微服务远程rpc调用能力
6.7 日常代码维护
01.描述
01.switch语句代替if语句
02.Optional类优化多层if嵌套判空
03.if语句使用策略模式+工厂模式代替
04.尽量避免出现while(true)
05.使用手动事务代替@Transactional
06.ThreadLocal(多线程共享变量、上下文信息)
07.注解:插入公共字段
08.注解:模块日志记录
09.注解:接口限流
10.注解:防止重复提交
11.接口幂等性(一是redisson分布锁、二是setnx命令+lua脚本)
12.Spring Cache(Caffeine实现)本地缓存作为一级缓存、Redis远程缓存作为二级缓存
02.描述
查看(表单):map<String, Object>
查看(表格):map<String,list<map>>
查看(通用):多个模块接口耦合、多个类型尽量解耦
插入:MERGE
更新:MERGE
删除:NOT EXISTS
7 OMS调控云平台
7.1 智慧发电信息管理系统:sxoms-nes
00.描述
在新能源场站智慧发电信息管理系统中,使用echarts折线图来统计风电、光伏每分钟的限电时段信息
01.限电时段信息
a.说明
创建一个二维数组 vCounts,用于存储风电(索引0)和光伏(索引1)在0到24小时内的受阻次数
1.数据查询:通过 dao.queryWithParamById("getSdCount", dto) 获取受阻统计数据
2.数据遍历与分类:从每个记录中提取 vtype(类型)和 szyy(受阻原因)。根据 vtype 判断是风电(索引0)还是光伏(索引1)
3.受阻时段解析:使用正则表达式 \\d+:\\d+(、)?[—,-]\\d+:\\d+ 提取所有受阻时段,例如 "08:00-10:00"。解析开始和结束小时,并在 vCounts 数组中对应位置累加受阻次数
b.代码
public class XssdfxServiceImpl implements IXssdfxService {
@Autowired
@Qualifier("omsDataSource")
DataSource omsDataSource;
@Override
public ResponseResult getSdCount(Map<String, Object> dto) {
JdbcDataAccess dao = new JdbcDataAccess(omsDataSource);
int[][] vCounts = new int[2][25];
try {
List<Map> ls = dao.queryWithParamById("getSdCount", dto);
if (ls != null) {
for (Map map : ls) {
String nylx = MapUtil.getStr(map, "vtype");
String szyy = MapUtil.getStr(map, "szyy", "");
int vType = "风电".equals(nylx) ? 0 : 1;
// 受阻时段
List<String> sd = ReUtil.findAll("\\d+:\\d+(、)?[—,-]\\d+:\\d+", szyy, 0);
for (int i = 0; i < sd.size(); i++) {
String[] vSd = sd.get(i).replaceAll("-", "—").split("—");
int s = Integer.parseInt(vSd[0].split(":|:")[0]);
int e = Integer.parseInt(vSd[1].split(":|:")[0]);
for (int j = s; j <= e; j++) {
vCounts[vType][j] += 1;
}
}
}
}
} catch (Exception e) {
log.error("查询风电光伏受阻统计列表异常:{}", e.getMessage());
return ResponseResult.failedResult("查询风电光伏受阻统计列表异常");
}
return ResponseResult.successResult(vCounts);
}
}
7.2 场站重载过载监控系统:sxoms-dcloud
00.描述
在场站重载过载监控系统中,通过每隔15分钟的记录,对电力曲线、主变重载、断面重载、交流线路进行统计
01.电力曲线、电力曲线明细
a.功能
上报省调PI6000
电网短期负荷预测,采集数据
日前供电能力:提前推送3天
日内最大供电能力:每15分钟更新
曲线导出
E文件上报华北
b.指标
日前负荷预测
日前预测最大供电能力
日内最大供电能力
日前预测最小供电能力
日内最小供电能力(预测)
日内最小供电能力(历史+预测)
直调最小可调
分布式光伏
弃电
山西发受电有功功率曲线
c.定时任务
GdqxFbsgfClycTask.java 10kV以下分布式光伏出力预测 每天 9点25分、9点55分 执行
GdqxRnzdgdnlTask.java 日内最大供电能力-采集 每小时 0分、15分、30分、45分 执行 DDTODKY_RNZDGDNL_
GdqxRnzdgdnlMxTask.java 日内最大供电能力-明细采集 每小时 0分、15分、30分、45分 执行 DDTODKY_RNZDGDNLMX_
GdqxRqzdgdnlTask.java 日前最大供电能力-采集 每天 9点6分、15点6分 执行 DDTODKY_ZDGDNL_
GdqxRnzxgdnlTask.java 日内最小供电能力(历史+预测) 每小时 7分、22分、37分、52分 执行 每次查看页面,实时计算 直调最小可调+分布式光伏+弃电+山西发受电有功功率曲线
YxpbTask.java 电压、电流、功率
d.优化
使用hutool的CronUtil工具类、@Scheduled注解、@Async注解、Spring-Retry重试框架来重构定时任务
采用IoUtil、FileUtil、ThreadUtil、AsyncUtil工具类来简化多线程
同时使用DateUtil、ReUtil、StrUtil、CollUtil、ListUtil、MapUtil、MapBuilder优化业务代码
对内置main函数的单文件编译class字节码文件进行定时任务部署
e.说明
以GdqxRnzdgdnlMxTask为例,操作如下:
1.使用正则(FILE_PREFIX + vDate + "(\\d{4})?" + FILE_SUFFIX)匹配文件列表
2.因每小时推1条实际,3条预测,因此需要对文件列表进行文件名排序
3.将每个文件解析为is流,并按照e文件的格式,将完整的文件数据读到List<Map>中
4.因达梦数据库服务器限制,每80条进行切割,使用MERGE语法分批入库
5.将解析后的文件,移动至bak目录
6.注:支持指定具体日期、日期范围,若遇到遇到宕机、升级等操作,可以补落差数据
-----------------------------------------------------------------------------------------------------
坑:
1.@async定时任务
2.重复数据(并发问题)(唯一索引)
3.边解析边入库,分批次入库
4.解析时间顺序
5.杜绝select+if/emtpy+insert+update,推荐merge+ConcurrentHashMap安全类
-----------------------------------------------------------------------------------------------------
MERGE INTO MW_APP.MWT_UD_TEST_CLASS t USING (
SELECT 'C001' AS CODE, '001' AS REMARK FROM DUAL UNION
SELECT 'C003' AS CODE, '003' AS REMARK FROM DUAL UNION
SELECT 'C005' AS CODE, '005' AS REMARK FROM DUAL
) f ON (t.CODE=f.CODE)
WHEN MATCHED THEN
UPDATE SET t.REMARK=f.REMARK
WHEN NOT MATCHED THEN
INSERT(OBJ_ID, OBJ_DISPIDX, CODE, REMARK) VALUES(my_sys.newguid(), 0, f.CODE, f.REMARK);
02.主变重载、断面重载、交流线路
a.主变重载
分析变压器的负载情况,找出负载率较高(≥65%)的变压器,可能用于设备负载监控或容量规划
-----------------------------------------------------------------------------------------------------
1.设置参数:在ratio子查询中,设置了一个阈值(65%)和日期范围(2024年5月)
2.数据收集:cz子查询从多个表中收集变电站、变压器和绕组的基本信息
3.计算超过阈值的时间:sj子查询计算每个设备在给定时间范围内超过阈值的次数
4.最大负载计算:p子查询计算每个设备在给定时间范围内的最大负载值
5.数据整合:t子查询将前面收集的所有信息整合在一起,并计算负载率(FZL)
6.筛选:s子查询筛选出负载率大于等于65%的记录
7.最终输出:主查询选择并格式化最终输出的列,包括变电站名称、电压等级、变压器名称、额定容量、最大负载、负载率等信息,并按最大负载降序排列
b.断面重载
分析电力系统中断面的负载情况,找出在特定时间段内(这里是2024年5月8日)发生过重载的断面,并按重载程度排序
这种分析可以帮助电力系统运营者识别系统中的瓶颈或潜在风险区域,为系统优化和升级提供依据
-----------------------------------------------------------------------------------------------------
1.设置参数:在ratio子查询中,设置了一个阈值(65%)和日期范围(2024年5月8日)
2.数据收集:cz子查询从SG_CON_SECTION_B和SG_CON_SECTION_P_LIMIT表中收集断面的基本信息,包括ID、名称和限制值
3.计算超过阈值的时间:sj子查询计算每个断面在给定时间范围内超过阈值的次数。它对每个小时(V00到V59)进行检查,如果该小时的负载率(实际值除以限制值)超过阈值,就计数为1
4.最大负载计算:p子查询计算每个断面在给定时间范围内的最大负载值
5.数据整合:主查询将前面收集的所有信息整合在一起,包括断面基本信息(cz),重载次数(sj.VAL as ZZSC),和最大负载值(p.VAL as FHZDZ)
6.筛选和排序:查询只选择重载次数大于等于1的断面,并按重载次数降序排列
c.交流线路
识别和分析可能存在重载问题的交流线路。它考虑了多个因素,如最大电流、负载率和重载持续时间
以提供全面的线路负载状况分析。这对于电网管理和维护非常有用,可以帮助识别需要特别关注或可能需要升级的线路
-----------------------------------------------------------------------------------------------------
1.基础数据收集:从OMS_DATACENTER.SG_DEV_ACLINE_H1_MEA_2024表中获取线路的基本信息和每小时的电流数据
2.计算每条线路一天中的最大电流:使用SG_DATACENTER.SG_DEV_ACLINE_H_STA_POW_2024表,计算每条线路在指定日期(2024-04-09)的最大电流值
3.获取地区信息:将线路信息与电网信息关联,获取每条线路所属的电网区域
4.计算重载时长:对每条线路,检查每个小时段(共60个小时段)的电流是否超过额定电流的65%。如果超过,该小时段计为重载。累加所有重载的小时段,得到总重载时长
5.汇总信息:将以上所有信息整合,包括线路ID、名称、所属电网、电压等级、额定电流、最大电流、负载率和重载时长
7.3 山西电网黑启动系统:sxoms-dcloud
00.描述
在山西电网黑启动系统中,通过对发电机组、交流线路、并联电抗进行数据量测,使用echarts绘制路线信息
01.后端
a.电网频率
查询电网频率数据:该方法从数据库中查询电网频率相关的数据,包括电网频率(dwpl)、母线频率(mxpl)和线路频率(xlpl)
b.地图
查询分步数据:该方法根据传入的步骤 ID(lid)查询不同步骤的数据,并将结果组织成 JSON 格式返回
参数处理:首先从 dto 中获取 vTime,计算出 vYear、vDate 和 vKey,并将这些参数添加到 dto 中
步骤处理:根据 lid 的值,调用相应的 step1 到 step12 方法来查询特定步骤的数据。如果 lid 不在 1 到 12 之间,则默认调用所有步骤的方法
标志位生成:调用 stepMapToList 方法,将步骤数据中的 accept 字段转换为一个布尔数组 flag,表示每个步骤是否被接受(通过)
c.7条线
查询线路数据:该方法根据传入的线路 ID(lid)查询不同线路的数据,并将结果组织成 JSON 格式返回
参数处理:与前两个方法相同,首先从 dto 中获取 vTime,计算出 vYear、vDate 和 vKey,并将这些参数添加到 dto 中
线路处理:根据 lid 的值,调用相应的 line1 到 line10 方法来查询特定线路的数据。如果 lid 不在 1 到 10 之间,则默认调用所有线路的方法
标志位生成:调用 lineMapToList 方法,将线路数据中的 accept 字段转换为一个布尔数组 flag,表示每条线路是否被接受(通过)
02.前端
a.电网频率
直接展示
b.地图
a.拆分和存储响应数据
const {flag, ...geoCoordData} = res.data.data;
_this.geoCoordFlag = flag;
_this.geoCoordData = geoCoordData;
b.处理地理坐标数据
a.处理geoCoordData
遍历 geoCoordData:对每个键(如不同的地理区域或数据类别)下的数据数组进行遍历
检查数据有效性:如果 item.accept 为 false,进一步检查数据内容:若 item.data 数组为空,设置 reason 为 "量测数据不存在"。若所有 dataItem.val 都为 0,设置 reason 为 "量测数据为0"
转换 accept 字段:将布尔值 accept 转换为中文描述 "成立" 或 "不成立"
b.处理geoCoordLine
遍历 geoCoordLine:对每一行(线路)进行遍历
更新布尔标志:从 geoCoordFlag 中获取对应的布尔值 booleanValue。查找当前行中是否已经存在布尔类型的项:若存在,更新该项为 booleanValue。若不存在,将 booleanValue 插入到行的开头
c.初始化分步骤路线
初始化 setpLines:清空现有的路线数组
遍历 geoCoordLine:对每条线路进行处理,逐步绘制路线。使用setTimeout,逐个绘制每条路线
c.7条线
a.拆分和存储响应数据
const {flag, ...lineData} = res.data.data;
_this.lineFlag = flag;
_this.lineData = lineData;
b.将lineData与lineList处理,得到newlineList
初始化 newlineList:清空现有的 newlineList 数组,准备存储新的处理结果
遍历 lineList:获取对应的 lineData:通过构造键名(如 L1, L2 等)从 lineData 中获取对应线路的数据
处理每个节点 (node):
检查 accept 字段:
如果 dataNode.accept 存在:
将 accept 转换为字符串。
根据 accept 值和 data 内容设置 reason 字段(例如:"量测数据不存在" 或 "量测数据为0")
将 accept 字段转换为中文描述 "成立" 或 "不成立"
处理复合字段 (jz, xl, dk):
提取各子字段的 accept 值,并计算整体的 accept 值(逻辑与)
检查数据存在性和数据值:判断各子字段的数据是否存在以及数据是否全为零
设置 reason 字段:根据具体条件(如数据全为零或数据不存在),设置详细的错误原因,涉及具体的组件(如发电机组、交流线路、并联电抗器)
将 accept 字段转换为中文描述。
将处理后的 newLine 添加到 newlineList
c.处理lineList
遍历 lineList:对每一行(row)进行遍历,获取其索引 rowIndex
遍历每一行的每个项(item):将 item.mark 设置为对应的 lineFlag 中的布尔值 flag(通过 rowIndex 索引)
7.4 厂站设备同步管理系统:sxoms-esms
00.描述
在厂站设备同步管理系统中,通过电网基本信息、调控中心基本信息、公司基本信息、厂站类型、电压等级对照表、行政区划等
页面操作来完成一次设备、二次设备、发电厂、发电机的数据迁移工作
01.定时任务
a.建表
新建一些元数据表对应旧系统
b.从OMS迁移数据到Dcloud
1.SQL映射:源表、目标表字段数量、命名要求一致
2.使用 Map + Static静态池 来存储表信息
3.标准对象,使用特定语句
4.结构对象,使用MERGE语句
c.代码
/**
* 插入
* @param item 数据记录
* @param type 发电机类型
* @param p_parObjID 父对象ID
* @param p_asctID 关联ID
*/
public void insertSgDict(Map item, String type, String p_parObjID, String p_asctID) {
BusinessRunTimeServiceAgent mw = getMw;
BusinessInvokeResult rs = mw.createBusinessDataAndLink(clsIds.get(type), p_parObjID, p_asctID, uid, null, null);
if (rs.isSuccessful()) {
BusinessData bd = (BusinessData) rs.getResultValue();
for (String key : (Set<String>)item.keySet()) {
bd.setAttributeValue(key.toUpperCase(), item.get(key) != null ? item.get(key) : null);
}
rs = mw.saveBusinessData(bd, uid, null, null);
if (rs.isSuccessful()) {
log.info("数据保存成功:" + bd.getId());
} else {
log.error("数据保存失败:" + rs.getResultHint());
}
} else {
log.error("数据创建失败:" + rs.getResultHint());
}
}
-----------------------------------------------------------------------------------------------------
/**
* 更新
* @param obj_id 操作id
* @param item 数据记录
* @param type 发电机类型
*/
public void updateSgDict(String obj_id, Map item, String type) {
BusinessRunTimeServiceAgent mw = getMw;
BusinessInvokeResult rs = mw.loadBusinessData(obj_id, clsIds.get(type), uid, null, null);
if (rs.isSuccessful()) {
BusinessData bd = (BusinessData) rs.getResultValue();
for (String key : (Set<String>)item.keySet()) {
bd.setAttributeValue(key.toUpperCase(), item.get(key) != null ? item.get(key) : null);
}
rs = mw.saveBusinessData(bd, uid, null, null);
if (rs.isSuccessful()) {
log.info("数据更新成功:" + bd.getId());
} else {
log.error("数据更新失败:" + rs.getResultHint());
}
} else {
log.error("数据加载失败:" + rs.getResultHint());
}
}
7.5 山西电力平衡精益管理系统:sxoms-lems
00.描述
在山西电力平衡精益管理系统中,利用平衡表填写功能,
进行最大负荷预测、装机容量、本地发电能力、非停受阻情况、晚峰平衡裕度的图表统计工作
完成年季平衡、年季平衡、周月平衡、日前日内平衡等页面功能
7.6 省地协同负荷预测全景分析平台:sxoms-fhycfx
00.描述
在省地协同负荷预测全景分析平台中,通过记录中长期负荷预测数据和系统负荷预测免考功能,对日、月、年维度的综合负荷预测指标进行统计
同时,利用短期系统负荷预测功能进行时段统计,并进行分行业负荷曲线分析
01.月综合负荷预测指标
16项指标(16条SQL) -> 中间表 -> 读取中间表(16条SQL)
02.地区母线负荷预测
11地市下的每个站点、条数做成树表,并标注出具体数量
7.7 智能辅助决策系统:sxoms-dss
00.描述
在智能辅助决策系统中,通过主子表记录每个机组每天的状态变化,利用日前状态变更表、历史状态变更表来维护数据
并统计189个机组中9种状态的开始、结束和持续时间,完成机组状态管理、统计、报表等页面功能
01.机组状态管理
默认展示一个月数据,针对每天的数据根据不同色块对不同状态进行展示,并且可以编辑每个时间段的状态
02.机组状态统计
对每天的9种状态具体机组的开机时间、结束时间、持续时间进行统计
03.机组状态报表
默认展示半个月数据,针对类型、每天(不同机组、不同状态)、容量、结束时间进行统计
04.定时任务
辅助决策日平衡_送华北,针对32项指标,每天96点数据,进行纯SQL实现
7.8 网厂信息交互平台:sxoms-ws
00.描述
在网厂信息交互平台中,完成了数据上报类中的在建储能项目月报上报、在运储能项目月报上报、极端天气新能源场站上报
TMS通信检修中的通信检修票管理、通信业务申请单、通信月检修计划、通信工作联系单、通信风险预警单、通信资源管理
通信方式单、风险缺陷单,同时使用异步定时任务解析TMS系统发送的e文件
01.在建储能项目月报上报、在运储能项目月报上报、极端天气新能源场站上报
基础CRUD
02.通信检修票管理、通信业务申请单、通信月检修计划、通信工作联系单、通信风险预警单、通信资源管理、通信方式单、风险缺陷单
基础CRUD + 上传、下载文件
03.定时任务
使用静态池存放表信息、操作模块,对E文件进行解析,并入库到SQL
7.9 配电信息检测平台:sxoms-dypt
00.描述
在配电信息检测平台,完成了山西配电网故障信息监测页面中地图导航、当日配网故障概况、当日故障恢复概况、
当日故障详情、近一月内故障趋势、当日故障恢复情况、近两月配变频停情况的模块开发
00.描述
地图导航:11个地市、每个地市(未分类,合并到某县区下)
当日配网故障概况
当日故障恢复概况
当日故障详情
近一月内故障趋势
7.10 省地县一体化调度管理系统:protal
00.描述
在省地县一体化调度管理系统中,使用jsp技术完成公司本部周碰头会材料页面、风电运行日报、电网运行情况月报的开发
并结合Aspose公司发布的jar包完成导出word、pptx、ppt和execl文件
01.导出WORD
文本 -> 邮件合并(取出数据集、格式化数据)
表格 -> 邮件合并(取出数据集、格式化数据)
图表 -> 查找【省网用电最大负荷】->替换前一个换行符
样式 -> 使用【魑,魅,魍,魉】来预处理第一遍文档,然后根据【标识】重构每一段文字的【大小、字体、是否加粗】、寻找索引合集来处理【关键字,正则】
02.导出PPTX
表格 -> 分页
图表 -> 某一页
文本 -> 取出数据集、格式化数据、将关键字进行html语义化+将整段拆分后再渲染+如果超过一页的内容,则克隆当前页并分页
03.导出EXCEL
Aspose
POI
EasyExcel
7.11 大数据平台:bigdata
00.描述
在大数据平台中,首先Kettle采集数据到Hbase,然后Flink消费Kafka中其他系统传来的数据,实时计算存储到HDFS和Hbase
并利用Spark数据清洗,Hive离线数据分析,最后Sqoop定时将数据导入到Dameng数据库
01.Kettle采集数据到Hbase
将CSV、Excel、E文件
对原始数据进行转换和清洗,比如去除数据中的空值、去重、格式化时间
过Kettle的HBase输出插件写入到HBase,确保数据一致性和正确性
定时任务来自动化数据导入
02.Flink消费Kafka中其他系统传来的数据,实时计算存储到HDFS和Hbase
使用Flink消费Kafka中的消息,编写Flink作业对数据进行实时计算和处理,包括流数据的聚合、过滤、窗口操作等
将处理后的数据分别写入到HDFS和HBase,供后续分析和查询使用
针对Kafka中可能出现的重复消息,我使用Flink的状态管理(state management)和Checkpoint机制来保证数据的一致性和容错性
03.Spark数据清洗
读取大规模数据:使用Spark的DataFrame和RDD来读取HDFS、Hive表中的数据。数据源包括CSV、JSON等格式
数据过滤和去重:在原始数据中常会出现一些无效数据或重复数据,比如空值、脏数据、重复记录等
数据格式转换:对于时间字段、数值字段等格式不统一的数据,使用Spark中的withColumn()和cast()函数进行格式转换,比如将时间戳转换为标准的时间格式,将字符串转化为数字
处理空值和异常值:使用na.fill()或na.drop()来处理数据中的空值,有时根据业务需求填充默认值或删除不完整记录
数据关联和补全:使用join()操作将来自不同数据源的数据进行关联匹配,比如从多个表中组合完整的信息。我们需要对主键或唯一标识进行匹配,并进行数据补全
04.Hive离线数据分析
设计Hive的表结构,并利用Hive进行大规模数据的离线处理,主要进行数据的分区管理、聚合分析和数据统计
使用Hive SQL编写复杂的查询,完成业务部门要求的多维数据分析和报表生成
优化Hive查询的性能,针对大表进行了分区和分桶操作,减少查询延迟
05.Sqoop定时将数据导入到Dameng数据库
使用Sqoop将HDFS中的清洗数据定时导出到达梦数据库,用于后续的数据报表生成和业务查询
编写Shell脚本,结合Crontab进行定时任务调度,实现自动化数据导出流程,减少人工干预
针对数据量大的情况,我通过调整并行度、批量导入参数来优化Sqoop的导入性能
00.描述
使用hutool的CronUtil工具类、@Scheduled注解、@Async注解、Spring-Retry重试框架来重构定时任务,
采用IoUtil、FileUtil、ThreadUtil、AsyncUtil工具类来简化多线程,
同时使用DateUtil、ReUtil、StrUtil、CollUtil、ListUtil、MapUtil、MapBuilder优化业务代码,
对内置main函数的单文件编译class字节码文件进行定时任务部署
---------------------------------------------------------------------------------------------------------
定时任务常见方法:
JDK自带Timer实现
Quartz框架集成实现
spring自带的Spring Task
xxl-Job轻量级分布式任务调度平台
---------------------------------------------------------------------------------------------------------
流程:
1.使用正则(FILE_PREFIX + vDate + "(\\d{4})?" + FILE_SUFFIX)匹配文件列表
2.因每小时推1条实际,3条预测,因此需要对文件列表进行文件名排序
3.将每个文件解析为is流,并按照e文件的格式,将完整的文件数据读到List<Map>中
4.因达梦数据库服务器限制,每80条进行切割,使用MERGE语法分批入库
5.将解析后的文件,移动至bak目录
6.注:支持指定具体日期、日期范围,若遇到遇到宕机、升级等操作,可以补落差数据
---------------------------------------------------------------------------------------------------------
坑:
1.@async定时任务
2.重复数据(并发问题)(唯一索引)
3.边解析边入库,分批次入库
4.解析时间顺序
5.杜绝select+if/emtpy+insert+update,推荐merge+ConcurrentHashMap安全类
---------------------------------------------------------------------------------------------------------
定时任务可能出现并发问题,并非高并发,是并发引起的重复插入问题:
手动事务
MERGE INTO语法
移除@Async注解(在大多数情况下,@Scheduled 注解已经在一个独立的线程池中运行,不需要再使用 @Async。移除 @Async 可以防止同一个定时任务被多个异步线程同时执行。)
使用同步锁,synchronized(如果确实需要异步执行,可以在方法内部添加同步锁,确保同一时间只有一个线程能够执行关键代码块。)
实现分布式锁(如果你的应用部署在多个实例上,可以使用分布式锁(如 Redis 或 Zookeeper)来确保同一时间只有一个实例执行任务。)
添加数据库唯一约束(在数据库层面添加唯一约束,防止插入重复数据。即使多个线程尝试插入相同的数据,数据库会阻止重复插入并抛出异常。)
使用数据库事务和乐观锁(确保检查和插入操作在一个事务中完成,防止竞态条件。可以使用乐观锁机制(如版本号)来处理并发插入。)
CopyOnWriteArrayList Collections.synchronizedList(list); ArrayList默认容量10,扩容为1.5倍
ConcurrentHashSet(hutool) Collections.synchronizedSet(set);
ConcurrentHashMap(java.util.concurrent) Collections.synchronizedMap(map); HashMap默认容量16,负载因子默认0.75,已用容量>总容量*负载因子时,HashMap扩容规则为当前容量翻倍
---------------------------------------------------------------------------------------------------------
多线程问题:
驾校练车:某个驾校每天只能给30个学生练车,有20个普通学生,10个vip。
开始时,普通和VIP并发叫号
要求:
①叫到vip的概率比普通的高(setPriority优先级)
②vip的练车时间是普通学生的3倍(Thread.sleep())
③要求vip学生必须在普通学员之前全部结束(构造方法:将vip线程传入到normal线程,然后join强制让vip插队,保证概率问题)
---------------------------------------------------------------------------------------------------------
如何解决线程安全问题?
不安全:当多个线程并发访问同一个资源时(对象、方法、代码块),经常会出现一些“不安全”的线程。
场景:a和b去卫生间。如果a正在使用,则加一把锁,表示正在使用...
加锁:可以保证多个线程串行执行,即不要相互争夺着抢占资源
01.定时任务
a.使用WITH语法
写s1至s32项指标,比如某个指标为《17.日前最大可调 修改为 6+9+10+11+12+13+14+15+28+20》
b.行列转换(SQL)
on 1=1 合并1行
MAX(DECODE(LX,'',''))
CASE WHEN MONTH_PEPIOD_CODE='1001' THEN '上旬' END
-----------------------------------------------------------------------------------------------------
ROW_NUMBER() OVER (PARTITION BY T1.RECODE_APP ORDER BY T1.VALUE) AS R2
ROW_NUMBER() OVER(PARTITION BY RQ ORDER BY DECODE(DQ, '太原', 1, '大同', 2, '朔州')) AS R
ROW_NUMBER() OVER(PARTITION BY T1.NF ORDER BY DECODE(T1.DQMC, '大同', 1, '临汾', 2, '朔州')) AS R
ROW_NUMBER() OVER(PARTITION BY DECODE(SQ, '全年', 1, '度夏', 2, '度冬', 3) ORDER BY DECODE(T1.DQMC, '大同', 1, '临汾', 2, '朔州') AS R
-----------------------------------------------------------------------------------------------------
ARRAY_ACCUM 收集数据
LISTAGG 收集数据
c.工具类(BACK)
正则匹配规则
扫描DDTODKY_20240412.RB文件
扫描"^20240510_\\d+.jpg$"文件
每4000行处理一次SQL
避免多任务在处理同一文件时产生冲突
-----------------------------------------------------------------------------------------------------
常见方法
扫描文件夹内容
文件操作:包括文件目录的新建、删除、复制、移动、改名等
文件判断:判断文件或目录是否非空,是否为目录,是否为文件等等
写入写出:readFile、writeFile
写入写出:file、string、inputStream
-----------------------------------------------------------------------------------------------------
常见方法
扫描文件夹内容
上传文件、下载文件、删除文件
上传文件:流上传
下载文件:流下载
重命名文件
-----------------------------------------------------------------------------------------------------
创建10个线程
创建14个线程给定时任务
循环创建14个异步线程
主线程直接结束,子线程后台运行
-----------------------------------------------------------------------------------------------------
ThreadUtil类
AsyncUtil类
ExecutorBuilder类
ConcurrencyTester类
-----------------------------------------------------------------------------------------------------
用于并发编程的工具和类
使用Executors创建线程池,使用executor.submit提交Callable对象,并使用CountDownLatch控制线程结束
使用CompletableFuture类来实现异步任务的组合和处理
使用Semaphore来控制同时访问某个资源的线程数量
02.定时任务可能出现并发问题,并非高并发,是并发引起的重复插入问题
a.描述
定时任务在某些天会插入重复的数据。
这通常是由于多个线程同时执行相同的任务,导致在检查数据存在性和插入数据之间存在竞态条件。
b.问题分析
a.@Async 与 @Scheduled 的组合使用:
@Scheduled 注解用于定时执行任务,默认情况下任务在单独的线程池中运行。
@Async 注解使方法异步执行,这意味着每次调度都会启动一个新的异步线程来执行任务。
结合使用这两个注解可能导致多个异步线程同时执行同一个定时任务,特别是在任务执行时间超过调度间隔时。这会导致多个线程同时进入 bmxzql() 方法,进而在检查数据库时都发现当天的数据不存在,从而执行多次插入操作。
b.竞态条件(Race Condition):
当多个线程同时检查数据库,发现当天的数据不存在后,都尝试插入数据,导致重复插入。
c.数据库约束不足:
数据库表中缺乏适当的唯一约束,无法阻止重复数据的插入。
c.解决方案
a.移除@Async注解
在大多数情况下,@Scheduled 注解已经在一个独立的线程池中运行,不需要再使用 @Async。移除 @Async 可以防止同一个定时任务被多个异步线程同时执行。
-------------------------------------------------------------------------------------------------
@Scheduled(cron = "0 40 9,10 * * ?")
public void execBmxzql() {
log.info("开始入库母线准确率定时任务...");
bmxzql();
log.info("结束入库母线准确率定时任务...");
}
b.使用同步锁
如果确实需要异步执行,可以在方法内部添加同步锁,确保同一时间只有一个线程能够执行关键代码块。
-------------------------------------------------------------------------------------------------
@Async
@Scheduled(cron = "0 40 9,10 * * ?")
public void execBmxzql() {
synchronized(this) {
log.info("开始入库母线准确率定时任务...");
bmxzql();
log.info("结束入库母线准确率定时任务...");
}
}
-------------------------------------------------------------------------------------------------
注意:这种方法适用于单实例应用。如果你的应用是分布式部署的(多个实例),需要使用分布式锁。
c.实现分布式锁
如果你的应用部署在多个实例上,可以使用分布式锁(如 Redis 或 Zookeeper)来确保同一时间只有一个实例执行任务。
-------------------------------------------------------------------------------------------------
使用 Redis 实现分布式锁的示例:
@Service
public class ScheduledTask {
@Autowired
private StringRedisTemplate redisTemplate;
@Scheduled(cron = "0 40 9,10 * * ?")
public void execBmxzql() {
String lockKey = "execBmxzqlLock";
Boolean success = redisTemplate.opsForValue().setIfAbsent(lockKey, "locked", 5, TimeUnit.MINUTES);
if (Boolean.TRUE.equals(success)) {
try {
log.info("开始入库母线准确率定时任务...");
bmxzql();
log.info("结束入库母线准确率定时任务...");
} finally {
redisTemplate.delete(lockKey);
}
} else {
log.warn("任务已被其他实例锁定,跳过执行...");
}
}
// 你的 bmxzql 方法
public void bmxzql() {
// ... 现有代码 ...
}
}
d.添加数据库唯一约束
在数据库层面添加唯一约束,防止插入重复数据。即使多个线程尝试插入相同的数据,数据库会阻止重复插入并抛出异常。
ALTER TABLE accuracy_rate ADD CONSTRAINT unique_date_area_station UNIQUE (rq, dq, cz, sbid);
e.使用数据库事务和乐观锁
确保检查和插入操作在一个事务中完成,防止竞态条件。可以使用乐观锁机制(如版本号)来处理并发插入。
@Transactional
public void bmxzql() {
// 检查数据是否存在
if (!oms.existsByToday()) {
// 插入数据
oms.insertAccuracyRate(reqMap);
} else {
// 更新数据
oms.updateAccuracyRate(reqMap);
}
}
f.手动事务
@Service
public class TransactionalService {
@Resource
private TransactionTemplate transactionTemplate;
public Boolean service() {
// 1.查询表
queryDTable1();
// 2.请求外部服务
outerServiceA();
outerServiceB();
outerServiceC();
transactionTemplate.execute((transactionStatus) -> {
try {
// 3.更新表
updateTable1();
updateTable2();
} catch (Exception e) {
transactionTemplate.setRollbackOnly();
log.error("更新失败!");
}
return transactionStatus;
});
return true;
}
}
d.优化后
手动事务
MERGE INTO语法
移除@Async注解(在大多数情况下,@Scheduled 注解已经在一个独立的线程池中运行,不需要再使用 @Async。移除 @Async 可以防止同一个定时任务被多个异步线程同时执行。)
使用同步锁(如果确实需要异步执行,可以在方法内部添加同步锁,确保同一时间只有一个线程能够执行关键代码块。)
实现分布式锁(如果你的应用部署在多个实例上,可以使用分布式锁(如 Redis 或 Zookeeper)来确保同一时间只有一个实例执行任务。)
添加数据库唯一约束(在数据库层面添加唯一约束,防止插入重复数据。即使多个线程尝试插入相同的数据,数据库会阻止重复插入并抛出异常。)
使用数据库事务和乐观锁(确保检查和插入操作在一个事务中完成,防止竞态条件。可以使用乐观锁机制(如版本号)来处理并发插入。)
CopyOnWriteArrayList Collections.synchronizedList(list); ArrayList默认容量10,扩容为1.5倍
ConcurrentHashSet(hutool) Collections.synchronizedSet(set);
ConcurrentHashMap(java.util.concurrent) Collections.synchronizedMap(map); HashMap默认容量16,负载因子默认0.75,已用容量>总容量*负载因子时,HashMap扩容规则为当前容量翻倍
-----------------------------------------------------------------------------------------------------
@Service
public class ScheduledTask {
@Autowired
private StringRedisTemplate redisTemplate;
@Autowired
private JdbcDataAccess emsJdbc;
@Autowired
private JdbcDataAccess omsJdbc;
@Autowired
private PlatformTransactionManager transactionManager; // 注入事务管理器
@Scheduled(cron = "0 40 9,10 * * ?")
public void execBmxzql() {
String lockKey = "execBmxzqlLock";
Boolean success = redisTemplate.opsForValue().setIfAbsent(lockKey, "locked", 5, TimeUnit.MINUTES);
if (Boolean.TRUE.equals(success)) {
try {
log.info("开始入库母线准确率定时任务...");
manuallyManagedTransactionBmxzql();
log.info("结束入库母线准确率定时任务...");
} catch (Exception e) {
log.error("定时任务执行异常: ", e);
} finally {
redisTemplate.delete(lockKey);
}
} else {
log.warn("任务已被其他实例锁定,跳过执行...");
}
}
public void manuallyManagedTransactionBmxzql() {
// 定义事务属性
DefaultTransactionDefinition def = new DefaultTransactionDefinition();
def.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED); // 设置事务传播行为
// 开启事务
TransactionStatus status = transactionManager.getTransaction(def);
try {
List<Map> maps = omsJdbc.queryWithParamById("selectTodayAccuracyRate", new HashMap<>());
String[] str = EveryDayPointVo.str;
String[] strs = EveryDayPointVo.strs;
List<Map> busMaps = emsJdbc.queryWithParamById("getBus", new HashMap<>());
if (busMaps.size() != 0) {
for (Map busMap : busMaps) {
List<BigDecimal> pointSum = new ArrayList<>();
List<Map> forecast = emsJdbc.queryWithParamById("getForecastList", busMap);
pointSum = getData(forecast, busMaps, str, strs, pointSum, busMaps.indexOf(busMap)); // 根据索引获取数据
BigDecimal result = getResult(strs, pointSum);
Map<String, Object> reqMap = new HashMap<>();
reqMap.put("rq", busMap.get("data_time"));
reqMap.put("sbid", busMap.get("id"));
reqMap.put("mxzql", result);
try {
if (maps.size() == 0) {
reqMap.put("id", UUID.randomUUID() + "-00001");
reqMap.put("dispidx", 1);
reqMap.put("dq", busMap.get("areaname"));
reqMap.put("cz", busMap.get("stationname"));
reqMap.put("sbmc", busMap.get("name"));
reqMap.put("sfty", "2");
omsJdbc.updateWithParamById("insertAccuracyRate", reqMap);
} else {
omsJdbc.updateWithParamById("updateAccuracyRate", reqMap);
}
} catch (DataIntegrityViolationException e) {
log.error("插入重复数据,忽略或处理异常: " + e.getMessage());
}
}
}
// 提交事务
transactionManager.commit(status);
} catch (Exception e) {
// 发生异常时回滚事务
transactionManager.rollback(status);
log.error("事务执行失败,已回滚: ", e);
}
}
// 模拟 getData 和 getResult 方法
private List<BigDecimal> getData(List<Map> forecast, List<Map> busMaps, String[] str, String[] strs, List<BigDecimal> pointSum, int index) {
// 数据处理逻辑
return pointSum;
}
private BigDecimal getResult(String[] strs, List<BigDecimal> pointSum) {
// 计算准确率逻辑
return BigDecimal.valueOf(99.99); // 假设返回一个精度值
}
}
e.优化前
@Async
@Scheduled(cron = "0 40 9,10 * * ?")
public void execBmxzql() {
log.info("开始入库母线准确率定时任务...");
bmxzql();
log.info("结束入库母线准确率定时任务...");
}
public void bmxzql() {
JdbcDataAccess jdbc = new JdbcDataAccess(emsDataSource);
JdbcDataAccess oms = new JdbcDataAccess(omsDataSource);
JdbcDataAccess jdbcDataAccess = new JdbcDataAccess();
List<Map> maps = oms.queryWithParamById("selectTodayAccuracyRate", new HashMap<>());
//预测
String[] str = EveryDayPointVo.str;
//实测
String[] strs = EveryDayPointVo.strs;
List<Map> busMaps = jdbc.queryWithParamById("getBus", new HashMap<>());
//查看当天是否有数据
if (maps.size() == 0) {
Map<String, Object> reqMap = new HashMap<>();
if (busMaps.size() != 0) {
for (int i = 0; i < busMaps.size(); i++) {
//96时点每个时点的累加值
List<BigDecimal> pointSum = new ArrayList<>();
//母线实测值
Map busMap = busMaps.get(i);
//母线预测值
List<Map> forecast = jdbc.queryWithParamById("getForecastList", busMap);
//计算母线
pointSum = getData(forecast, busMaps, str, strs, pointSum, i);
//母线准确率结果
BigDecimal result = getResult(strs, pointSum);
reqMap.put("id", UUID.randomUUID() + "-00001");
reqMap.put("dispidx", 1);
reqMap.put("rq", busMap.get("data_time"));
reqMap.put("dq", busMap.get("areaname"));
reqMap.put("cz", busMap.get("stationname"));
reqMap.put("sbmc", busMap.get("name"));
reqMap.put("sbid", busMap.get("id"));
reqMap.put("mxzql", result);
reqMap.put("sfty", "2");
//插入结果集
oms.updateWithParamById("insertAccuracyRate", reqMap);
}
}
} else {
Map<String, Object> reqMap = new HashMap<>();
if (busMaps.size() != 0) {
for (int i = 0; i < busMaps.size(); i++) {
//96时点每个时点的累加值
List<BigDecimal> pointSum = new ArrayList<>();
//母线实测值
Map busMap = busMaps.get(i);
//母线预测值
List<Map> forecast = jdbc.queryWithParamById("getForecastList", busMap);
//计算母线
pointSum = getData(forecast, busMaps, str, strs, pointSum, i);
//母线准确率结果
BigDecimal result = getResult(strs, pointSum);
reqMap.put("rq", busMap.get("data_time"));
reqMap.put("sbid", busMap.get("id"));
reqMap.put("mxzql", result);
//更新结果集
oms.updateWithParamById("updateAccuracyRate", reqMap);
}
}
}
}
8 京云新闻
8.1 基于ELementUl完成后台平台端管理系统设计,整合SpringSecurity完成不同角色、权限认证与授权
8.2 使用阿里云对象存储OSS存储图片、文件等,完成素材图片的上传功能
01.依赖
<dependency>
<groupId>com.aliyun.oss</groupId>
<artifactId>aliyun-sdk-oss</artifactId>
<version>3.10.2</version>
</dependency>
02.配置文件
sky:
alioss:
endpoint: oss-cn-beijing.aliyuncs.com
bucket-name: sky-take-out-refer
03.批量注值
@Component
@ConfigurationProperties(prefix = "sky.alioss")
@Data
public class AliOssProperties {
private String endpoint;
private String accessKeyId;
private String accessKeySecret;
private String bucketName;
}
04.工具类
@Data
@AllArgsConstructor
@Slf4j
public class AliOssUtil {
private String endpoint;
private String accessKeyId;
private String accessKeySecret;
private String bucketName;
/**
* 文件上传
*
* @param bytes
* @param objectName
* @return
*/
public String upload(byte[] bytes, String objectName) {
// 创建OSSClient实例。
OSS ossClient = new OSSClientBuilder().build(endpoint, accessKeyId, accessKeySecret);
try {
// 创建PutObject请求。
ossClient.putObject(bucketName, objectName, new ByteArrayInputStream(bytes));
} catch (OSSException oe) {
System.out.println("Caught an OSSException, which means your request made it to OSS, "
+ "but was rejected with an error response for some reason.");
System.out.println("Error Message:" + oe.getErrorMessage());
System.out.println("Error Code:" + oe.getErrorCode());
System.out.println("Request ID:" + oe.getRequestId());
System.out.println("Host ID:" + oe.getHostId());
} catch (ClientException ce) {
System.out.println("Caught an ClientException, which means the client encountered "
+ "a serious internal problem while trying to communicate with OSS, "
+ "such as not being able to access the network.");
System.out.println("Error Message:" + ce.getMessage());
} finally {
if (ossClient != null) {
ossClient.shutdown();
}
}
//文件访问路径规则 https://BucketName.Endpoint/ObjectName
StringBuilder stringBuilder = new StringBuilder("https://");
stringBuilder
.append(bucketName)
.append(".")
.append(endpoint)
.append("/")
.append(objectName);
log.info("文件上传到:{}", stringBuilder.toString());
return stringBuilder.toString();
}
}
05.注入IOC容器
@Configuration
@Slf4j
public class OssConfiguration {
@Bean
@ConditionalOnMissingBean
public AliOssUtil aliOssUtil(AliOssProperties aliOssProperties){
log.info("开始创建阿里云文件上传工具类对象:{}",aliOssProperties);
return new AliOssUtil(aliOssProperties.getEndpoint(),
aliOssProperties.getAccessKeyId(),
aliOssProperties.getAccessKeySecret(),
aliOssProperties.getBucketName());
}
}
06.测试:控制台
2023-08-23 17:56:59.826 INFO 6204 --- [ main] com.sky.config.OssConfiguration : 开始创建阿里云文件上传工具类对象:AliOssProperties(endpoint=oss-cn-beijing.aliyuncs.com, bucketName=sky-take-out-refer)
2023-08-23 17:57:00.140 INFO 6204 --- [ main] com.sky.config.WebMvcConfiguration : 开始注册自定义拦截器...
2023-08-23 17:57:00.305 INFO 6204 --- [ main] com.sky.config.WebMvcConfiguration : 扩展消息转换器...
---------------------------------------------------------------------------------------------------------
返回oss路径:
https://sky-take-out-refer.oss-cn-beijing.aliyuncs.com/be79e262-f8b3-4bcf-bb19-22690991ee8a.jpg
07.测试:Controller层
@RestController
@RequestMapping("/admin/common")
@Api(tags = "通用接口")
@Slf4j
public class CommonController {
@Autowired
private AliOssUtil aliOssUtil;
/**
* 文件上传
* @param file
* @return
*/
@PostMapping("/upload")
@ApiOperation("文件上传")
public Result<String> upload(MultipartFile file){
log.info("文件上传:{}",file);
try {
//原始文件名
String originalFilename = file.getOriginalFilename();
//截取原始文件名的后缀 dfdfdf.png
String extension = originalFilename.substring(originalFilename.lastIndexOf("."));
//构造新文件名称
String objectName = UUID.randomUUID().toString() + extension;
//文件的请求路径
String filePath = aliOssUtil.upload(file.getBytes(), objectName);
return Result.success(filePath);
} catch (IOException e) {
log.error("文件上传失败:{}", e);
}
return Result.error(MessageConstant.UPLOAD_FAILED);
}
}
8.3 利用Freemarker+Nginx实现页面静态化,并将静态资源自动发布到Nginx,实现动静分离
01.实现原理
准备模板(创建一个模板对象,加载模板路径)
查询数据(把页面要展示的数据查询出来,放在一个map中)
调用模板对象的解析方法process
02.实现步骤
a.获取文章内容
ApArticleContent apArticleContent = apArticleContentMapper.selectOne(Wrappers.<ApArticleContent>lambdaQuery().eq(ApArticleContent::getArticleId, 1390536764510310401L));
b.文章内容通过freemarker生成html文件
Template template = configuration.getTemplate("article.ftl");
params.put("content", JSONArray.parseArray(apArticleContent.getContent()));
template.process(params, out);
c.把html文件上传到minio中
String path = fileStorageService.uploadHtmlFile("", apArticleContent.getArticleId() + ".html", is);
d.修改ap_article表,保存static_url字段
article.setId(apArticleContent.getArticleId());
article.setStaticUrl(path);
apArticleMapper.updateById(article);
8.4 使用MongoDB实现搜索历史记录、并以非结构化数据存储用户评论、评论点赞、评论回复等数据
01.搜索历史记录
a.输入关键词
用户在搜索框中输入关键词
b.搜索
系统执行搜索操作,并返回搜索结果给用户
c.记录关键词(异步请求)
搜索操作的同时,异步记录用户输入的关键词
d.查询搜索记录
检查MongoDB中是否已有该用户的搜索记录
e.判断是否存在
如果搜索记录存在,则更新到最新时间
如果搜索记录不存在,则继续下一步
f.总数据量是否超过10
检查该用户的搜索记录总量是否超过10条
g.替换最后一条数据
如果搜索记录总量超过10条,则删除最早的搜索记录,并保存新的关键词
h.保存关键词
将新的关键词添加到用户的搜索记录中
02.用户评论、评论点赞、评论回复
a.数据模型
a.评论模型
id: 唯一标识符
userId: 评论者的用户ID
content: 评论内容
timestamp: 评论时间
likes: 点赞数量
replies: 回复列表(可以是嵌套评论对象)
b.用户模型(可选)
id: 用户ID
username: 用户名
profilePicture: 头像等用户信息
b.功能模块
a.添加评论
用户提交评论时,检查用户是否已登录。
将评论存入数据库,记录时间戳和初始点赞数(0)。
b.点赞功能
用户点击“点赞”按钮,更新相应评论的点赞数。
需要处理用户重复点赞的情况,通常可通过记录用户和评论的关系来避免。
c.回复评论
用户在查看评论时,可以选择回复某个评论。
将回复存入相应评论的 replies 列表中,以维护评论的层级结构。
8.5 使用Redis的Hash存储验证码,并为其设置相应的过期时间,然后使用阿里云SMS服务发送短信验证码
00.验证码,先存后发
01.总结
选择String字符串:当验证码较短且对安全性要求不高时,字符串简单直观
选择Hash哈希:当验证码的安全性和存储效率是关键考虑因素时,使用哈希更加合适。特别是当验证码用于长期存储或在高并发场景下
02.选择Hash哈希?需要存储与验证码相关的额外信息(如生成时间、验证码的类型等),哈希可以将这些信息结构化地存储在一个键下
选择哈希键:选择一个合适的键来存储哈希数据,例如 captcha:user123,其中 user123 是用户的唯一标识符
定义字段:在哈希中定义存储验证码及附加信息的字段,例如 code、created_at(生成时间)、type(验证码类型)等
存储数据:使用 HSET 命令将验证码及其相关信息存储到 Redis 哈希中
# 存储验证码及附加信息
HSET captcha:user123 code 4567
HSET captcha:user123 created_at 1687995600
HSET captcha:user123 type "email_verification"
03.基本流程
1.前端:访问登录页面
2.后端:第1步,生成【key】+【code】,并将该kv键值存到redis,【code经base64输出codeBase64 Image】
第2步,发送【key】、【codeBase64 Image】
3.前端:发送【用户名】、【密码】、【key】、【codelnput】
4.后端:用【key】获取【code】,然后【code比较codelnput】
5.前端:将【比较结果】进行显示,【验证码不正确/正确】
8.6 使用Redis的Hash缓存高频数据,如:公司组织架构、菜单结构数据、banner等
01.公司组织架构:
适合的数据结构: 字符串 (String) 或 哈希 (Hash)
原因: 公司组织架构通常是一个复杂的树状结构,可能需要存储大量的嵌套数据。如果组织架构的结构固定且不复杂,可以使用字符串来存储 JSON 数据。如果需要存储的字段较多且经常需要访问其中的部分字段,则可以使用哈希。
示例:
// 使用字符串
jedis.set("organization_structure", jsonString);
// 使用哈希
jedis.hset("organization:dept:1", "name", "HR");
jedis.hset("organization:dept:1", "manager", "John Doe");
02.菜单结构数据:
适合的数据结构: 哈希 (Hash) 或 字符串 (String)
原因: 菜单结构通常具有层级和多个字段(如菜单项名称、链接等),哈希可以有效地存储这些字段并提供快速访问。如果菜单结构比较简单或者不需要分字段访问,可以使用字符串存储 JSON 数据。
示例:
// 使用哈希
jedis.hset("menu:main", "home", "/home");
jedis.hset("menu:main", "about", "/about");
// 使用字符串
jedis.set("menu_structure", jsonString);
03.Banner:
适合的数据结构: 哈希 (Hash) 或 有序集合 (Sorted Set)
原因: Banner 通常包含多个字段(如标题、图片 URL、链接等),哈希可以有效地存储这些字段。如果需要按某种标准排序(如展示优先级或点击量),有序集合会更合适。
示例:
// 使用哈希
jedis.hset("banner:1", "title", "Summer Sale");
jedis.hset("banner:1", "image", "summer_sale.jpg");
// 使用有序集合
jedis.zadd("banners", priority, "banner:1");
04.首页通告:
适合的数据结构: 列表 (List) 或 有序集合 (Sorted Set)
原因: 首页通告通常是按时间顺序排列的,因此使用列表(方便插入和读取)或有序集合(可以按时间戳排序)是合适的。如果通告需要按发布时间排序并且有相关分数,可以选择有序集合。
示例:
// 使用列表
jedis.lpush("notices", jsonString);
// 使用有序集合
jedis.zadd("notices", timestamp, jsonString);
8.7 使用Redis的ZSet定时计算前7天内的热点文章、热点资讯,并部署到xxl-job分布式任务调度平台
00.流程
a.定时任务 - 每天凌晨
系统每天凌晨执行一次定时任务,查询前7天的文章
b.查询前7天的文章
从数据库或文章源查询过去7天的所有文章
c.计算文章分值
对每篇文章进行分值计算,分值计算的权重如下
阅读权重:1
点赞权重:3
评论权重:5
收藏权重:8
d.查询所有的频道
系统查询所有的频道信息
e.检索出每个频道的文章
从所有频道中检索出对应的文章
f.按照分值排序
将所有文章按照计算出的分值进行排序
g.取30条分值较高的文章
从排序后的文章中选取分值较高的前30条文章
h.将推荐的文章数据存入Redis
选取的30条文章数据将存入Redis,以便快速访问和推荐
01.总结
Redis的ZSet的score相同时,怎么进行排序?
先说明 Redis 的默认行为,即分数相同时按字典顺序排序
然后,利用这一特性来控制排序,例如,通过修改元素名称来确保排序的稳定性和可预测性
当多个元素的分数相同时,Redis 会按照成员的字典顺序(lexicographical order)进行排序
02.本周热议
a.本周热议的【基本原理】
缓存热评文章——哈希表 Hash
评论数量排行——有序列表 sortedSet:ZADD(添加)、ZREVRANGE(展示)、ZUNIONSTORE(并集)
ZADD key score member [[score member] [score member] ...]
b.本周热议的【初始化操作】
a.第一步
项目启动前,获取【近 7 天文章】
b.第二步
初始化【近 7 天文章】的总评论量(先使用 SortedSet 集合对【排行榜 7 天内全部文章】进行 zadd 操作,
并设置它们 expire 为 7 天;再使用 Hash 哈希表对【排行榜 7 天内全部文章】进行 hexists 判断,再 hset 缓存操作)
a.具体1
添加 add——将【近 7 天文章】创建日期时间作为 key 值,每篇文章对应的 id 作为它的 value 值,每篇文章对应的评论 comment 作为它的 score 值,
并使用 redis 的工具类(RedisUtil),对文章的具体属性进行 zSet()缓存操作
b.具体2
过期 expire——让【近 7 天文章】的 key 过期: 7-(当前时间-创建时间)= 过期时间
c.具体3
缓存——缓存【近 7 天文章】的一些基本信息,例如文章 id,标题 title,评论数量,作者信息...方便访问【近 7 天文章】时,直接 redis,而非 MySQL
先对文章进行 EXISTS 判断其缓存是否存在,如果 false 不存在,则再 hset 缓存操作
c.第三步
对【近 7 天文章】做并集运算(zUnionAndStore), 并使用根据评论量的数量从大到小进行展示(zrevrange)
c.本周热议的【更新操作】
a.第一步
自增/自减评论数
b.第二步
更新这篇文章的缓存时间,并更新这篇文章的基本信息
c.第三步
对【近 7 天文章】重新做并集运算(zUnionAndStore), 并使用根据评论量的数量从大到小进行展示(zrevrange)
8.8 使用Redis+Lua脚本实现滑动窗口限流算法,针对用户行为,如:频繁点赞、评论、发送验证码
01.基本思路
滑动窗口限流的基本思路是根据给定的时间窗口内的请求数量来判断是否超出限制
滑动窗口限流将时间划分为更小的窗口,每次请求都会记录时间戳,并清理掉超出窗口时间的请求
Redis + Lua脚本组合可以帮助我们实现这种限流算法,Lua脚本的优势在于它在Redis内具有原子性
02.Lua脚本限流算法逻辑
1.获取当前时间戳
2.计算滑动窗口的起始时间戳
3.删除小于起始时间戳的记录,确保滑动窗口内的数据不会累积
4.统计滑动窗口内的请求数量
5.如果请求数未超出限流阈值,则记录当前请求,并返回通过;否则,返回限流提示
03.Lua脚本实现
local key = KEYS[1]
local limit = tonumber(ARGV[1])
local window = tonumber(ARGV[2])
local now = tonumber(ARGV[3])
-- 删除过期的记录
redis.call('ZREMRANGEBYSCORE', key, 0, now - window)
-- 统计当前窗口内的请求数
local count = redis.call('ZCARD', key)
if count < limit then
-- 插入当前请求
redis.call('ZADD', key, now, now)
-- 设置key的过期时间为窗口时间,以防key长期存在
redis.call('EXPIRE', key, window)
return 1 -- 表示请求通过
else
return 0 -- 表示限流
end
04.java注解
a.创建注解@RateLimit
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface RateLimit {
int limit(); // 每个时间窗口允许的最大请求数
int window(); // 时间窗口大小(秒)
}
b.将滑动窗口限流算法的核心逻辑封装到一个服务类中,该类会通过Lua脚本在Redis中执行限流操作
import org.springframework.stereotype.Service;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
@Service
public class RateLimiterService {
private static final String SCRIPT = "local key = KEYS[1]\n" +
"local limit = tonumber(ARGV[1])\n" +
"local window = tonumber(ARGV[2])\n" +
"local now = tonumber(ARGV[3])\n" +
"redis.call('ZREMRANGEBYSCORE', key, 0, now - window)\n" +
"local count = redis.call('ZCARD', key)\n" +
"if count < limit then\n" +
" redis.call('ZADD', key, now, now)\n" +
" redis.call('EXPIRE', key, window)\n" +
" return 1\n" +
"else\n" +
" return 0\n" +
"end";
private final JedisPool jedisPool;
public RateLimiterService(JedisPool jedisPool) {
this.jedisPool = jedisPool;
}
public boolean tryAcquire(String key, int limit, int window) {
try (Jedis jedis = jedisPool.getResource()) {
long now = System.currentTimeMillis();
Object result = jedis.eval(SCRIPT, 1, key, String.valueOf(limit), String.valueOf(window * 1000), String.valueOf(now));
return Integer.parseInt(result.toString()) == 1;
}
}
}
c.创建限流拦截器
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
@Aspect
@Component
public class RateLimitAspect {
@Autowired
private RateLimiterService rateLimiterService;
@Around("@annotation(rateLimit)")
public Object rateLimit(ProceedingJoinPoint joinPoint, RateLimit rateLimit) throws Throwable {
String key = "rate:limiter:" + joinPoint.getSignature().toShortString();
int limit = rateLimit.limit();
int window = rateLimit.window();
boolean allowed = rateLimiterService.tryAcquire(key, limit, window);
if (allowed) {
return joinPoint.proceed();
} else {
throw new RuntimeException("请求频率过高,请稍后再试");
}
}
}
d.使用示例
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class UserController {
@RateLimit(limit = 5, window = 60) // 每分钟最多5次请求
@GetMapping("/like")
public String like(@RequestParam String userId) {
return "点赞成功";
}
}
8.9 使用Kafka Stream对用户的点赞和阅读行为进行实时的流式处理,实现实时计算热门文章功能
00.流程
a.用户点赞行为和用户阅读行为
用户在应用中进行点赞和阅读操作
b.发送消息给stream流式处理
用户的点赞和阅读行为会生成相应的消息,并通过Kafka发送到stream流式处理系统
c.Kafka Stream聚合处理
Kafka Stream处理这些用户行为数据,对数据进行聚合和分析
d.更新数据库数量
处理后的数据通过Kafka更新到数据库中,保持数据的最新状态
e.重新计算文章分值
系统重新计算文章分值,计算时会考虑当日热度的权重整体*3,以增加当日热度的影响
f.查询Redis对应数据
系统查询Redis中当前存储的文章分值数据
g.比较分值
将重新计算的文章分值与Redis中的数据进行比较
h.替换
如果重新计算的分值大于Redis中的分值,则更新Redis中的数据
i.存入Redis
最终将当前频道和推荐的文章分值存入Redis,以便快速访问和推荐
8.10 使用RabbitMQ的延迟队列保证消息可靠性和按时处理,应用于发布文章、审核文章场景中
01.总结
a.使用Redis延迟队列实现(zset)
添加任务:在用户操作(如发起订单、发布文章)时,计算任务的执行时间,并将任务添加到有序集合中
轮询定时处理:后台线程定期检查有序集合中到期的任务,并处理这些任务
任务处理:根据任务ID执行具体的业务逻辑,例如发布文章或审核文章
b.采用RabbitMQ的延时队列而不采用Redis的延时队列
采用RabbitMQ的延时队列,可以利用其可靠的消息持久化、原生的延时消息支持、高并发处理能力和丰富的路由功能
更适合发布文章、审核文章和取消订单等需要可靠延时处理的场景
而Redis虽然可以通过定制实现延时队列,但在可靠性和功能性上有所不足
02.实现方式
a.使用RabbitMQ延迟队列实现
a.安装插件
启用rabbitmq_delayed_message_exchange插件
rabbitmq-plugins enable rabbitmq_delayed_message_exchange
b.创建延迟交换机和队列
创建一个x-delayed-message类型的交换机
创建一个普通队列,并将其绑定到延迟交换机
c.发送延迟消息
在用户发起订单时,向延迟交换机发送一条消息,设置消息的延迟时间为15分钟(900000毫秒)
d.消费延迟消息
消费者监听队列,当延迟时间到达时,接收消息并检查订单支付状态
如果订单未支付,则执行取消订单操作
b.使用RabbitMQ死信队列实现
a.创建普通队列和死信交换机
创建一个普通队列,并设置其消息TTL(Time-To-Live)为15分钟
创建一个死信交换机和死信队列
b.配置死信队列
配置普通队列的死信参数,将消息过期后转发到死信交换机
将死信交换机绑定到死信队列
c.发送订单消息
在用户发起订单时,向普通队列发送一条消息
d.消费死信队列消息
消费者监听死信队列,当消息从普通队列转发到死信队列时,接收消息并检查订单支付状态
如果订单未支付,则执行取消订单操作
c.对比总结
延迟队列:消息在指定时间后才会被处理。适用于需要精确定时的任务
死信队列:消息在过期或被拒绝后转发到另一个队列处理。适用于处理失败或需要额外处理的消息
03.死信队列
a.说明
a.定义
消费失败的消息存放的队列
b.消息消费失败的原因:
消息被拒绝并且消息没有重新入队(requeue=false)
消息超时未消费
达到最大队列长度
c.工作原理
当普通队列中有死信时,RabbitMQ 就会自动的将这个消息重新发布到设置的死信交换机去,然后被路由到死信队列
可以监听死信队列中的消息做相应的处理
b.实现方法
a.创建主队列
首先,你需要创建一个主队列,用于发布文章。文章将被发送到这个主队列,但不会立即处理
b.创建死信队列
接下来,你需要创建一个死信队列。这个队列将用于存储那些需要延迟发布的文章
c.定义延迟规则
在发布文章时,将文章消息发布到主队列,但是设置一个延迟时间。你可以使用 RabbitMQ 的消息属性来指定延迟时间
d.设置消费者(审核文章)
创建一个消费者,用于从死信队列中获取延迟发布的文章消息,并将其发布到主队列中以实现实际发布
e.启动消费者(审核文章)
启动上述消费者程序,它会监听死信队列,并在延迟时间结束后将文章发布到主队列,实现延迟发布效果
8.11 使用ElasticSearch实现全文检索,引入IK中文分词器,并使用RabbitMQ同步MySQL数据的增删改操作
01.创建索引和映射
PUT请求添加映射: http://192.168.200.130:9200/app_info_article
GET请求查询映射:http://192.168.200.130:9200/app_info_article
DELETE请求,删除索引及映射:http://192.168.200.130:9200/app_info_article
GET请求,查询所有文档:http://192.168.200.130:9200/app_info_article/_search
02.数据初始化到索引库
@SpringBootTest
@RunWith(SpringRunner.class)
public class ApArticleTest {
@Autowired
private ApArticleMapper apArticleMapper;
@Autowired
private RestHighLevelClient restHighLevelClient;
/**
* 注意:数据量的导入,如果数据量过大,需要分页导入
* @throws Exception
*/
@Test
public void init() throws Exception {
//1.查询所有符合条件的文章数据
List<SearchArticleVo> searchArticleVos = apArticleMapper.loadArticleList();
//2.批量导入到es索引库
BulkRequest bulkRequest = new BulkRequest("app_info_article");
for (SearchArticleVo searchArticleVo : searchArticleVos) {
IndexRequest indexRequest = new IndexRequest().id(searchArticleVo.getId().toString())
.source(JSON.toJSONString(searchArticleVo), XContentType.JSON);
//批量添加数据
bulkRequest.add(indexRequest);
}
restHighLevelClient.bulk(bulkRequest, RequestOptions.DEFAULT);
}
}
03.文章搜索功能实现
a.配置
elasticsearch:
host: 127.0.0.1
port: 9200
b.配置
@Getter
@Setter
@Configuration
@ConfigurationProperties(prefix = "elasticsearch")
public class ElasticSearchConfig {
private String host;
private int port;
@Bean
public RestHighLevelClient client(){
System.out.println(host);
System.out.println(port);
return new RestHighLevelClient(RestClient.builder(
new HttpHost(
host,
port,
"http"
)
));
}
}
c.Controller层
@RestController
@RequestMapping("/api/v1/article/search")
public class ArticleSearchController {
@Autowired
private ArticleSearchService articleSearchService;
@PostMapping("/search")
public ResponseResult search(@RequestBody UserSearchDto dto) throws IOException {
return articleSearchService.search(dto);
}
}
d.Service层
/**
* es文章分页检索
*
* @param dto
* @return
*/
@Override
public ResponseResult search(UserSearchDto dto) throws IOException {
//1.检查参数
if(dto == null || StringUtils.isBlank(dto.getSearchWords())){
return ResponseResult.errorResult(AppHttpCodeEnum.PARAM_INVALID);
}
ApUser user = AppThreadLocalUtil.getUser();
//异步调用 保存搜索记录
if(user != null && dto.getFromIndex() == 0){
apUserSearchService.insert(dto.getSearchWords(), user.getId());
}
//2.设置查询条件
SearchRequest searchRequest = new SearchRequest("app_info_article");
//条件构建器
SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
//布尔查询
BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery();
//关键字的分词之后查询 我想买一个手机 --》 我 一个 手机
QueryStringQueryBuilder queryStringQueryBuilder = QueryBuilders.queryStringQuery(dto.getSearchWords()).field("title").field("content").defaultOperator(Operator.OR);
boolQueryBuilder.must(queryStringQueryBuilder);
//查询小于mindate的数据
RangeQueryBuilder rangeQueryBuilder = QueryBuilders.rangeQuery("publishTime").lt(dto.getMinBehotTime().getTime());
boolQueryBuilder.filter(rangeQueryBuilder);
//分页查询
searchSourceBuilder.from(0);
searchSourceBuilder.size(dto.getPageSize());
//按照发布时间倒序查询
searchSourceBuilder.sort("publishTime", SortOrder.DESC);
//设置高亮 title
HighlightBuilder highlightBuilder = new HighlightBuilder();
highlightBuilder.field("title");
highlightBuilder.preTags("<font style='color: red; font-size: inherit;'>");
highlightBuilder.postTags("</font>");
searchSourceBuilder.highlighter(highlightBuilder);
searchSourceBuilder.query(boolQueryBuilder);
searchRequest.source(searchSourceBuilder);
SearchResponse searchResponse = restHighLevelClient.search(searchRequest, RequestOptions.DEFAULT);
//3.结果封装返回
List<Map> list = new ArrayList<>();
SearchHit[] hits = searchResponse.getHits().getHits();
for (SearchHit hit : hits) {
String json = hit.getSourceAsString();
Map map = JSON.parseObject(json, Map.class);
//处理高亮
if(hit.getHighlightFields() != null && hit.getHighlightFields().size() > 0){
Text[] titles = hit.getHighlightFields().get("title").getFragments();
String title = StringUtils.join(titles);
//高亮标题
map.put("h_title",title);
}else {
//原始标题
map.put("h_title",map.get("title"));
}
list.add(map);
}
return ResponseResult.okResult(list);
}
04.新增文章创建索引
a.文章微服务(生产者):文章审核成功使用kafka发送消息
/**
* 送消息,创建索引
* @param apArticle
* @param content
* @param path
*/
private void createArticleESIndex(ApArticle apArticle, String content, String path) {
SearchArticleVo vo = new SearchArticleVo();
BeanUtils.copyProperties(apArticle,vo);
vo.setContent(content);
vo.setStaticUrl(path);
kafkaTemplate.send(ArticleConstants.ARTICLE_ES_SYNC_TOPIC, JSON.toJSONString(vo));
}
b.搜索微服务(消费者):搜索微服务接收消息,添加数据到索引库
@Component
@Slf4j
public class SyncArticleListener {
@Autowired
private RestHighLevelClient restHighLevelClient;
@KafkaListener(topics = ArticleConstants.ARTICLE_ES_SYNC_TOPIC)
public void onMessage(String message){
if(StringUtils.isNotBlank(message)){
log.info("SyncArticleListener,message={}",message);
SearchArticleVo searchArticleVo = JSON.parseObject(message, SearchArticleVo.class);
IndexRequest indexRequest = new IndexRequest("app_info_article");
indexRequest.id(searchArticleVo.getId().toString());
indexRequest.source(message, XContentType.JSON);
try {
restHighLevelClient.index(indexRequest, RequestOptions.DEFAULT);
} catch (IOException e) {
e.printStackTrace();
log.error("sync es error={}",e);
}
}
}
}
05.RabbitMQ同步MySQL数据的增删改操作
a.介绍
当数据发生增、删、改时,要求对elasticsearch中数据也要完成相同操作
步骤:
单机部署并启动MQ(单机部署在MQ部分有讲)
接收者中声明exchange、queue、RoutingKey
在hotel-admin发送者中的增、删、改业务中完成消息发送
在hotel-demo接收者中完成消息监听,并更新elasticsearch中数据
启动并测试数据同步功能
b.对发送者和消费者都添加依赖和yaml信息
a.引入依赖
<!--amqp-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
b.yaml
spring:
rabbitmq: #MQ配置
host: 192.168.194.131 # 主机名
port: 5672 # 端口
virtual-host: / # 虚拟主机
username: itcast # 用户名
password: 123321 # 密码
c.声明交换机、队列
a.声明队列交换机名称
在hotel-admin发送者和hotel-demo消费者中的cn.itcast.hotel.constatnts包下新建一个类MqConstants
package cn.itcast.hotel.constatnts;
public class MqConstants {
/**
* 交换机
*/
public final static String HOTEL_EXCHANGE = "hotel.topic";
/**
* 监听新增和修改的队列
*/
public final static String HOTEL_INSERT_QUEUE = "hotel.insert.queue";
/**
* 监听删除的队列
*/
public final static String HOTEL_DELETE_QUEUE = "hotel.delete.queue";
/**
* 新增或修改的RoutingKey
*/
public final static String HOTEL_INSERT_KEY = "hotel.insert";
/**
* 删除的RoutingKey
*/
public final static String HOTEL_DELETE_KEY = "hotel.delete";
}
b.声明队列交换机
在hotel-demo消费者中,定义配置类,声明队列、交换机:
package cn.itcast.hotel.config;
import cn.itcast.hotel.constants.MqConstants;
import org.springframework.amqp.core.Binding;
import org.springframework.amqp.core.BindingBuilder;
import org.springframework.amqp.core.Queue;
import org.springframework.amqp.core.TopicExchange;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class MqConfig {
@Bean
public TopicExchange topicExchange(){
return new TopicExchange(MqConstants.HOTEL_EXCHANGE, true, false);
}
@Bean
public Queue insertQueue(){
return new Queue(MqConstants.HOTEL_INSERT_QUEUE, true);
}
@Bean
public Queue deleteQueue(){
return new Queue(MqConstants.HOTEL_DELETE_QUEUE, true);
}
@Bean
public Binding insertQueueBinding(){
return BindingBuilder.bind(insertQueue()).to(topicExchange()).with(MqConstants.HOTEL_INSERT_KEY);
}
@Bean
public Binding deleteQueueBinding(){
return BindingBuilder.bind(deleteQueue()).to(topicExchange()).with(MqConstants.HOTEL_DELETE_KEY);
}
}
c.发送MQ消息
在hotel-admin发送者中的增、删、改业务中分别发送MQ消息
-----------------------------------------------------------------------------------------------------
@PostMapping
public void saveHotel(@RequestBody Hotel hotel){
hotelService.save(hotel);
rabbitTemplate.convertAndSend(MqConstants.HOTEL_EXCHANGE, MqConstants.HoTEL_INSERT_KEY, hotel.getId());
}
-----------------------------------------------------------------------------------------------------
@PutMapping()
public void updateById(@RequestBody Hotel hotel){
if (hotel.getId() == null) {
throw new InvalidParameterException("id不能为空");
}
hotelService.updateById(hotel);
rabbitTemplate.convertAndSend(MqConstants.HOTEL_EXCHANGE, MqConstants.HOTEL_INSERT_KEY, hotel.getId());
}
-----------------------------------------------------------------------------------------------------
@DeleteMapping("/{id}")
public void deleteById(@PathVariable("id")Long id){
hotelService.removeById(id);
rabbitTemplate.convertAndSend(MqConstants.HOTEL_EXCHANGE, MqConstants.HOTEL_DELETE_KEY, id);
}
d.接收MQ消息
a.介绍
hotel-demo接收到MQ消息要做的事情包括:
新增消息:根据传递的hotel的id查询hotel信息,然后新增一条数据到索引库
删除消息:根据传递的hotel的id删除索引库中的一条数据
b.写SDL业务
首先在hotel-demo的cn.itcast.hotel.service包下的IHotelService中新增新增、删除业务
void deleteById(Long id);
void insertById(Long id);
-------------------------------------------------------------------------------------------------
给hotel-demo中的cn.itcast.hotel.service.impl包下的HotelService中实现业务:
@Override
public void deleteById(Long id) {
try {
// 1.准备Request
DeleteRequest request = new DeleteRequest("hotel", id.toString());
// 2.发送请求
client.delete(request, RequestOptions.DEFAULT);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
@Override
public void insertById(Long id) {
try {
// 0.根据id查询酒店数据
Hotel hotel = getById(id);
// 转换为文档类型
HotelDoc hotelDoc = new HotelDoc(hotel);
// 1.准备Request对象
IndexRequest request = new IndexRequest("hotel").id(hotel.getId().toString());
// 2.准备Json文档
request.source(JSON.toJSONString(hotelDoc), XContentType.JSON);
// 3.发送请求
client.index(request, RequestOptions.DEFAULT);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
c.编写监听器
在hotel-demo中的cn.itcast.hotel.mq包新增一个类:
package cn.itcast.hotel.mq;
import cn.itcast.hotel.constants.MqConstants;
import cn.itcast.hotel.service.IHotelService;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
@Component
public class HotelListener {
@Autowired
private IHotelService hotelService;
/**
* 监听酒店新增或修改的业务
* @param id 酒店id
*/
@RabbitListener(queues = MqConstants.HOTEL_INSERT_QUEUE)
public void listenHotelInsertOrUpdate(Long id){
hotelService.insertById(id);
}
/**
* 监听酒店删除的业务
* @param id 酒店id
*/
@RabbitListener(queues = MqConstants.HOTEL_DELETE_QUEUE)
public void listenHotelDelete(Long id){
hotelService.deleteById(id);
}
}
d.测试
用postman调用增加/删除/修改mysql数据库的接口,然后去页面搜索看看删除的数据还是否能查到,
或者修改/增加的数据能不能查出来
8.12 对接大数据平台组,使用Flink消费Kafka中的海量数据,实时计算存储到HDFS和Hbase,并利用Spark数据清洗,Hive离线数据分析,最后Sqoop定时将HBase数据迁移至MySQL,并通过Flink实时计算到ElasticSearch
8.13 使用ElasticSearch结合Logstash、Kibana完成统一日志管理,服务上报日志,统一进行日志监控
01.介绍
a.概念
elasticsearch结合kibana、Logstash、Beats,也就是elastic stack(ELK)
ELK是Elasticsearch、Logstash、Kibana的简称,这三者是核心套件,但并非全部
-----------------------------------------------------------------------------------------------------
Elasticsearch是实时全文搜索和分析引擎,提供搜集、分析、存储数据三大功能
是一套开放REST和JAVA API等结构提供高效搜索功能,可扩展的分布式系统
它构建于Apache Lucene搜索引擎库之上
b.elasticsearch
elasticsearch是elastic stack的核心,负责存储、搜索、分析数据
Elasticsearch:Elasticsearch是一个分布式、实时的搜索和分析引擎。它使用倒排索引来快速存储、搜索和分析大量的数据
Elasticsearch提供了强大的全文搜索、复杂查询、聚合和地理空间搜索等功能
c.Logstash
Logstash是一个用来搜集、分析、过滤日志的工具。它支持几乎任何类型的日志
包括系统日志、错误日志和自定义应用程序日志。它可以从许多来源接收日志
这些来源包括 syslog、消息传递(例如 RabbitMQ)和JMX
它能够以多种方式输出数据,包括电子邮件、websockets和Elasticsearch
-----------------------------------------------------------------------------------------------------
Logstash是一个开源的数据收集和处理工具。它可以从不同的数据源收集数据
并对数据进行过滤、转换和格式化,然后将数据发送到目标存储(如Elasticsearch)中
-----------------------------------------------------------------------------------------------------
一般工作方式为c/s架构,client端安装在需要收集日志的主机上
server端负责将收到的各节点日志进行过滤、修改等操作在一并发往elasticsearch上去
d.Kibana
Kibana是一个基于Web的图形界面,用于搜索、分析和可视化存储在 Elasticsearch指标中的日志数据
它利用Elasticsearch的REST接口来检索数据,不仅允许用户创建他们自己的数据的定制仪表板视图,还允许他们以特殊的方式查询和过滤数据
Kibana可以为 Logstash 和 ElasticSearch 提供的日志分析友好的 Web 界面,可以帮助汇总、分析和搜索重要数据日志
-----------------------------------------------------------------------------------------------------
Kibana是一个用于数据可视化和分析的开源工具
它提供了一个用户友好的界面,可以实时地搜索、分析和可视化存储在Elasticsearch中的数据
e.为什么用到ELK
一般我们需要进行日志分析场景:直接在日志文件中 grep、awk 就可以获得自己想要的信息
但在规模较大的场景中,此方法效率低下,面临问题包括日志量太大如何归档、文本搜索太慢怎么办
如何多维度查询。需要集中化的日志管理,所有服务器上的日志收集汇总
-----------------------------------------------------------------------------------------------------
常见解决思路是建立集中式日志收集系统,将所有节点上的日志统一收集,管理,访问
一般大型系统是一个分布式部署的架构,不同的服务模块部署在不同的服务器上,问题出现时
大部分情况需要根据问题暴露的关键信息,定位到具体的服务器和服务模块
构建一套集中式日志系统,可以提高定位问题的效率
-----------------------------------------------------------------------------------------------------
一个完整的集中式日志系统,需要包含以下几个主要特点:
①收集-能够采集多种来源的日志数据
②传输-能够稳定的把日志数据传输到中央系统
③存储-如何存储日志数据
④分析-可以支持 UI 分析
⑤警告-能够提供错误报告,监控机制
-----------------------------------------------------------------------------------------------------
ELK提供了一整套解决方案,并且都是开源软件,之间互相配合使用,完美衔接,高效的满足了很多场合的应用
目前主流的一种日志系统
02.使用Java代码实现ELK的入门示例
a.第1步:安装和配置Elasticsearch
从Elasticsearch官方网站(https://www.elastic.co/downloads/elasticsearch)下载并安装最新版本的Elasticsearch
启动Elasticsearch,并确保它正常运行在默认端口9200上
b.第2步:安装和配置Logstash
从Logstash官方网站(https://www.elastic.co/downloads/logstash)下载并安装最新版本的Logstash
创建一个名为logstash.conf的配置文件,用于定义数据源、过滤器和输出目标的配置。例如:
input {
file {
path => "/path/to/your/log/file.log"
start_position => "beginning"
}
}
filter {
# 添加过滤器配置,例如对日志进行解析、转换或过滤
}
output {
elasticsearch {
hosts => ["localhost:9200"]
index => "logs"
}
}
-----------------------------------------------------------------------------------------------------
启动Logstash并指定配置文件:bin/logstash -f logstash.conf
c.第3步:安装和配置Kibana
从Kibana官方网站(https://www.elastic.co/downloads/kibana)下载并安装最新版本的Kibana
启动Kibana,并确保它正常运行在默认端口5601上
在Kibana中创建索引模式,指定Elasticsearch中的索引名称和字段映射
d.第4步:在Java应用程序中记录日志
在Java项目中使用日志框架(如Log4j、Slf4j等)记录日志
配置日志输出到Logstash的IP和端口
e.第5步:查看和分析日志数据
打开Kibana的Web界面(通常是http://localhost:5601)
创建可视化图表和仪表板,用于展示和分析存储在Elasticsearch中的日志数据
03.将SpringBoot日志实时输入到Es中
a.配置Logstash
在 config 目录下,添加 logstash-springboot.conf 文件,内容如下:
input {
tcp {
mode => "server"
host => "0.0.0.0"
port => 4560
codec => json_lines
}
}
filter {
}
output {
elasticsearch {
hosts => ["127.0.0.1:9200","127.0.0.1:9201","127.0.0.1:9202"]
index => "log-javaboy-dev-%{+yyyy.MM.dd}"
}
}
-----------------------------------------------------------------------------------------------------
在 config/pipelines.yml 文件中,加载 logstash-springboot.conf 配置文件
- pipeline.id: log_dev
path.config: "/Users/sang/workspace/elasticsearch/logstash-7.10.2/config/logstash-springboot.conf"
-----------------------------------------------------------------------------------------------------
启动 Logstash
进入到 bin 目录下,执行 ./logstash 命令启动即可(启动之前确保 Es 已经启动)。看到如下内容表示启动成功:
b.SpringBoot日志
a.依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>net.logstash.logback</groupId>
<artifactId>logstash-logback-encoder</artifactId>
<version>6.6</version>
</dependency>
b.然后在 resources 目录下创建 logback-spring.xml 文件,将日志输出到 logstash 中
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE configuration>
<configuration>
<include resource="org/springframework/boot/logging/logback/defaults.xml"/>
<include resource="org/springframework/boot/logging/logback/console-appender.xml"/>
<!--应用名称-->
<property name="APP_NAME" value="logstash"/>
<!--日志文件保存路径-->
<property name="LOG_FILE_PATH" value="${LOG_FILE:-${LOG_PATH:-${LOG_TEMP:-${java.io.tmpdir:-/tmp}}}/logs}"/>
<contextName>${APP_NAME}</contextName>
<!--每天记录日志到文件appender-->
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>${LOG_FILE_PATH}/${APP_NAME}-%d{yyyy-MM-dd}.log</fileNamePattern>
<maxHistory>30</maxHistory>
</rollingPolicy>
<encoder>
<pattern>${FILE_LOG_PATTERN}</pattern>
</encoder>
</appender>
<!--输出到logstash的appender-->
<appender name="LOGSTASH" class="net.logstash.logback.appender.LogstashTcpSocketAppender">
<!--可以访问的logstash日志收集端口-->
<destination>127.0.0.1:4560</destination>
<encoder charset="UTF-8" class="net.logstash.logback.encoder.LogstashEncoder"/>
</appender>
<root level="INFO">
<appender-ref ref="CONSOLE"/>
<appender-ref ref="FILE"/>
<appender-ref ref="LOGSTASH"/>
</root>
</configuration>
c.创建一个 HelloController 用来测试
@RestController
public class HelloController {
private static final Logger logger = LoggerFactory.getLogger(HelloController.class);
@GetMapping("/hello")
public void hello() {
logger.info("hello logstash!");
}
}
c.Kibana
在 Kibana 中,点击创建一个索引规则:log-javaboy-dev-*
然后点击下一步,选择 @timestamp。
最后,在 discover 中可以查看日志信息
9 星链微软采购ESD平台
9.1 使用RestTemplate来发送请求,进行与微软CIS系统的第三方联调,并同时对涉及的微软接口进行业务封装
01.配置信息
# 微软三方
ms:
# base地址
url: https://sandapac.channelinclusiontest.microsoft.com/channelinclusionREST.svc
# ChannelGuid
channelGuid: 7526bb6e-e0f1-4f41-a1b3-4571dd643f49
# timeout
timeout: 60
countrycode: CN
lang: zh-CN
storeid: N/A
# AAD认证
aad:
# 证书位置
pkcs12Certificate: D:\spark\xinlian\zi2\ekostar-admin\ekostar-sys-microsoft\src\main\resources\CIS_CISESD Yunnan Nantian China_New.pfx
# 证书密码
certificatePassword: NewPW!
# clientId
clientId: 6C41A07C-673E-4DB8-B172-F3E751007F75
# AzureActiveDirectoryInstance AAD获取认证令牌的实例
authority: https://login.microsoftonline.com/msretailfederationppe.onmicrosoft.com
# 静态声明应用程序级权限
scope: https://sandbox.esd.channelinclusion.microsoft.com/.default
02.工具类
@Component
public class HttpClientUtil {
private static String authenticationRedisKey = "AAD_Token_Authentication";
@Value("${ms.url}")
private String msUrl;
@Value("${ms.timeout}")
private long msTimeOut;
@Autowired
private RestTemplateConfig restTemplateConfig;
@Autowired
private RedisCache redisCache;
/**
* 获取redis中存储的Token(AAD认证)
*/
private String getTokenInfo() {
if (!redisCache.hasKey(authenticationRedisKey)) {
try {
throw new Exception("AAD认证信息获取失败!");
} catch (Exception e) {
e.printStackTrace();
}
}
String token = redisCache.getCacheObject(authenticationRedisKey);
return token;
}
/**
* 发起GET请求
*
* @param url 请求链接
* @param params 请求参数
* @param c 请求输入类型
* @return
* @param <T>
*/
public <T> T get(String url, Map<String, Object> params, Class<T> c) {
RestTemplate restTemplate = restTemplateConfig.restTemplate();
HttpHeaders headers = new HttpHeaders();
String token = getTokenInfo();
// 设置BearerAuth,即Token
headers.setBearerAuth(token);
// 设置ContentType,即application,x-www-form-urlencoded
headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
HttpEntity httpEntity = new HttpEntity(null, headers);
if (null == params || params.isEmpty()) {
return restTemplate.exchange(msUrl + url, HttpMethod.GET, httpEntity, c).getBody();
} else {
UriComponentsBuilder builder = UriComponentsBuilder.fromHttpUrl(msUrl + url);
params.forEach((k, v) -> {
builder.queryParam(k, v);
});
return restTemplate.exchange(builder.build().toString(), HttpMethod.GET, httpEntity, c, params).getBody();
}
}
/**
* 发起POST请求
*
* @param url 请求链接
* @param params 请求参数
* @param c 请求输入类型
* @return
* @param <T>
*/
public <T> T post(String url, Map<String, Object> params, Class<T> c) {
RestTemplate restTemplate = restTemplateConfig.restTemplate();
HttpHeaders headers = new HttpHeaders();
String token = getTokenInfo();
// 设置BearerAuth,即Token
headers.setBearerAuth(token);
// 设置ContentType
headers.setContentType(MediaType.APPLICATION_XML);
HttpEntity httpEntity = new HttpEntity(params, headers);
if (null == params || params.isEmpty()) {
return restTemplate.exchange(msUrl + url, HttpMethod.POST, httpEntity, c).getBody();
} else {
UriComponentsBuilder builder = UriComponentsBuilder.fromHttpUrl(msUrl + url);
params.forEach(builder::queryParam);
return restTemplate.exchange(builder.build().toString(), HttpMethod.POST, httpEntity, c, params).getBody();
}
}
/**
* 发起携带XML内容 的 POST请求
*
* @param url 请求链接
* @param xmlData 序列化字符串
* @return
*/
public String sendPostXmlRequest(String url, String xmlData) {
RestTemplate restTemplate = restTemplateConfig.restTemplate();
HttpHeaders headers = new HttpHeaders();
String token = getTokenInfo();
// 设置BearerAuth,即Token
headers.setBearerAuth(token);
// 设置ContentType
headers.setContentType(MediaType.APPLICATION_XML);
HttpEntity<String> entity = new HttpEntity<>(xmlData, headers);
String result = restTemplate.postForObject(msUrl + url, entity, String.class);
return result;
}
}
9.2 使用Redis存储微软认证模块中的Token认证信息,以提供便捷的访问,以便在向CIS系统发起请求时使用
01.认证模块
1.在下单前认证下单者身份,应用微软AAD身份认证机制
需有在有效期内的X.509公钥证书(在生产环境测试前1个月给微软)
2.每30分钟call一次AAD身份认证令牌,如果call多次仍未收到,则每10分钟call一次,直到成功
刷新前自动查询令牌是否已失效
3.如认证失败,则自动发起3次认证请求
设置每次请求的间隔期依次延长
4.如果上述3次认证请求均失败,则系统自动刷新DNS缓存,之后再次发送认证请求
02.代码
@Component
public class AuthenticationListener implements ServletContextListener {
private static String authenticationRedisKey = "AAD_Token_Authentication";
@Autowired
private AuthenticationThread authenticationThread;
@Autowired
private AADTokenUtil aadTokenUtil;
@Autowired
private RedisCache redisCache;
public void contextDestroyed(ServletContextEvent e) {
if (authenticationThread != null && authenticationThread.isInterrupted()) {
authenticationThread.interrupt();
}
}
public void contextInitialized(ServletContextEvent e) {
authenticationThread.start(); // servlet 上下文初始化时启动 socket
if (!redisCache.hasKey(authenticationRedisKey)) {
try {
aadTokenUtil.acquireToken();
} catch (Exception ex) {
ex.printStackTrace();
}
}
}
}
9.3 在下单模块中,通过count计数来追踪成功和失败的订单条数;如果采购失败,引发异常要求重新进行采购
01.创建一个计数器变量 count,用于追踪成功和失败的订单数量。
int count = 0;
02.在下单模块中,当成功下单时,增加成功计数
try {
// 进行下单操作,如果下单成功
// ...
// 增加成功计数
count++;
} catch (OrderException e) {
// 处理下单失败的情况
// ...
// 如果下单失败,引发异常
throw new PurchaseRetryException("采购失败,请重新进行采购");
}
03.编写一个循环来重新尝试采购,直到成功或达到最大重试次数
int maxRetries = 3; // 最大重试次数
for (int retry = 0; retry < maxRetries; retry++) {
try {
// 进行采购操作
// ...
// 如果采购成功,增加成功计数并退出循环
count++;
break;
} catch (PurchaseRetryException e) {
// 处理采购失败的情况
// ...
if (retry == maxRetries - 1) {
// 达到最大重试次数仍然失败,可以选择抛出异常或记录日志
// ...
}
}
}
9.4 在退货模块中,采用字符串拼接的方式将XML信息序列化,随后以POST请求的方式发送给CIS系统
01.代码
/**
* 向微软退货申请 发送xml形式内容
*/
public class TokenReturnRequest {
//客户交易ID
private String clientTransactionId;
//token code
private String code;
//固定值GUID,微软提供ChannelGuid
private String billToAccountId;
public String getClientTransactionId() {
return clientTransactionId;
}
public void setClientTransactionId(String clientTransactionId) {
this.clientTransactionId = clientTransactionId;
}
public String getCode() {
return code;
}
public void setCode(String code) {
this.code = code;
}
public String getBillToAccountId() {
return billToAccountId;
}
public void setBillToAccountId(String billToAccountId) {
this.billToAccountId = billToAccountId;
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append("<TokenReturnRequest xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\" xmlns=\"http://schemas.datacontract.org/2004/07/LOE.Common.Contracts.Fulfillment.V3\">");
sb.append("<RequestContext>");
sb.append("<ClientTransactionId>").append(getClientTransactionId()).append("</ClientTransactionId>");
sb.append("<PartnerAttributes xsi:nil=\"true\" />");
sb.append("</RequestContext>");
sb.append("<Token>");
sb.append("<Code>").append(getCode()).append("</Code>");
sb.append("</Token>");
sb.append("<Billing>");
sb.append("<BillToAccountId>").append(getBillToAccountId()).append("</BillToAccountId>");
sb.append("<PurchaseOrderId xsi:nil=\"true\" />");
sb.append("</Billing>");
sb.append("</TokenReturnRequest>");
return sb.toString();
}
}
9.5 在交付模块中,针对选中的激活码(code),生成Excel文件,并以字节数组流的形式将其附加到邮件中
01.代码
/**
* 交付
*/
@Override
@Transactional(rollbackFor = Exception.class)
public AjaxResult orderInfo(List<Long> tokenId, String operName) {
List<TokenForEmailVO> dataList = new ArrayList<>();
Date date = new Date();
List<Token> tokenByIds = tokenMapper.selectTokenByIds(tokenId);
Long orderId = null;
if (!CollectionUtils.isEmpty(tokenByIds)) {
for (Token token : tokenByIds) {
if (token.getStatus() != null && token.getStatus().equals(2)) {
try {
throw new Exception("有商品已进行过交付,请回到列表页并刷新!");
} catch (Exception e) {
e.printStackTrace();
}
}
if (orderId == null) {
orderId = token.getOrderId();
}
token.setDelMan(operName);
token.setDelTime(date);
token.setStatus(2);
token.setReturnFailRea(null);
TokenForEmailVO tokenForEmailVO = new TokenForEmailVO();
tokenForEmailVO.setToken(token.getTokenCode());
tokenForEmailVO.setTokenUrl(token.getTokenUrl());
dataList.add(tokenForEmailVO);
}
/**
* 批量更新
*/
tokenMapper.updateBatchToken(tokenByIds);
Token token = new Token();
token.setOrderId(orderId);
List<Token> tokenList = tokenMapper.selectTokenList(token);
Boolean isAll = true;
for (Token item : tokenList) {
if (item.getStatus().equals(1)) {
isAll = false;
}
}
PurchaseOrder purchaseOrder = purchaseOrderMapper.selectPurchaseOrderById(orderId);
purchaseOrder.setOrderStatus(isAll ? 4L : 3L); //部分、全部
purchaseOrder.setDelMan(operName);
purchaseOrder.setDelTime(date);
purchaseOrderMapper.updatePurchaseOrder(purchaseOrder);
// 获取资源文件存放路径,用于临时存放生成的excel文件
// String path = Objects.requireNonNull(this.getClass().getClassLoader().getResource("")).getPath();
// 文件名:采用UUID,防止多线程同时生成导致的文件重名
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
String format = simpleDateFormat.format(date);
String fileName = String.format("Office产品密钥-星链南天SO %s.xlsx", format);
try (ByteArrayOutputStream out = ExcelUtils.generateExcel(dataList, TokenForEmailVO.class)) {
// 生成excel文件
// 发送邮件
String subject = "订单交付//星链南天 SO //" + purchaseOrder.getCusName();
String content = "尊敬的客户:\n" +
"您好,附件是您采购的产品,敬请查收。\n" +
"星链南天 SO 单号:\n" +
"交付产品数量:" + tokenByIds.size() + "\n" +
"\n" +
"感谢您对微软产品和星链南天的支持与信任。\n" +
"如有问题,请随时与我们联系。";
emailUtil.sendEmail(subject, content, false, fileName, new ByteArrayResource(out.toByteArray()));
return AjaxResult.success("交付邮件发送成功!");
} catch (IOException e) {
log.error(String.format("生成excel失败,原因:%s", e));
e.printStackTrace();
} catch (MessagingException e) {
log.error(String.format("邮件发送失败,原因:%s", e));
e.printStackTrace();
}
}
return AjaxResult.error("邮件发送失败!");
}
9.6 利用easy-excel实现了数据报表的导入与导出功能
01.写Excel
@Test
public void simpleWrite() {
// 注意 simpleWrite在数据量不大的情况下可以使用(5000以内,具体也要看实际情况),数据量大参照 重复多次写入
// 写法1 JDK8+
// since: 3.0.0-beta1
String fileName = TestFileUtil.getPath() + "simpleWrite" + System.currentTimeMillis() + ".xlsx";
// 这里 需要指定写用哪个class去写,然后写到第一个sheet,名字为模板 然后文件流会自动关闭
// 如果这里想使用03 则 传入excelType参数即可
EasyExcel.write(fileName, DemoData.class)
.sheet("模板")
.doWrite(() -> {
// 分页查询数据
return data();
});
}
02.读Excel
@Test
public void simpleRead() {
// 写法1:JDK8+ ,不用额外写一个DemoDataListener
// since: 3.0.0-beta1
String fileName = TestFileUtil.getPath() + "demo" + File.separator + "demo.xlsx";
// 这里默认每次会读取100条数据 然后返回过来 直接调用使用数据就行
// 具体需要返回多少行可以在`PageReadListener`的构造函数设置
EasyExcel.read(fileName, DemoData.class, new PageReadListener<DemoData>(dataList -> {
for (DemoData demoData : dataList) {
log.info("读取到一条数据{}", JSON.toJSONString(demoData));
}
})).sheet().doRead();
}