Lollipop's Blog

日常随笔和学习笔记

lollipop 这个昵称,或者说是网名,我是从2016年开始用起的。源头是一篇高中英语完型填空的文章。这篇文章的内容我附在最后,作者从儿时起就好奇舔多少下能吃到 Tootsie Pop 中心为引子,讲高中时将竞选模拟联合国主席类比此事,看到他人一两次成功,自己屡战屡败,三次才能上岸,直至 11 年后访问官网才明白,能到 “中心” 取决于自己选择舔多少下,而非模仿他人。

虽然是一篇考试用的文章,但是它依然很中我意。你知道的,那时候的一群中二少年,会对英文的网名有特别的喜欢,那是一种潮流。就像再往前,大家喜欢用特别复杂的字写一句很“花泽类”的句子。但是,我对这个单词的喜欢也有一些特别的理由。一方面,我在我的高中那帮同学里确实不算聪明,大家都没把我当作聪明人来看(当然我自己也不会)。但是我觉得我有我的特别,我觉得我也不用循规蹈矩,和他们一样走一条循规蹈矩却又泥泞的路,我想说的是高考的普招。另一方面,棒棒糖又足够贴合我的性格,喜欢傻乐又喜欢逗别人开心,像糖一样甜;同时又有一些坚韧,一些果断,一些乐观。

我应该是一个坚定的唯物主义者,但是世上确实有太多解释不了的巧合。17年高考,我确实和大家走了一条不一样的道。我的天赋点全点在了那两门不算分的小科目。2017年是江苏省综评录取的第一年,综评的规则将不算分的物理和化学,以很大比例算进了折算成绩。我成了第一批吃螃蟹的人,而且是极大的获利者,比普招分数线低了17分进了东大。而又后来,18年不知为何,坚定地从东大退学,之后是高考又高考。总共三次高考的经历,又是切合了当时文章里“3”这个数字。

从这段时期来看,我确实如同棒棒糖一样,是有着一层硬壳的。从截止当下的人生的这段最大低谷中走了出来,如今回过头来,依然可以笑着面对这段经历。和这些比我小的同学们一块备考时,我还是经常拿自己开涮,尤其是考的差的时候,嘴里嚷嚷着我要考清华之类的疯癫语言,有种范进中举后的既视感(虽然还没中举),尽管是言不由衷。

其实,世上的绝大多数人都一样,有过夜不能寐,躲在被窝里悄悄呜咽的经历。然而却要在人前,装的毫不在乎,极其坚强的样子。尤其对于男孩儿/男人,尤其是对我而言,有的时候坚强和独立,并不是自己本来的样子。是社会的凝视,或者说,是别人的眼光让“我”觉得“我”应当如此。
“硬壳”有的时候只是“流心”的伪装,至少我觉得我是这样。心情需要是“泄洪口”的,但找不到好倾诉的对象是常有的事情。不想让父母担心;不想让“哥们”觉得自己软弱,或者称为他们的谈资……不想炸毁的堤坝,却是洪水泛滥的推手。

突然想写这些内容,是因为这篇文章,突然又进入了我的视线。也是出于我对过去这几年,来到南大的每一个脚步的总结和对未来展望。

在南大的日子,其实没有做什么决策。至今为止,每一个步子,都没有过自己的选择。是时间在我背后推着我往前走的,每一步都是理所当然,而且是唯一的解。当”考虑未来“这个话题突然摆到我的面前时,我是有些懵逼的。去哪里工作?做什么工作?犹豫,纠结和停滞不前成了最近生活的主旋律。有时候,回过头去,回看自己走到现在的路,会考虑,在某一个时间点,果断地做出一些行动,自己手上的资源会不会更多一些,留给自己选择的机会会不会更多一些。

棒棒糖在慢慢融化,我好像不那么像过去的自己,不能像lollipop一样,虽然胆怯,但是却能勇敢冲入大雨之中。

How many licks does it take to get the center of a Tootsie Pop?

The first time I heard this in the Tootsie Pop commercial, I was five years old. I immediately started licking and counting. After about two hundred licks or so, I stopped. The temptation of the chewy center had proven to be too great, and I bit my way through the hard shell to the very center. Besides, I already knew how many licks it took to get to the center–three. That’s how many licks it took the owl in the commercial to the center, so that, to me, was the right answer.

In high school. I held to the Tootsie Pop philosophy. To me, the answer was still always three licks.

In my freshman year. I joined the Model United Nations program in my school. The Chair position had become the center of the Tootsie Pop and my owls had become various other students. The first so-called “owl” was Eric who had luckily landed the prestigious Chair Position. So, I decided, if Eric reached the center in only one lick, that’s how many licks it should take me. I went to the tryouts with a view to obtaining the position but failed.

At the end of my sophomore year, a new owl named Iris had also achieved the chair position after trying twice. I began working hard again. But then again, I did not make success of it.

Now, slightly frustrated after going through two owls, I found a new ow, Evan. It had taken him three licks to get to the “center”. Three was all I could afford. It was widely known that senior year was the last year to become Chair I thought about quitting the program, but on second thoughts, I decided to continue.

Eleven years later, I visited the official Tootsie Pop website to find the real answer to the question that had troubled me my entire high school life. I finally understood. However many licks it takes to get to the center of the Tootsie Pop depends on however many licks I choose to take – not how many the other owls take.

小米

2023年6月至10月在小米实习。产品线是游戏媒体后端,主要负责的是游戏媒体后端的社区业务。

移库问题

讲讲细节?

当时的移库任务并不复杂。因为业务需求,需要原来放在两个表里面的配置项合并。因为数据库被包装好了,不太方便直接向数据库输入sql,通过 SpringBoot 应用,相当于实现一个脚本。这个工作并不复杂。

数据库里面存储的是一些基于规则引擎的配置文件,或许是json文件?业务合并后,也需要将业务的规则引擎配置文件合并。

如何处理大型表的数据库移库需求?

首先要明确迁移的数据库的状态和迁移时的可用性要求,是同构的还是异构的?数据量的大小?数据库在迁移的时候必须保障原有服务吗?允许服务降级吗?数据库的QPS是怎样的?

迁移方案有两个思路:一种是“停机迁移”,简单粗暴但风险高,需要和业务方反复确认停机时间窗口是否可接受;另一种是“在线迁移”,通过全量+增量的方式逐步同步数据,最后只切少量停机时间。比如先用工具全量复制历史数据,再通过监听Binlog实时同步增量变更,最终在业务低峰期停写旧库、追平增量后切换流量。但这里有个陷阱:如果业务有长事务或者大表,增量追平可能会耗费远超预期的时间。

还要注意Null值问题。

在迁移时,需要注意性能优化,迁移时禁用外键和索引可以提速,但完成后必须记得重建,否则查询性能可能断崖式下跌,要尽量保障数据库迁移时间的贷款和物理机性能,尽量减少迁移需要的时间,尽量选择QPS不繁忙的时间进行数据迁移。

迁移后的验证和切换。应当通过抽样或者其他方式校验新库和老库中的数据一致性,比如md5校验值或者抽样检测。应当灰度地把流量切换到新库,观察慢查询和错误日志,再逐步放开写操作。保留好回滚的手段。

工具的选择往往事半功倍。比如同构迁移可以用数据库自带的工具链(如MySQL的mysqldump+Binlog),而跨云或异构迁移(比如Oracle到PostgreSQL)可能需要借助Alibaba Cloud DTS这类服务。没有银弹,所以迁移过程不能掉以轻心,应当时刻监控数据,结合人工检查。多方校验。

什么是Null值问题

Null值导致迁移失败的直接原因

问题场景 说明 示例
目标表字段不允许Null 目标数据库的字段定义为NOT NULL,但源数据中存在Null值,导致插入失败。 迁移时抛出错误:ERROR: null value in column "user_id" violates not-null constraint
数据类型不兼容 源数据库允许Null的字段类型在目标数据库中可能不支持。 Oracle的VARCHAR2允许Null,但迁移到某些NoSQL数据库时可能无法隐式处理。
默认值冲突 目标表字段定义了默认值,但Null值可能覆盖默认逻辑,导致数据不一致。 源数据中status为Null,目标表定义status INT DEFAULT 1,迁移后status变为Null而非1。

Null值引发的逻辑隐式问题

问题场景 说明
索引失效 目标数据库中Null值可能导致索引不包含这些记录(如唯一索引允许多个Null)。
聚合函数偏差 SUM()AVG()等函数忽略Null值,可能导致迁移后统计结果与源库不一致。
应用逻辑异常 应用程序可能未处理目标库中Null值的特殊含义(如空字符串与Null的混淆)。
外键约束失效 外键字段为Null时,可能绕过约束检查,破坏数据完整性。

跨数据库系统的Null值差异

不同数据库对Null值的处理规则可能不同,迁移时需特别注意:

数据库 Null值特性
Oracle 空字符串''被存储为Null。
PostgreSQL 空字符串''与Null是两种不同的值。
MySQL 严格模式下禁止插入Null到NOT NULL字段,非严格模式会尝试转换(如Null转默认值)。
SQL Server 空字符串与Null分离,但比较时NULL = NULL返回False

什么是规则引擎

规则引擎(Rule Engine)是一种将业务规则与应用程序代码解耦的技术,通过预定义的逻辑模型(如条件-动作规则)动态执行决策流程。它允许非技术人员(如业务人员)直接管理规则,而无需修改底层代码,适用于需要频繁调整业务策略的场景(如风控、营销活动、流程审批等)。

组成:

  1. 规则定义:用特定语法或可视化界面描述条件(Condition)和动作(Action)。
  2. 规则存储:持久化规则(如数据库、文件、配置中心)。
  3. 规则编译:将规则转换为可执行的内部结构(如决策树、Rete网络)。
  4. 推理执行:根据输入数据匹配规则并触发动作。

规则引擎算法

  1. Rete算法
  2. PHREAK 算法
  3. Leaps
  • 先收集所有可能的规则匹配,再按优先级批量触发动作。
  • 将规则分解为可并行处理的子网络,提升多核利用率。
  • 根据规则条件概率排序事实,优先匹配高概率命中条件。
  • 将无依赖的规则分组并行执行(如按业务模块分组)。

Viewpoint 社区项目缓存不一致问题

原始方案核心缺陷分析

  1. 数据持久化不可靠
    • Write-Behind模式依赖缓存层完成数据持久化,在缓存宕机时未持久化的内存数据会完全丢失
    • 缺乏持久化队列保护,无法应对节点故障、网络分区等异常场景
    • 前端调用点赞接口不校验或者校验错误用户是否已经点赞导致重复点赞
  2. 缓存-数据库同步机制缺陷
    • 采用简单TTL过期策略,在缓存重建时可能覆盖其他未同步的修改(写覆盖问题)
    • 缺乏版本控制机制,无法处理并发场景下的操作时序问题
  3. 异常处理机制缺失
    • 没有定义明确的故障恢复流程(如缓存击穿后的数据重建策略)
    • 缺乏异步操作的状态监控与补偿机制
    • 未考虑分布式场景下的脑裂问题应对方案

优化方案设计原则

  1. 可靠性优先

    • 所有写操作必须至少持久化到磁盘日志(WAL)后才能响应成功
    • 采用异步批处理代替实时双写,降低数据库压力
  2. 最终一致性模型

    • 允许前台展示延迟数据(如点赞数±100内随机波动)
    • 关键操作(如实际点赞关系)保持强一致性
  3. 分层容错设计

    mermaid

    复制

    1
    2
    3
    4
    5
    6
    7
    8
    graph TD
    A[客户端] --> B[本地内存缓存]
    B --> C{缓存命中?}
    C -->|Yes| D[返回缓存值]
    C -->|No| E[Redis集群]
    E --> F{Redis可用?}
    F -->|Yes| G[返回并更新本地缓存]
    F -->|No| H[降级读数据库+本地限流]

关键改进措施

  1. 持久化层重构

    • 引入消息队列作为持久化日志层,写操作分布式事务,只需要更新了缓存之后就返回,更新数据库的操作在后台运行
    • 设计幂等消费者,确保至少一次投递语义
  2. 缓存更新策略优化

    • 采用多级TTL策略:

      • 基础TTL(5分钟):常规过期时间
      • 动态TTL(1-30分钟):根据写频率自动调整
      • 强制TTL(1小时):绝对最长存活时间
    • 实现延迟双删模式:

      复制

      1
      2
      3
      4
      更新流程:
      1. 删除缓存
      2. 更新数据库
      3. 延时500ms再次删除缓存
  3. 异常处理增强

    • 设计熔断降级策略:
      • 当Redis错误率>30%时,自动切换为直连数据库模式
      • 对非核心操作(如点赞数展示)实施随机降级(30%请求直接返回默认值)
    • 建立补偿任务:
      • 每小时执行缓存与数据库的差异校验
      • 对偏差超过阈值的热点数据触发主动预热

如何记录用户和帖子之间的点赞关系

  1. 索引策略

    • 主索引:创建(user_id, post_id)联合唯一索引(防止重复点赞)
    • 覆盖索引:对查询频率高的场景建立(post_id, user_id)倒排索引
  2. 分库分表

    • 对帖子进行hash 分库存储
  3. Redis 使用 Set 存储热点帖子的点赞信息

方案对比分析

维度 原始方案 优化方案
数据安全性 内存数据易丢失 通过WAL日志保障持久化
写吞吐量 最高5k QPS 可达50k QPS
一致性延迟 分钟级不一致 秒级最终一致性
故障恢复时间 依赖人工介入(小时级) 自动恢复(分钟级)
复杂度 中高(需维护消息队列)

适用场景建议

  1. 推荐使用优化方案场景
    • 社交媒体点赞/阅读数统计
    • 商品库存的准实时展示
    • 新闻资讯类PV/UV统计
  2. 不建议使用场景
    • 金融账户余额变更
    • 医疗设备实时监控
    • 需要ACID事务保证的核心业务

该方案在保证最终一致性的前提下,通过架构分层和异步化设计实现了性能与可靠性的平衡,适用于对数据实时性要求不苛刻但需要高吞吐的场景。对于关键业务数据仍需采用同步写+分布式事务的强一致性方案。

Write Behind Caching 最适合以下场景

✅ 高并发写入且允许最终一致性

✅ 数据库写入性能不足需削峰填谷

✅ 非关键数据需快速响应(如统计类、日志类)

帖子推荐

需求背景

为提升平台内容分发效率,需实现一套基于运营策略的帖子推荐系统。系统需提供帖子预览列表页帖子详情阅读页两个核心接口,支持未来灵活扩展推荐算法(如大模型、大数据分析等)。当前首期采用人工运营配置的固定推荐策略,需保障架构的可扩展性,同时优化接口性能与数据加载效率。


核心设计方案

  1. 推荐策略模块(策略模式落地)
    • 抽象统一的推荐策略接口 RecommendationStrategy,定义排序算法、过滤规则等方法。
    • 一期实现:通过运营配置的固定优先级规则(如置顶帖、人工加权、时间衰减)生成推荐列表。
    • 扩展预留:后续可通过新增策略实现类(如 AIModelStrategyUserBehaviorStrategy)无缝接入大模型或用户行为分析算法,无需修改主流程代码。
  2. 接口与数据聚合设计
    • 帖子预览接口:返回推荐列表的概要信息(标题、作者、缩略图、点赞数等),按策略排序。
    • 帖子详情接口:提供帖子完整内容,并一次性返回关联的前10条热门评论(按点赞数排序),减少客户端多次请求。
    • 数据打包优化:通过DTO(Data Transfer Object)聚合帖子基础信息、用户基础信息、评论数据,采用嵌套结构避免前端拼接逻辑。
  3. 缓存与性能优化
    • 热点数据缓存:对运营配置的固定推荐位帖子(如置顶帖、轮播帖),采用Redis缓存并设置较长过期时间(如12小时),通过定时任务预热更新。
    • 评论缓存:为每个帖子缓存前10条热门评论,采用旁路缓存策略(Cache-Aside),优先读取缓存,失效时异步回源数据库。
    • 降级兜底:缓存失效或数据库压力过大时,自动降级返回基础运营配置列表,保障服务可用性。

技术亮点

  • 扩展性保障:策略模式隔离算法与业务逻辑,未来新增推荐策略仅需实现接口,符合开闭原则。
  • 性能友好:接口数据聚合减少HTTP请求次数,缓存设计降低数据库QPS压力(预估减少70%以上读请求)。
  • 业务解耦:运营配置规则(如权重、生效时间)独立存储于配置中心,支持动态调整无需发版。

补充说明

  • 数据一致性:采用双删策略(更新DB后先删缓存再延迟二次删除),避免极端场景下的缓存脏数据。
  • 监控报警:对缓存命中率、接口响应时间、推荐策略执行耗时等指标埋点监控,异常时触发告警。

通过上述设计,系统在满足当前运营需求的同时,为未来技术演进预留充足空间,兼顾性能、可维护性与架构弹性。

相关帖子

标签体系构建(运营侧)

  1. 建立标签知识库
  • 使用图数据库(Neo4j)构建多级标签体系
  • 包含标签属性:名称、权重、层级关系、同义词库
  • 提供可视化运营后台管理界面

基于TF-IDF 实现标签分析

计算标签

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
def extract_tags(text):
# 预处理
cleaned_text = preprocess(text)

# TF-IDF关键词提取
tfidf_keywords = tfidf_vectorizer.transform([cleaned_text])

# BERT语义嵌入
bert_embedding = bert_model.encode([cleaned_text])

# 标签匹配
candidate_tags = hybrid_match(tfidf_keywords, bert_embedding)

# 置信度过滤
final_tags = [tag for tag in candidate_tags if tag.confidence > threshold]

return final_tags

计算相似度

def calculate_similarity(post_a, post_b):
tags_a = get_post_tags(post_a)
tags_b = get_post_tags(post_b)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
def calculate_similarity(post_a, post_b):
tags_a = get_post_tags(post_a)
tags_b = get_post_tags(post_b)

# 计算基础相似度(Jaccard)
base_sim = len(set(tags_a) & set(tags_b)) / len(set(tags_a) | set(tags_b))

# 层级权重调整
hierarchy_sim = adjust_by_hierarchy(tags_a, tags_b)

# 语义相似度(使用标签向量)
semantic_sim = cosine_similarity(tag_vectors[tags_a], tag_vectors[tags_b])

return 0.4*base_sim + 0.3*hierarchy_sim + 0.3*semantic_sim

存储设计

数据库存储:

1
2
3
4
5
6
7
8
9
10
11
12
13
CREATE TABLE post_tags (
post_id BIGINT,
tag_id INT,
confidence FLOAT,
PRIMARY KEY (post_id, tag_id)
);

CREATE TABLE tag_relations (
tag_id INT,
related_tag_id INT,
relevance_score FLOAT,
PRIMARY KEY (tag_id, related_tag_id)
);

倒排索引

1
2
3
4
5
6
7
8
9
10
11
{
"index": "posts",
"body": {
"mappings": {
"properties": {
"tags": { "type": "nested" },
"vector": { "type": "dense_vector" }
}
}
}
}

收获

主要的成长点在于:

  1. 这个是我第一次完整地接触工业实践,包括 RPC 在学校里比较少用到的技术,设计模式在实际实践里的真实应用;
  2. 技术规范的过程,主要是设计文档和接口文档的编写,学校里的项目产生的团队合作都比较小规模,对文档的要求没那么高。这次实践让我真实认识到文档的重要性,把老师课堂上讲的内容映射到实践中来。
  3. 完整的流程,从需求分析,到设计,开发,测试和上线的软件工程全过程实践。

校园实践项目

secareer@nju

数据库设计

核心在于四张表:

  1. 用户表,用于记录各类型的用户信息,包括导师,学生,辅导员和实验室管理员
  2. 学生导师关系表,用于记录学生的实际导师和挂名导师,单独一张表方便导出导入。主键索引是student_id,添加联合索引(student_id, mentor_id)
  3. 申请记录:用于记录每一位学生的实习申请记录。主键索引application_id,辅助索引 student_id
  4. 操作记录:用于记录在某个时间节点,某个人员对某个记录做了何种操作。添加辅助索引application_id

操作记录表和申请记录表记录了主要的业务逻辑。通过操作记录表记录人员的操作信息,结合系统日志作为后续责任划分的依据。后续考虑增加机器码等信息。

使用事务保障对两张表的修改的原子性。

minio

MinIO作为一款基于Golang 编程语言开发的一款高性能的分布式式存储方案的开源项目,有十分完善的官方文档。

支持多种客户端,兼容AWS 的API

因为无法使用云服务商的对象存储服务而选择。

docker

Docker的优势应该包括环境一致性、快速部署、资源高效和易于扩展。这些都是用户可能关心的点,特别是对于开发、测试和部署流程中的问题。

Docker 通过容器化技术,将应用与环境“打包”,解决了开发到部署的协作难题。它是现代 DevOps、云原生和微服务的基础设施核心,几乎成为软件开发的标配工具

nginx

作为网关和前端服务器。

ci-cd

传统开发中,多人协作时频繁合并代码到主分支会导致大量冲突和兼容性问题。CI(持续集成)通过高频次代码提交与自动化构建/测试,确保每次变更都能快速发现冲突和错误,避免长期分支带来的集成难题。传统瀑布模型下,版本发布周期长(如每月/季度),无法快速响应需求。CD(持续交付/部署)通过自动化将代码快速推向生产环境,实现按需发布,缩短迭代周期。开发、测试、生产环境差异常导致“在我机器上能运行”的问题。CI/CD 结合容器化(如 Docker)和基础设施即代码(IaaC),确保环境一致性。

Tai-e

tai-e 是静态程序分析课程的课程实验。

静态分析是一种在不实际运行程序的情况下,通过分析源代码或中间表示(IR)来推断程序行为的技术。其主要目标包括:

  • 代码优化:如删除冗余代码、内联函数等。
  • 缺陷检测:如空指针解引用、内存泄漏等。
  • 安全性验证:如信息流分析、权限控制等73。

静态分析通常基于抽象解释(Abstract Interpretation)理论,通过近似(Over-approximation 或 Under-approximation)在精度与效率之间权衡。

主要是实现一个静态程序分析的工具,用于进行常量分析,活跃变量分析,上下文敏感和不敏感的指针分析等。

指针分析是静态分析中的关键技术,用于确定程序中的指针(变量或字段)可能指向哪些对象。它在编译优化、漏洞检测和调用图构建中起基础作用。

比如说,一个代码里面,经过非运行时的推断,无论经过怎样的运行路径,一个变量的值都是一个常量,那么我们就可以认为这个变量是一个常量。那我可以在编译期间就进行优化。基于常量分析,我们可以做到死代码分析,知道哪些分支的判断条件是不变的,哪些代码是永远不会执行的。

指针分析相对来说比较复杂,因为指针的引用关系是动态的,我们需要在静态分析的时候,模拟出动态的引用关系。如对于非上下文敏感的指针分析中,将同一个指针指向的对象抽象化为一个对象;而在上下文敏感的指针分析中则是会研究在不同情况下,对该指针指向的对象进行细粒度的分析,比如从哪一行开始创建的。基于指针分析,可以实现逃逸分析,分析一个变量的引用是否会逃逸到函数外部。从而可以进行栈上分配,减少堆上内存的分配。获得优化。

这门课程和我们的研究方向有很大的类似性。我们对分布式协议的验证,并不能在运行时验证,因为分布式系统的状态空间太大,有些状态转移的路径,访问的概率很低。我们需要在静态分析的时候,通过工具,例如tla+,将分布式协议的各个状态抽象出来,然后对每个可达的然后进行验证,确保总体状态符合安全属性。

该内容自用为主

[TOC]

计算机网络

多层网络模型

四层网络模型是TCP/IP定义的:网络接口层,网络层,传输层,应用层。

七层网络模型是OSI定义的:物理层,数据链路层, 网络层,传输层,会话层,表现层,应用层。

网络接口层的传输单位是帧(frame),IP 层的传输单位是包(packet),TCP 层的传输单位是段(segment),HTTP 的传输单位则是消息或报文(message)。但这些名词并没有什么本质的区分,可以统称为数据包。

越低的层次,包的size越小。

封装.png (905×501)

应用层向传输层传播message 时,需要告知传输层的目标IP

TCP和UDP

TCP 是面向连接的,因而双方要维护连接的状态,这些带状态位的包的发送,会引起双方的状态变更。

TCP和 UDP 都工作于传输层。

TCP和UDP可以使用同一个端口

TCP Transmission Control Protocol

面向连接,字节流,可靠

定位 TCP连接:源/目标 socket(IP: Port)

TCP 优先分片,尽量不要让 IP层分片

TCP 报文格式

tcp报文格式

重传机制

序列号和确认应答。

  1. 超时重传:一段时间(RTT,环回时间)后还没收到确认号就重传
  2. 快速重传:三次重复的ACK,代表新的消息没有被接收,就要重传
  3. SACK
  4. D-SACK

滑动窗口确认

ACK(N) 代表接受到了所有 SEQ< N 的报文

拥塞控制

在网络繁忙的时候,控制数据包的数量,尽量避免大量数据丢失。

  1. 慢启动:一点点加快数据包的数量
  2. 拥塞避免
  3. 拥塞发送
  4. 快速恢复

流量控制

控制发送端向接收端发送数据的速度,保障接收端的处理能力

滑动窗口

三次握手和四次挥手

TCP三次握手.drawio.png (1221×1019) 四次挥手

第三次握手可以携带数据

为什么是三次握手

同步两个方向的初始化序号,防止重复历史连接。

为什么是四次挥手

确保一来一回两个方向上的连接关闭。第二次和第三次不能简单合并的原因是,客户端关闭连接后,服务端可能还有数据包没发送给客户端。

第四次挥手后等待2MSL

确保最后一个ACK报文能被服务端收到,2MSL如果服务端没收到的情况下,客户端还可以收到 FIN,再进行一次重传。

SYN攻击

伪造IP,占满服务器的半连接队列。

SEQ ACK字段

  • 序列号是随机初始化的(通过初始序列号,ISN),后续的序列号按字节流顺序递增。例如,如果第一个报文段的序列号是 1000,且数据长度为 100 字节,那么下一个报文段的序列号将是 1100

  • 确认号表示接收方期望接收的下一个字节的序列号。例如,如果接收方发送的确认号是 1100,则表示接收方已经成功接收了序列号 10001099 的数据,期望接收序列号为 1100 的数据。

UDP User Datagram Protocol

无连接

udp

HTTP 和 HTTPS

HTTP的改进过程

  • HTTP/1.0:基本功能,每次请求需新建连接。
  • HTTP/1.1:持久连接、管道化、Host头字段压缩、缓存控制,断点重传。
  • HTTP/2:二进制协议、多路复用、头部压缩、服务器推送。
  • HTTP/3:基于QUIC、内置加密、改进的多路复用、连接迁移。

GET 和 POST 方法都是安全和幂等的吗?

GET 不修改服务器上的资源,所以是幂等的。POST需要求改服务器资源,所以不是。

GET 请求可以记录为标签,可以被缓存。

HTTPS 链接建立方式

  • ClientHello:客户端向服务器发送ClientHello消息,包含支持的TLS版本、加密套件列表和随机数1。
  • ServerHello:服务器回应ServerHello消息,选择TLS版本、加密套件,并发送服务器随机数2。
  • 服务器证书:服务器发送其数字证书,包含公钥和证书颁发机构(CA)信息。
  • 密钥交换:客户端验证服务器证书,生成预主密钥3(Pre-Master Secret),用服务器公钥加密后发送给服务器。非对称加密
  • 会话密钥生成:客户端和服务器使用预主密钥3和随机数1,2 生成会话密钥(Session Keys),用于后续通信的加密和解密。之后就都是对称加密了。
  • 完成消息:双方发送Finished消息,验证握手过程是否成功。
HTTPS connect

HTTPS的安全风险在于?

CA的泄露,信任了不该信任的证书。

HTTP 粘包问题

接收端不知道报文的边界在哪里

解决方案:

  1. 通过给出特殊字符标记边界(回车或者换行符)

  2. 自定义消息结构,在包头中定义长度

RPC remote procedure call

比 HTTP/1.1 更加精简,像调用本地方法一样调用远程方法。

但是约束和自定义的内容更多。

websocket

从 http 升级,用于网页游戏等场景

OS 操作系统

操作系统的功能:向下管理各种硬件资源,向上为软件提供统一的资源访问接口和服务

OS 软件服务

I/O设计三者的核心区别

维度 同步阻塞 I/O 同步非阻塞 I/O I/O 多路复用 异步I/O
阻塞行为 全程阻塞直到数据就绪并拷贝完成 仅数据拷贝阶段可能短暂阻塞 仅阻塞在 select/epoll 等待就绪事件 无阻塞
数据就绪检查 无检查,被动等待完成 需应用主动轮询(循环重试) 内核通知就绪的 fd(无需轮询) 通知
资源消耗 高(每个连接一个线程) 低(单线程处理,但 CPU 轮询开销大) 低(单线程高效管理多连接) 内核异步将数据从内核空间拷贝到应用程序空间,内核自动完成的。
适用场景 低并发、简单任务 低并发但需快速响应其他任务 高并发、短耗时操作
Netty

核心过程:

  1. 内核准备数据
  2. 数据从内核态拷贝到用户态

调用内核态功能的方式

  1. 系统调用:直接访问内核功能。
  2. 库函数:封装系统调用,提供高级接口。
  3. 设备文件:通过文件操作访问硬件。
  4. 信号:处理异步事件。
  5. 共享内存和消息队列:实现进程间通信。
  6. 文件映射:高效访问文件内容。
  7. 内核模块:通过 ioctl() 实现自定义功能。

select poll epoll

select 数组实现

select 是最早的 I/O 多路复用机制,其实现相对简单,但效率较低。

数据结构

select 使用三个位掩码(fd_set)来表示需要监控的文件描述符集合:

  • readfds:监控可读事件。
  • writefds:监控可写事件。
  • exceptfds:监控异常事件。

每个 fd_set 是一个固定大小的位数组(通常是 1024 位,由 FD_SETSIZE 定义),每一位对应一个文件描述符。

工作流程

  1. 用户空间到内核空间的拷贝
    • 用户调用 select 时,需要将三个 fd_set 从用户空间拷贝到内核空间。
  2. 内核轮询检查
    • 内核遍历所有被监控的文件描述符,检查它们的状态(是否可读、可写或异常)。
    • 内核的时间复杂度为 O(n),其中 n 是最大的文件描述符值。
  3. 内核空间到用户空间的拷贝
    • 内核将修改后的 fd_set 拷贝回用户空间,表示哪些文件描述符已就绪。
  4. 用户空间遍历
    • 用户需要遍历所有文件描述符,检查哪些位被置位,以确定哪些文件描述符已就绪。

poll 链表实现

poll 是对 select 的改进,解决了文件描述符数量限制的问题。

数据结构

  • poll 使用一个 pollfd 结构体数组来表示需要监控的文件描述符集合:

    c

    复制

    1
    2
    3
    4
    5
    struct pollfd {
    int fd; // 文件描述符
    short events; // 监控的事件(如 POLLIN、POLLOUT)
    short revents; // 返回的事件
    };
  • 每个 pollfd 结构体包含一个文件描述符和需要监控的事件。

工作流程

  1. 用户空间到内核空间的拷贝
    • 用户调用 poll 时,需要将 pollfd 数组从用户空间拷贝到内核空间。
  2. 内核轮询检查
    • 内核遍历 pollfd 数组,检查每个文件描述符的状态。
    • 内核的时间复杂度为 O(n),其中 n 是 pollfd 数组的长度。
  3. 内核空间到用户空间的拷贝
    • 内核将修改后的 pollfd 数组拷贝回用户空间,表示哪些文件描述符已就绪。
  4. 用户空间遍历
    • 用户需要遍历 pollfd 数组,检查 revents 字段,以确定哪些文件描述符已就绪。

epoll

epoll 是 Linux 2.6 引入的高效 I/O 多路复用机制,采用事件驱动模型,解决了 selectpoll 的性能问题。

数据结构

  • epoll 使用三个系统调用:
    • epoll_create:创建一个 epoll 实例,返回一个文件描述符。
    • epoll_ctl:向 epoll 实例中添加、修改或删除需要监控的文件描述符。
    • epoll_wait:等待事件发生,返回就绪的文件描述符。
  • epoll 使用红黑树和双向链表来管理文件描述符和事件:
    • 红黑树:用于高效地存储和查找文件描述符。
    • 就绪链表:用于存储已就绪的文件描述符。

工作流程

  1. 初始化
    • 调用 epoll_create 创建一个 epoll 实例。
    • 调用 epoll_ctlepoll 实例中添加需要监控的文件描述符和事件。
  2. 事件注册
    • 内核将文件描述符和事件注册到红黑树中。
  3. 事件等待
    • 用户调用 epoll_wait,内核检查红黑树中的文件描述符,将就绪的文件描述符添加到就绪链表中。
    • 内核只返回就绪的文件描述符,时间复杂度为 O(1)。
  4. 事件通知
    • 用户从 epoll_wait 中获取就绪的文件描述符,无需遍历所有文件描述符。

触发模式

  • 水平触发(LT)
    • 只要文件描述符处于就绪状态,epoll_wait 就会一直通知。
    • 类似于 selectpoll 的行为。
  • 边缘触发(ET)
    • 只有当文件描述符状态发生变化时,epoll_wait 才会通知。
    • 需要用户一次性处理所有数据,否则可能会丢失事件。

零拷贝

存储结构

越往上的内存越快,但是越珍贵,所以体积越小。

虚拟内存

虚拟内存的空间寻址空间很大,超过了物理内存的空间。

CPU的 MMU管理虚拟内存地址空间和物理内存空间的映射关系

基于局部性原则设计。

  • 时间局部性(Temporal Locality):如果一个数据被访问过,那么它在不久的将来很可能再次被访问。
  • 空间局部性(Spatial Locality):如果一个数据被访问过,那么它附近的数据也可能会被访问。

虚拟内存可以隔离开各个进程的地址空间,提高安全性和稳定性。

虚拟内存会使用磁盘空间作为支持。

内存分段和分页

分段是按照进程需求分配内存空间,分页是按照进程需求向上取整分配内存空间。

内存页是固定大小,内存段时可变大小。

分段产生外部碎片,分页产生内部碎片

处理外部碎片的方式是内存交换。(JVM 内存管理)

段页式内存

虚拟地址由两部分组成:段号(Segment Number)段内偏移量(Offset)

段号用于查找段表,得到该段的页表基址。

段内偏移量被进一步划分为 页号(Page Number)页内偏移量(Page Offset)

页号用于查找页表,得到物理页框号。

物理地址 = 物理页框号 × 页大小 + 页内偏移量。

每个进程都有自己的页表

缺页中断

内存页不在物理内存种,需要发生缺页终端,将内存页从虚拟内存加载到物理内存中。

虚拟内存地址分为两个部分,虚拟页号 + 页内偏移量。

虚拟页号通过 MMU 的页表转换为物理内存地址,得到物理内存地址。

内存替换策略有:

  • FIFO:简单但性能较差。
  • LRU:性能较好但实现复杂。
  • OPT:理论最优但无法实现。
  • Clock:性能和实现复杂度适中。
  • NRU:简单且性能较好。
  • LFU:适合访问频率差异大的场景。
  • 工作集模型:适合长时间运行的进程

双队列实现的LRU算法

处理预读失效

G1 / INooDB / Linux kernal

主队列(Main Queue)

  • 存储当前在内存中的页面。
  • 使用 FIFO(先进先出) 的顺序管理页面。

辅助队列(Auxiliary Queue)

  • 存储最近被访问的页面。
  • 使用 LRU(最近最少使用) 的顺序管理页面。

页面访问规则

  • 当页面被访问时,如果它在主队列中,则将其移动到辅助队列的头部。
  • 如果页面在辅助队列中,则将其移动到辅助队列的头部。
  • 如果页面不在任何队列中,则将其加入主队列。

实现上只需要一个链表

多级页表 页表缓存

page-table

越高级别的页表区别越细粒度

多级页表效率太低,TLB(快表,页表缓存,Translation Lookaside Buffer) 提高效率

用户空间和内核空间

kernal-user-space

缓存一致性

todo

如何处理预读失效和缓存污染

预读失效

使用两队列的LRU算法:

inactive list/active list : 两个 FIFO 队列

空间局部性可以让要读的页周围的页加载到inactive list ,只有真正被使用的被使用的页才会被拉到 active list 的头部。

缓存污染

提高进入 active list 的难度

例如 Linux kernal 只有第二次使用的页才能加入 active list。 INooDB 只有超过间隔1s的两次访问才会把表拿到 active list 中

既可以通过两个链表,也可以通过一个链表实现:

two-queue-lru

进程管理和调度

并发和并行

并发在一个CPU上交替使用时间片

并行在多个CPU上同步执行

进程的状态

PCB 进程控制块 Process Control Block

  • 进程标识符(PID):唯一标识一个进程。
  • 进程状态:如运行、就绪、阻塞等。
  • 程序计数器(PC):指向下一条要执行的指令。
  • CPU 寄存器:保存进程的寄存器状态。
  • 内存管理信息:如页表基址、内存分配情况等。
  • I/O 状态信息:如打开的文件、使用的设备等。
  • 调度信息:如优先级、调度队列等。
  • 记账信息:如 CPU 使用时间、内存使用量等。
  • ……

进程上下文

  • CPU 寄存器:如通用寄存器、程序计数器、栈指针等。
  • 程序计数器(PC):指向下一条要执行的指令。
  • 内存状态:如页表基址、内存映射等。
  • 堆栈:保存函数调用、局部变量等信息。
  • 其他状态:如浮点寄存器、控制寄存器等
  • ……

线程

线程是进程拥有的,进程的一条执行路径

同一个进程内多个线程之间可以共享代码段、数据段、打开的文件等资源,但每个线程各自都有一套独立的寄存器和栈,这样可以确保线程的控制流是相对独立的。

线程间共享进程的地址空间和文件资源。

线程是CPU调度的最小单位。进程是资源分配的最小单位。

线程分类

用户线程

内核线程

轻量级进程

线程和进程的区别

特性 进程 线程
地址空间 独立 共享
内存开销
上下文切换 慢,开销大 快,开销小
通信 需要 IPC 机制,开销较大 共享内存,直接通信
创建销毁 开销大,较慢 开销小,较快
并发性
崩溃影响 一个进程崩溃不会影响其他进程 一个线程崩溃可能导致整个进程崩溃,JVM不会

调度策略

  1. 抢占式调度策略
  2. 非抢占式调度策略

进程调度的原因

  1. I/O处理时间,CPU利用率低,让出CPU时间片可以提高CPU利用率;
  2. 不让长任务一直占用CPU,不让短任务长时间等待;
  3. 交互式应用应当有较快的响应效率

调度算法

  1. 先来先服务(First-Come, First-Served, FCFS)
  2. 短作业优先(Shortest Job First, SJF)
  3. 优先级调度(Priority Scheduling)
  4. 时间片轮转(Round Robin, RR)
  5. 多级队列调度(Multilevel Queue Scheduling)
  6. ……

协程,线程,进程

协程(Coroutine)线程(Thread)进程(Process) 是计算机科学中用于实现并发执行的三种不同机制。它们在资源占用、调度方式、并发模型等方面有显著区别。以下是它们的详细对比:


1. 进程(Process)

  • 定义
    • 进程是操作系统资源分配的基本单位,是程序的一次执行实例。
    • 每个进程都有独立的内存空间、文件描述符、环境变量等。
  • 特点
    • 独立性:进程之间相互隔离,一个进程崩溃不会影响其他进程。
    • 资源开销大:创建和切换进程需要较大的系统开销(如内存、CPU)。
    • 通信复杂:进程间通信(IPC)需要通过管道、消息队列、共享内存等机制。
  • 适用场景
    • 需要高隔离性的任务。
    • 多任务操作系统中的任务管理。

2. 线程(Thread)

  • 定义
    • 线程是进程内的执行单元,是 CPU 调度的基本单位。
    • 一个进程可以包含多个线程,这些线程共享进程的内存空间和资源。
  • 特点
    • 轻量级:线程的创建和切换开销比进程小。
    • 共享资源:线程共享进程的内存空间,因此需要同步机制(如锁)来避免竞争条件。
    • 并发性:多线程可以实现真正的并行执行(在多核 CPU 上)。
  • 适用场景
    • 需要高并发处理的任务(如网络服务器、GUI 应用程序)。
    • 任务间需要共享数据的场景。

3. 协程(Coroutine)

  • 定义
    • 协程是一种用户态的轻量级线程,由程序员显式控制调度。
    • 协程在同一个线程内运行,通过协作式调度实现并发。
  • 特点
    • 用户态调度:协程的调度由程序控制,不依赖操作系统。
    • 低开销:协程的创建和切换开销极小,通常只需要保存和恢复少量寄存器。
    • 非抢占式:协程主动让出执行权,而不是被操作系统强制切换。
    • 单线程并发:协程在单线程内实现并发,适合 I/O 密集型任务。
  • 适用场景
    • 高并发的 I/O 密集型任务(如网络爬虫、异步编程)。
    • 需要高效处理大量任务的场景。

三者的对比

特性 进程(Process) 线程(Thread) 协程(Coroutine)
定义 操作系统资源分配的基本单位 进程内的执行单元 用户态的轻量级线程
内存空间 独立的内存空间 共享进程的内存空间 共享线程的内存空间
创建/切换开销
调度方式 操作系统调度 操作系统调度 用户程序调度
并发性 多进程并行 多线程并行 单线程内并发
通信机制 管道、消息队列、共享内存等 共享内存 直接共享变量
隔离性
适用场景 高隔离性任务 高并发任务 I/O 密集型任务

进程间通信

通信方式

  1. 管道
  2. 消息队列
  3. 共享内存
  4. 信号
  5. 信号量
  6. socket

竞争和协作

**竞争:**多线程间共享数据,需要定义临界区,只有一个线程进入临界区。

**同步:**互相等待和唤醒

实现方式

  1. 信号量
  2. PV

具体参见 JVM

中断

数据库

数据库不应当是黑盒的,开发人员必须深入了解你所使用的数据库的体系结构和特征。

范式

范式 描述
1NF 每一列都是原子性的,没有重复的组。
2NF 满足1NF,且非主键列完全依赖于主键。
3NF 满足2NF,且非主键列之间没有传递依赖。消除传递依赖
BCNF 满足3NF,且所有函数依赖的左部必须是超键。
4NF 表中不存在非平凡的多值依赖。

执行过程

mysql 执行过程

如果一个用户已经建立了连接,即使管理员中途修改了该用户的权限,也不会影响已经存在连接的权限。修改完成后,只有再新建的连接才会使用新的权限设置。

MySQL 8.0 版本直接将查询缓存删掉了,也就是说 MySQL 8.0 开始,执行一条 SQL 查询语句,不会再走到查询缓存这个阶段了。

主键索引的 B+ 树

索引

在建立表的时候,引擎就会建立聚簇索引。

  1. 如果没有显式指定主键
    • InnoDB会自动选择一个唯一且非空的列作为聚簇索引。
    • 如果没有这样的列,InnoDB会自动创建一个隐藏的自增列(通常是6字节的ROW_ID)作为聚簇索引。
  2. 如果指定了主键
    • 主键会自动成为聚簇索引
    • 主键必须是唯一的,且不能包含NULL值。

索引分类

  • 按「数据结构」分类:B+tree索引、Hash索引、Full-text索引
  • 按「物理存储」分类:聚簇索引(主键索引)二级索引(辅助索引)
  • 按「字段特性」分类:主键索引、唯一索引、普通索引、前缀索引
  • 按「字段个数」分类:单列索引、联合索引

B+ Tree Index 的优势

一个值对应一个指针。

主键索引的 B+Tree 和二级索引的 B+Tree 区别

主键索引的 B+Tree 的叶子节点存放的是实际数据,所有完整的用户记录都存放在主键索引的 B+Tree 的叶子节点里;

二级索引的 B+Tree 的叶子节点存放的是主键值,而不是实际数据。

什么时候不需要回表查询

覆盖索引

当查询的数据是能在二级索引的 B+Tree 的叶子节点里查询到,这时就不用再查主键索引查。这种在二级索引的 B+Tree 就能查询到结果的过程就叫作「覆盖索引」,也就是只需要查一个 B+Tree 就能找到数据。

联合索引失效

使用联合索引时,存在最左匹配原则,也就是按照最左优先的方式进行索引的匹配。在使用联合索引进行查询的时候,如果不遵循「最左匹配原则」,联合索引会失效,这样就无法利用到索引快速查询的特性了。

比如,如果创建了一个(a,b,c)联合索引,如果查询条件是以下这几种,就可以匹配上联合索引:

1
2
3
where a=1;
where a=1 and b=2 and c=3;
where a=1 and b=2:

需要注意的是,因为有查询优化器,所以a字段在 where 子句的顺序并不重要。但是,如果查询条件是以下这几种,因为不符合最左匹配原则,所以就无法匹配上联合索引,联合索引就会失效:

1
2
3
where b=2;
where c=3;
where b=2 and c=3;

上面这些查询条件之所以会失效,是因为(a,b,c)1联合索引,是先按a排序,在a相同的情况再按 b排序,在b相同的情况再按c排序。所以,b和c是全局无序,局部相对有序的,这样在没有遵循最左匹配原则的情况下,是无法利用到索引的。

索引下推

MySQL 5.6后通过联合索引可以判断另一个值是否满足条件,去除不符合的,减少回表次数;区别于覆盖索引。

MyISAM 引擎和 InnoDB B+Tree索引有什么区别

虽然,InnoDB 和 MyISAM 都支持 B+ 树索引,但是它们数据的存储结构实现方式不同。

不同之处在于:

  • InnoDB 存储引擎:B+树索引的叶子节点保存数据本身
  • ;MyISAM 存储引擎:B+树索引的叶子节点保存数据的物理地址

索引失效

  • 对索引使用左右模糊匹配

  • 对索引使用函数

    因为索引保存的是索引字段的原始值,而不是经过函数计算后的值,自然就没办法走索引了。

    不过,从 MySQL 8.0 开始,索引特性增加了函数索引,即可以针对函数计算后的值建立一个索引,也就是说该索引的值是函数计算后的值,所以就可以通过扫描索引来查询数据。

  • 对索引使用表达式

  • 对索引进行隐式转换

    VARCHAR转换为Int会导致索引失效,准确的说是如果查询语句中使用INT但是索引本身是VARCHAR会有可能导致索引失效。

    1
    2
    3
    4
    5
    6
    7
    DATETIME > TIMESTAMP > DATE
    |
    DECIMAL > DOUBLE > FLOAT > BIGINT > INT > MEDIUMINT > SMALLINT > TINYINT
    |
    CHAR/VARCHAR/TEXT(字符串)
    |
    BINARY/BLOB(二进制)

    向上转换

  • 联合索引非最左匹配

  • WHERE 子句中的 OR

    在 WHERE 子句中,如果在 OR 前的条件列是索引列,而在 OR 后的条件列不是索引列,那么索引会失效。

Count()

count(*)= count(1)>count(主键字段)>count(字段)

count(*)= count(1): MySQL 会对 count(*) 和 count(1) 尽量使用体积较小的二级索引

count(主键字段) 必然走主键索引,体积可能比二级索引大。

为什么要通过遍历的方式来计数?

使MyISAM 引擎 O(1)复杂度,每张 MyISAM 的数据表都有一个 meta 信息有存储了row_count值,由表级锁保证一致性。

wInnoDB 存储引擎是支持事务的,同一个时刻的多个查询,由于多版本并发控制(MVCC)的原因,InnoDB 表“应该返回多少行”也是不确定的,所以无法像 MyISAM一样,只维护一个 row_count 变量。

存储

innodb-store-page info-store-page

为什么2000W行会影响性能

索引结构不会影响单表最大行数,2000W 也只是推荐值,超过了这个值可能会导致 B + 树层级更高,影响查询性能。

事务

事务的四个性质 ACID

  • 原子性(atomicity)

    一个事务中的所有操作,要么全部完成,要么全部不完成,不会结束在中间某个环节。

  • 一致性(Consistency)

    是指事务操作前和操作后,数据满足完整性约束,数据库保持一致性状态。

  • 隔离性(Isolation)

    数据库允许多个并发事务同时对其数据进行读写和修改的能力,隔离性可以防止多个事务并发执行时由于交叉执行而导致数据的不一致,因为多个事务同时使用相同的数据时,不会相互干扰,每个事务都有一个完整的数据空间,对其他并发事务是隔离的。

  • 持久性(durability)

    事务处理结束后,对数据的修改就是永久的,即便系统故障也不会丢失。

InnoDB 引擎通过什么技术来保证事务的这四个特性的呢?

  • 持久性是通过 redo log (重做日志)来保证的;

  • 原子性是通过 undo log(回滚日志) 来保证的;

  • 隔离性是通过 MVCC(多版本并发控制) 和锁机制来保证的;

  • 一致性则是通过持久性+原子性+隔离性来保证;

并行事务产生的问题和事务隔离级别

读未提交 >脏读 >读已提交 > 不可重复读 > 可重复读>幻读> 可串行化

读未提交:一个写事务还没有提交,它的修改就被其它事务看到了,这就是脏读;

读已提交:一个事务提交之后,它做的变更才能被其他事务看到;

可重复读:一个事务执行过程中看到的数据,一直跟这个事务启动时看到的数据是一致的,InnoDB 默认事务隔离级别

可串行化:完全单线程执行,一个事务执行时,另外的事务被阻塞。

读提交和可重复读事务隔离如何实现 MVCC

通过Read View 来实现的,它们的区别在于创建 Read View 的时机不同。

read-view

Read View 有四个重要的字段:

  • m_ids :指的是在创建 Read View 时,当前数据库中「活跃事务」的事务 id 列表,注意是一个列“活跃事务”指的就是,启动了但还没提交的事务。
  • min_trx_id :指的是在创建 Read View 时,当前数据库中「活跃事务」中事务 id 最小的事务,也就是m ids 的最小值。
  • max_trx _id : 这个并不是 m_ids 的最大值,而是创建 Read View 时当前数据库中应该给下一个事务的id 值,也就是全局事务中最大的事务 id 值 +1:
  • creator_trx_id : 指的是创建该 Read View 的事务的事务 id.

只有提交了事务才可以被这个版本的MVCC看到

record

对于使用 InnoDB 存储引擎的数据库表,它的聚簇索引记录中都包含下面两个隐藏列:

  • trx_id,当一个事务对某条聚簇索引记录进行改动时,就会把该事务的事务 id 记录在 trx_id 隐藏列里;
  • roll_pointer,每次对某条聚簇索引记录进行改动时,都会把旧版本的记录写入到 undo 日志中,然后这个隐藏列是个指针,指向每一个旧版本记录,于是就可以通过它找到修改前的记录

这种通过「版本链」来控制并发事务访问同一个记录时的行为就叫 MVCC(多版本并发控制)。

不同的 Read View 会记录哪些应用这个版本的事务Id

可重复读隔离级别是启动事务时生成一个 Read View,然后整个事务期间都在用这个 Read View。

读提交隔离级别是在每次读取数据时,都会生成一个新的 Read View。

基于可重复读,如何实现避免幻读

可以分为两个情况:

  1. 快照读,在这个事务开始的时候指定好 Read View,以后的读操作都是基于这个 Read View ,不论其他事务是否改变数据,当前事务都是基于固定的 Read View,自然不会出现幻读。
  2. 当前读,会给表添加间隙锁,组织一些写操作。

基于可重复读的事务隔离级别,幻读不能被完美避免。在间隙锁不能锁定的地方添加到新的数据,就会出现幻读。

有哪些锁

全局锁

锁定整个表。全数据库/模式在只读状态下。

用于做全库逻辑备份。

表级别锁

表锁

表锁除了会限制别的线程的读写外,也会限制本线程接下来的读写操作。

用于调整表结构。

元数据锁 MDL

Meta Data Lock

MDL 是为了保证当用户对表执行 CRUD 操作时,防止其他线程对这个表结构做了变更。

为了能安全的对表结构进行变更,在对表结构变更前,先要看看数据库中的长事务,是否有事务已经对表加上了 MDL 读锁,如果可以考虑 kill 掉这个长事务,然后再做表结构的变更。

不修改表结构的事务相当于读,改变表结构的事务相当于写操作。

意向锁

对记录上锁之前需要给表上对应的意向锁,用于提示表中的记录是否被上锁。

意向共享锁和意向独占锁是表级锁,不会和行级的共享锁和独占锁发生冲突,而且意向锁之间也不会发生冲突,只会和共享表锁和独占表锁发生冲突。

AUTO-INC 锁

用于实现自增字段。

在MySQL 5.1.22 版本前,修改自增字段的请求需要获得锁之后才能执行。

在MySQL 5.1.22 版本后,InnoDB 存储引擎提供了一种轻量级的锁来实现自增。

一样也是在插入数据的时候,会为被 AUTO_INCREMENT 修饰的字段加上轻量级锁,然后给该字段赋值一个自增的值,就把这个轻量级锁释放了,而不需要等待整个插入语句执行完后才释放锁。

InnoDB 存储引擎提供了个 innodb_autoinc_lock_mode 的系统变量,是用来控制选择用 AUTO-INC锁,还是轻量级的锁。

  • 当 innodb_autoinc_lock_mode=0,就采用 AUTO-INC锁,语句执行结束后才释放锁;·
  • 当 innodb_autoinc_lock_mode =2,就采用轻量级锁,申请自增主键后就释放锁,并不需要等语句执行后才释放。
  • 当innodb autoinc lock_mode =1:
    • 普通 insert 语句,自增锁在申请之后就马上释放0
    • 类似 insert .. select 这样的批量插入数据的语句,自增锁还是要等语句结束后才被释放;

行级别锁

InnoDB 引擎是支持行级锁的,而 MyISAM 引擎并不支持行级锁。

行级别锁有三类:

  1. Record Lock:记录锁
  2. Gap Lock: 间隙锁
  3. Next-Key Lock: Record Lock + Gap Lock 的组合,锁定一个范围,并且锁定记录本身。
记录锁
间隙锁
Next-Key Lock

间隙锁是的目的是为了防止幻读;

X型和S型没什么区别。间隙锁的意义只在于阻止区间被插入,因此是可以共存的。一个事务获取的间隙锁不会阻止另一个事务获取同一个间隙范围的间隙锁,共享(S型)和排他(X型)的间隙锁是没有区别的,他们相互不冲突,且功能相同。

插入意向锁

间隙锁给某一区间的值加读锁,插入意向锁名字虽然有意向锁,但是它并不是意向锁,它是一种特殊的间隙锁,属于行级别锁,它锁住的是插入的点

插入意向锁和间隙锁是不能共存的

日志

日志有哪些

日志类型 逻辑日志 物理日志 主要作用
二进制日志 bin ✔️ 主从复制、数据恢复
重做日志 redo ✔️ 崩溃恢复,确保事务持久性
回滚日志 undo ✔️ 事务回滚、MVCC

undo log 和 redo log 都是 InnoDB 引擎做的日志,bin log 是服务层做的日志。

undo log 一开始并不会持续化到磁盘中,redo log 一开始就持续化磁盘中。

undo log 内容

undo-log version-linkedlisk

在MVCC 中见到过,undo log 和 Read View 配合实现 MVCC。

针对插入和更新会有特殊操作:

  1. 对于delete操作,并不是直接删除掉一条记录,而是对记录标记未删除,删除的实际操作由purge 线程执行

  2. 对如update操作,如果是对主键的更新,是删除原来的记录后新增一条新的记录,如果是普通的更新,则是普通的修改数据。

    原因在于:

    1. 索引结构和B+ Tree的结构,主键值决定了数据行的物理存储位置,如果直接修改主键值,会导致数据行在 B+ 树中的位置发生变化。严重的时候可能需要对B+树进行重新平衡
    2. InnoDB 通过 多版本并发控制(MVCC) 实现事务隔离。修改主键时,旧版本的数据需要保留(通过 Undo Log)
    3. InnoDB 的二级索引叶子节点存储的是主键值先删除后插入 的机制可以统一处理二级索引

redo log

redo log 防止缓存没有写入磁盘而崩溃的情景

WAL(Write Ahead Logging): 为了防止断电导致数据丢失的问题,当有一条记录需要更新的时候,InnoDB 引擎就会先更新内存(同时标记为脏页),然后将本次对这个页的修改以 redo log 的形式记录下来。再在合适的时候将缓存写入磁盘.

redo-log-step

当系统崩溃时,虽然脏页数据没有持久化,但是 redo log 已经持久化,接着 MySQL 重启后,可以根据 redo log 的内容,将所有数据恢复到最新的状态。

redo 刷盘时机
  1. Mysql 关闭;
  2. 当 redo log buffer 中记录的写入量大于 redo log buffer 内存空间的一半时,会触发落盘;
  3. 每隔1s
  4. 每次事务提交时都将缓存在 redo log buffer 里的 redo log 直接持久化到磁盘(这个策略可由 innodb_flush_log_at_trx_commit 参数控制)。
innodb_flush_log_at_trx_commit
写满了 redo log 怎么办
redo-log-store

InnoDB 存储引擎会先写 ib_logfile0 文件,当 ib_logfile0 文件被写满的时候,会切换至 ib_logfile1 文件,当 ib_logfile1 文件也被写满时,会切换回 ib_logfile0 文件。

如果 write pos 追上了 checkpoint,就意味着 redo log 文件满了,这时 MySQL 不能再执行新的更新操作,也就是说 MySQL 会被阻塞(因此所以针对并发量大的系统,适当设置redo log 的文件大小非常重要),此时会停下来将 Buffer Pool 中的脏页刷新到磁盘中,然后标记 redo log 哪些记录可以被擦除,接着对旧的 redo log 记录进行擦除,等擦除完旧记录腾出了空间,checkpoint 就会往后移动(图中顺时针),然后 MySQL恢复正常运行,继续执行新的更新操作。

bin log

可以认为是数据库服务器的日志binlog,写满了会换个文件接着写;用于备份恢复、主从复制;

事务执行过程中,先把日志写到 binlog cache(Server 层的 cache),事务提交的时候,再把 binlog cache 写到 binlog 文件中。

三种格式:

  1. STATEMENT:每一条修改数据的 SQL 都会被记录到 binlog 中(相当于记录了逻辑操作,所以针对这种格式, binlog 可以称为逻辑日志),主从复制中 slave 端再根据 SQL 语句重现。但 STATEMENT 有动态函数的问题,比如你用了 uuid 或者 now 这些函数,你在主库上执行的结果并不是你在从库执行的结果,这种随时在变的函数会导致复制的数据不一致;
  2. ROW:记录行数据最终被修改成什么样了(这种格式的日志,就不能称为逻辑日志了),不会出现 STATEMENT 下动态函数的问题。但 ROW 的缺点是每行数据的变化结果都会被记录,比如执行批量 update 语句,更新多少行数据就会产生多少条记录,使 binlog 文件过大,而在 STATEMENT 格式下只会记录一个 update 语句而已;
  3. MIXED:混合模式

两阶段提交

对 redo log 做两阶段提交

在持久化 redo log 和 binlog 这两份日志的时候,如果出现半成功的状态,就会造成主从环境的数据不一致性。这是因为 redo log 影响主库的数据,binlog 影响从库的数据,所以 redo log 和 binlog 必须保持一致才能保证主从数据一致。

two-phase-commit

主从复制

主从复制原理

  1. 主库(Master):
    • 记录所有数据变更到 二进制日志(Binary Log, binlog)
    • 从库通过读取 binlog 同步数据。
  2. 从库(Slave):
    • 连接到主库,读取 binlog 并写入 中继日志(Relay Log)
    • 重放中继日志中的事件,实现数据同步。
模式 特点 适用场景
异步复制 主库提交事务后立即响应客户端,不等待从库同步(默认模式)。 对性能要求高,允许短暂延迟。
半同步复制 主库提交事务后至少等待一个从库确认收到数据,再响应客户端。 要求数据一致性较高。
组复制(MGR) 基于 Paxos 协议的多主同步,支持自动故障转移和高可用(需 MySQL Group Replication)。 高可用集群环境。

Buffer pool

用缓存来降低IO瓶颈;

使用两队列的 LRU(Least Recent Used)算法进行替换;

在开启了慢 SQL 监控后,如果你发现「偶尔」会出现一些用时稍长的 SQL,这可因为脏页在刷新到磁盘时导致数据库性能抖动。如果在很短的时间出现这种现象,就需要调大 Buffer Pool 空间或 redo log 日志的大小。

SQL 优化

查询语句优化

索引相关优化

  • 合理添加索引:为 WHERE、JOIN、ORDER BY、GROUP BY 子句中的列创建适当索引
  • 避免索引失效
    • 避免在索引列上使用函数或计算
    • 避免使用 !=<>NOT INIS NULL 等可能导致索引失效的操作符
    • 注意 LIKE 查询以通配符开头(%abc)会导致索引失效
  • 覆盖索引:尽量让查询只需要通过索引就能获取所需数据
  • 索引合并:合理使用复合索引,注意最左前缀原则
  • 自增索引只在最右边增加,不分裂原有节点

查询结构优化

  • **避免 SELECT **:只查询需要的列

  • 合理使用 JOIN

    • 小表驱动大表
    • 避免多表 JOIN 时产生笛卡尔积
  • 优化子查询

    • 将某些子查询改写为 JOIN
    • 使用 EXISTS 代替 IN 当外表大而内表小时
  • 分页优化

    • 避免大偏移量分页(如 LIMIT 10000, 20
    • 使用延迟关联或记录上次查询位置
  • 最好必要用可变长字符串作为索引

    • 索引占用空间大

    • 查询性能较低:比较的效率低

    • 维护成本高

      • B+ 树频繁的分裂
      • 空间碎片化

      (优化方向:前缀索引,hash,编码映射

数据库设计优化

表结构设计

  • 合理的数据类型:使用最小够用的数据类型(如能用 TINYINT 不用 INT)
  • 适当的范式化:平衡范式化和反范式化,避免过度范式化导致过多 JOIN
  • 垂直拆分:将不常用的大字段拆分到单独表
  • 水平拆分:对超大表考虑分表策略(按时间、ID范围等)
  • 分区

索引设计

  • 主键选择:使用自增整型或业务无关ID作为主键
  • 复合索引顺序:将选择性高的列放在前面
  • 避免冗余索引:定期检查并删除未使用的索引

执行计划优化

  • 分析执行计划:使用 EXPLAINEXPLAIN ANALYZE 查看查询执行路径
  • 识别性能瓶颈
    • 关注全表扫描(ALL 访问类型)
    • 注意临时表(Using temporary)和文件排序(Using filesort)
  • 强制索引:必要时使用 FORCE INDEX 引导优化器选择更好的索引

数据库参数优化

服务器配置

  • 缓冲池大小:合理设置 innodb_buffer_pool_size(通常为物理内存的50-70%)
  • 连接数配置:调整 max_connections 避免过多连接导致资源耗尽
  • 日志配置:平衡事务日志(redo log)和二进制日志(binlog)的性能影响

存储引擎优化

  • InnoDB 优化
    • 调整 innodb_flush_log_at_trx_commit(安全性 vs 性能平衡)
    • 配置合适的 innodb_file_per_table
  • MyISAM 优化(如使用):
    • 调整 key_buffer_size
    • 定期执行 OPTIMIZE TABLE

监控与持续优化

  • 慢查询日志:定期分析慢查询并优化
  • 性能监控:监控QPS、TPS、连接数等关键指标
  • 定期维护
    • 执行 ANALYZE TABLE 更新统计信息
    • 对碎片化严重的表进行优化(OPTIMIZE TABLE

分表 vs 分库分表

特性 分表 分库分表
目标 优化单表性能 突破单库存储和性能上限
数据分布 单库多表 多库多表
查询复杂度 跨表查询需要手动处理 跨库查询需要复杂的逻辑
事务处理 单库事务,较简单 跨库事务,较复杂
扩展性 有限,受限于单库性能 高,支持大规模扩展
开发难度 较低 较高
运维成本 较低 较高
适用场景 单表数据量大,性能优化 数据量非常大,需要高并发高可用

IO瓶颈

  • 热点数据太多,数据缓存不够,每次查询产生大量IO——分库、垂直分表

  • 网络IO瓶颈,请求的数据太多,带宽不够、连接数过多——分库

CPU瓶颈

  • SQL问题,join、group by、order by——SQL优化,构建索引
  • 单表数据量过大,扫描行太多,SQL效率过低——水平分表

缓存 Redis

数据结构与实现

key-value 存储

有哪些数据结构

基础数据类型和实现方式

  1. String: SDS实现。 不仅可以存储字符流,还可以存储二进制流,长度复杂度为O(1),不会有缓存区溢出问题;对整数,可以转化为 Int 类型
  2. Hash:少:listpack;多:哈希表实现。listpack是 ziplist 的改进版本,更加紧凑,没有tail指针
  3. Set:少:整数集合;多:哈希表
  4. ZSet:少 listpack;多:跳表
  5. List:少 listpack;多:双向链表

新数据类型

  1. Bitmap 位图:二值状态统计系统
  2. GEO 地理信息
  3. stream 流:可以用于实现消息队列
  4. hyperloglog 计数器

线程模型

Redis基于内存,瓶颈主要在于内存和网络的带宽。如果采用多线程模型,可能带来额外的开销。网络采用I/O多路复用

Redis 6.0 以后,网络IO的吞吐量成为瓶颈,所以引入了多线程处理网络I/O。命令的执行还是基于主线程。

持久化

AOF 日志和 RDB快照

AOF 日志

将对 redis的CRUD操作以命令的形式存入到磁盘中

WAL(Write Ahead Logging): 先写数据,再更新日志。

RDB 快照

将某一时刻的内存数据以二进制形式存入磁盘

bgsave 不会阻塞主线程

如何实现持久化

结合 AOF 日志和 RDB 快照

以RDB快照为基线(前半部分),AOF 日志记录基线上的修改。

在 AOF 重写日志时,fork 出来的重写子进程会先将与主线程共享的内存数据以 RDB 方式写入到 AOF 文件,然后主线程处理的操作命令会被记录在重写缓冲区里,重写缓冲区里的增量命令会以 AOF 方式写入到 AOF 文件,写入完成后通知主进程将新的含有 RDB 格式和 AOF 格式的 AOF 文件替换旧的的 AOF 文件。

实战

功能

消息队列(延迟队列)

  1. 通过lua脚本订阅Key过期事件,消息主题应当存储在 Hash 表中
  2. Sorted Set 指定每个消息的过期时间,消费端轮询

实现分布式锁:因为处理过程单线程,所以保证只有一个用户可以获得共享资源

何时更新缓存

  1. 先更新数据库再删除缓存
  2. 在高命中率的条件下,可以同时更新数据库和缓存,但是需要分布式锁,或者及时从数据库更新缓存中的数据,保证数据一致性

总的来说,缓存是数据库的补充,一切以数据库为准。

缓存失效

缓存雪崩

大量缓存同时失效,导致大量请求送到数据库

解决方案:

  1. 不要让缓存数据同时失效
  2. 设置缓存不过期

缓存击穿

大量请求访问热点数据,但是缓存中没有

解决方案:

  1. 预加载
  2. 互斥锁,对同一个数据的访问,只有一个线程被接受

缓存穿透

数据既不在缓存中,也不在数据库中

解决方案:

  1. 非法请求限制
  2. 设置返回空值
  3. 布隆过滤器
布隆过滤器

初步过滤。Bloom Filter 会使用一个较大的 bit 数组来保存所有的数据,数组中的每个元素都只占用 1 bit ,并且每个元素只能是 0 或者 1(代表 false 或者 true),这也是 Bloom Filter 节省内存的核心所在。这样来算的话,申请一个 100w 个元素的位数组只占用 1000000Bit / 8 = 125000 Byte = 125000/1024 KB ≈ 122KB 的空间.

对对象做hash,将对应的hash位置做修改,1表示存在,0表示不存在。

综上,我们可以得出:布隆过滤器说某个元素存在,小概率会误判。布隆过滤器说某个元素不在,那么这个元素一定不在。

缓存一致性

浅谈缓存最终一致性的解决方案-腾讯云开发者社区-腾讯云

旁路缓存 Cache aside

读时,如果数据在缓存中,则直接返回,如果不在缓存中,那就从数据库获取并更新到缓存

写时,写直达数据库,然后删除缓存。

为什么要删除缓存?

写数据性能较差,两个连续的写请求可能导致无效缓存(缓存落后)。

为什么先更新数据库?

写请求的处理效率较低。连续到来的写读事务,第一个事务若先删除了缓存,第二个事务可能把旧的值重新写回缓存中。导致缓存落后。

延迟双删,在更新数据库前后都删除缓存。

是否还有风险?

是,比如一读一写,读把数据写入缓存,但是写操作并没有成功删除缓存,导致写之前的数据被更新到缓存。

解决办法

  1. 删除重试机制

  2. 基于bin log 日志分析

  3. 数据传输服务,将缓存和数据库打包管理。

Read Though

Write Though

Write Behind

Write Around

如果一些非核心业务,对一致性的要求较弱,可以选择在 cache aside 读模式下增加一个缓存过期时间,在写请求中仅仅更新数据库,不做任何删除或更新缓存的操作,这样,缓存仅能通过过期时间失效。

大Key 和 热Key

Redisson

分布式锁

JAVA

基础

封装、继承、多态

**封装:**将数据和行为捆绑在一起,并对外隐藏内部实现细节。

**继承:**子类自动获得父类的属性和方法,实现代码复用和层次化设计。

多态:重写和重载

JDK JRE JVM

**JDK: ** Java Development Kit

**JRE: ** Java Runtime Environment

**JVM: ** Java Virtual Machine

跨平台,编译型和解释型

Java的跨平台能力源于其独特的**”编译+解释”混合执行模式**,完美结合了编译型语言和解释型语言的特点:

编译阶段(编译型语言特性)

  • 源代码被编译成字节码(.class文件)
  • 字节码是平台无关的中间代码,类似抽象指令集
  • javac HelloWorld.java → 生成HelloWorld.class

执行阶段(解释型语言特性)

  • JVM(Java虚拟机)解释执行字节码
  • 不同平台有对应的JVM实现(Windows/Linux/macOS等)
  • java HelloWorld → JVM实时将字节码转换为机器码执行

一次编译,到处运行:同一份字节码可在所有支持JVM的平台运行

数据类型

装箱和拆箱

装箱拆箱是Java中基本数据类型与包装类之间的转换机制

享元模式

享元模式是一种结构型设计模式,主要用于减少内存使用,它通过共享多个对象共有的相同状态(内在状态),来支持大量细粒度对象的高效复用。

典型例子:各种包装类,常量池

String

字符串常量池在方法区/永生代(8以前),堆(8及以后)

1
2
3
4
5

String a = "abc";
String b = new String("abc");
System.out.println(a == b);
-> false

StringBuilder 和 StringBuffer

StringBuffer 是线程安全的StringBuilder

在所有修改的方法上添加 synchronized 保证线程安全

toString 利用 toStringCache: CopyOnWrite

反射

反射的常见用途

  1. 加载数据库驱动
  2. Spring 配置文件加载和管理
  3. 代理
  4. 注解

注解

实现@Annotation接口

通过反射获得注解

代理

JDK 动态代理和 CGLIB 动态代理对比

  1. JDK 动态代理基于组合,只能代理实现了接口的类或者直接代理接口,而 CGLIB 基于继承,代理未实现任何接口的类。 另外, CGLIB 动态代理是通过生成一个被代理类的子类来拦截被代理类的方法调用,因此不能代理声明为 final 类型的类和方法。
  2. 就二者的效率来说,大部分情况都是 JDK 动态代理更优秀,随着 JDK 版本的升级,这个优势更加明显。

静态代理和动态代理的对比

  1. 灵活性:动态代理更加灵活,不需要必须实现接口,可以直接代理实现类,并且可以不需要针对每个目标类都创建一个代理类。另外,静态代理中,接口一旦新增加方法,目标对象和代理对象都要进行修改,这是非常麻烦的!
  2. JVM 层面:静态代理在编译时就将接口、实现类、代理类这些都变成了一个个实际的 class 文件。而动态代理是在运行时动态生成类字节码,并加载到 JVM 中的。

Throwable

异常

分类

  1. 运行时异常(RuntimeException, Unchecked Exception):这类异常在编译时期就必须被捕获或者声明抛出。它们通常是外部错误,如文件不存在(FileNotFoundException)、类未找到(ClassNotFoundException)等。非运行时异常强制程序员处理这些可能出现的问题,增强了程序的健壮性。可以不 try-catch 或者 throws

  2. 非运行时异常:这类异常在编译时期就必须被捕获或者声明抛出。它们通常是外部错误,如文件不存在(FileNotFoundException)、类未找到(ClassNotFoundException)等。非运行时异常强制程序员处理这些可能出现的问题,增强了程序的健壮性。

    必须检查。

面向对象

设计原则

  1. 单一职责原则:一个类应该只有一个引起它变化的原因(即只负责一项职责)
  2. 开闭原则:对扩展开放,对修改关闭
  3. 里氏替换法则:子类必须能够完全替换它们的父类而不影响程序的正确性
  4. 接口隔离原则:客户端不应该被迫依赖它不需要的接口
  5. 依赖倒置原则:高层模块不应依赖低层模块,二者都应依赖抽象
  6. 迪米特法则,最小知识原则:一个对象应该对其他对象保持最少的了解

非静态内部类/静态内部类的区别

  • 非静态内部类依赖于外部类的实例,而静态内部类不依赖于外部类的实例。
  • 非静态内部类可以访问外部类的实例变量和方法,而静态内部类只能访问外部类的静态成员。
  • 非静态内部类不能定义静态成员,而静态内部类可以定义静态成员。
  • 非静态内部类在外部类实例化后才能实例化,而静态内部类可以独立实例化。
  • 非静态内部类可以访问外部类的私有成员,而静态内部类不能直接访问外部类的私有成员,需要通过实例化外部类来访问。

实现深拷贝的方式

  1. 实现 Cloneable 接口并重写 clone() 方法
  2. 使用序列化和反序列化
  3. 手动递归复制

泛型和泛型擦除

类似于 CPP 中的模板。但是 CPP 中的模板可以根据你的实例化方式创建一份对应类型的具体代码。

泛型的主要目的是在编译时提供更强的类型检查,并且在编译后能够保留类型信息,避免了在运行时出现类型转换异常。减少强制类型转换。

泛型擦除(Type Erasure) 是 Java 泛型实现的一种机制,它的核心思想是:在编译时保留泛型信息以进行类型检查,但在运行时将泛型类型替换为其原始类型(Raw Type)或边界类型(Bound Type)。这种机制是为了兼容 Java 的早期版本(Java 5 之前没有泛型)而设计的。

运行时并不检查泛型的类型。

序列化和反序列化

不用java 原生的序列化方法

实现Serializable接口

transient 关键字

阻止序列化,只修饰 field

static 也不会被序列化

集合

Collections

java 提供的工具类

集合遍历方式

  1. for
  2. for-each 语法糖
  3. 迭代器
  4. 列表迭代器
  5. forEach
  6. Stream API (JAVA8)

ArrayList

扩容

1.5倍扩容

转换线程安全

1
List<String> synchronizedList = Collections.synchronizedList(arrayList);

全局锁

HashMap

链表与红黑树

当冲突链表长度超过8时,转化为红黑树,红黑树的节点数 <= 6时,转化为链表

优先考虑扩容的问题

put过程

默认大小 扩容

16

扩容因子 0.75 超过 0.75 扩容 (方便计算?)

为什么String适合做key

String 是 static final的,字面一样的 String hashcode 和 equals 方法返回一定。

Set

通过HashMap 的 keySet 实现

有序集合: TreeSet LinkedHashSet

多线程集合

List -> Vector, CopyOnWriteList; Vector 给所有方法加锁

Map -> HashTable, ConcurrentHashMap; HashTable 给全局加锁

CopyOnWriteList实现

CopyOnWriteList:

ReentrantLock 保证只有一个线程进行写操作。

写时复制一份副本,对副本进行修改,再将指针指向副本。

ConcurrentHashMap 实现

1.7 以前

分段锁是可以重入的

1.8 以后

如果存储位置为空则使用 volatile 加 CAS (乐观锁) 来初始化

如果容器不为空,则根据存储的元素计算该位置是否为空

如果存储的元素为空,用 CAS 乐观锁设置节点

如果不为空,使用 synchronize 悲观锁,搜出旧的节点并设置新的值,并要计算是否需要转型。

ConcurrentSkipListMap

特性 ConcurrentSkipListMap ConcurrentHashMap
实现方式 基于跳表 基于哈希表
有序性 有序(按键排序) 无序
时间复杂度 查找、插入、删除:O(log n) 查找、插入、删除:平均 O(1)
适用场景 需要有序映射或范围查询 需要高并发访问

Java 升级

ver8

  1. lambda
  2. stream
  3. 接口默认方法
  4. string 优化
  5. 去除永生代,改到 meta space,String常量池移动到堆里,String新的实现
  6. ……

ver11

  1. 局部变量推断 lambda
  2. 新的字符串和文件 API
  3. ZGC
  4. 单文件运行
  5. ……

ver17

  1. Sealed 类,指定继承者
  2. 增强的伪随机数生成器
  3. instanceof 类型转换
  4. switch -> yield,
  5. stream 增强
  6. ……

JVM

JAVA 内存设计

meta space 设计在直接内存(direct memory)上

除BootStrapClassLoader 其他在堆内存中,类的元数据在 meta space

元空间中的内容

使用本地内存

  1. 类元数据
    1. 类信息
    2. 类方法
    3. 类字段
    4. 常量池:数字常量、符号引用
  2. 类静态变量
  3. 方法字节码
  4. 注解信息: 类 方法 字段的注解
  5. 类加载器
  6. 运行常量池

程序计数器是每个线程独有的

程序计数器用于记录线程执行到的地方

四种引用

  • 强引用指的就是代码中普遍存在的赋值方式,比如A a = new A()这种。强引用关联的对象,永远不会被GC回收。
  • 软引用可以用SoftReference来描述,指的是那些有用但是不是必须要的对象。系统在发生内存溢出前会对这类引用的对象进行回收。
  • 弱引用可以用WeakReference来描述,他的强度比软引用更低一点,弱引用的对象下一次GC的时候一定会被回收,而不管内存是否足够。
  • 虚引用也被称作幻影引用,是最弱的引用关系,可以用PhantomReference来描述,他必须和ReferenceQueue一起使用,同样的当发生GC的时候,虚引用也会被回收。可以用虚引用来管理堆外内存。

弱引用的应用场景:

  • 缓存

  • 对象池

  • 防止内存泄漏

类加载和初始化

类的生命周期

  • 加载:获取.class 字节流,在元数据区创建 Class 对象
  • 连接
    • 验证: 验证 class 字节流的正确性
    • 准备 :给静态字段分配空间并设置默认值
    • 解析:将符号引用,转化为直接引用
  • 初始化:执行静态代码块,并给 final static字段赋值。按照顺序执行静态代码块.
  • 使用
  • 卸载:如果有下面的情况,类就会被卸载:1. 该类所有的实例都已经被回收,也就是Java堆中不存在该类的任何实例。2. 加载该类的ClassLoader已经被回收。 3. 类对应的Java.lang.Class对象没有任何地方被引用,无法在任何地方通过反射访问该类的方法。 1 和 3 有共通点

双亲委派模型

其实就是父委派模型,只有当父加载器无法加载类时,才自身加载类,当然父加载器也是看他的父加载器能不能加载。

BootstrapClassLoader 是根,用CPP实现

如何打破双亲委派模型?

  1. 自定义类加载器并重写 loadClass 方法。
  2. 使用线程上下文类加载器。
  3. 使用 OSGi 模块化加载。
  4. 使用 Java 9+ 的模块化系统。
  5. 在热部署场景中使用独立的类加载器。

static final 变量的赋值

  • 如果它是编译时常量,其值在编译阶段就已经确定,并直接嵌入到字节码中,准备阶段
  • 如果它是非编译时常量,其值会在类初始化阶段通过静态代码块或方法调用赋值,初始化阶段

垃圾回收

判断垃圾的方式

  1. 引用计数器

  2. 可达性分析

    GC root 绝大多数是静态变量;本地方法引用的对象

垃圾回收区域

堆和方法区

分代

  • 新生代

    • Eden
    • survivor
  • 老生代

为什么要survivor 区域

绝大多数对象,即使一次不会gc,第二次也会被gc

Minor GC, Major GC, Full GC

Minor GC 对新生代进行垃圾挥手

Major GC 对老年代进行垃圾回收 Stop the world

Full GC Minor + Major GC

垃圾回收器

CMS

对老年代进程垃圾回收

一个大对象如果太大,可能会横跨多个 Region 来存放。

CMS 的优点是:并发收集、低停顿。

缺点:对CPU资源敏感,标记清除算法产生内存碎片

G1

分区垃圾回收器大于分代,标记-整理,提高CPU领用率,减少STW时间

内存被划分为多个 region,每个region 可以是每一个代的元素

使用标记整理法处理内存碎片

停顿时间: 复制,初始标记,在标记,清理

ZGC

低延迟 高吞吐量

在 Java21 中,引入了分代 ZGC,暂停时间可以缩短到 1 毫秒以内。

I/O和直接内存

在JVM 内存外的一个部分,直接在物理机内存上,通过直接内存减少用户态和内核态切换的问题。

JVM 新技术

逃逸分析

是目前Java虚拟机中比较前沿的优化技术。这是一种可以有效减少Java 程序中同步负载和内存堆分配压力的跨函数全局数据流分析算法。通过逃逸分析,Java Hotspot编译器能够分析出一个新的对象的引用的使用范围从而决定是否要将这个对象分配到堆上。

  1. 同步消除:如果一个对象被逃逸分析发现只能被一个线程所访问,那对于这个对象的操作可以不同步。

  2. 栈上分配:如果确定一个对象不会逃逸出线程之外,那让这个对象在栈上分配内存将会是一个很不错的主意,对象所占用的内存空间就可以随栈帧出栈而销毁。

  3. 标量替换:如果一个对象被逃逸分析发现不会被外部方法访问,并且这个对象可以拆散,那么程序真正执行的时候将可能不去创建这个对象,而改为直接创建它的若干个比这个方法使用的成员变量来代替。

JIT

JIT(Just-In-Time) 是一种动态编译技术,全称为 Just-In-Time Compilation(即时编译)。它主要用于提高程序的运行效率,特别是在解释型语言或虚拟机环境中(如 Java 的 JVM、.NET 的 CLR)。JIT 的核心思想是在程序运行时将热点代码(频繁执行的代码)动态编译为机器码,从而提升执行速度。

线程局部字段优化(Thread-Local Field Optimization)

或者称之为 “选择性线程局部变量”(Selective Thread-Local Variables)

这是一种在 Java 或其他编程语言中用于优化多线程程序性能的技术。

背景

在传统的多线程编程中,线程本地存储(Thread-Local Storage, TLS)通常用于为每个线程创建独立的变量副本,以避免线程间的竞争条件。然而,传统的线程本地存储会将整个对象或数据结构复制到每个线程的本地内存中,这可能会导致内存浪费和性能开销,尤其是当对象较大或线程数量较多时。

技术原理

线程局部字段优化 的核心思想是:

  • 只拷贝需要的字段:而不是将整个对象复制到线程本地内存中,只复制那些真正需要在线程间隔离的字段。
  • 减少内存开销:通过只复制必要的字段,减少了内存占用和复制开销。
  • 提高缓存局部性:由于只复制少量字段,数据更有可能被缓存在 CPU 缓存中,从而提高访问速度

JVM 调优

如何处理OOM异常

  1. 发现问题

    有可能是内存泄露,内存不足,外部链接未释放,配置不当等问题。

    前期在服务运行过程中,可以使用Prometheus、VisualVM或者Arthas这种工具来监控运行过程内存异常情况,比如Full GC后内存占用没有明显下降,并且内存占用在持续走高,说明服务中可能出现了内存泄漏。

  2. 定位问题

    启用堆转储:

    添加JVM参数 -XX:+HeapDumpOnOutOfMemoryError,在OOM时自动生成堆转储文件(heap dump)。

    使用 -XX:HeapDumpPath= 指定堆转储文件的保存路径。

    分析堆转储文件:

    使用工具(如 Eclipse MAT、VisualVM、JProfiler)分析堆转储文件,查看内存中占用最多的对象及其引用链

    通过线程Thread的methodHandler去定位OOM时执行的代码,找到深堆较大的对象,然后再去对应的业务代码中分析这些对象创建使用存在的内存泄漏问题,最后修复代码并验证测试。

    GC日志分析:

    添加JVM参数 -XX:+PrintGCDetails 和 -XX:+PrintGCDateStamps,生成GC日志。

    使用工具(如 GCViewer)分析GC日志,查看GC频率、停顿时间和内存回收情况。

垃圾回收优化

分析工具:使用GCViewergceasy.io等工具分析GC日志,关注:

  • Full GC频率:频繁Full GC可能需增大堆或优化对象生命周期。
  • STW时间:暂停时间过长需调整收集器或目标停顿时间。

JIT 和 AOT

JIT:对于热点代码,开启JIT内联优化

AOT:通过静态分析的方式分辨哪些代码应当直接编译为机器码,减少JIT预热的困难

监控与诊断工具

  1. 基础工具
    • jps:查看JVM进程。
    • jstat:监控内存和GC状态(如jstat -gcutil <pid> 1000)。
    • jmap:生成堆转储(jmap -dump:format=b,file=heap.hprof <pid>)。
    • jstack:抓取线程栈(分析死锁或高CPU问题)。
  2. 可视化工具
    • VisualVM:实时监控堆、线程、CPU。
    • MAT(Memory Analyzer):分析内存泄漏。
    • Arthas:在线诊断工具(动态查看类加载、方法执行耗时)

Java 并发

多线程基础

多线程开发原则

  1. 原子性
  2. 可见性
  3. 一致性

为什么实现 Runnable/Callable 接口比直接继承Thread更好

  1. 避免了 Java 单继承的局限性,Java 不支持多重继承,因此如果我们的类已经继承了另一个类,就不能再继承 Thread 类了。
  2. 适合多个相同的程序代码去处理同一资源的情况,把线程、代码和数据有效的分离,更符合面向对象的设计思想。Callable 接口与 Runnable 非常相似,但可以返回一个结果。
  3. 通常不使用直接显示创建线程的方式实现多线程。

线程状态

blocked 和 waiting 区别

blocked: 获得 synchronized 失败

waiting: wait join park sleep

线程安全

原子性:一个操作的完整性

可见性:一个操作对其他线程的可见性

死锁,活锁,饥饿

死锁

多个线程因 互相等待对方释放资源 而陷入无限阻塞的状态,导致所有线程都无法继续执行。

条件:互斥,持有并等待,不可剥夺,循环等待;

活锁

线程 不断重复执行某个操作,但由于外部条件未满足(如其他线程的干扰),始终无法推进任务。

与死锁的区别:线程未被阻塞,而是“忙等”(主动重试但无效)。

饥饿

某个线程 长期无法获取所需资源(如 CPU 时间片、锁),导致任务无法执行

JMM java memory model

与区分 JVM内存区分, 实现多线程通信和同步

主内存与本地内存

主内存是所有线程共享的内容。

每个线程都有一个私有的本地内存,本地内存存储了该线程以读 / 写共享变量的副本。

  • JMM 不拷贝完整对象:仅按需复制被访问的字段到线程本地内存。
  • 同步决定可见性:通过 volatilesynchronized 等机制控制内存同步范围。
  • 性能与正确性平衡:合理使用同步机制避免过度拷贝,同时保障多线程数据一致性

Happens-Before

如果操作 A happens-before 操作 B,那么 A 的结果对 B 可见,且编译器和处理器不会对 A 和 B 重排序到违反这一关系的顺序。

happens-before 不意味着时间上的先后,而是逻辑上的可见性保证

不可见问题

每个线程对堆中的数据设置有高速缓存,导致线程私有的脏数据没有写入公共堆中,从而其他线程看不到本线程做出的修改。

volatile 关键字

指令重排序可以保证串行语义一致,但是没有义务保证多线程间的语义也一致 ,所以在多线程下,指令重排序可能会导致一些问题。

禁止指令重排序,保证变量可见性

volatile 关键字能保证数据的可见性,但不能保证数据的原子性。synchronized 关键字两者都能保证

ThrealLocal

每个线程独有的 ThreadLocalMap 以 ThreadLocal 对象为key 去Map 中取 Value

ThreadLocalMap 通过开放地址法解决冲突问题

Future

Future 是 Java 5 引入的一个接口,用于表示一个异步计算的结果。它提供了一种检查计算是否完成的方式,并可以获取计算结果。

核心方法

  • boolean isDone():判断任务是否完成。
  • V get():获取计算结果,如果任务未完成,则阻塞当前线程直到任务完成。
  • V get(long timeout, TimeUnit unit):在指定时间内获取计算结果,超时抛出 TimeoutException
  • boolean cancel(boolean mayInterruptIfRunning):尝试取消任务。

CompletableFuture

CompletableFuture 是 Java 8 引入的一个类,实现了 Future 接口,并提供了更强大的功能。它支持任务之间的依赖关系、异常处理、组合操作等,是 Future 的增强版。

核心方法:

  1. 创建任务
    • CompletableFuture.runAsync(Runnable runnable):执行无返回值的任务。
    • CompletableFuture.supplyAsync(Supplier<U> supplier):执行有返回值的任务。
  2. 任务回调
    • thenApply(Function<T, U> fn):对任务结果进行处理。
    • thenAccept(Consumer<T> action):消费任务结果。
    • thenRun(Runnable action):任务完成后执行操作。
  3. 任务组合
    • thenCompose(Function<T, CompletableFuture<U>> fn):将两个任务串行执行。
    • thenCombine(CompletionStage<U> other, BiFunction<T, U, V> fn):将两个任务并行执行并合并结果。
  4. 异常处理
    • exceptionally(Function<Throwable, T> fn):处理异常并返回默认值。
    • handle(BiFunction<T, Throwable, U> fn):无论是否发生异常,都会执行。
  5. 手动完成
    • complete(T value):手动完成任务并设置结果。
    • completeExceptionally(Throwable ex):手动完成任务并抛出异常。

Condition

Condition 是 Java 中用于线程间协调的工具类,属于 java.util.concurrent.locks 包。它与 Lock 接口配合使用,提供了比 Objectwait()notify()notifyAll() 更灵活的线程等待和唤醒机制。

Condition 的核心功能

Condition 允许线程在某些条件下等待,并在条件满足时被唤醒。它的主要方法包括:

  1. 等待
    • await():使当前线程等待,直到被唤醒或中断。
    • await(long time, TimeUnit unit):使当前线程等待,直到被唤醒、中断或超时。
    • awaitUninterruptibly():使当前线程等待,直到被唤醒(不可中断)。
  2. 唤醒
    • signal():唤醒一个等待的线程。
    • signalAll():唤醒所有等待的线程。

Condition 的使用场景

Condition 通常用于实现复杂的线程同步机制,例如:

  • 生产者-消费者模型。
  • 线程间的条件等待和通知。
  • 替代 Objectwait()notify(),提供更细粒度的控制。

Condition 的基本用法

Condition 必须与 Lock 一起使用。通过 Lock.newCondition() 方法可以创建一个 Condition 对象

常见并发容器

ConcurrentHashMap

TODO

CopyOnWriteArrayList

TODO

ConcurrentLinkedQueue

TODO

AQS 抽象队列同步器

AQS 核心思想是,如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并且将共享资源设置为锁定状态。如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制,这个机制 AQS 是基于 CLH 锁 (Craig, Landin, and Hagersten locks) 进一步优化实现的。

AQS 队列是双向链表 CLH锁列表是单列表

lock

重写获取/释放方法实现各种锁

独占模式

在独占模式下,一个任务弹出后,直接通知他的

共享模式

乐观锁和悲观锁

悲观锁适用于写多读少,而乐观锁适用于读多写少。

悲观锁

悲观锁总是假设最坏的情况,认为共享资源每次被访问的时候就会出现问题(比如共享数据被修改)。共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程

像 Java 中synchronizedReentrantLock等独占锁就是悲观锁思想的实现。

乐观锁

乐观锁总是假设最好的情况,认为共享资源每次被访问的时候不会出现问题,线程可以不停地执行,无需加锁也无需等待,只是在提交修改的时候去验证对应的资源(也就是数据)是否被其它线程修改了(具体方法可以使用版本号机制或 CAS 算法)。

高并发的场景下,乐观锁相比悲观锁来说,不存在锁竞争造成线程阻塞,也不会有死锁的问题,在性能上往往会更胜一筹。但是,如果冲突频繁发生(写占比非常多的情况),会频繁失败和重试,这样同样会非常影响性能,导致 CPU 飙升。

乐观锁一般会使用版本号机制或 CAS 算法实现,CAS 算法相对来说更多一些。

synchronized 关键字

可以修饰的对象

  1. 实例方法(对象本身)
  2. 静态方法 (对象.class)
  3. 代码块 (自定义)

可重入

不可以对构造函数使用,但是可以对构造函数中的代码块使用

JDK1.6 偏向锁和轻量级锁 解决内核态和用户态切换的问题

四个状态: 锁升级顺序:无锁-> 偏向锁 -> 轻量级锁 -> 重量级锁

锁降级发生在垃圾回收的 STW 堆闲置的锁降级。

偏向锁:其实也没有锁,只有一个线程访问同步块。

轻量级锁:通过自旋实现,不断获取锁。有冲突升级到轻量级

重量级锁:线程阻塞实现。和1.6以前的实现方式一致。10次自旋失败后升级至重量级

锁信息存储在对象头 markword 中。

CAS

JNI 内联实现

乐观锁,无锁,冲突->失败->重试 循环

UnSafe 保证原子性

实现

  1. 访问一个值,记录当前值
  2. 基于访问记录计算新值
  3. 准备把新值写入;写入前,确认

公平锁和非公平锁

公平锁 : 锁被释放之后,先申请的线程先得到锁。性能较差一些,因为公平锁为了保证时间上的绝对顺序,上下文切换更频繁。

非公平锁:锁被释放之后,后申请的线程可能会先获取到锁,是随机或者按照其他优先级排序的。性能更好,但可能会导致某些线程永远无法获取到锁。

问题

ABA问题->解决办法:时间戳和版本号

线程池

线程池就是管理一系列线程的资源池。当有任务要处理时,直接从线程池中获取线程来处理,处理完之后线程并不会立即被销毁,而是等待下一个任务。

目的是降低创建和销毁线程所浪费的资源和时间,提高响应效率和提高对线程资源的管理。

建议通过 ThreadPoolExecutor 构造函数的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。Executors容易产生 OOM问题。

线程池的参数 744

7个参数

  1. 核心线程数
  2. 最大线程数
  3. 等待队列(长度,排序方式任务优先级)
  4. 最大非核心线程存活时间
  5. 最大非核心线程存活时间单位
  6. 拒绝策略
  7. 线程工厂

核心线程数,最大线程数,等待队列可以动态修改

4个拒绝策略

  1. 直接拒绝并抛弃:ThreadPoolExecutor.DiscardPolicy

  2. 抛出异常:ThreadPoolExecutor.AbortPolicy

  3. 使用 Caller 线程执行:ThreadPoolExecutor.CallerRunsPolicy 阻塞 Caller 线程,让 Caller线程执行任务,这样可以减缓线程池任务提交速度;如果Caller 线程关闭,则直接抛弃这个任务

    如果走到CallerRunsPolicy的任务是个非常耗时的任务,且处理提交任务的线程是主线程,可能会导致主线程阻塞,影响程序的正常运行。

  4. 丢弃等待队列中存活时间最长的任务:ThreadPoolExecutor.DiscardOldestPolicy

4个阻塞队列(和 **ThreadPoolExecutor**中的等待队列不同,不重要)

  1. LinkedBlockingQueue(有界阻塞队列):链表;FixedThreadPool 只有核心线程数量,没有最大线程数SingleThreadExecutor只有一个核心线程。
  2. SynchronousQueue(同步队列):没有容量,不存储元素,目的是保证对于提交的任务,用于 CachedThreadPool 总有线程执行
  3. DelayedWorkQueue(延迟队列):堆,ScheduledThreadPoolSingleThreadScheduledExecutor 。优先执行到了执行时间的任务。
  4. ArrayBlockingQueue(有界阻塞队列):数组,容量固定。

线程池调优

CPU 密集型任务 (N): 这种任务消耗的主要是 CPU 资源,线程数应设置为 N(CPU 核心数)。由于任务主要瓶颈在于 CPU 计算能力,与核心数相等的线程数能够最大化 CPU 利用率,过多线程反而会导致竞争和上下文切换开销。

I/O 密集型任务(M * N): 这类任务大部分时间处理 I/O 交互,线程在等待 I/O 时不占用 CPU。 为了充分利用 CPU 资源,线程数可以设置为 M * N,其中 N 是 CPU 核心数,M 是一个大于 1 的倍数,建议默认设置为 2 ,具体取值取决于 I/O 等待时间和任务特点,需要通过测试和监控找到最佳平衡点。

程池中线程异常后,销毁还是复用?

简单来说:使用execute()时,未捕获异常导致线程终止,线程池创建新线程替代;使用submit()时,异常被封装在Future中,线程继续复用。

这种设计允许submit()提供更灵活的错误处理机制,因为它允许调用者决定如何处理异常,而execute()则适用于那些不需要关注执行结果的场景。

线程池和 ThreadLocal 共用的坑

线程池和 ThreadLocal共用,可能会导致线程从ThreadLocal获取到的是旧值/脏数据。这是因为线程池会复用线程对象,与线程对象绑定的类的静态属性 ThreadLocal 变量也会被重用,这就导致一个线程可能获取到其他线程的ThreadLocal 值。

定时任务

cheduledThreadPoolExecutor 继承了ThreadPoolExecutor,并实现了ScheduledExecutorService接口。`

DelayedWorkQueue

当一个线程成为 leader,它只需等待队首任务的 delay 时间即可,其他线程会无条件等待。leader 取到任务返回前要通知其他线程,直到有线程成为新的 leader。每当队首的定时任务被其他更早需要执行的任务替换,leader 就设置为 null,其他等待的线程(被当前 leader 通知)和当前的 leader 重新竞争成为 leader。

DelayedWorkQueue 是一个优先级队列,它可以保证每次出队的任务都是当前队列中执行时间最靠前的,由于它是基于堆结构的队列,堆结构在执行插入和删除操作时的最坏时间复杂度是 O(logN)

所有线程都会有三种身份中的一种:leader、follower,以及一个干活中的状态:processor。它的基本原则是,永远最多只有一个 leader。所有 follower 都在等待成为 leader。leader成为 processor后,通知follower选举新的 leader

Spring SpringBoot

spring-frame

设计模式

  1. 代理
  2. 工厂方法
  3. 单例
  4. 模板方法
  5. 观察者
  6. 包装器
  7. 适配器

动态代理和静态代理

代理产生的时间,编译时还是运行时

  • 动态谈代理基于反射和代码生产实现

  • 静态代理需要在编译时就确认好被代理的类。

DI和IoC 依赖注入和控制反转

IoC是目标,依赖注入是实现方式。要使用,就通过名字或者类型从容器中拿。

Bean 生命周期

Spring Bean的生命周期包括实例化、属性赋值、初始化、使用和销毁等多个阶段。

对于非单例的(非全局作用域的)Bean,如原型类型的,Spring 只负责创建,销毁应当用户自行完成。 destory()

Bean 线程安全

Bean 应当是无状态的

如何避免循环依赖

三级缓存

使用三个缓存来解决循环依赖:

  • 一级缓存(singletonObjects):存放完全初始化的单例 Bean。

  • 二级缓存(earlySingletonObjects):存放提前暴露的 Bean 实例(已实例化但未完全初始化)。

  • 三级缓存(singletonFactories):存放 Bean 工厂对象,用于生成提前暴露的 Bean 实例。

一个对象初始化的时候会把自己的工厂放到三级缓存。循环依赖发现时,会利用A的工厂生产一个早期引用,并放入二级缓存,同时删除三级缓存工厂。都完成初始化后,将两个对象放入一级缓存。

为什么不二级缓存

可能产生多个不同的 Bean 也就是说,A 依赖的 B,B依赖的A并不是原来的A。

常见注解

  • @AutoWired 自动装配 Bean
  • @Configuration 配置类
  • @Bean 标记方法返回的对象是Bean 可以指定名称
  • Service Controller @Repository

Bean 作用域

  1. singleton 单例 全局作用域
  2. Prototype 原型 每个请求适用一个
  3. Request 每个HTTP请求一个
  4. Session 绘画
  5. Application
  6. WebSocket
  7. 自定义作用域

Bean是线程不安全的,但是应当是无状态的,在需要控制线程安全的时候,开发人员应当选择合适的作用域。

AOP面向切面

主要基于动态代理

AOP 常见注解

  • @Aspect:用于定义切面,标注在切面类上
  • @Pointcut:定义切点,标注在方法上,用于指定连接点。
  • @Before:在方法执行之前执行通知。
  • @After:在方法执行之后执行通知。
  • @Around:在方法执行前后都执行通知。
  • @AfterReturning:在方法执行后返回结果后执行通知。
  • @AfterThrowing:在方法抛出异常后执行通知。
  • @Advice:通用的通知类型,可以替代@Before、@After等。

AOP 有哪些环绕方式?

AOP 一般有 5 种环绕方式:

  • 前置通知 (@Before)
  • 返回通知 (@AfterReturning)
  • 异常通知 (@AfterThrowing)
  • 后置通知 (@After)
  • 环绕通知 (@Around)

多个切面的情况下,可以通过 @Order 指定先后顺序,数字越小,优先级越高

Spring AOP 发生在什么时候?

Spring AOP 基于运行时代理机制,这意味着 Spring AOP 是在运行时通过动态代理生成的,而不是在编译时或类加载时生成的

AOP的使用场景有哪些?

AOP 的使用场景有很多,比如说日志记录、事务管理、权限控制、性能监控等。

说说 Spring AOP 和 AspectJ AOP 区别?

Spring AOP 属于运行时增强,主要具有如下特点:

  1. 基于动态代理来实现,默认如果使用接口的,用 JDK 提供的动态代理实现,如果是方法则使用 CGLIB 实现
  2. Spring AOP 需要依赖 IoC 容器来管理,并且只能作用于 Spring 容器,使用纯 Java 代码实现

AspectJ 属于静态织入,通过修改代码来实现,在实际运行之前就完成了织入,所以说它生成的类是没有额外运行时开销的,一般有如下几个织入的时机:

注解

@Autwired和@Resource注解的区别

特性 @Autowired @Resource
来源 Spring 特有 JSR-250 标准
默认注入方式 按类型(byType) 按名称(byName)
指定名称 需结合 @Qualifier 直接通过 name 属性指定
适用范围 字段、构造方法、Setter 方法、普通方法 字段、Setter 方法
灵活性 支持 required=false,支持 @Primary 不支持 required,不支持 @Primary
推荐场景 Spring 项目,按类型注入 按名称注入,非 Spring 环境

事务 @Transactional

Java 事务的核心原理是 通过事务管理器绑定数据库连接,利用 AOP 代理拦截方法实现事务的开启、提交和回滚

声明式事务基于AOP实现。不足的地方是,声明式事务管理最细粒度只能作用到方法级别,无法像编程式事务那样可以作用到代码块级别。

事务的传播机制

传播行为:定义事务方法调用时的边界策略。

传播类型 是否需要事务 事务独立性 典型场景
REQUIRED 可选 如果当前存在事务,则加入该事务。
如果当前没有事务,则新建一个事务。
通用业务逻辑
SUPPORTS 可选 如果当前存在事务,则加入该事务。

如果当前没有事务,则以非事务方式执行。
查询操作
MANDATORY 必须 必须加入已有事务 关键数据变更
REQUIRES_NEW 强制新建 新建独立事务,挂起外层事务 独立提交的操作(如日志)
NOT_SUPPORTED 禁止 非事务执行,挂起外层事务 非事务资源操作
NEVER 禁止 必须非事务环境 严格无事务场景
NESTED 可选 如果当前存在事务,则创建一个嵌套事务(基于 Savepoint)。
嵌套事务回滚不影响外层事务,但外层事务回滚会触发嵌套事务回滚。
如果当前没有事务,则等同于 REQUIRED。
部分回滚的子操作

事务失效

  1. 未捕获异常
  2. 非收件异常
  3. 传播属性设置不当
  4. 多数据源配置错误
  5. 事务调用非事务

事务的使用场景

1. 需要原子性(Atomicity)的操作

  • 金融交易:如转账(A账户扣款,B账户加款)。
  • 订单处理:下单后同时扣减库存、生成订单记录。
  • 批量数据操作:如导入数据时多个表的关联更新。

2. 需要隔离性(Isolation)的并发场景

  • 多个用户同时操作同一数据时,避免脏读、不可重复读、幻读等问题。
    (例如:订票系统中多个用户抢同一张票)

3. 复杂业务逻辑

  • 跨多个步骤的操作,若中间步骤失败需回滚(如注册用户时需同时初始化权限表、日志表等)

如何防止长事务

为什么长事务?

  1. 锁资源占用:长时间持有锁,导致其他事务阻塞(如行锁、表锁)。
  2. 死锁风险:多个事务相互等待资源,引发死锁。
  3. 回滚开销大:事务越久,回滚日志(undo log)体积越大,失败时恢复越慢。
  4. 性能瓶颈:高并发场景下,长事务会显著降低数据库吞吐量。

如何避免

  1. 拆分大事务,将事务异步处理。
  2. 减少持有锁的时间
  3. 将非数据库事务移出
  4. 设置事务超时
  5. 优化SQL

SpringBoot vs Spring

  1. 提供了 约定优于配置 的理念,默认配置了大量常用的功能(如内嵌服务器、数据源、安全等)
  2. 自动装配@EnableAutoConfiguration
  3. 内置了 TomcatJettyUndertow 等服务器,可以直接运行 JAR 文件。
  4. 使用 MavenGradle 时,依赖管理更加简单和一致。通过 Starter 依赖(如 spring-boot-starter-webspring-boot-starter-data-jpa),自动引入相关依赖和兼容版本。
  5. 默认使用 application.propertiesapplication.yml 文件进行配置。
  6. 支持微服务
  7. ……
特性 Spring Spring Boot
配置方式 手动配置(XML/Java) 自动配置,约定优于配置
内嵌服务器 无,需外部服务器 内置 Tomcat/Jetty/Undertow
依赖管理 手动管理 Starter 依赖,Maven, Gradle 自动管理
打包方式 WAR 文件 可执行 JAR 文件
监控和管理 需手动集成 Actuator 内置 Actuator,开箱即用
微服务支持 需手动集成 Spring Cloud 与 Spring Cloud 无缝集成
学习曲线 较陡 较平缓
适用场景 大型企业级应用 快速开发、微服务架构

如何理解约定大于配置

减少手动配置,自动化配置,默认配置,约定项目结构

分布式

分布式理论

CAP

  • 一致性 Consistency
  • 可用性 Availability
  • 分区容错性 Partition Tolerance

只能保证其中两个,但是一定要保证分区容错性。

BA SE

BASE 的主要含义:

  • Basically Available(基本可用)

    假设系统出现了不可预知的故障,但还是能用,只是相比较正常的系统而言,可能会有响应时间上的损失,或者功能上的降级。

  • Soft State(软状态)

    要求多个节点的数据副本都是一致的,这是一种“硬状态”。

    软状态也称为弱状态,相比较硬状态而言,允许系统中的数据存在中间状态,并认为该状态不影响系统的整体可用性,即允许系统在多个不同节点的数据副本存在数据延时。

  • Eventually Consistent(最终一致性)

    上面说了软状态,但是不应该一直都是软状态。在一定时间后,应该到达一个最终的状态,保证所有副本保持数据一致性,从而达到数据的最终一致性。这个时间取决于网络延时、系统负载、数据复制方案设计等等因素。

Paxos 算法

提议者的提案被超过半数的接收者接受就交给学习者,学习者将结果返回给客户端

Raft 算法

基于 Multi-Paxos 实现

三个角色

  • Leader:负责发起心跳,响应客户端,创建日志,同步日志。
  • Candidate:Leader 选举过程中的临时角色,由 Follower 转化而来,发起投票参与竞选。
  • Follower:接受 Leader 的心跳和日志同步数据,投票给 Candidate。

容灾

如果 Leader 崩溃,集群中的节点在 electionTimeout 时间内没有收到 Leader 的心跳信息就会触发新一轮的选主,在选主期间整个集群对外是不可用的。

如果 Follower 和 Candidate 崩溃,处理方式会简单很多。之后发送给它的 RequestVoteRPC 和 AppendEntriesRPC 会失败。由于 raft 的所有请求都是幂等的,所以失败的话会无限的重试。如果崩溃恢复后,就可以收到新的请求,然后选择追加或者拒绝 entry。

选举

一个 Follower 收不到 Leader 的心跳信息和请求,就认为 Leader 失效,将自己设置为 Candidate

Candidate 成为 Leader 的条件是获得过半数投票。

一次投票至裁决一个 Candidate。

工作

entry :<index,term,cmd>

只有leader 可以创建entry

在两个日志里,有两个 entry 拥有相同的 index 和 term,那么它们一定有相同的 cmd

在两个日志里,有两个 entry 拥有相同的 index 和 term,那么它们前面的 entry 也一定相同

分布式协议

  1. 2PC 两阶段提交
  2. TCC

分布式ID

  1. 全局唯一
  2. 高性能,生产块
  3. 高可用
  4. 方便
  5. 有序递增
  6. 有含义
  7. 能独立部署

算法

雪花算法

时间戳+设备ID+信息序列号

分布式锁

互斥,高可用,可重入

实现方式

Zookeeper

Redis:

  • 如果 key 不存在,则可以插入,表示加锁成功;若 key 存在 则表示加锁失败
  • 锁要设置过期时间
  • 加锁要实现原子性 NX 选项
  • 分辨客户端,以便实现可重入,避免误删除

消息队列

MQ 的核心机制是通过 生产者(Producer)发布消息 到指定主题(Topic)或队列(Queue),消费者(Consumer)订阅并消费消息。以下是具体实现原理:

使用场景

  1. 系统解耦,多个系统依赖同一核心服务,需避免直接耦合。
  2. 异步处理,非核心操作耗时较长,需避免阻塞主流程。
  3. 流量削峰:突发高流量场景(如秒杀、抢购),避免压垮下游系统。
  4. 日志服务
  5. 分布式事务最终一致性

组件

以RocketMQ 为例

注册中心 NameServer

NameServer 是一个无状态的服务器,角色类似于 Kafka 使用的 Zookeeper,但比 Zookeeper 更轻量。

每个 NameServer 结点之间是相互独立,彼此没有任何信息交互。

功能:

  • 和 Broker 结点保持长连接。

  • 维护 Topic 的路由信息。

生产者 Producer

消息生产者,业务端负责发送消息,由用户自行实现和分布式部署。

Producer由用户进行分布式部署,消息由Producer通过多种负载均衡模式发送到Broker集群,发送低延时,支持快速失败。

RocketMQ 提供了三种方式发送消息:同步、异步和单向

  • 同步发送:同步发送指消息发送方发出数据后会在收到接收方发回响应之后才发下一个数据包。一般用于重要通知消息,例如重要通知邮件、营销短信。
  • 异步发送:异步发送指发送方发出数据后,不等接收方发回响应,接着发送下个数据包,一般用于可能链路耗时较长而对响应时间敏感的业务场景,例如用户视频上传后通知启动转码服务。
  • 单向发送:单向发送是指只负责发送消息而不等待服务器回应且没有回调函数触发,适用于某些耗时非常短但对可靠性要求并不高的场景,例如日志收集

消费者 Consumer

消息消费者,负责消费消息,一般是后台系统负责异步消费。

Consumer也由用户部署,支持 PUSH 和 PULL 两种消费模式,支持集群消费广播消费,提供实时的消息订阅机制

  • Pull:拉取型消费者(Pull Consumer)主动从消息服务器拉取信息,只要批量拉取到消息,用户应用就会启动消费过程,所以 Pull 称为主动消费型。
  • Push:推送型消费者(Push Consumer)封装了消息的拉取、消费进度和其他的内部维护工作,将消息到达时执行的回调接口留给用户应用程序来实现。所以 Push 称为被动消费类型,但其实从实现上看还是从消息服务器中拉取消息,不同于 Pull 的是 Push 首先要注册消费监听器,当监听器处触发后才开始消费消息。

存储者 Broker

消息存储和中转角色,负责存储和转发消息。

Broker 内部维护着一个个 Consumer Queue,用来存储消息的索引,真正存储消息的地方是 CommitLog(日志文件)。

单个 Broker 与所有的 Nameserver 保持着长连接和心跳,并会定时将 Topic 信息同步到 NameServer,和 NameServer 的通信底层是通过 Netty 实现的。

Broker 可以配置两种角色:Master 和 Slave,Master 角色的 Broker 支持读和写,Slave 角色的 Broker 只支持读,Master 会向 Slave 同步消息。

也就是说 Producer 只能向 Master 角色的 Broker 写入消息,Consumer 可以从 Master 和 Slave 角色的 Broker 读取消息。

只要消息被刷盘持久化至磁盘文件 CommitLog 中,那么 Producer 发送的消息就不会丢失。正因为如此,Consumer 也就肯定有机会去消费这条消息。当无法拉取到消息后,可以等下一次消息拉取,同时服务端也支持长轮询模式,如果一个消息拉取请求未拉取到消息,Broker 允许等待 30s 的时间,只要这段时间内有新消息到达,将直接返回给消费端。

刷盘策略

  • 同步刷盘:在消息达到 Broker 的内存之后,必须刷到 commitLog 日志文件中才算成功,然后返回 Producer 数据已经发送成功。
  • 异步刷盘:异步刷盘是指消息达到 Broker 内存后就返回 Producer 数据已经发送成功,会唤醒一个线程去将数据持久化到 CommitLog 日志文件中。

产品

实践

消息堆积是

消费堆积是什么?

消息堆积通常是由于 消息生产速度 > 消息消费速度 导致的,具体原因可能包括:

  1. 生产者流量剧增
  2. 消费者处理能力不足
    1. 消费者性能太差
    2. 消费者宕机
  3. 消费队列设不合理
    1. 消息分布不均匀
    2. 分区/队列数量不足:不能合理扩容消费端
  4. 其他原因

如何解决?

  1. 扩容消费者,解决消费者性能问题
  2. 优先处理重要消息
  3. 限流和降级

一些工具

微服务

层级 典型组件 功能
客户端层 浏览器、App、第三方服务 发起请求
入口层(网关层) Nginx、Spring Cloud Gateway 路由转发、SSL终止、全局限流
服务治理层 Sentinel、Hystrix、Istio 流量控制、熔断降级、负载均衡
业务服务层 微服务(如订单服务、用户服务) 处理具体业务逻辑
基础设施层 数据库、消息队列、缓存 数据持久化与通信

Kubernetes

Google 开发,基于Golang,可拓展。

容器编排,自动扩展,负载均衡,服务发现,多平台(本地,云,融合云)。

Nacos

Nacos 是一个功能强大的服务发现和配置管理平台,适用于微服务架构和云原生应用。

支持AP/CP双模式,默认AP模式。AP 是通过 Nacos 自研的 Distro 协议来保证的,CP 是通过 Nacos 的 JRaft 协议来保证的。

Zookeeper

  • 分布式协调服务,支持服务发现和配置管理。
  • 强一致性(CP 系统)意味着在选举期间是不可用的
  • 提供分布式锁、选举等功能。

Eureka

高可用优先,容忍短暂数据不一致

Elastic Stack

倒排索引

索引类型 数据结构 典型应用场景
正排索引 文档ID → 关键词列表 文档内容展示
倒排索引 关键词 → 文档ID列表(及位置) 关键词搜索、全文检索

Nginx

Nginx 是一个功能强大且灵活的服务器软件,适用于多种场景,包括:

  • 静态资源服务。HTTP/HTTPS服务器
  • 反向代理和负载均衡。
  • 缓存加速和安全防护。
  • 微服务网关和 API 管理。

负载均衡算法有

  1. 轮询
  2. hash 可配置加权
  3. 最少连接

网关功能有

  1. 访问控制
  2. 限流、熔断
  3. 校验、授权

实现限流的方式

  1. 固定窗口

    时间划分为固定的窗口(如 1 秒、1 分钟),在每个窗口内统计请求数量。如果请求数量超过阈值,则拒绝后续请求。

  2. 滑动窗口

    将时间划分为更小的窗口(如 1 秒划分为 10 个 100 毫秒的窗口),统计最近一段时间内的请求数量。如果请求数量超过阈值,则拒绝后续请求。

  3. 令牌桶

    系统以固定速率向桶中添加令牌。每个请求需要消耗一个令牌。如果桶中没有令牌,则拒绝请求。

  4. 漏桶

    将请求看作水滴,漏桶以固定速率漏水(处理请求)。如果桶满了(请求超过容量),则丢弃新请求。

lua 脚本

Lua 是一种轻量小巧的脚本语言,用标准C语言编写并以源代码形式开放, 其设计目的是为了嵌入应用程序中,从而为应用程序提供灵活的扩展和定制功能。

  • 动态请求处理和内容生成。
  • 访问控制、限流和负载均衡。
  • 缓存、日志记录和请求/响应修改。
  • 身份验证、授权和 WebSocket 支持。

Lua 脚本的灵活性和高性能使其成为扩展 Nginx 功能的强大工具,特别适用于需要动态逻辑和高并发的场景。OpenResty 是一个集成了 Lua 的 Nginx 发行版,推荐使用它来简化 Lua 脚本的开发和部署。

一致性hash 算法

一致性哈希算法正好解决了简单哈希算法在分布式集群中存在的动态伸缩的问题。降低节点上下线的过程中带来的数据迁移成本,同时节点数量的变化与分片原则对于应用系统来说是无感的,使上层应用更专注于领域内逻辑的编写,使得整个系统架构能够动态伸缩,更加灵活方便。

一致性哈希算法是分布式系统中的重要算法,使用场景也非常广泛。主要是是负载均衡、缓存数据分区等场景。

图解一致性哈希算法,看这一篇就够了! -阿里云开发者社区

Dubbo

Dubbo 是一款由阿里巴巴开源的高性能、轻量级的 Java RPC(远程过程调用)框架,主要用于分布式服务之间的通信。它提供了服务治理、负载均衡、服务注册与发现等功能,帮助开发者构建高性能的分布式系统。

  • 服务注册与发现
    • 服务提供者将服务注册到注册中心(如 Zookeeper、Nacos)。
    • 服务消费者从注册中心订阅服务,获取提供者的地址列表。
  • 远程调用
    • 支持多种协议(如 Dubbo 协议、HTTP、RMI 等)进行远程调用。
  • 负载均衡
    • 提供多种负载均衡策略(如随机、轮询、最少活跃调用等)。
  • 容错机制
    • 支持集群容错策略(如失败重试、快速失败、失败安全等)。
  • 服务治理
    • 提供动态配置、服务降级、流量控制等功能。
  • 监控与管理
    • 支持服务调用统计、链路追踪、服务治理中心等功能。

Netty

Netty是一个非阻塞I/O客户端-服务器框架,主要用于开发Java网络应用程序,如协议服务器和客户端。异步事件驱动的网络应用程序框架和工具用于简化网络编程,例如TCP和UDP套接字服务器。Netty包括了反应器编程模式的实现。Netty最初由JBoss开发,现在由Netty项目社区开发和维护。

Reference

主页 | 二哥的Java进阶之路

Java 面试指南 | JavaGuide

小林coding

NJU 目前正在使用的统一身份认证系统

对大部分校内用户而言,即使申请到 *.nju.edu.cn 的域名,其域名的所有权也不会直接移交到应用开发者,而是通过反代(通常是nginx)转发到开发者的应用服务器。

基于此实现了校内的统一身份认证。所有用户发往应用程序的流量均经过认证服务器转发,对于已登录的用户,认证服务器会在转发过程中带上用户信息。因此,应用程序的域名并不会解析到实际的应用服务,而是直接解析到认证服务器。认证服务器内部再通过 nginx 将流量代理回应用服务器。换言之,认证服务器拦截了所有的对应用的流量,只将带有认证信息的请求转发到应用;拦截没有认证信息的,并要求用户进行认证。

image-20250109223956231

优点

这样做无疑是让应用服务变得更加简单了。应用服务无需关注登录的细节,包括密码校验,错误密码重置等。应用默认所有的访问用户都进行了登录,只需要对用户登录ID进行权限校验即可。

对于校园内的简单应用,用户数量不大,这样做也缩短了开发周期和难度。

劣势

任何稍微复杂的应用都有自己的会话数据与状态需要保存,所以此点带来的简化相当有限。

流量全部流经统一认证服务器:应用服务器的一切流量全部流经统一认证服务器。

  • 统一认证服务器出现问题,会导致下游的所有服务向外暴露,导致虚假的用户访问;
  • 用户信息将被截获,导致用户信息暴露。

另外

  • 对于大规模场景,所有流量都需要通过统一认证服务,对统一认证服务器带来过多压力。

  • HTTPS 加密数据过早解密。由于用户流量在认证服务器上就被解密,认证服务器到应用服务器的通信是明文的。攻击者仅需要截取校园网上认证服务器与应用服务器之间的通信,即可泄露大量信息。

  • 认证服务器与应用服务器的鉴权是单向的,现有方案未提供 IP 以外的任何手段。一旦攻击者对应用服务器实施 IP 伪造攻击,或网络中传输中任意点进行篡改攻击,应用服务器就会认为此请求来自认证服务器而接受其中指定的用户信息。

  • 当认证服务器崩溃时,所有下级服务器直接对用户不可用,无论用户是否需要访问需要鉴权的资源。同时,一旦一个应用向上造成认证服务器出问题,所有应用都会受到连带的影响,这是不可接受的。

  • 在配置时,认证服务器和应用服务器的IP变化,都导致彼此的配置文件修改。

……

应用场景也非常有限。

其他统一身份认证鉴权方案

高校 选用**/**支持方式 支撑资料
上海交通大学 OAuth/OIDC 白雪松, 杜晋博 & 王罡. 高校信息化环境下OAuth授权体系的研究与实践. 华东师范大学学报(自然科学版) 240–245 (2015).https://developer.sjtu.edu.cn/auth/oidc.html
北京大学 OAuth 欧阳荣彬,杨旭. 基于OAuth的数据共享方案研究. 华东师范大学学报(自然科学版) 2015, 471 (2015). https://iaaa.pku.edu.cn/iaaa/oauth.jsp
中国人民大学 OAuth 直接询问
西安交通大学 CAS 冯兴利, 洪丹丹, 罗军锋 & 锁志海. 高校统一身份认证系统集群压力测试研究. 中国教育网络 76–78 (2018).
中国科学技术大学 CAS https://ustcnet.ustc.edu.cn/2015/0326/c11150a120769/page.htm
浙江大学 CAS https://zjuam.zju.edu.cn/cas/login
中国科学院 OAuth http://hf.cas.cn/xwzx1/tztg/202107/t20210728_6149276.html
中国科技云 OAuth https://www.cstcloud.cn/jswd
CARSI 教育网联邦认证和资源共享服务 SAML,对接:LDAP、OAuth 2、CAS https://carsi.atlassian.net/wiki/spaces/CAW/pages/27100896/5+11《CARSI 资源共享服务属性要求》
SEAC 上海教育认证中心 OAuth 冯骐, 朱宇红, 柯立新 & 沈富可. 上海是怎么做区域教育身份认证体系的? 中国教育网络 70–73 (2016)https://eac.cloud.sh.edu.cn/document/tech/OAuth.html

好的鉴权方式应当是应用服务的附加品,也就是说,用户的访问应当直接打到应用服务器上,再由应用服务器自主时候进行用户身份认证。

以下内容由GPT生成

CAS 鉴权

CAS鉴权的实现一般包括以下步骤:

  1. 用户访问应用程序(CAS客户端)
  • 用户访问某个需要认证的应用程序,假设这个应用程序已经与CAS服务器集成。
  • 如果用户尚未登录,应用程序会将用户重定向到CAS认证服务器进行身份验证。
  1. 重定向到CAS服务器
  • 用户的浏览器会被重定向到CAS服务器,通常带有目标URL(即用户最初访问的应用页面)作为参数。
  • 重定向请求一般是以https://cas-server/login?service={target-URL}的形式发送。
  1. 用户认证
  • 用户在CAS服务器上输入用户名和密码,CAS服务器验证用户身份。
  • 如果用户名和密码正确,CAS服务器生成一个票据(Ticket),通常是TGT(Ticket Granting Ticket),作为用户的身份标识。
  1. CAS服务器返回服务票据(Service Ticket, ST)
  • 用户身份验证成功后,CAS服务器生成一个ST(Service Ticket),并将其发送到用户的浏览器。
  • 用户的浏览器会将ST发送回最初的应用程序(CAS客户端)。
  1. 客户端向CAS服务器验证ST
  • 应用程序(CAS客户端)接收到ST后,会将该票据发送到CAS服务器进行验证。验证的请求通常是通过https://cas-server/serviceValidate?service={target-URL}&ticket={ST}的URL发送。
  • CAS服务器检查ST的有效性,确保它没有过期或被篡改。
  1. CAS服务器返回用户信息
  • 如果ST有效,CAS服务器会返回用户的身份信息(例如用户名、角色等)以及认证成功的标志。
  • 如果ST无效,CAS服务器会返回错误信息,应用程序则需要提示用户重新登录。
  1. 用户访问应用程序资源
  • 一旦CAS客户端验证通过,用户就可以访问应用程序的资源。应用程序通常会使用返回的用户身份信息来执行权限控制或角色验证等。
  1. 单点登录的持续性
  • 用户在整个CAS系统中只需要登录一次(即在CAS服务器上),然后可以无缝访问所有集成了CAS认证的应用程序,直到会话过期或用户主动登出。

用户授权(Authorization Request)

  • 用户尝试通过第三方应用(客户端)访问自己的受保护资源。
  • 客户端将用户重定向到授权服务器的授权端点,通常包括以下信息:
    • response_type=code:表示客户端希望使用授权码模式(Authorization Code Grant)。
    • client_id:客户端的唯一标识。
    • redirect_uri:授权服务器将用户重定向回的URI,包含授权码。
    • scope:客户端请求访问的权限范围(可选)。
    • state:客户端用来防止CSRF攻击的随机字符串(可选)。

例如:

1
https://authorization-server.com/authorize?response_type=code&client_id=client_id&redirect_uri=https://client.com/callback&scope=read write&state=random_state

用户登录与授权

  • 授权服务器提示用户登录(如果尚未登录),并请求用户授权该客户端访问其资源。
  • 用户同意授权后,授权服务器生成授权码,并将其附加到重定向URI中,返回给客户端。

例如:

1
https://client.com/callback?code=authorization_code&state=random_state

客户端请求访问令牌(Token Request)

  • 客户端接收到授权码后,将其与其他必要的参数(如client_idclient_secret)一起发送到授权服务器的令牌端点,交换访问令牌。

请求示例:

1
2
3
4
5
6
7
POST https://authorization-server.com/token
Content-Type: application/x-www-form-urlencoded
grant_type=authorization_code
code=authorization_code
redirect_uri=https://client.com/callback
client_id=client_id
client_secret=client_secret

授权服务器返回访问令牌(Access Token)

  • 如果授权码有效,授权服务器会响应客户端一个包含访问令牌(Access Token)、令牌类型(通常是Bearer)以及可选的刷新令牌(Refresh Token)的JSON对象。

示例响应:

1
2
3
4
5
6
{
"access_token": "access_token_value",
"token_type": "Bearer",
"expires_in": 3600,
"refresh_token": "refresh_token_value"
}

客户端访问受保护资源

  • 客户端使用获得的访问令牌(Access Token)来请求资源服务器的受保护资源。
  • 客户端将访问令牌附加在HTTP请求的Authorization头中,发送给资源服务器:

请求示例:

1
2
GET https://resource-server.com/protected/resource
Authorization: Bearer access_token_value

资源服务器验证访问令牌

  • 资源服务器检查请求中包含的访问令牌是否有效。如果有效,资源服务器返回受保护资源;否则,返回错误(例如401 Unauthorized)。

(可选)使用刷新令牌获取新令牌

  • 如果访问令牌过期且客户端仍需访问受保护资源,客户端可以使用刷新令牌(Refresh Token)向授权服务器请求新的访问令牌。

请求示例:

1
2
3
4
5
6
POST https://authorization-server.com/token
Content-Type: application/x-www-form-urlencoded
grant_type=refresh_token
refresh_token=refresh_token_value
client_id=client_id
client_secret=client_secret

授权服务器响应新令牌:

1
2
3
4
5
{
"access_token": "new_access_token_value",
"token_type": "Bearer",
"expires_in": 3600
}

references

统一认证交流.pptx

QQ最近老是给我推送8年9年之前自己发的说说。可恶的是,我大概还记得当时发那篇说说的具体场景。
前两天,发了交通补助和助教补助,转手就借国补把自己房间里的空调换掉了,回念一想,那是我家的第一台空调,家电下乡之前买的,都已经陪伴了我们16年了。
至于2005年的一些事情,我还有些映像:我爷爷在我家堂屋的红木门前,舌头包着上嘴唇,在一个木头块上车孔;在我家四面(有些)漏风的厨房里,一边发抖,一边用筷子搭桥等开饭。
转眼就要到2025年了。

0%