上午培训内容:react相关知识点
React 简介与环境搭建
- 核心特点:介绍了 React 的组件化、响应式数据更新及单向数据流特性。
- 脚手架搭建:演示了使用 Vite 创建 React + TypeScript 项目的全过程,包括依赖安装与目录结构解析。 TypeScript类型检查,当类型报错时,代码实际上仍然能通过编译。
模块化与组件基础
- 导入导出:讲解了 ES Module 的默认导出与具名导出的区别及使用规范。
- JSX 语法:介绍了 JSX 的基本用法,包括表达式嵌入、条件渲染、循环渲染以及与 HTML 的差异(如 className、事件命名)。
组件通信与状态管理
- Props 传递:演示了父组件向子组件传递数据,以及默认值处理和类型定义。
- Hooks 使用:重点讲解了 useState 钩子函数, use开头的即为钩子函数,类如useState、useEffect等,说明其如何实现响应式状态更新及避免/闭包陷阱/的正确写法。
知识点:React State 的闭包陷阱(Stale Closure Trap)是 React Hooks 中最常见、也最容易被误解的问题之一。它的核心本质是:在异步操作或回调函数中,捕获了旧渲染周期的状态值(State),导致获取到的始终是“过期”的数据。
要彻底理解并解决这个问题,我们需要从原理、典型场景和解决方案三个维度来剖析。
一、 为什么会发生闭包陷阱?(核心原理)
要理解闭包陷阱,首先要明白 React 函数组件的渲染机制:
- 渲染即快照:React 的函数组件并不是一个长期存在的实体,而是 React 反复调用的函数。每次组件渲染,都会产生一个包含当前 State 和 Props 的不可变快照。
- 闭包捕获旧值:JavaScript 的闭包会“记住”它被创建时的词法环境。当你在
useEffect或事件处理函数中创建定时器、异步请求时,这些回调函数会捕获创建那一刻的 State 值。 - 陷阱产生:如果后续 State 更新了,组件触发了重新渲染,但那个异步回调函数依然持有旧渲染周期的闭包。它完全不知道 State 已经发生变化,从而表现出“状态没有更新”的错觉。
二、 典型的闭包陷阱场景
最经典的场景是在 useEffect 中使用定时器或异步操作,且依赖数组为空 []:
function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {
const timer = setInterval(() => {
// ❌ 闭包陷阱:这里的 count 永远是 0
console.log('Current count:', count);
}, 1000);
return () => clearInterval(timer);
}, []); // 空依赖数组,导致 effect 只在首次渲染时执行
return <button onClick={() => setCount(count + 1)}>+1</button>;
}
现象:无论你怎么点击按钮,页面显示的 count 在增加,但控制台每秒打印的永远是初始值 0。因为 setInterval 的回调在首次渲染时被创建,闭包锁死了当时的 count = 0。
三、 如何优雅地解决闭包陷阱?
针对不同的业务需求,React 提供了多种破局方案:
方案 1:正确声明依赖数组(刷新闭包)
如果你希望回调函数能拿到最新的 State,最正规的做法是将该 State 加入 useEffect 的依赖数组。
useEffect(() => {
const timer = setInterval(() => {
console.log('Current count:', count); // ✅ 每次都能拿到最新值
}, 1000);
return () => clearInterval(timer); // 清理上一次的定时器
}, [count]); // ✅ 将 count 加入依赖
- 注意:当
count变化时,React 会执行清理函数(清除旧定时器),然后重新执行 Effect(创建新定时器)。这能确保拿到最新值,但频繁重建定时器可能会带来性能开销。
方案 2:函数式更新(Functional Updates)
如果你的目的仅仅是基于旧状态来更新状态(例如 count + 1),完全不需要在闭包中读取 count,可以直接使用 setState 的函数形式。
useEffect(() => {
const timer = setInterval(() => {
// ✅ 传入函数,React 会保证传入最新的 state,无需依赖 count
setCount(prevCount => prevCount + 1);
}, 1000);
return () => clearInterval(timer);
}, []); // 保持空数组即可!
- 优势:这是最推荐的更新模式,既避免了闭包陷阱,又避免了不必要的 Effect 重新执行。
方案 3:使用 useRef 作为可变容器
如果你需要在定时器或异步回调中读取最新的状态,但又不想频繁重建 Effect,可以使用 useRef。useRef 返回的对象在组件整个生命周期中保持不变,修改它的 .current 属性不会触发重渲染。
function Counter() {
const [count, setCount] = useState(0);
const countRef = useRef(count);
// 每次渲染同步最新值到 ref
useEffect(() => {
countRef.current = count;
}, [count]);
useEffect(() => {
const timer = setInterval(() => {
// ✅ 通过 ref 获取最新值,完美避开闭包陷阱
console.log('Latest count:', countRef.current);
}, 1000);
return () => clearInterval(timer);
}, []); // 保持空数组
return <button onClick={() => setCount(c => c + 1)}>+1</button>;
}
- 优势:兼顾了“只执行一次”和“获取最新值”的需求,是处理复杂异步逻辑(如 WebSocket、Canvas 动画)的利器。
方案 4:使用 useReducer
当状态逻辑非常复杂,或者多个状态相互依赖时,可以使用 useReducer。因为 dispatch 函数的引用是永远稳定的,不需要加入任何依赖数组,从根本上杜绝了闭包陷阱。
总结
React 的闭包陷阱并不是 Bug,而是 React 坚持“每次渲染都有独立 Props 和 State”这一设计哲学的必然结果。理解“渲染即快照”的概念后,根据场景选择函数式更新、补全依赖数组或 useRef,就能游刃有余地应对各种异步状态管理问题。
下午培训内容:jdbc与mysql相关
核心知识点概览
| 模块 | 关键内容 | 最佳实践/注意事项 |
|---|---|---|
| SQL 规范 | DDL(定义表结构)与 DML(增删改查);禁止硬删除,必须使用软删除(状态字段变更) | 生产环境严禁直接执行 DELETE,需准备回滚 SQL |
| 字段类型 | ID 推荐使用 BIGINT 或自定义字符串规则;字符串优先选 VARCHAR;避免使用 TEXT 占用过大空间 | 字符集必须使用 `utf8mb4以支持特殊字符 |
| 索引优化 | 基于 B+ 树结构,支持等值、范围查询;联合索引遵循“最左匹配”原则 | 索引并非越多越好,需权衡更新性能;避免在索引列上使用函数或隐式转换导致失效 |
| JDBC 编程 | 包含加载驱动、获取连接、创建 Statement、执行 SQL、处理结果、释放资源六步 | 必须使用 PreparedStatement 防止 SQL 注入;资源释放需在 finally 块中倒序关闭 |
| 连接池 | 引入 Druid 等连接池管理数据库连接,减少 TCP 握手开销 | 需合理配置初始连接数、最大连接数及超时时间 |
SQL 开发与规范
在业务开发中,数据操作需严格区分 DDL 和 DML。DDL 涉及表结构修改(如新增字段),风险较高,需评估对历史数据的影响 。DML 操作中,严禁使用物理删除(Hard Delete),应通过增加“是否删除”状态字段实现软删除,以便数据追溯和恢复 。此外,ID 生成不建议仅依赖数据库自增,大型项目常采用程序生成的字符串 ID 以具备业务含义 。
Q:为什么企业通常不支持使用外键?
Claude:好问题。你在前面的建表脚本里我写了 FOREIGN KEY ... ON DELETE CASCADE——这在演示/小项目里可以,但企业生产环境几乎不用。
为什么不推荐?原因分三层
第一层:性能代价,每次写入都在加锁
外键不是一个”声明”,而是一个运行时的检查动作。当你往 grades 表插入一条成绩:
INSERT INTO grades (student_id, ...) VALUES (5, ...);
MySQL 内部实际执行了:
1. 在 grades 表上加意向锁
2. 去 students 表对 id=5 那行加共享锁(S锁)
3. 检查 id=5 是否存在
4. 存在 → 释放锁,插入成功
5. 不存在 → 回滚
问题在第 2 步:子表的写操作,会锁住父表的行。高并发下,大量成绩插入会竞争 students 表的行锁,产生阻塞链:
线程A: INSERT grades(student_id=5) → 锁住 students.id=5(等A提交才释放)
线程B: UPDATE students SET name='新名' WHERE id=5 → 被阻塞!
线程C: INSERT grades(student_id=5) → 也被阻塞!
如果线程 A 的事务很慢(比如批量导入几百条),后面全在排队。
第二层:级联操作是”原子弹”
-- 你现在的定义
FOREIGN KEY (student_id) REFERENCES students(id)
ON DELETE CASCADE
一句话读懂这一行:删一个学生,他的所有成绩瞬间消失,不可恢复。
-- 有人不小心写了:
DELETE FROM students WHERE id = 1;
-- 学生没了,4条成绩也没了。没有警告,没有确认。
真实场景更可怕:
-- 有人写错条件:
DELETE FROM students WHERE enrollment_date > '2000-01-01';
-- 整张 grades 表可能被级联清空
企业里数据是命根子。CASCADE DELETE 意味着一个初级开发的失误可以让数据直接蒸发。所以大多数公司用软删除(is_deleted = 1),而不是物理级联删除。
第三层:架构上,数据校验归应用层管
现代企业架构的趋势:
传统: 数据库负责一切约束(外键、触发器、存储过程)
→ 数据库成了"大黑盒",逻辑散落在 SQL 和代码两处
现代: 数据库只管存储,所有业务规则在应用层
→ 校验逻辑有版本控制、能写单元测试、能 code review
一个典型的学生插入成绩的场景,业务校验都比外键做得多:
// 应用层校验 — 逻辑清晰,可测试,可打日志
public void addGrade(Long studentId, String subject, Double score) {
Student student = studentMapper.findById(studentId);
if (student == null) {
throw new BusinessException("学生不存在: " + studentId); // 业务异常,有上下文
}
if (student.getDeleted() == 1) {
throw new BusinessException("学生已注销,无法录入成绩");
}
if (score < 0 || score > 100) {
throw new BusinessException("成绩超出范围");
}
// ... 再插入
}
外键只能告诉你”插入失败(违反约束)“,不能告诉你为什么。应用层校验可以返回精确的错误信息、记录审计日志、触发补偿流程。
额外:分库分表后,外键直接不成立
当单表数据量过亿,企业会做水平分库:
DB1: students_1 ~ 100000 DB2: students_100001 ~ 200000
grades 对应 grades 对应
外键跨不了数据库。这时候”数据完整性”只能靠应用层保证。从一开始就不用外键,未来分库时就不用重构。
那你们建表脚本里的外键应该去掉吗?
对,生产环境改成这样:
-- 生产环境写法:只留索引,不加外键
CREATE TABLE grades (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
student_id BIGINT NOT NULL,
...
-- 保留索引,保证 JOIN 查询性能
INDEX idx_student_id (student_id)
-- 不加 FOREIGN KEY 约束
);
索引保留(否则查某个学生的成绩就是全表扫描),外键去掉(校验交给应用层)。
一句话总结:
外键本质是用数据库的性能和运维灵活性,去换一个应用层也能做的事。小项目、内部工具用它省心,企业高并发场景下得不偿失。
索引原理与失效场景
索引底层采用 B+ 树结构,通过多叉平衡降低磁盘 I/O 次数 。使用联合索引时,查询条件必须符合“最左前缀”匹配原则,否则索引可能失效 。常见失效场景包括:对索引列进行函数运算、类型隐式转换(如字符串与数字比较)、前缀模糊查询以及不满足最左匹配 。可通过 EXPLAIN 命令分析 SQL 执行计划,关注 type 和 key 字段判断索引使用情况 。
Q:使用索引能够提升查询性能的底层逻辑是? Claude:
你的表还需要在哪些列加索引?
先看现状 — 打开 sql/schema.sql 已有的索引:
students: grades:
PK id ✅ PK id ✅
UK student_no ✅ UK (student_id, subject, semester) ✅
idx_name ✅ idx_subject ✅
idx_major_class ✅ idx_semester ✅
students 表建议加 1 个:
enrollment_date —— 必加。成绩管理系统几乎一定会有”查 2024 级学生”、“按入学年份统计”这类需求。目前这个列没有任何索引,每次按入学时间过滤都是全表扫描。
性别、年龄不加——性别只有 3 种值,选择性太低,建了也用不上(优化器会直接选全表扫描);年龄同理,值太集中。
grades 表建议加 1 个复合索引:
(semester, subject, score) —— 这是成绩表最重要的索引。原因是成绩查询几乎都围绕”学期 + 学科”展开:
-- 这三种查询是最常见的,一个复合索引全部覆盖:
WHERE semester = '2024-2025-2' AND subject = 'java' AND score < 60 -- 查不及格
WHERE semester = '2024-2025-2' AND subject = 'java' ORDER BY score -- 排名
WHERE semester = '2024-2025-2' GROUP BY subject -- 各科平均
复合索引”最左前缀”规则让这三个查询都能命中。
可考虑删除的冗余索引:
idx_semester和idx_subject—— 如果你建了(semester, subject, score),单独查WHERE semester =能用最左前缀命中,单独查WHERE subject =反而不能。所以idx_subject保留(应付不带学期的单科查询),idx_semester可以删(被复合索引最左列覆盖)。
索引为什么能加速查询?本质是减少磁盘 IO
关键认知:CPU 和内存都很快,查询的瓶颈在磁盘 IO。
内存访问: ~100 ns
SSD 随机读: ~100,000 ns (内存的 1000 倍)
机械硬盘: ~10,000,000 ns (内存的 10 万倍)
没有索引 → 全表扫描,O(n)
SELECT * FROM students WHERE student_no = '2024001';
数据库一行一行从磁盘读到内存,逐一比对。100 万行的表,平均读 50 万次,每次都是一次随机 IO。就像一本没有目录的书,你要找”第五章”,只能从第 1 页开始逐页翻。
有索引 → B+ 树查找,O(log n)
索引是一棵 B+ 树,结构像多级目录:
["2024001" ~ "2024400"] ← 根节点(常驻内存)
/ | \
["001"~"133"] ["134"~"266"] ["267"~"400"] ← 中间节点
/ \ / \ / \
叶子↔叶子↔ 叶子↔叶子↔ 叶子↔叶子↔ ← 存主键值
找 student_no = '2024001':
- 读根节点 → 确定去左侧分支(1 次 IO)
- 读中间节点 → 确定去最左叶子(1 次 IO)
- 读叶子节点 → 拿到
id = 1(1 次 IO) - 用
id = 1去主键索引拿完整行(1 次 IO,这叫回表)
100 万行 → 4 次 IO 搞定。100 万次 vs 4 次,差距就在这里。
B+ 树为什么这么”矮”?
InnoDB 一个节点 16KB,每个索引条目大约十几个字节。一个节点能存 上千个 key:
树高 1 层 → 1,000 行
树高 2 层 → 1,000 × 1,000 = 100 万行
树高 3 层 → 10 亿行
你的表存 100 万条成绩,树高不超过 3 层,查找任何一条最多 3 次磁盘 IO。
叶子节点的链表 — 范围查询的杀手锏
B+ 树相比普通 B 树多了一个关键设计:叶子节点是双向链表。
[叶子1] ↔ [叶子2] ↔ [叶子3] ↔ [叶子4] ↔ ...
查 WHERE score BETWEEN 60 AND 80:B+ 树定位到 60,然后顺着链表往右走,读到 80 就停。中间的读取是顺序 IO,比随机 IO 快几十倍。
覆盖索引 — 连回表都省了
二级索引叶子只存主键。如果查询的列全在索引里,不用回表,又省一次 IO:
-- 索引: (semester, subject, score)
-- ✅ 覆盖索引,不回表
SELECT semester, subject, score FROM grades WHERE semester = '2024-2025-2';
-- ❌ 需要回表(id, student_id, exam_date 不在索引里)
SELECT * FROM grades WHERE semester = '2024-2025-2';
排序也被加速
SELECT * FROM grades WHERE subject = 'java' ORDER BY score DESC;
B+ 树叶子节点本身就是有序的。(subject, score) 索引里,subject='java' 区间的数据已经按 score 排好了,直接读就行——不需要额外排序(filesort)。100 万行的排序从几秒变几十毫秒。
一句话总结
索引快就快在:把 O(n) 的”逐行翻”变成 O(log n) 的”翻目录”,把大量随机 IO 变成少量定位 + 顺序读取。你的表最需要加的是 enrollment_date 索引和 (semester, subject, score) 复合索引。
JDBC 编程与安全防护
JDBC 是 Java 连接数据库的标准 API,核心步骤包括加载驱动、建立连接、创建 Statement、执行 SQL、处理 ResultSet 及释放资源 。为防止 SQL 注入,必须使用 PreparedStatement 进行预编译,避免直接拼接字符串 。资源释放必须在 finally 块中执行,且需按创建顺序的逆序关闭(ResultSet → Statement → Connection),并在使用前判空 。实际项目中通常引入 Druid 等连接池来统一管理连接,提升性能 。
关于池化思想(来源Claude)
一句话理解池化
池化 = 提前造好一批资源放着,用完归还,而不是用完就扔、用的时候再建。
用餐厅类比:
没有池化(每次 new + destroy): 池化(连接池):
客人来 → 买锅 → 做菜 → 扔锅 后厨常备 10 口锅
客人来 → 买锅 → 做菜 → 扔锅 客人来 → 拿锅 → 做菜 → 放回
客人来 → 买锅 → 做菜 → 扔锅 客人来 → 拿锅 → 做菜 → 放回
↑ 买锅 + 扔锅 的开销巨大 ↑ 造一次,复用无数次
核心矛盾:创建和销毁资源的成本远高于复用的成本。
为什么”创建”这么贵?
以数据库连接为例,建立一次 TCP 连接 + MySQL 握手:
1. TCP 三次握手 ~1.5 RTT
2. MySQL 认证握手(密码校验) ~1 RTT
3. MySQL 返回握手确认 ~1 RTT
4. 字符集/时区协商 ~1 RTT
─────────────────────────────────────────
总计: 4-5 次网络往返 ≈ 50-200ms
而连接建立后,执行一条 SELECT * FROM students WHERE id = 1 只需要 几毫秒。如果不复用连接,每次查询的实际耗时是:
建连接(100ms) + 执行SQL(2ms) + 关连接(10ms) = 112ms
1000 次查询就是 112 秒。连接池把 100ms 的建连开销一次性支付,后续只花 2ms:
预热建10个连接(一次性1000ms) + 执行1000次SQL(2ms × 1000) = 3秒
112 秒 → 3 秒,提升 37 倍。
池化的通用模式(四个要素)
所有池化技术都是同一套逻辑:
┌──────────────────────────────────────────┐
│ 资源池 │
│ ┌─────┐ ┌─────┐ ┌─────┐ ┌─────┐ │
│ │ 空闲 │ │ 空闲 │ │ 使用中│ │ 空闲 │ ... │
│ └─────┘ └─────┘ └─────┘ └─────┘ │
│ ↑ 借 ↓ 还 │
│ ┌─────────────────────────────┐ │
│ │ 调用方 │ │
│ └─────────────────────────────┘ │
└──────────────────────────────────────────┘
四个核心参数:
• 核心数 (core size) — 池里最少保持几个(不回收)
• 最大数 (max size) — 池里最多允许几个(超出排队等待)
• 空闲超时 (idle timeout) — 空闲多久后回收多余连接
• 等待超时 (wait timeout) — 借不到时最多等多久(超时报错)
六大应用领域
1. 数据库连接池(最典型)
| 实现 | 所属生态 |
|---|---|
| HikariCP | Spring Boot 默认,当前性能最优 |
| Druid | 阿里出品,带 SQL 监控墙 |
| DBCP2 | Apache,老牌 |
# Spring Boot 中你就在用池化,只是没感知
spring.datasource.hikari.maximum-pool-size=20
spring.datasource.hikari.minimum-idle=5
每一条 jdbc:mysql://... 的背后都不是裸连接,而是从池里拿的。
2. 线程池
线程创建代价极高——每个线程在操作系统层面要分配独立的栈空间(默认 1MB),创建和销毁涉及内核态切换。
// 不用池: 每来一个任务 new 一个线程
new Thread(() -> doWork()).start(); // 创建 ~1ms,栈占 1MB,任务完就扔
// 用池: 固定 10 个 worker 线程反复用
ExecutorService pool = Executors.newFixedThreadPool(10);
pool.submit(() -> doWork()); // 复用已有线程,0 创建开销
Java 里最常用的 ThreadPoolExecutor 参数设计和连接池完全一致:
corePoolSize = 5 → 常驻 5 个线程
maxPoolSize = 10 → 忙时最多扩到 10 个
keepAliveTime = 60s → 多余线程空闲 60s 后回收
workQueue 容量 = 100 → 任务排队队列
3. HTTP 连接池
微服务时代,服务 A 调服务 B 走 HTTP。每次请求如果重新建 TCP 连接,开销同样巨大。
// OkHttp / Apache HttpClient 内部都有连接池
// 同一个 host:port 的连接被复用
PoolingHttpClientConnectionManager cm = new PoolingHttpClientConnectionManager();
cm.setMaxTotal(200); // 总共最多 200 个连接
cm.setDefaultMaxPerRoute(20); // 每个目标服务最多 20 个
浏览器也是同理——Chrome 对同一域名默认保持 6 个 TCP 连接复用(HTTP/1.1 的 keep-alive)。
4. 对象池
有些对象创建成本极高,用完可以重置再给下一个人:
// StringBuilder 可以 reset 后复用
// Apache Commons Pool2 就是通用对象池框架
GenericObjectPool<StringBuilder> pool = new GenericObjectPool<>(new StringBuilderFactory());
pool.setMaxTotal(50);
StringBuilder sb = pool.borrowObject(); // 借
sb.append("hello");
pool.returnObject(sb); // 还(自动 reset)
Jedis(Redis 客户端)内部就用 Commons Pool2 管理 Redis 连接。 你写 jedis.set("key", "val"),背后是从池里拿连接、用完归还。
5. 内存池
malloc / free 的系统调用频繁了也会成为瓶颈。内存池预先向 OS 申请一大块,自己管理分配和回收:
C++ STL allocator、Netty 的 PooledByteBuf、JVM 的 TLAB 全是这个思路。
Netty 中:
PooledByteBufAllocator → 堆外内存池,零拷贝 + 复用
UnpooledByteBufAllocator → 每次 new,用完等 GC
这就是为什么 Netty 做网络编程性能碾压传统 BIO——内存都不需要频繁申请释放。
6. 常量池 / 字符串池
JVM 内部就在用池化:
String s1 = "hello"; // 从字符串常量池拿
String s2 = "hello"; // 同一个引用,不会重复创建
String s3 = new String("hello"); // 强行 new,绕开池
System.out.println(s1 == s2); // true — 池里同一个对象
System.out.println(s1 == s3); // false — 堆上新对象
Integer 也一样,-128 ~ 127 范围的整数被 JVM 缓存在常量池中。
一张表总结
| 领域 | 池化的资源 | 创建成本 | 典型实现 |
|---|---|---|---|
| 数据库连接池 | TCP 连接 + DB 会话 | 100ms + 握手 | HikariCP, Druid |
| 线程池 | OS 线程 + 栈空间 | 1ms + 1MB 内存 | ThreadPoolExecutor |
| HTTP 连接池 | TCP 连接 | 50ms + 握手 | HttpClient, OkHttp |
| 对象池 | 重量级 Java 对象 | new + init | Commons Pool2, Jedis |
| 内存池 | 堆外/堆内内存块 | malloc/free | Netty PooledByteBuf |
| 字符串池 | String 对象 | new + GC 回收 | JVM String Pool |
池化的本质不是”让你更快”,而是”避免重复做慢的事情”。 任何创建和销毁成本远高于使用成本的资源,都值得被池化。反过来,轻量级对象(比如一个 DTO)就别池化了——JVM 的年轻代 GC 创建和回收它们几乎零代价,加个池反而把代码搞复杂了。