0%

责任链模式(Chain of Responsibility Pattern):请求的链式传递与处理

责任链模式是行为型设计模式的一种,核心思想是将多个请求处理器串联成一条链,使请求沿着链传递,直到被某个处理器处理为止。这种模式通过分离请求的发送者和接收者,实现了两者的解耦,本质是 “分离职责,动态组合处理流程”。就像公司的审批流程 —— 请假申请会依次经过组长、部门经理、总经理审批,每个环节若有权限处理则审批,否则传递给下一级,核心是 “链式传递,各司其职”。

责任链模式的核心结构

责任链模式

责任链模式通过两个核心角色实现请求的链式处理,结构简洁且灵活性高:

抽象处理器(Handler)

  • 定义处理请求的接口,声明处理方法(如handleRequest()),并持有下一个处理器(successor)的引用。
  • 提供设置下一个处理器的方法(如setSuccessor()),用于构建责任链。
  • 示例:Approver(审批者抽象类,声明approve(Request request)方法)。

具体处理器(ConcreteHandler)

  • 实现抽象处理器接口,处理自身职责范围内的请求:
    • 若能处理请求,则直接处理;
    • 若不能处理,则将请求传递给下一个处理器(successor)。
  • 示例:TeamLeader(组长)、DepartmentManager(部门经理)、GeneralManager(总经理)。

代码实现示例

以 “请假审批流程” 为例,展示责任链模式的实现:不同天数的请假申请由不同层级的审批者处理(组长批 1-3 天,部门经理批 4-7 天,总经理批 8 天以上),申请会沿责任链传递直到被处理。

1. 抽象处理器与请求类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// 请求类:封装请假信息
public class LeaveRequest {
private String employee; // 员工姓名
private int days; // 请假天数

public LeaveRequest(String employee, int days) {
this.employee = employee;
this.days = days;
}

public String getEmployee() { return employee; }
public int getDays() { return days; }
}

// 抽象处理器:审批者
public abstract class Approver {
protected Approver successor; // 下一个审批者

public Approver(Approver successor) {
this.successor = successor;
}

// 设置下一个审批者
public void setSuccessor(Approver successor) {
this.successor = successor;
}

// 处理审批请求(抽象方法,由子类实现)
public abstract void approve(LeaveRequest request);
}

2. 具体处理器(各级审批者)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
// 具体处理器1:组长(处理1-3天请假)
public class TeamLeader extends Approver {
public TeamLeader(Approver successor) {
super(successor);
}

@Override
public void approve(LeaveRequest request) {
if (request.getDays() <= 3) {
// 组长可处理,直接审批
System.out.printf("组长批准%s请假%d天%n",
request.getEmployee(), request.getDays());
} else if (successor != null) {
// 无法处理,传递给下一级
successor.approve(request);
} else {
System.out.println("无人处理该请求");
}
}
}

// 具体处理器2:部门经理(处理4-7天请假)
public class DepartmentManager extends Approver {
public DepartmentManager(Approver successor) {
super(successor);
}

@Override
public void approve(LeaveRequest request) {
if (request.getDays() <= 7) {
System.out.printf("部门经理批准%s请假%d天%n",
request.getEmployee(), request.getDays());
} else if (successor != null) {
successor.approve(request);
} else {
System.out.println("无人处理该请求");
}
}
}

// 具体处理器3:总经理(处理8天以上请假)
public class GeneralManager extends Approver {
public GeneralManager(Approver successor) {
super(successor);
}

@Override
public void approve(LeaveRequest request) {
// 总经理可处理所有超出部门经理权限的请求
System.out.printf("总经理批准%s请假%d天%n",
request.getEmployee(), request.getDays());
}
}

3. 客户端使用(构建责任链并处理请求)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class ChainOfResponsibilityDemo {
public static void main(String[] args) {
// 构建责任链:组长 → 部门经理 → 总经理
Approver generalManager = new GeneralManager(null); // 链尾,无下一级
Approver deptManager = new DepartmentManager(generalManager);
Approver teamLeader = new TeamLeader(deptManager);

// 处理不同天数的请假请求
LeaveRequest request1 = new LeaveRequest("张三", 2); // 2天(组长处理)
LeaveRequest request2 = new LeaveRequest("李四", 5); // 5天(部门经理处理)
LeaveRequest request3 = new LeaveRequest("王五", 10); // 10天(总经理处理)

teamLeader.approve(request1);
teamLeader.approve(request2);
teamLeader.approve(request3);
}
}
输出结果
1
2
3
组长批准张三请假2天
部门经理批准李四请假5天
总经理批准王五请假10天

责任链模式的核心优势

  1. 请求发送者与接收者解耦
    发送者只需将请求交给责任链的第一个处理器,无需知道具体由哪个处理器处理,减少了对象间的直接依赖。
  2. 动态组合责任链
    责任链的结构可动态调整(如新增审批环节、改变审批顺序),只需修改处理器间的引用关系,无需修改发送者或处理器的逻辑。
  3. 单一职责原则
    每个处理器仅负责自身职责范围内的请求,职责清晰,便于维护和扩展(如新增 “总监” 审批者只需添加新的具体处理器)。
  4. 灵活性高
    可根据需求灵活调整链的长度和处理逻辑,支持请求的部分处理或全链路传递。

适用场景

  1. 多对象可处理同一请求,但处理者不确定
    如:
    • 审批流程(请假、报销):不同金额 / 天数由不同层级处理。
    • 异常处理:不同类型的异常由不同的异常处理器处理。
    • 日志系统:不同级别的日志(DEBUG、INFO、ERROR)由不同的日志处理器输出。
  2. 需要动态指定请求处理流程
    如过滤器链(Filter Chain):Web 开发中,请求会依次经过多个过滤器(如权限校验、参数过滤、日志记录),每个过滤器可决定是否继续传递请求。
  3. 避免请求发送者与多个处理者直接耦合
    如 GUI 事件冒泡:按钮点击事件会从组件向上传递到父容器,直到被处理或到达顶层容器。

优缺点分析

优点

  • 解耦性好:发送者无需知道具体处理者,处理者也无需知道请求的来源。
  • 灵活性高:可动态调整责任链的结构和处理顺序。
  • 扩展性强:新增处理者只需实现接口并加入链中,对现有代码无侵入。

缺点

  • 请求可能未被处理:若责任链中没有处理器能处理请求,请求可能 “丢失”(需在链尾添加默认处理器避免)。
  • 性能损耗:请求可能需要经过多个处理器才能被处理,尤其是长链场景。
  • 调试难度增加:请求的传递路径不直观,排查问题时需跟踪整个链的执行流程。

经典应用案例

  1. Java Servlet 的过滤器链(FilterChain)
    Servlet 规范中的FilterChain是责任链模式的典型实现:请求会依次经过所有注册的Filter(过滤器),每个过滤器可处理请求或传递给下一个过滤器,最终到达Servlet
  2. Spring 的拦截器链(HandlerInterceptor)
    Spring MVC 的拦截器链允许在请求处理前、处理中、处理后执行逻辑,拦截器通过preHandle()postHandle()等方法构成责任链,可决定是否继续传递请求。
  3. 日志框架的处理器链
    如 Logback 的Appender链:日志事件会被传递给多个Appender(如控制台、文件、数据库),每个Appender可处理日志或继续传递。
  4. 事件冒泡机制
    GUI 框架(如 Swing、Vue)中的事件冒泡:用户操作事件(如点击)会从触发组件向上传递到父组件,直到被处理或到达顶层容器。

总结

责任链模式通过将请求处理器串联成链,实现了请求发送与处理的解耦,支持动态组合处理流程。其核心价值在于分离职责并允许请求沿链传递,特别适合审批、过滤、事件处理等场景。使用时需注意避免责任链过长导致的性能问题,并确保每个请求都能被处理(如在链尾添加默认处理器)

MySQL 连接数问题深度解析:从错误原因到根治方案

“too many connections” 错误是 MySQL 中常见的连接数超限问题,表面原因是当前连接数超过了 max_connections 限制,但本质往往是连接管理不当或应用设计缺陷。本文将从错误原理、诊断方法到优化方案,全面解决连接数过多的问题。

“too many connections” 错误原理

MySQL 对同时建立的连接数有上限控制(由 max_connections 参数定义),当新请求的连接数超过该值时,会拒绝连接并抛出错误:
ERROR 1040 (08004): Too many connections

连接数上限的限制因素

max_connections 并非可以无限调大,受以下因素制约:

  • 内存资源:每个 MySQL 连接会占用一定内存(约 200KB~ 几 MB,取决于查询复杂度),过多连接会耗尽服务器内存。
  • 文件描述符:MySQL 每个连接对应一个文件描述符,操作系统对进程的文件描述符有上限(如 ulimit -n 通常默认 1024)。
  • 性能瓶颈:过多连接会导致 MySQL 线程上下文切换频繁,CPU 负载飙升,反而降低性能。

默认值与有效上限

  • 默认 max_connections151(MySQL 5.7+),最小为 1,理论最大可设为 100000(但受系统限制)。
  • 实际有效上限:通常建议不超过 500~1000(除非服务器内存充足且连接均为短连接)。

连接数过多的常见原因

解决 “too many connections” 的核心是找到连接数激增的根源,而非单纯调大 max_connections。常见原因包括:

应用未正确释放连接

  • 连接泄漏:应用使用连接后未调用 close() 释放(如 Java 未关闭 Connection,Python 未关闭 cursor),导致连接长期处于 Sleep 状态。
  • 连接池配置不当:连接池的 maxPoolSize 设得过大(如超过 max_connections),或未设置 maxIdleTime 回收闲置连接。

慢查询 / 长事务阻塞连接

  • 耗时过长的查询(如未加索引的全表扫描)或未提交的长事务,会导致连接长期处于 RunningLocked 状态,无法释放,新连接不断累积。
阅读全文 »

Java 网络 IO 模型详解:从 Linux 内核到实践

网络 IO 是分布式系统的核心基础,其性能直接影响程序的并发能力和响应速度。本文从 Linux 内核的 5 种网络 IO 模型出发,解析 Java 中 BIO、NIO 等模型的实现原理、优缺点及适用场景,帮助理解高并发网络编程的底层逻辑。

Linux 网络 IO 模型:5 种核心模式

网络 IO 的本质是 “数据从外部设备(如网卡)传输到用户进程缓冲区” 的过程,涉及两个关键阶段:

  1. 数据准备阶段:数据从设备复制到内核缓冲区(Kernel Buffer)。
  2. 数据复制阶段:数据从内核缓冲区复制到用户进程缓冲区(User Buffer)。

根据这两个阶段的 “等待方式” 不同,Linux 定义了 5 种 IO 模型:

阻塞 IO 模型(Blocking IO)

  • 核心特点:进程发起 IO 操作后,会一直阻塞(挂起),直到两个阶段完成(数据准备 + 复制到用户缓冲区)才唤醒。
  • 流程:
    • 用户进程调用 recvfrom 系统调用,内核开始数据准备。
    • 数据未准备好时,进程进入阻塞状态(释放 CPU)。
    • 数据准备完成后,内核将数据从内核缓冲区复制到用户缓冲区,然后唤醒进程,recvfrom 返回。
  • 优缺点:实现简单,但进程阻塞期间无法处理其他任务,并发能力极差。

非阻塞 IO 模型(Non-Blocking IO)

  • 核心特点:进程发起 IO 操作后不阻塞,若数据未准备好,立即返回错误(如 EWOULDBLOCK);进程需定期轮询检查数据是否就绪。
  • 流程:
    • 用户进程调用 recvfrom,若数据未准备好,内核立即返回错误(非阻塞)。
    • 进程不断轮询调用 recvfrom,直到数据准备好。
    • 数据准备完成后,内核将数据复制到用户缓冲区,recvfrom 返回成功。
  • 优缺点:进程无需阻塞,但轮询会消耗 CPU 资源,效率较低。

IO 复用模型(IO Multiplexing)

  • 核心特点:通过 select/poll/epoll 等系统调用,单个进程可同时监控多个文件描述符(FD)的 IO 事件,阻塞等待任一事件就绪后再处理。
  • 流程:
    • 进程调用 select,传入需监控的 FD 集合,阻塞等待。
    • 内核监控这些 FD,当任一 FD 数据就绪(或超时),select 返回就绪的 FD 数量。
    • 进程遍历就绪的 FD,调用 recvfrom 完成数据复制。
  • 关键改进:
    • select/poll:采用轮询方式检查 FD 状态,支持的 FD 数量有限(select 通常为 1024)。
    • epoll(Linux 2.6+):基于事件驱动,通过回调函数通知就绪 FD,无 FD 数量限制,性能远超 select/poll
  • 优缺点:单进程管理多 FD,减少线程开销,适合高并发;但仍需主动调用 recvfrom 完成数据复制(同步模型)。

信号驱动 IO 模型(Signal-Driven IO)

  • 核心特点:进程通过 sigaction 注册信号处理函数,内核数据准备好后发送 SIGIO 信号通知进程,进程再调用 recvfrom 复制数据。
  • 流程:
    • 进程调用 sigaction 注册信号处理函数(非阻塞,立即返回)。
    • 数据准备好时,内核发送 SIGIO 信号,触发处理函数。
    • 处理函数中调用 recvfrom,将数据从内核缓冲区复制到用户缓冲区。
  • 优缺点:数据准备阶段非阻塞,但数据复制阶段仍需进程主动处理,适用场景有限(如 UDP 协议)。

异步 IO 模型(Asynchronous IO)

阅读全文 »

Java 压缩与解压缩操作详解:ZIP 与 GZIP 实践

在 Java 中,处理压缩文件是常见需求,尤其是在文件传输、存储优化等场景。JDK 内置了对 ZIP 和 GZIP 格式的支持,通过 java.util.zip 包中的类可实现压缩和解压缩操作。本文将详细介绍 ZIP 和 GZIP 的使用方法,包括单文件 / 多文件压缩、文件夹递归压缩及解压缩的具体实现。

ZIP 压缩与解压缩

ZIP 是一种广泛使用的归档格式,支持将多个文件或文件夹压缩到一个 .zip 文件中,且可保留目录结构。Java 中通过 ZipInputStream(解压缩)和 ZipOutputStream(压缩)实现 ZIP 操作。

ZIP 解压缩(ZipInputStream

ZipInputStream 用于读取 ZIP 文件中的条目(文件或文件夹),通过 getNextEntry() 遍历所有条目,再通过输入流读取具体内容。

核心步骤:
  1. 创建 ZipInputStream 关联 ZIP 文件输入流。
  2. 循环调用 getNextEntry() 获取每个 ZipEntry(条目)。
  3. 对每个条目,通过 ZipInputStream 读取数据(若为文件夹则跳过内容读取)。
  4. 处理完成后调用 closeEntry() 关闭当前条目,最终关闭流。
示例:解压缩 ZIP 文件到指定目录
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
import java.io.*;
import java.nio.charset.StandardCharsets;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;

public class ZipUncompressor {
/**
* 解压缩 ZIP 文件到目标目录
* @param zipFilePath ZIP 文件路径
* @param destDir 目标目录
*/
public static void unzip(String zipFilePath, String destDir) throws IOException {
// 创建目标目录(不存在则创建)
File destDirFile = new File(destDir);
if (!destDirFile.exists()) {
destDirFile.mkdirs();
}

try (ZipInputStream zis = new ZipInputStream(
new BufferedInputStream(new FileInputStream(zipFilePath)),
StandardCharsets.UTF_8)) { // 指定编码(解决中文乱码)

ZipEntry entry;
// 遍历所有条目
while ((entry = zis.getNextEntry()) != null) {
String entryName = entry.getName();
File entryFile = new File(destDirFile, entryName);

if (entry.isDirectory()) {
// 若为文件夹,创建目录
entryFile.mkdirs();
} else {
// 若为文件,读取内容并写入目标文件
// 确保父目录存在
File parentDir = entryFile.getParentFile();
if (!parentDir.exists()) {
parentDir.mkdirs();
}

try (BufferedOutputStream bos = new BufferedOutputStream(
new FileOutputStream(entryFile))) {
byte[] buffer = new byte[1024];
int len;
while ((len = zis.read(buffer)) != -1) {
bos.write(buffer, 0, len);
}
}
}
zis.closeEntry(); // 关闭当前条目
}
}
}

public static void main(String[] args) throws IOException {
unzip("test.zip", "unzip_result");
System.out.println("解压缩完成!");
}
}
注意事项:
  • 中文乱码ZipInputStream 构造器需指定编码(如 StandardCharsets.UTF_8),避免条目名称中文乱码。
  • 目录处理:通过 entry.isDirectory() 判断是否为文件夹,需创建对应目录结构。
阅读全文 »

计算机中的浮点数:二进制表示与存储机制

浮点数是计算机中表示小数的核心方式,其设计巧妙但也因二进制特性存在独特的精度问题。以下从二进制小数表示、浮点数结构、IEEE 754 标准等方面详细解析:

二进制表示小数:精度问题的根源

计算机以二进制存储数据,十进制小数需转换为二进制的 “负指数幂之和”,这是浮点数精度问题的核心原因。

二进制小数的表示规则

二进制小数的整数部分与整数的二进制表示一致(如十进制5→二进制101),小数部分则通过 “2 的负指数幂累加” 表示:

  • 二进制小数 b₀.b₁b₂...bₙ 对应十进制的值为:
    b₀×2⁰ + b₁×2⁻¹ + b₂×2⁻² + ... + bₙ×2⁻ⁿ
    (其中b₀是整数位,b₁~bₙ是小数位,取值为 0 或 1)

示例

  • 二进制 1.011 转换为十进制:
    1×2⁰ + 0×2⁻¹ + 1×2⁻² + 1×2⁻³ = 1 + 0 + 0.25 + 0.125 = 1.375(精确表示)。
  • 十进制 0.1 转换为二进制:
    结果是无限循环小数 0.0001100110011...,计算机无法用有限位存储,只能近似表示(这就是0.1 + 0.2 ≠ 0.3的原因)。

浮点数的组成:符号、尾数与指数

浮点数通过 “科学计数法” 的二进制形式表示,核心是用符号、尾数、指数三部分描述一个数,格式为:

1
2
3
4
5
// 指数也叫做阶码,阶码一般是用移码表示,尾数一般是补码表示(IEEE754标准中尾数也可以用原码表示),阶码的正负叫阶符,尾数的符号位叫数符
// 阶码决定数的表示范围
// 尾数决定数的有效精度
// 对阶时,小数向大数看齐,通过较小的尾数右移实现的
符号 尾数 * 基数 ^ (指数)

如果浮点数的阶码(包括一位阶符)用R位的移码表示,尾数(包括一位数符)用M位的补码表示,则浮点数表示的数值范围时

最大正数为 $+(1-2^{-M+1})*2^{2^{R-1}-1}$

最小负数为$-1*2^{(2^{R-1}-1)}$

(计算机中基数固定为 2,因此无需存储,只需记录符号、尾数和指数)

阅读全文 »