1. MySQL事务隔离级别深度解析
1.1 四种隔离级别特性对比
MySQL的事务隔离级别是数据库并发控制的核心机制,直接决定了事务之间的可见性规则。让我们通过一个银行转账的案例来理解这四种级别:
-
读未提交(Read Uncommitted):就像两个会计同时操作同一本账簿,A会计刚用铅笔修改了金额(未提交),B会计立刻就能看到这个修改。如果A最后决定擦掉修改(回滚),B之前看到的就是"脏"数据。实际开发中几乎不会使用这个级别。
-
读已提交(Read Committed):B会计必须等A会计用钢笔正式写完修改(提交)后才能看到新数据。但有个问题:B在上午和下午查看同一账户时,可能会看到不同的余额(因为A可能在中午修改并提交了数据)。
-
可重复读(Repeatable Read):B会计在早上打开账簿时会拍张快照,这一整天都只看这张快照,不管A会计怎么修改真实账簿。这是MySQL的默认级别,但要注意"幻读"问题——就像B发现早上账簿有100页,下午突然变成101页了(A新增了记录)。
-
串行化(Serializable):给账簿加上锁,一次只允许一个会计操作。最安全但效率最低,就像银行金库只能一个人进出。
1.2 InnoDB如何解决幻读问题
InnoDB通过组合拳解决幻读这个棘手问题:
-
MVCC快照读:每个事务启动时都会获得一个"数据快照"。对于普通SELECT查询,就像在看一张不会变的照片,自然看不到其他事务新增的数据。
-
Next-Key Lock当前读:当执行SELECT...FOR UPDATE这类"当前读"时,InnoDB会给查到的记录加上两种锁:
- Record Lock:锁住具体记录(如锁住id=5的行)
- Gap Lock:锁住记录之间的间隙(如防止在id=5和id=10之间插入新数据)
实际案例:假设有个会议室预定系统,事务A查询"11:00-12:00未被预定的会议室"(返回3间)。如果没有间隙锁,事务B可能在这期间插入新的预定记录,导致事务A后续操作出现幻读。通过锁定时间范围的间隙,就能彻底避免这个问题。
2. MySQL索引原理与优化实践
2.1 B+树索引的智慧
为什么MySQL选择B+树而不是其他数据结构?这就像图书馆的图书管理系统:
- B树:类似每本书都放在特定的书架上,找书时要从头开始逐层查找。每层书架都可能有你要的书。
- B+树:所有书籍只放在最底层的"叶子书架"上,上层书架只放目录卡片。而且叶子书架之间用绳子串联起来,方便范围查找。
这种设计带来三大优势:
- IO次数少:一般3-4层就能存2000万条数据(假设每页100条记录)
- 范围查询快:找到起点后顺着链表就能获取所有相关记录
- 更适合磁盘存储:非叶子节点能存储更多索引项
2.2 联合索引的最左前缀原则
对于联合索引(a,b,c),它的排列方式就像电话簿:先按姓氏(a)排序,同姓氏按名字(b)排,最后按年龄(c)排。因此:
WHERE a=1 AND b=2 AND c=3:三列都能用上索引(就像知道完整姓名找电话)WHERE a=1 AND b>2:只能用到a和b两列(b的范围查询打断了c的有序性)WHERE b=2:完全用不上索引(就像只知道名字不知道姓氏)
开发技巧:在电商系统中,对于
(category_id, price)的联合索引:
WHERE category_id=5 AND price>100能充分利用索引- 但
ORDER BY price的查询就用不到这个索引
3. Redis高并发选课系统设计
3.1 四层防御架构
设计一个能承受万人同时抢课的分布式系统,需要像军事防御一样层层设防:
-
前端防线:
- 按钮防抖:点击后禁用按钮3秒
- 本地缓存:即使疯狂点击也只会发送第一个请求
- 验证码:拦截脚本攻击
-
Redis核心战场:
lua复制-- 选课Lua脚本伪代码 if redis.call('SISMEMBER', course_selected_key, student_id) == 1 then return "ALREADY_SELECTED" end if tonumber(redis.call('GET', course_stock_key)) <= 0 then return "NO_STOCK" end redis.call('DECR', course_stock_key) redis.call('SADD', course_selected_key, student_id) return "SUCCESS"这个原子操作能同时解决:
- 超卖问题(库存扣减)
- 重复选课(Set去重)
- 并发冲突(Lua脚本的原子性)
-
MQ异步削峰:
- 将写数据库操作转化为消息
- Kafka分区键设为课程ID,保证同一课程的消息顺序处理
-
MySQL最终一致:
sql复制CREATE TABLE student_course ( id BIGINT PRIMARY KEY, student_id BIGINT, course_id BIGINT, UNIQUE KEY uk_sc (student_id, course_id) );唯一索引是最后的安全网,即使Redis和MQ都出问题,数据库层面也能保证数据正确。
3.2 Redis持久化策略选择
根据选课系统的特点,推荐混合持久化配置:
conf复制# redis.conf
save 900 1 # 15分钟至少有1个变更就保存
appendonly yes # 开启AOF
aof-use-rdb-preamble yes # 开启混合持久化
- RDB:定时快照,适合做灾难恢复
- AOF:记录每个写操作,最多丢失1秒数据
- 混合模式:重写时先用RDB格式存储基础数据,再追加增量AOF
4. JVM内存模型与类加载机制
4.1 内存区域精讲
JVM内存就像一个现代化工厂:
-
堆区(Heap):原料仓库,存放所有对象实例。分为:
- 新生代(Eden + Survivor):新到的原料
- 老年代:长期存放的原料
- 垃圾回收主要在这里进行
-
虚拟机栈(Stack):每条生产线都有自己的工作台,存放:
- 局部变量表:当前工序的原料
- 操作数栈:正在加工的零件
- 方法出口:工序完成后返回的位置
-
方法区(Metaspace):工厂的设计图纸库,存储:
- 类信息
- 常量池
- 静态变量
- JIT编译后的代码
4.2 双亲委派机制的实战意义
类加载流程就像公司采购审批:
- 员工(应用类加载器)想买新电脑,先问部门经理(扩展类加载器)
- 部门经理再请示CEO(启动类加载器)
- CEO说"公司已有这类设备",就直接用公司的
- 如果CEO说没有,才由下级逐级处理
这个机制解决了两个关键问题:
- 安全:防止核心类被篡改(如自定义的java.lang.String)
- 统一:保证基础类在所有地方表现一致
案例:如果自己编写了java.util.HashMap,由于双亲委派机制,实际加载的仍然是JDK自带的HashMap类。
5. 算法实战:链表与数组操作
5.1 链表反转的迭代与递归
迭代法:像翻转一串珍珠项链
java复制public ListNode reverseList(ListNode head) {
ListNode prev = null;
while (head != null) {
ListNode next = head.next; // 暂存下一颗珍珠
head.next = prev; // 当前珍珠指向前一颗
prev = head; // 前移prev指针
head = next; // 前移head指针
}
return prev; // 新链表的头
}
递归法:从后向前翻转
java复制public ListNode reverseList(ListNode head) {
if (head == null || head.next == null) {
return head;
}
ListNode newHead = reverseList(head.next); // 先翻转后面的部分
head.next.next = head; // 后一个节点指向前一个
head.next = null; // 断开原方向
return newHead;
}
5.2 三数之和的优化思路
解决三数之和问题的关键在于避免O(n³)的暴力解法:
java复制public List<List<Integer>> threeSum(int[] nums) {
Arrays.sort(nums); // 先排序是基础
List<List<Integer>> res = new ArrayList<>();
for (int i = 0; i < nums.length-2; i++) {
if (i > 0 && nums[i] == nums[i-1]) continue; // 去重
int left = i+1, right = nums.length-1;
while (left < right) {
int sum = nums[i] + nums[left] + nums[right];
if (sum == 0) {
res.add(Arrays.asList(nums[i], nums[left], nums[right]));
while (left<right && nums[left]==nums[left+1]) left++; // 跳过重复
while (left<right && nums[right]==nums[right-1]) right--;
left++; right--;
}
else if (sum < 0) left++;
else right--;
}
}
return res;
}
优化要点:
- 排序后可以方便地跳过重复元素
- 固定一个数后,转化为两数之和问题
- 双指针法将时间复杂度从O(n²)降到O(n)
6. 测试工程师必备工具链
6.1 Charles抓包实战技巧
HTTPS抓包配置:
- 安装Charles根证书到设备
- 在手机上配置代理(IP:电脑内网IP,端口:8888)
- 对于Android 7+,需要修改应用网络安全配置
常用功能场景:
- 接口Mock:将线上API映射到本地JSON文件,模拟各种响应
- 性能测试:设置带宽限制模拟2G/3G网络
- 安全测试:修改请求参数测试边界情况
6.2 京东搜索测试用例设计
功能性测试:
markdown复制| 测试类型 | 测试用例示例 | 预期结果 |
|----------------|-----------------------------|------------------------|
| 基础搜索 | 输入"iPhone 15" | 显示相关手机商品 |
| 联想词 | 输入"苹果" | 提示"苹果手机"/"水果" |
| 特殊字符 | 输入"手机&!@" | 正常处理或友好提示 |
| 空搜索 | 点击搜索按钮不输入任何内容 | 显示热门推荐或历史搜索 |
性能测试要点:
- 模拟1000并发搜索请求
- 99%的响应时间应<500ms
- 长期运行内存无泄漏
安全测试:
- SQL注入测试:输入
' OR 1=1 -- - XSS测试:输入
<script>alert(1)</script> - 超长字符串测试(超过1000个字符)