Java后端开发面试题详细解答
一、编程实现题
1. 判断今天是一年当中的第几天
import java.time.LocalDate; import java.util.Calendar; public class DayOfYear { // 方法1:使用Java 8 LocalDate(推荐) public static int getDayOfYear1() { LocalDate today = LocalDate.now(); return today.getDayOfYear(); } // 方法2:使用Calendar public static int getDayOfYear2() { Calendar calendar = Calendar.getInstance(); return calendar.get(Calendar.DAY_OF_YEAR); } // 方法3:手动计算(面试常考) public static int getDayOfYear3(int year, int month, int day) { int[] daysOfMonth = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}; // 判断闰年 if (isLeapYear(year)) { daysOfMonth[1] = 29; } int totalDays = 0; // 累加前面月份的天数 for (int i = 0; i < month - 1; i++) { totalDays += daysOfMonth[i]; } // 加上当月的天数 totalDays += day; return totalDays; } // 判断闰年 private static boolean isLeapYear(int year) { return (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0); } public static void main(String[] args) { System.out.println("方法1: " + getDayOfYear1()); System.out.println("方法2: " + getDayOfYear2()); System.out.println("方法3: " + getDayOfYear3(2024, 12, 15)); } }2. 输出200以内的质数
public class PrimeNumber { // 方法1:基础判断法 public static void printPrimes1(int n) { for (int i = 2; i <= n; i++) { if (isPrime(i)) { System.out.print(i + " "); } } } private static boolean isPrime(int num) { if (num < 2) return false; // 只需判断到sqrt(num)即可 for (int i = 2; i <= Math.sqrt(num); i++) { if (num % i == 0) { return false; } } return true; } // 方法2:埃拉托斯特尼筛法(效率更高) public static void printPrimes2(int n) { boolean[] notPrime = new boolean[n + 1]; for (int i = 2; i <= Math.sqrt(n); i++) { if (!notPrime[i]) { // 将i的倍数标记为非质数 for (int j = i * i; j <= n; j += i) { notPrime[j] = true; } } } // 输出质数 for (int i = 2; i <= n; i++) { if (!notPrime[i]) { System.out.print(i + " "); } } } // 方法3:Java 8 Stream(简洁写法) public static void printPrimes3(int n) { java.util.stream.IntStream.rangeClosed(2, n) .filter(PrimeNumber::isPrime) .forEach(num -> System.out.print(num + " ")); } public static void main(String[] args) { System.out.println("200以内的质数:"); printPrimes2(200); } }输出结果:
2 3 5 7 11 13 17 19 23 29 31 37 41 43 47 53 59 61 67 71 73 79 83 89 97 101 103 107 109 113 127 131 137 139 149 151 157 163 167 173 179 181 191 193 197 199二、Linux 相关
1. Linux 编辑文件的命令
# 常用编辑器 vim filename # 最常用 vi filename # 基础版本 nano filename # 简单易用Vim 详细操作:
┌─────────────────────────────────────────────────────────────┐ │ Vim 三种模式 │ ├─────────────────────────────────────────────────────────────┤ │ │ │ 命令模式 ───────[i/a/o]────────> 插入模式 │ │ ↑ ↓ │ │ └────────────[Esc]───────────────┘ │ │ │ │ │ └────────[:wq/:q!]─────────> 退出 │ │ │ └─────────────────────────────────────────────────────────────┘操作 | 命令 | 说明 |
进入编辑模式 |
| 在光标前插入 |
| 在光标后插入 | |
| 在下一行插入 | |
| 在上一行插入 | |
跳转最后一行 |
| 命令模式下按大写G |
| 输入冒号+美元符 | |
跳转指定行 |
| 跳转到第n行,如 |
| 跳转到第n行,如 | |
跳转第一行 |
| 跳转到首行 |
| 跳转到第1行 | |
保存退出 |
| 保存并退出 |
| 强制退出不保存 | |
| 仅保存 |
2. Linux 下查看进程号的方法
# 方法1:ps 命令(最常用) ps -ef | grep java # 查看java进程 ps -aux | grep nginx # 查看nginx进程 ps -ef | grep 进程名 | grep -v grep # 过滤grep自身 # 方法2:pgrep 命令(直接获取PID) pgrep java # 获取java进程号 pgrep -f "spring-boot" # 按完整命令行匹配 # 方法3:pidof 命令 pidof nginx # 获取nginx的PID # 方法4:top 命令(动态查看) top # 实时查看进程 top -p PID # 查看指定进程 # 方法5:通过端口查进程 lsof -i:8080 # 查看8080端口的进程 netstat -tlnp | grep 8080 # 查看8080端口 ss -tlnp | grep 8080 # 更现代的方式 # 方法6:jps命令(查看Java进程) jps # 查看所有Java进程 jps -l # 显示完整包名 jps -v # 显示JVM参数三、数据库相关
1. 查询张三的电话、成绩
-- 假设表结构 -- 学生表 student: id, name, phone -- 成绩表 score: id, student_id, subject, score -- 方法1:内连接 SELECT s.name, s.phone, sc.subject, sc.score FROM student s INNER JOIN score sc ON s.id = sc.student_id WHERE s.name = '张三'; -- 方法2:左连接(即使没有成绩也显示) SELECT s.name, s.phone, sc.subject, sc.score FROM student s LEFT JOIN score sc ON s.id = sc.student_id WHERE s.name = '张三'; -- 方法3:子查询 SELECT name, phone, (SELECT GROUP_CONCAT(subject, ':', score) FROM score WHERE student_id = student.id) AS scores FROM student WHERE name = '张三'; -- 方法4:查询总成绩 SELECT s.name, s.phone, SUM(sc.score) AS total_score FROM student s LEFT JOIN score sc ON s.id = sc.student_id WHERE s.name = '张三' GROUP BY s.id, s.name, s.phone;2. 删除重复数据只保留一行
-- 假设表名为 t_data,数据完全相同 -- 方法1:使用 ROW_NUMBER()(MySQL 8.0+) DELETE FROM t_data WHERE id IN ( SELECT id FROM ( SELECT id, ROW_NUMBER() OVER( PARTITION BY col1, col2, col3 -- 按所有列分组 ORDER BY id ) AS rn FROM t_data ) t WHERE rn > 1 ); -- 方法2:使用临时表(通用方法) -- 步骤1:创建临时表保留不重复数据 CREATE TABLE t_data_temp AS SELECT DISTINCT * FROM t_data; -- 步骤2:删除原表数据 TRUNCATE TABLE t_data; -- 步骤3:将数据导回 INSERT INTO t_data SELECT * FROM t_data_temp; -- 步骤4:删除临时表 DROP TABLE t_data_temp; -- 方法3:使用 MIN(id) 保留最小id的记录 DELETE FROM t_data WHERE id NOT IN ( SELECT min_id FROM ( SELECT MIN(id) AS min_id FROM t_data GROUP BY col1, col2, col3 ) t ); -- 方法4:自连接删除(MySQL 5.7) DELETE t1 FROM t_data t1 INNER JOIN t_data t2 WHERE t1.id > t2.id AND t1.col1 = t2.col1 AND t1.col2 = t2.col2 AND t1.col3 = t2.col3;3. MySQL 相关八股
┌─────────────────────────────────────────────────────────────────────┐ │ MySQL 核心知识点 │ ├─────────────────────────────────────────────────────────────────────┤ │ │ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐│ │ │ 索引 │ │ 事务 │ │ 锁机制 │ │ 日志系统 ││ │ │ ───────── │ │ ───────── │ │ ───────── │ │ ───────── ││ │ │ • B+树 │ │ • ACID │ │ • 行锁 │ │ • redo log ││ │ │ • 聚簇索引 │ │ • 隔离级别 │ │ • 表锁 │ │ • undo log ││ │ │ • 覆盖索引 │ │ • MVCC │ │ • 间隙锁 │ │ • binlog ││ │ │ • 最左前缀 │ │ • 脏读/幻读 │ │ • 临键锁 │ │ • slow log ││ │ └─────────────┘ └─────────────┘ └─────────────┘ └─────────────┘│ │ │ └─────────────────────────────────────────────────────────────────────┘3.1 索引相关
/** * 索引类型: * 1. B+树索引(最常用) * 2. Hash索引 * 3. 全文索引 * 4. R-Tree索引 */ // B+树优势: // - 叶子节点形成有序链表,范围查询高效 // - 非叶子节点只存key,扇出高,树矮 // - 所有数据都在叶子节点,查询稳定 // 聚簇索引 vs 非聚簇索引 // 聚簇索引:叶子节点存储完整行数据(InnoDB主键索引) // 非聚簇索引:叶子节点存储主键值(需要回表) // 覆盖索引:查询的列都在索引中,无需回表 SELECT id, name FROM user WHERE name = 'xxx'; -- 如果(name)索引包含id // 最左前缀原则 // 索引(a, b, c) WHERE a = 1 -- 使用索引 WHERE a = 1 AND b = 2 -- 使用索引 WHERE a = 1 AND b = 2 AND c = 3-- 使用索引 WHERE b = 2 -- 不使用索引 WHERE a = 1 AND c = 3 -- 只用a部分3.2 事务 ACID
┌────────────────────────────────────────────────────────────┐ │ ACID 特性 │ ├────────────┬───────────────────────────────────────────────┤ │ Atomicity │ 原子性:事务要么全部成功,要么全部失败 │ │ 原子性 │ 实现:undo log(回滚日志) │ ├────────────┼───────────────────────────────────────────────┤ │ Consistency│ 一致性:数据库从一个一致状态转换到另一个状态 │ │ 一致性 │ 实现:由其他三个特性保证 │ ├────────────┼───────────────────────────────────────────────┤ │ Isolation │ 隔离性:并发事务之间互不干扰 │ │ 隔离性 │ 实现:锁 + MVCC │ ├────────────┼───────────────────────────────────────────────┤ │ Durability │ 持久性:事务提交后永久保存 │ │ 持久性 │ 实现:redo log(重做日志) │ └────────────┴───────────────────────────────────────────────┘3.3 隔离级别
┌──────────────────┬─────────┬───────────────┬─────────┐ │ 隔离级别 │ 脏读 │ 不可重复读 │ 幻读 │ ├──────────────────┼─────────┼───────────────┼─────────┤ │ READ UNCOMMITTED │ ✓ │ ✓ │ ✓ │ │ READ COMMITTED │ ✗ │ ✓ │ ✓ │ │ REPEATABLE READ │ ✗ │ ✗ │ ✓* │ │ SERIALIZABLE │ ✗ │ ✗ │ ✗ │ └──────────────────┴─────────┴───────────────┴─────────┘ * MySQL的RR级别通过MVCC+间隙锁解决了大部分幻读问题3.4 MVCC(多版本并发控制)
┌─────────────────────────────────────────────────────────┐ │ MVCC 原理 │ ├─────────────────────────────────────────────────────────┤ │ │ │ 每行记录包含隐藏列: │ │ • DB_TRX_ID : 最近修改的事务ID │ │ • DB_ROLL_PTR : 回滚指针,指向undo log │ │ • DB_ROW_ID : 隐藏主键 │ │ │ │ ReadView(读视图): │ │ • m_ids : 活跃事务ID列表 │ │ • min_trx_id : 最小活跃事务ID │ │ • max_trx_id : 预分配事务ID │ │ • creator_trx_id: 创建该ReadView的事务ID │ │ │ │ 可见性判断: │ │ 1. trx_id < min_trx_id → 可见 │ │ 2. trx_id >= max_trx_id → 不可见 │ │ 3. min <= trx_id < max: │ │ - 在m_ids中 → 不可见 │ │ - 不在m_ids中 → 可见 │ │ │ └─────────────────────────────────────────────────────────┘4. 实习中用 MySQL 做了哪些工作(参考回答)
1. 数据库表设计 - 根据业务需求设计表结构 - 确定主键、索引、外键关系 - 进行数据库范式优化(3NF) 2. SQL 编写与优化 - 编写复杂查询语句(多表JOIN、子查询) - 使用 EXPLAIN 分析慢查询 - 优化索引提升查询性能 3. 数据迁移与同步 - 使用 mysqldump 进行数据备份 - 编写数据迁移脚本 - 处理历史数据归档 4. 性能优化实践 - 慢查询日志分析 - 索引优化(创建、删除冗余索引) - SQL语句改写优化 5. 日常运维 - 监控数据库性能指标 - 处理锁等待问题 - 数据一致性校验5. SQL 优化方法
┌─────────────────────────────────────────────────────────────┐ │ SQL 优化思路 │ ├─────────────────────────────────────────────────────────────┤ │ │ │ 1. 分析阶段 │ │ ├── EXPLAIN 查看执行计划 │ │ ├── 关注 type、key、rows、Extra │ │ └── 慢查询日志分析 │ │ │ │ 2. 索引优化 │ │ ├── 创建合适索引 │ │ ├── 避免索引失效(函数、类型转换、OR) │ │ ├── 使用覆盖索引 │ │ └── 删除冗余索引 │ │ │ │ 3. SQL 改写 │ │ ├── 避免 SELECT * │ │ ├── 小表驱动大表 │ │ ├── 用 EXISTS 替代 IN │ │ ├── 避免在 WHERE 中使用函数 │ │ └── 合理使用 LIMIT │ │ │ │ 4. 架构优化 │ │ ├── 读写分离 │ │ ├── 分库分表 │ │ ├── 添加缓存层(Redis) │ │ └── 数据归档 │ │ │ └─────────────────────────────────────────────────────────────┘-- 常见索引失效场景 -- 1. 使用函数 WHERE DATE(create_time) = '2024-01-01' -- 失效 WHERE create_time >= '2024-01-01' AND create_time < '2024-01-02' -- 有效 -- 2. 类型转换 WHERE phone = 13800138000 -- phone是varchar,失效 WHERE phone = '13800138000' -- 有效 -- 3. LIKE以%开头 WHERE name LIKE '%张%' -- 失效 WHERE name LIKE '张%' -- 有效 -- 4. OR条件 WHERE a = 1 OR b = 2 -- 如果b没索引,整体失效 -- 5. != 或 NOT IN WHERE status != 1 -- 可能失效6. 分库分表优化
┌─────────────────────────────────────────────────────────────┐ │ 分表后数据量仍然大的优化方案 │ ├─────────────────────────────────────────────────────────────┤ │ │ │ 1. 继续分表(增加分片数) │ │ └── 原来10张表 → 扩展到100张表 │ │ │ │ 2. 引入分库 │ │ ├── 水平分库:相同结构,数据分散到多个库 │ │ └── 垂直分库:按业务拆分到不同库 │ │ │ │ 3. 冷热数据分离 │ │ ├── 热数据:MySQL(近3个月数据) │ │ └── 冷数据:归档到HBase/ES/文件系统 │ │ │ │ 4. 引入缓存 │ │ ├── Redis缓存热点数据 │ │ └── 本地缓存(Caffeine) │ │ │ │ 5. 读写分离 │ │ ├── 主库:写操作 │ │ └── 从库:读操作(多个从库负载均衡) │ │ │ │ 6. 使用分布式数据库 │ │ ├── TiDB │ │ ├── OceanBase │ │ └── CockroachDB │ │ │ │ 7. 数据压缩和归档 │ │ ├── 定期归档历史数据 │ │ └── 使用压缩表 │ │ │ │ 8. 优化分片策略 │ │ ├── 根据查询模式选择分片键 │ │ └── 避免数据倾斜 │ │ │ └─────────────────────────────────────────────────────────────┘// 分片策略示例 // 1. 按用户ID取模 int shardIndex = userId % 10; // 分10张表 // 2. 按时间范围 String tableSuffix = dateFormat.format(createTime); // 按月分表 // 3. 一致性Hash(扩容友好) int hash = consistentHash(userId);7. MySQL 锁相关
┌─────────────────────────────────────────────────────────────────┐ │ MySQL 锁分类 │ ├─────────────────────────────────────────────────────────────────┤ │ │ │ 按粒度分: │ │ ┌────────────┐ ┌────────────┐ ┌────────────┐ │ │ │ 表锁 │ │ 行锁 │ │ 页锁 │ │ │ │ ──────── │ │ ──────── │ │ ──────── │ │ │ │ MyISAM │ │ InnoDB │ │ BDB │ │ │ │ 开销小 │ │ 开销大 │ │ 介于两者 │ │ │ │ 并发度低 │ │ 并发度高 │ │ │ │ │ └────────────┘ └────────────┘ └────────────┘ │ │ │ │ 按类型分: │ │ ┌────────────┐ ┌────────────┐ │ │ │ 共享锁(S) │ │ 排他锁(X) │ │ │ │ ──────── │ │ ──────── │ │ │ │ 读锁 │ │ 写锁 │ │ │ │ 多个事务 │ │ 只允许一个 │ │ │ │ 可同时持有│ │ 事务持有 │ │ │ └────────────┘ └────────────┘ │ │ │ │ InnoDB 行锁类型: │ │ ┌────────────┐ ┌────────────┐ ┌────────────┐ │ │ │ 记录锁 │ │ 间隙锁 │ │ 临键锁 │ │ │ │ Record Lock│ │ Gap Lock │ │ Next-Key │ │ │ │ ──────── │ │ ──────── │ │ ──────── │ │ │ │ 锁定单行 │ │ 锁定间隙 │ │ 记录+间隙 │ │ │ │ │ │ 防止幻读 │ │ 默认行锁 │ │ │ └────────────┘ └────────────┘ └────────────┘ │ │ │ └─────────────────────────────────────────────────────────────────┘-- 共享锁(S锁) SELECT * FROM table WHERE id = 1 LOCK IN SHARE MODE; -- 排他锁(X锁) SELECT * FROM table WHERE id = 1 FOR UPDATE; -- 意向锁(表级锁,自动加) -- IS锁:事务想要获取表中某些行的共享锁 -- IX锁:事务想要获取表中某些行的排他锁四、计算机网络相关
1. TCP 4次挥手
┌──────────────────────────────────────────────────────────────────┐ │ TCP 四次挥手过程 │ ├──────────────────────────────────────────────────────────────────┤ │ │ │ 客户端 服务端 │ │ (主动关闭方) (被动关闭方) │ │ │ │ │ │ │ ─────── FIN=1, seq=u ───────────> │ │ │ │ 第一次挥手 │ │ │ FIN_WAIT_1 │ │ │ │ │ │ │ │ <─────── ACK=1, ack=u+1 ───────── │ │ │ │ 第二次挥手 │ │ │ FIN_WAIT_2 CLOSE_WAIT │ │ │ │ │ │ │ (服务端继续发送剩余数据) │ │ │ │ │ │ │ │ <─────── FIN=1, seq=v ────────── │ │ │ │ 第三次挥手 │ │ │ TIME_WAIT LAST_ACK │ │ │ │ │ │ │ ─────── ACK=1, ack=v+1 ─────────> │ │ │ │ 第四次挥手 │ │ │ │ CLOSED │ │ │ │ │ │ 等待2MSL │ │ │ CLOSED │ │ │ │ │ │ └──────────────────────────────────────────────────────────────────┘为什么是4次不是3次?
核心原因:TCP是全双工通信,关闭连接需要双方都确认 ┌─────────────────────────────────────────────────────────────┐ │ 为什么不能是3次? │ ├─────────────────────────────────────────────────────────────┤ │ │ │ 三次握手可以合并的原因: │ │ → 建立连接时,服务端收到SYN后可以立即返回SYN+ACK │ │ → 因为服务端此时准备好了,可以一起发送 │ │ │ │ 四次挥手不能合并的原因: │ │ → 服务端收到FIN后,可能还有数据没发送完 │ │ → 必须先发ACK确认收到FIN │ │ → 等数据发完后,再发送自己的FIN │ │ → 第二次和第三次之间可能有时间间隔 │ │ │ │ 可以合并成3次的特殊情况: │ │ → 服务端没有数据要发送时,可以将ACK和FIN一起发送 │ │ → 这就是TCP延迟确认机制 │ │ │ └─────────────────────────────────────────────────────────────┘TIME_WAIT 等待 2MSL 的原因:
1. 确保最后的ACK能到达服务端 - 如果ACK丢失,服务端会重发FIN - 客户端需要能够重新发送ACK 2. 确保旧连接的数据包在网络中消失 - 防止新连接收到旧连接的延迟数据 - MSL(Maximum Segment Lifetime)= 报文最大生存时间2. HTTP 中 GET 和 POST 的区别
注意:GET和POST是HTTP协议的方法,不是TCP的概念
┌─────────────────┬────────────────────────┬────────────────────────┐ │ 对比项 │ GET │ POST │ ├─────────────────┼────────────────────────┼────────────────────────┤ │ 语义 │ 获取资源(幂等) │ 提交数据(非幂等) │ ├─────────────────┼────────────────────────┼────────────────────────┤ │ 参数位置 │ URL中(?key=value) │ 请求体中 │ ├─────────────────┼────────────────────────┼────────────────────────┤ │ 数据长度 │ URL有长度限制(2KB左右) │ 理论上无限制 │ ├─────────────────┼────────────────────────┼────────────────────────┤ │ 安全性 │ 参数暴露在URL中 │ 参数在请求体中,相对安全│ ├─────────────────┼────────────────────────┼────────────────────────┤ │ 缓存 │ 可被浏览器缓存 │ 不会被缓存 │ ├─────────────────┼────────────────────────┼────────────────────────┤ │ 书签/历史记录 │ 可保存为书签 │ 不能保存为书签 │ ├─────────────────┼────────────────────────┼────────────────────────┤ │ 编码类型 │ application/x-www-form │ 多种(包括multipart) │ ├─────────────────┼────────────────────────┼────────────────────────┤ │ TCP数据包 │ 1个(header+data一起) │ 2个(先header后data)* │ └─────────────────┴────────────────────────┴────────────────────────┘ * 某些浏览器实现中POST会发2次,但这不是标准规定// Java 中的使用示例 @RestController public class UserController { // GET 请求 - 获取数据 @GetMapping("/user/{id}") public User getUser(@PathVariable Long id) { return userService.findById(id); } // POST 请求 - 创建数据 @PostMapping("/user") public User createUser(@RequestBody User user) { return userService.save(user); } }3. 输入 URL 并回车后的完整过程
┌─────────────────────────────────────────────────────────────────────┐ │ 输入 URL 到页面展示的完整过程 │ ├─────────────────────────────────────────────────────────────────────┤ │ │ │ 1. URL 解析 │ │ ├── 浏览器解析URL(协议、域名、端口、路径) │ │ └── 检查是否有缓存(强缓存 → 协商缓存) │ │ │ │ 2. DNS 解析(域名 → IP地址) │ │ ├── 浏览器DNS缓存 │ │ ├── 操作系统DNS缓存(/etc/hosts) │ │ ├── 路由器DNS缓存 │ │ ├── ISP DNS服务器 │ │ └── 根域名服务器 → 顶级域名服务器 → 权威域名服务器 │ │ │ │ 3. 建立 TCP 连接(三次握手) │ │ ├── SYN │ │ ├── SYN + ACK │ │ └── ACK │ │ │ │ 4. TLS 握手(HTTPS) │ │ ├── 客户端发送支持的加密套件 │ │ ├── 服务端选择加密套件,发送证书 │ │ ├── 客户端验证证书,生成对称密钥 │ │ └── 建立加密通道 │ │ │ │ 5. 发送 HTTP 请求 │ │ ├── 请求行(GET /path HTTP/1.1) │ │ ├── 请求头(Host, User-Agent, Cookie...) │ │ └── 请求体(POST请求) │ │ │ │ 6. 服务器处理请求 │ │ ├── Nginx/Apache 接收请求 │ │ ├── 负载均衡转发到应用服务器 │ │ ├── 应用处理(Spring MVC) │ │ └── 返回响应 │ │ │ │ 7. 浏览器接收响应 │ │ ├── 解析响应头(状态码、Content-Type...) │ │ └── 接收响应体 │ │ │ │ 8. 页面渲染 │ │ ├── 解析HTML → DOM树 │ │ ├── 解析CSS → CSSOM树 │ │ ├── 合并成渲染树 │ │ ├── 布局(Layout)计算位置 │ │ ├── 绘制(Paint) │ │ └── 合成(Composite) │ │ │ │ 9. TCP 连接关闭(四次挥手) │ │ (如果是 Keep-Alive 则复用连接) │ │ │ └─────────────────────────────────────────────────────────────────────┘详细流程图: 用户 ─────> 浏览器 ─────> DNS服务器 │ │ │ IP地址 │ │<─────────────┘ │ ├──────> TCP三次握手 ──────> 服务器 │ │ ├──────> TLS握手(HTTPS)──────>│ │ │ ├──────> HTTP请求 ─────────────>│ │ │ │<─────────── HTTP响应 ─────────┤ │ │ ├──────> 解析渲染 │ │ │ └──────> TCP四次挥手 ──────────>│五、操作系统相关
1. 怎么解决死锁
┌─────────────────────────────────────────────────────────────┐ │ 死锁的四个必要条件 │ ├─────────────────────────────────────────────────────────────┤ │ 1. 互斥条件:资源只能被一个进程使用 │ │ 2. 持有并等待:持有资源同时等待其他资源 │ │ 3. 不可剥夺:已获得的资源不能被强制剥夺 │ │ 4. 循环等待:进程间形成循环等待链 │ └─────────────────────────────────────────────────────────────┘解决死锁的四种策略:
┌─────────────────────────────────────────────────────────────────────┐ │ 解决死锁的方法 │ ├─────────────────────────────────────────────────────────────────────┤ │ │ │ 1. 预防死锁(破坏必要条件) │ │ ├── 破坏互斥条件:不太现实 │ │ ├── 破坏持有并等待:一次性申请所有资源 │ │ ├── 破坏不可剥夺:申请不到时释放已有资源 │ │ └── 破坏循环等待:按序申请资源(资源排序法) │ │ │ │ 2. 避免死锁 │ │ └── 银行家算法:分配前判断是否处于安全状态 │ │ │ │ 3. 检测死锁 │ │ ├── 资源分配图法 │ │ └── 死锁检测算法 │ │ │ │ 4. 解除死锁 │ │ ├── 终止进程:终止死锁进程释放资源 │ │ ├── 资源剥夺:从死锁进程强制剥夺资源 │ │ └── 进程回退:回退到安全状态 │ │ │ └─────────────────────────────────────────────────────────────────────┘Java 中处理死锁:
// 1. 按固定顺序获取锁(破坏循环等待) public class DeadlockPrevention { private final Object lock1 = new Object(); private final Object lock2 = new Object(); // 始终按相同顺序获取锁 public void method1() { synchronized (lock1) { synchronized (lock2) { // do something } } } public void method2() { synchronized (lock1) { // 同样先获取lock1 synchronized (lock2) { // do something } } } } // 2. 使用 tryLock 设置超时 public class TryLockExample { private final Lock lock1 = new ReentrantLock(); private final Lock lock2 = new ReentrantLock(); public void method() { try { if (lock1.tryLock(1, TimeUnit.SECONDS)) { try { if (lock2.tryLock(1, TimeUnit.SECONDS)) { try { // do something } finally { lock2.unlock(); } } } finally { lock1.unlock(); } } } catch (InterruptedException e) { Thread.currentThread().interrupt(); } } } // 3. 使用 jstack 检测死锁 // jstack <pid> | grep -A 30 "deadlock"六、测试用例设计
1. 面试聊天界面测试用例
┌─────────────────────────────────────────────────────────────────────┐ │ 面试聊天界面测试用例 │ ├───────────────┬─────────────────────────────────────────────────────┤ │ 测试类型 │ 测试点 │ ├───────────────┼─────────────────────────────────────────────────────┤ │ │ • 正常发送文本消息 │ │ │ • 发送空消息 │ │ 功能测试 │ • 发送超长消息(边界值测试) │ │ │ • 发送特殊字符(emoji、HTML标签) │ │ │ • 消息发送成功/失败状态显示 │ │ │ • 消息时间戳显示 │ │ │ • 消息已读/未读状态 │ ├───────────────┼─────────────────────────────────────────────────────┤ │ │ • 输入框焦点获取 │ │ 输入测试 │ • 复制粘贴功能 │ │ │ • 键盘回车发送 │ │ │ • 输入法切换 │ ├───────────────┼─────────────────────────────────────────────────────┤ │ │ • 滚动加载历史消息 │ │ 交互测试 │ • 下拉刷新 │ │ │ • 点击发送按钮 │ │ │ • 消息列表滚动 │ │ │ • 新消息自动滚动到底部 │ ├───────────────┼─────────────────────────────────────────────────────┤ │ │ • 消息到达时间(响应时间 < 1s) │ │ 性能测试 │ • 大量消息时的渲染性能 │ │ │ • 并发消息处理能力 │ │ │ • 内存占用 │ ├───────────────┼─────────────────────────────────────────────────────┤ │ │ • 网络断开时的表现 │ │ 异常测试 │ • 弱网环境下消息发送 │ │ │ • 消息发送超时处理 │ │ │ • 重复点击发送按钮 │ ├───────────────┼─────────────────────────────────────────────────────┤ │ │ • Chrome/Firefox/Safari/Edge │ │ 兼容性测试 │ • 不同分辨率显示 │ │ │ • 移动端适配 │ │ │ • 不同操作系统 │ ├───────────────┼─────────────────────────────────────────────────────┤ │ │ • XSS攻击防护(脚本注入) │ │ 安全测试 │ • 消息加密传输 │ │ │ • 敏感信息过滤 │ └───────────────┴─────────────────────────────────────────────────────┘2. 铅笔测试用例
┌─────────────────────────────────────────────────────────────────────┐ │ 铅笔测试用例 │ ├───────────────┬─────────────────────────────────────────────────────┤ │ 测试类型 │ 测试点 │ ├───────────────┼─────────────────────────────────────────────────────┤ │ │ • 能否正常书写 │ │ 功能测试 │ • 书写是否流畅清晰 │ │ │ • 橡皮擦功能(如果有) │ │ │ • 削铅笔后能否继续使用 │ ├───────────────┼─────────────────────────────────────────────────────┤ │ │ • 长度符合标准 │ │ 外观测试 │ • 直径符合标准 │ │ │ • 外观颜色、印刷、商标 │ │ │ • 表面光滑无毛刺 │ │ │ • 笔芯居中 │ ├───────────────┼─────────────────────────────────────────────────────┤ │ │ • 笔芯硬度(HB、2B、2H等) │ │ 性能测试 │ • 书写长度(能写多少米) │ │ │ • 笔芯断裂强度 │ │ │ • 书写线条粗细均匀度 │ ├───────────────┼─────────────────────────────────────────────────────┤ │ │ • 铅笔木材是否易削 │ │ 材质测试 │ • 木材是否开裂 │ │ │ • 油漆是否脱落 │ │ │ • 材料环保性(无毒) │ ├───────────────┼─────────────────────────────────────────────────────┤ │ │ • 用力书写是否断裂 │ │ 压力测试 │ • 弯曲测试(弯曲后恢复能力) │ │ │ • 跌落测试 │ ├───────────────┼─────────────────────────────────────────────────────┤ │ │ • 高温环境(50°C) │ │ 环境测试 │ • 低温环境(-20°C) │ │ │ • 潮湿环境 │ │ │ • 长期存放(保质期) │ ├───────────────┼─────────────────────────────────────────────────────┤ │ │ • 使用是否舒适(手感) │ │ 用户体验 │ • 握笔疲劳度 │ │ │ • 重量是否适中 │ ├───────────────┼─────────────────────────────────────────────────────┤ │ │ • 儿童误食安全性 │ │ 安全测试 │ • 笔尖是否过于尖锐 │ │ │ • 符合相关安全标准 │ ├───────────────┼─────────────────────────────────────────────────────┤ │ │ • 包装完整性 │ │ 包装测试 │ • 标签信息完整 │ │ │ • 运输途中是否损坏 │ └───────────────┴─────────────────────────────────────────────────────┘七、Redis 与分布式相关
1. 实习中 Redis 主要用来干什么(参考回答)
1. 缓存层 - 热点数据缓存(用户信息、商品详情) - 减少数据库压力 - 设置合理的过期时间 - 解决缓存穿透、击穿、雪崩问题 2. 分布式锁 - 防止重复提交 - 分布式定时任务 - 库存扣减等并发场景 3. 分布式Session - 用户登录状态共享 - Token存储 4. 计数器/限流 - 接口访问频率限制 - 点赞数、阅读数统计 5. 排行榜 - 使用ZSet实现排行榜 - 积分排名、销量排名 6. 消息队列(简单场景) - 使用List实现简单队列 - 异步任务处理2. 分布式锁实现
┌─────────────────────────────────────────────────────────────────────┐ │ 分布式锁实现方式 │ ├─────────────────────────────────────────────────────────────────────┤ │ │ │ 1. Redis 实现 │ │ ├── SETNX + EXPIRE(有问题,非原子) │ │ ├── SET key value EX seconds NX(推荐) │ │ └── Redisson(更完善的实现) │ │ │ │ 2. Zookeeper 实现 │ │ └── 临时顺序节点 │ │ │ │ 3. 数据库实现 │ │ └── 唯一索引 + 记录行 │ │ │ └─────────────────────────────────────────────────────────────────────┘// 方式1:基于Redis命令实现 @Component public class RedisDistributedLock { @Autowired private StringRedisTemplate redisTemplate; /** * 加锁 * @param lockKey 锁的key * @param requestId 请求标识(防止误删) * @param expireTime 过期时间 */ public boolean tryLock(String lockKey, String requestId, long expireTime) { Boolean result = redisTemplate.opsForValue() .setIfAbsent(lockKey, requestId, expireTime, TimeUnit.SECONDS); return Boolean.TRUE.equals(result); } /** * 释放锁(使用Lua脚本保证原子性) */ public boolean unlock(String lockKey, String requestId) { String script = "if redis.call('get', KEYS[1]) == ARGV[1] then " + " return redis.call('del', KEYS[1]) " + "else " + " return 0 " + "end"; Long result = redisTemplate.execute( new DefaultRedisScript<>(script, Long.class), Collections.singletonList(lockKey), requestId ); return Long.valueOf(1).equals(result); } } // 方式2:使用Redisson(推荐生产使用) @Component public class RedissonLockService { @Autowired private RedissonClient redissonClient; public void doWithLock(String lockKey) { RLock lock = redissonClient.getLock(lockKey); try { // 尝试加锁,最多等待10秒,锁持有30秒自动释放 boolean isLocked = lock.tryLock(10, 30, TimeUnit.SECONDS); if (isLocked) { try { // 业务逻辑 doBusiness(); } finally { lock.unlock(); } } } catch (InterruptedException e) { Thread.currentThread().interrupt(); } } }分布式锁如何保证并发安全?
┌─────────────────────────────────────────────────────────────────────┐ │ 分布式锁并发安全保证 │ ├─────────────────────────────────────────────────────────────────────┤ │ │ │ 1. 原子性操作 │ │ └── SET key value EX seconds NX 是原子命令 │ │ │ │ 2. 唯一标识(防止误删) │ │ └── value存储requestId,释放时校验 │ │ │ │ 3. 过期时间(防止死锁) │ │ └── 设置合理的过期时间 │ │ │ │ 4. 看门狗机制(Redisson) │ │ └── 自动续期,防止业务未完成锁已释放 │ │ │ │ 5. 可重入性 │ │ └── Redisson支持可重入锁 │ │ │ │ 6. 红锁算法(RedLock) │ │ └── 多Redis实例,多数派获取锁成功 │ │ │ └─────────────────────────────────────────────────────────────────────┘3. Redis 高并发
┌─────────────────────────────────────────────────────────────────────┐ │ Redis 高并发解决方案 │ ├─────────────────────────────────────────────────────────────────────┤ │ │ │ 1. 主从复制(读写分离) │ │ ┌─────────┐ │ │ │ Master │ ────写────> 应用 │ │ └────┬────┘ │ │ │复制 │ │ ┌────┴────┐ │ │ │ Slave │ <───读──── 应用 │ │ └─────────┘ │ │ │ │ 2. 哨兵模式(高可用) │ │ • 监控主从节点状态 │ │ • 自动故障转移 │ │ • 客户端自动发现新主节点 │ │ │ │ 3. Cluster 集群(水平扩展) │ │ • 16384个槽位分布到多个节点 │ │ • 数据分片,突破单机内存限制 │ │ • 每个主节点有从节点备份 │ │ │ │ 4. 客户端优化 │ │ • 连接池 │ │ • Pipeline批量操作 │ │ • 多线程/异步操作 │ │ │ │ 5. 数据结构优化 │ │ • 选择合适的数据结构 │ │ • 避免大Key │ │ • 设置合理过期时间 │ │ │ └─────────────────────────────────────────────────────────────────────┘4. 分布式了解
┌─────────────────────────────────────────────────────────────────────┐ │ 分布式核心知识 │ ├─────────────────────────────────────────────────────────────────────┤ │ │ │ 1. CAP 理论 │ │ ├── C (Consistency) : 一致性 │ │ ├── A (Availability) : 可用性 │ │ └── P (Partition tolerance): 分区容错性 │ │ → 三者只能满足其二,分布式系统必须保证P │ │ │ │ 2. BASE 理论 │ │ ├── BA (Basically Available) : 基本可用 │ │ ├── S (Soft state) : 软状态 │ │ └── E (Eventually consistent): 最终一致性 │ │ │ │ 3. 分布式事务 │ │ ├── 2PC(两阶段提交) │ │ ├── 3PC(三阶段提交) │ │ ├── TCC(Try-Confirm-Cancel) │ │ ├── SAGA │ │ └── 消息队列最终一致性 │ │ │ │ 4. 分布式一致性算法 │ │ ├── Paxos │ │ ├── Raft │ │ └── ZAB(Zookeeper) │ │ │ │ 5. 分布式ID生成 │ │ ├── UUID │ │ ├── 数据库自增 │ │ ├── Redis INCR │ │ ├── 雪花算法(Snowflake) │ │ └── 号段模式 │ │ │ │ 6. 服务治理 │ │ ├── 服务注册与发现(Nacos、Eureka) │ │ ├── 负载均衡(Ribbon、LoadBalancer) │ │ ├── 熔断降级(Sentinel、Hystrix) │ │ └── 配置中心(Nacos、Apollo) │ │ │ └─────────────────────────────────────────────────────────────────────┘// 雪花算法示例 public class SnowflakeIdGenerator { // 起始时间戳 (2020-01-01) private final long START_TIMESTAMP = 1577808000000L; // 各部分位数 private final long SEQUENCE_BITS = 12L; // 序列号占位 private final long WORKER_BITS = 5L; // 机器ID占位 private final long DATACENTER_BITS = 5L; // 数据中心占位 private long workerId; private long datacenterId; private long sequence = 0L; private long lastTimestamp = -1L; public synchronized long nextId() { long currentTimestamp = System.currentTimeMillis(); if (currentTimestamp < lastTimestamp) { throw new RuntimeException("时钟回拨"); } if (currentTimestamp == lastTimestamp) { sequence = (sequence + 1) & 4095; // 4095 = 2^12 - 1 if (sequence == 0) { currentTimestamp = waitNextMillis(lastTimestamp); } } else { sequence = 0L; } lastTimestamp = currentTimestamp; return ((currentTimestamp - START_TIMESTAMP) << 22) | (datacenterId << 17) | (workerId << 12) | sequence; } private long waitNextMillis(long lastTimestamp) { long timestamp = System.currentTimeMillis(); while (timestamp <= lastTimestamp) { timestamp = System.currentTimeMillis(); } return timestamp; } }