腾讯暑期一面凉经
tcp慢启动阈值确定
默认情况应该由接收方确定,或者根据RFC建议设置为无限大,直到首次丢包
在实际应用中,比如linux,是设置了10xMss。
页中断策略和虚拟内存技术联系
当内存中没有我们需要的数据页,会触发缺页中断,将我们需要的数据页拉入内存,其中会涉及一些页面置换算法,LRU,LFU,Clock。
关于如何确定JVM的一些启动参数
堆内存
Xms初始堆内存 Xmx最大堆内存
最好是设置为系统的内存50%-70%
预留给操作系统,线程栈,方法区一定的内存
新生代老年代的内存设置
默认是1:2
对于web应用程序,短周期的这个对象比较多,新生代的内存就可以分配的大一些,可以设置为1:1
对于一些缓存应用,长生命周期的对象比较多,就要增大老年代的比列,可以设为1:3
垃圾回收器的选择
| GC算法 | 适用场景 | 关键参数 |
|---|---|---|
| G1 GC | 通用场景(平衡吞吐量和延迟) | -XX:+UseG1GC -XX:MaxGCPauseMillis=200 -XX:ParallelGCThreads=4 |
| ZGC/Shenandoah | 超大堆内存(>32G)、低延迟需求 | -XX:+UseZGC -Xmx16g 或 -XX:+UseShenandoahGC |
| Parallel GC | 高吞吐量(批处理任务) | -XX:+UseParallelGC -XX:ParallelGCThreads=8 -XX:MaxGCPauseMillis=500 |
| CMS | 老年代低延迟(已逐步被G1替代) | -XX:+UseConcMarkSweepGC -XX:CMSInitiatingOccupancyFraction=75 |
10亿个数字找top1000(topk问题)
//其实是最小堆的思想,java中有优先级队列可以使用
public class Top1000 {
//优先级队列
public static List<Integer> top1000(List<Integer> list) {
PriorityQueue<Integer> queue = new PriorityQueue<>((a,b)->list.get(b)-list.get(a));
for (int i = 0; i < list.size(); i++) {
if (queue.size()<1000){
queue.add(i);
}else if (list.get(i)>list.get(queue.peek())){
queue.remove();
queue.add(i);
}
}
List<Integer> result= new ArrayList<>();
while (!queue.isEmpty()){
result.add(queue.poll());
}
return result;
}
}
//也可以自己构建最小堆
public class MinHeap {
private int[] heap;
private int size;
private final int capacity;
public MinHeap(int capacity) {
this.capacity = capacity;
this.heap = new int[capacity];
this.size = 0;
}
public void add(int num) {
if (size < capacity) {
// 堆未满时插入末尾并上浮
heap[size] = num;
siftUp(size);
size++;
} else if (num > heap[0]) {
// 替换堆顶并下沉
heap[0] = num;
siftDown(0);
}
}
private void siftUp(int index) {
while (index > 0) {
int parent = (index - 1) / 2;
if (heap[index] >= heap[parent]) break;
swap(index, parent);
index = parent;
}
}
private void siftDown(int index) {
int smallest = index;
int left = 2 * index + 1;
int right = 2 * index + 2;
if (left < size && heap[left] < heap[smallest]) {
smallest = left;
}
if (right < size && heap[right] < heap[smallest]) {
smallest = right;
}
if (smallest != index) {
swap(index, smallest);
siftDown(smallest);
}
}
private void swap(int i, int j) {
int temp = heap[i];
heap[i] = heap[j];
heap[j] = temp;
}
public List<Integer> getSortedResult() {
// 将堆排序并逆序输出
int[] copy = Arrays.copyOf(heap, size);
Arrays.sort(copy);
List<Integer> result = new ArrayList<>();
for (int i = copy.length - 1; i >= 0; i--) {
result.add(copy[i]);
}
return result;
}
public static void main(String[] args) {
int[] nums = new int[1_000_000_000]; // 假设已初始化数据
MinHeap minHeap = new MinHeap(1000);
for (int num : nums) {
minHeap.add(num);
}
List<Integer> top1000 = minHeap.getSortedResult();
System.out.println("Top 1000 numbers: " + top1000);
}
}
快排和归并排序
| 排序算法 | 最佳时间复杂度 | 平均时间复杂度 | 最差时间复杂度 | 空间复杂度 | 是否稳定 | 是否原地排序 |
|---|---|---|---|---|---|---|
| 冒泡排序 | O(n) | O(n²) | O(n²) | O(1) | 是 | 是 |
| 选择排序 | O(n²) | O(n²) | O(n²) | O(1) | 否 | 是 |
| 插入排序 | O(n) | O(n²) | O(n²) | O(1) | 是 | 是 |
| 希尔排序 | O(n log n) | O(n log² n) | O(n²) | O(1) | 否 | 是 |
| 快速排序 | O(n log n) | O(n log n) | O(n²) | O(log n)~O(n) | 否 | 是 |
| 归并排序 | O(n log n) | O(n log n) | O(n log n) | O(n) | 是 | 否 |
| 堆排序 | O(n log n) | O(n log n) | O(n log n) | O(1) | 否 | 是 |
| 计数排序 | O(n + k) | O(n + k) | O(n + k) | O(n + k) | 是 | 否 |
| 桶排序 | O(n + k) | O(n + k) | O(n²) | O(n + k) | 是 | 否 |
| 基数排序 | O(n × k) | O(n × k) | O(n × k) | O(n + k) | 是 | 否 |
//快速排序
确定一个基准,主要是使用双指针,一次确定一个整数的位置,还是分治思想
public class QuickSort {
public void quickSort(int []nums,int start,int end){
if (start<end){
int base=nums[start];
int left=start;
int right=end;
while (left<right){
while (left<right&&nums[right]>=base){
right--;
}
nums[left]=nums[right];
while (left<right&&nums[left]<=base){
left++;
}
nums[right]=nums[left];
}
nums[left]=base;
quickSort(nums,start,left-1);
quickSort(nums,left+1,end);
}
}
}
//归并排序
先分,再并,需要借助额外的存储空间,真正的排序步骤在合并这里
public class MergeSort {
public static void mergeSort(int[] arr) {
if (arr == null || arr.length <= 1) return;
int[] temp = new int[arr.length]; // 预分配临时数组
sort(arr, 0, arr.length - 1, temp);
}
private static void sort(int[] arr, int left, int right, int[] temp) {
if (left>=right)
return;
int mid=left+(right-left)/2;
sort(arr,left,mid,temp);
sort(arr,mid+1,right,temp);
merge( left,mid,right,temp,arr);
}
private static void merge(int left, int mid, int right, int[] temp,int[] arr) {
int l=left;
int r=mid+1;
int t=0;
while (l<=mid&&r<=right){
if (arr[l]<=arr[r])
temp[t++]=arr[l++];
else
temp[t++]=arr[r++];
}
while (l<=mid){
temp[t++]=arr[l++];
}
while (r<=right){
temp[t++]=arr[r++];
}
t=0;
while (t<=right)
arr[left++]=temp[t++];
}
// 测试用例
public static void main(String[] args) {
int[] arr = {12, 11, 13, 5, 6, 7};
System.out.println("排序前: " + Arrays.toString(arr));
mergeSort(arr);
System.out.println("排序后: " + Arrays.toString(arr));
// 输出: 排序后: [5, 6, 7, 11, 12, 13]
}
}
时间复杂度和空间复杂度的计算方法
空间复杂度
| 复杂度等级 | 典型场景 | 示例算法 |
|---|---|---|
| O(1) | 仅用固定数量变量 | 交换两数、数组遍历 |
| O(n) | 存储与输入规模正相关的数据结构 | 数组拷贝、哈希表存n个元素 |
| O(n²) | 二维结构(如矩阵、DP表) | 动态规划中的二维数组 |
| O(log n) | 递归分治(每次问题规模减半) | 二分查找的递归实现 |
对于递归操作,还是要看他的递归深度
时间复杂度
对于常见的操作可以列表计算
对于递归
形如T(N) = a* T(N/b)+ O(N^d)(其中a、b、d都是常数)的递归函数可以直接通过Master公式来确定时间复杂度其中a是递归路数,b是递归问题规模(理解成每次把整体问题规模分解成的子问题规模),O(N^d)是除递归操作外的时间复杂度
如果
1.log(b,a)<d,复杂度为O(N^d)
2.log(b,a)>d,复杂度为O(N^log(b,a))
3.log(b,a)=d,复杂度为O(N^d*logn)
快重传和快恢复
快重传(Fast Retransmit)与快恢复(Fast Recovery)分析
快重传和快恢复是 TCP Reno 算法中解决网络拥塞的核心机制,旨在减少丢包恢复时间并提高网络吞吐量。以下是两者的详细解析及其协同工作原理:
1. 快重传(Fast Retransmit)
触发条件
- 发送方连续收到 3个重复ACK(Dup-ACK),表明某个数据包已丢失,但后续数据包仍能到达接收方。
核心逻辑
- 立即重传:检测到3个Dup-ACK后,无需等待超时(RTO),直接重传丢失的数据包。
- 避免超时等待:传统TCP Tahoe在丢包时需等待超时触发重传,而快重传显著降低恢复延迟。
示例场景
发送方发送数据包:1, 2, 3, 4, 5
接收方收到:1, 3, 4, 5 → 连续返回ACK=2(3次)
发送方立即重传数据包2。
2. 快恢复(Fast Recovery)
目标
- 在重传丢失包后,避免拥塞窗口(cwnd)重置为1(传统慢启动策略),而是平滑恢复发送速率。
关键步骤
- 调整窗口:
•ssthresh = max(cwnd / 2, 2 * MSS)
•cwnd = ssthresh + 3 * MSS(补偿已确认的3个Dup-ACK对应的数据包)。 - 线性增长:
• 后续每收到一个Dup-ACK,cwnd += 1 MSS(允许发送新数据包,避免管道枯竭)。 - 退出条件:
• 收到新数据的ACK时,将cwnd设为ssthresh,进入拥塞避免阶段(线性增长)。
优势
- 避免吞吐量骤降:传统TCP Tahoe在丢包后重置
cwnd=1,导致吞吐量断崖式下跌;快恢复通过保持较高cwnd,维持网络利用率。
3. 快重传与快恢复的协同流程
[发送数据] → [收到3个Dup-ACK] → 触发快重传 → 进入快恢复阶段 → 调整cwnd/ssthresh → 线性增长直至新ACK到达 → 退出快恢复
4. 与传统策略(TCP Tahoe)对比
| 机制 | TCP Tahoe | TCP Reno(快重传+快恢复) |
|---|---|---|
| 丢包响应 | 仅依赖超时重传 | 快重传触发快速重传 |
| 窗口调整 | 超时后cwnd=1,重启慢启动 | 快恢复保持cwnd,进入拥塞避免 |
| 吞吐量影响 | 高延迟,低恢复效率 | 低延迟,高吞吐量保持 |
5. 实际应用与优化
- 重复ACK阈值:Linux内核允许通过参数调整触发快重传的Dup-ACK次数(默认3次,
sysctl net.ipv4.tcp_reordering)。 - 部分确认处理:若在快恢复期间收到部分新ACK(非完全恢复),可能触发多次快重传。
- 与现代算法结合:CUBIC等算法保留快重传机制,但使用不同拥塞窗口增长策略(如三次函数)。
6. 典型问题与调优
- 过早退出快恢复:若网络仍拥塞,可能反复触发快重传,需结合RTT动态调整阈值。
- 乱序与丢包区分:网络乱序也可能导致Dup-ACK,可通过时间戳选项(
TCP Timestamps)优化判断逻辑。
总结
快重传通过3个Dup-ACK触发快速重传,快恢复通过动态调整cwnd避免吞吐量暴跌,二者协同实现了高效丢包恢复与网络资源利用。此机制是TCP Reno的核心改进,奠定了现代拥塞控制算法的基础。在实际网络环境中,需结合具体协议栈参数(如Linux内核配置)和网络特性(如RTT、乱序率)进一步调优。
Mysql InnoDB引擎的加锁原则
加锁的基本单位是邻键锁next-keylock
查找过程中访问到的对象才会加锁
唯一索引等值查询,邻键锁会退化成记录锁
唯一索引和非唯一索引在等值查询上的区别是,唯一索引在遍历到第一不符合查询条件的值时就停止,不会再往后遍历,而普通索引就还会往后遍历记录第一不满足查询条件的这个记录,将这个作为右边界。
字节国际电商与数字化一面
分布式锁实现原理
在分布式系统中,网络分区(Network Partition)可能导致多个客户端同时持有同一资源的锁,从而破坏互斥性。这一问题通常被称为“脑裂”(Split-Brain)。以下是详细解释及解决方案:
一、问题背景
场景示例:
- 系统架构:假设一个分布式锁服务由3个节点(Node A、B、C)组成,客户端需要多数节点(≥2)确认才能获取锁。
- 网络分区发生:网络故障导致节点被分割为两组:
• 子网络1:Node A + 客户端1。
• 子网络2:Node B、C + 客户端2。 - 锁竞争:
• 客户端1在子网络1中成功获取锁(Node A确认)。
• 客户端2在子网络2中成功获取同一资源的锁(Node B、C确认)。 - 结果:两个客户端同时持有锁,导致数据不一致。
二、根本原因
- 分区期间的状态隔离:子网络之间无法同步锁状态,各自独立决策。
- 锁超时机制失效:若客户端无法续期锁,但分区恢复前其他客户端误判锁已释放。
- 多数派协议的局限性:RedLock等算法依赖多数节点响应,但分区可能导致多数派分布在多个子网络中。
三、解决方案
1. 多数派协议(如RedLock)
- 原理:客户端需在多数节点(N/2+1)上成功获取锁,确保分区后仅一个子网络满足多数条件。
- 示例:5节点集群,分区为3+2:
• 只有包含3节点的子网络能形成多数派,允许获取锁。
• 另一子网络无法满足多数条件,拒绝加锁请求。 - 缺点:
• 分区可能导致系统不可用(如5节点分3+2时,3节点子网络可工作,但2节点子网络无法处理新请求)。
• 时钟漂移问题可能导致锁提前释放(需配合NTP同步)。
2. 租约机制(如etcd/ZooKeeper)
- 原理:锁绑定租约(Lease),客户端需定期续期(KeepAlive)。若续期失败(如网络分区),租约到期后锁自动释放。
- 流程:
- 客户端1获取锁并绑定租约(30秒),每10秒续期。
- 网络分区导致续期失败,租约到期后锁自动删除。
- 客户端2在分区恢复后获取锁。
- 优点:避免分区期间锁长期被占用。
- 缺点:业务处理时间必须小于租约时间,否则锁可能被提前释放。
3. 共识算法(如Raft/Paxos)
- 原理:写操作需多数节点持久化确认,确保分区后仅多数派子网络可修改状态。
- 示例:etcd基于Raft协议:
• 写请求需多数节点确认。
• 分区后,仅多数派子网络可选举新Leader并处理写请求。
• 少数派子网络无法提交新操作,保持只读状态。 - 优点:强一致性保证。
- 缺点:复杂度高,性能较低。
4. 客户端容错设计
- 幂等性:业务逻辑需支持重复执行,避免锁失效后重复操作导致错误。
- 锁持有者验证:操作共享资源前,再次检查锁状态。
if (lock.isHeldByCurrentThread()) {
// 执行操作
}
5. 超时时间动态调整
- 自适应超时:根据历史业务处理时间动态调整锁超时,降低锁提前释放概率。
- 监控告警:检测网络分区事件,触发人工介入或自动修复。
四、方案对比
| 方案 | 一致性 | 可用性 | 复杂度 | 适用场景 |
|---|---|---|---|---|
| 多数派协议 | 弱 | 中 | 低 | 允许偶发锁冲突的高并发场景 |
| 租约机制 | 强 | 高 | 中 | 需要自动释放的强一致性场景 |
| 共识算法 | 强 | 低 | 高 | 金融、交易等强一致性场景 |
| 客户端容错 | 无 | 高 | 低 | 所有场景(必备) |
五、总结
网络分区导致多客户端持锁的问题需通过协议设计、租约管理和客户端容错等多层机制解决:
- 协议层面:采用多数派或共识算法,限制分区期间的锁分配。
- 租约层面:通过心跳续期和自动释放,避免锁长期滞留。
- 客户端层面:设计幂等操作和锁状态二次验证。
实际选型需权衡一致性、可用性与性能,并结合业务场景定制方案(如电商秒杀用RedLock+幂等,金融交易用etcd+Raft)。
http和https的区别
在面试中回答HTTP与HTTPS的区别时,可以按照以下逻辑进行简明扼要的阐述,突出技术要点和实际场景的理解:
1. 核心区别
“HTTP是明文传输的超文本协议,而HTTPS是HTTP的安全版本,通过SSL/TLS协议对传输数据进行加密和身份认证,解决HTTP的三大安全隐患:窃听、篡改和伪装。”
2. 分点对比(技术细节)
- 加密与安全性
HTTP的数据未加密,传输内容可能被中间人窃听或篡改;HTTPS通过SSL/TLS加密(如AES、RSA等算法),确保数据保密性和完整性(通过哈希校验),防止中间人攻击。 - 端口与协议层次
HTTP默认使用80端口,HTTPS默认443端口。SSL/TLS介于传输层(如TCP)和应用层(HTTP)之间,为HTTP提供加密通道。 - 证书与身份认证
HTTPS需由权威CA(如Let’s Encrypt)签发数字证书,验证服务器身份,防止钓鱼网站。浏览器会展示证书信息(如小锁图标),增强用户信任。 - 性能影响
HTTPS因加密/解密和握手过程(如TLS四次握手)会略增延迟,但通过硬件加速(如SSL专用芯片)、会话复用和HTTP/2优化,性能差距已不明显。现代网站普遍采用HTTPS,牺牲少量性能换取安全是必要选择。 - SEO与合规性
搜索引擎(如Google)优先索引HTTPS页面,且现代浏览器对HTTP页面标记“不安全”。GDPR等数据法规也要求敏感信息必须加密传输。
3. 实际意义(升华回答)
“HTTPS不仅是技术升级,更是构建网络信任的基础。它防止用户密码、支付信息等泄露,提升品牌可信度。如今,Let’s Encrypt等免费证书的普及,以及HTTP/3对HTTPS的深度集成,使得全站HTTPS已成为行业标准。”
回答示例:
“HTTP和HTTPS的核心区别在于安全性。HTTP传输数据是明文的,易被窃听或篡改;而HTTPS通过SSL/TLS协议加密数据,并使用数字证书验证服务器身份。例如,当用户访问网银时,HTTPS会加密交易信息,同时证书确保连接的是真实的银行服务器,而非钓鱼网站。此外,HTTPS使用443端口,且对SEO有正向影响。尽管早期因加密计算影响性能,但现代优化手段(如会话复用、硬件加速)已使性能损耗可忽略。因此,HTTPS已成为保障隐私和信任的必备技术。”
TLS和SSL
在面试中回答TLS与SSL的区别时,应结合协议演进、安全机制、性能优化和实际应用进行结构化阐述。以下是简明总结:
1. 历史与版本演进
- SSL(Secure Sockets Layer):
由Netscape于1990年代开发(SSL 2.0/3.0),因漏洞(如POODLE攻击)被逐步淘汰。 - TLS(Transport Layer Security):
TLS是SSL的标准化升级版,由IETF维护,版本包括TLS 1.0(1999)、1.2(2008)、1.3(2018)。TLS 1.0对应SSL 3.1,但不向后兼容。
2. 核心区别
| 维度 | SSL(3.0及更早) | TLS(1.2/1.3) |
|---|---|---|
| 安全性 | ❌ 弱算法(RC4、MD5)、无前向保密 | ✅ 强算法(AES-GCM、ChaCha20)、强制前向保密(ECDHE) |
| 性能 | 握手慢(2-RTT) | TLS 1.3优化至1-RTT甚至0-RTT |
| 漏洞修复 | 易受攻击(如POODLE、BEAST) | 修复已知漏洞,禁用不安全功能(如压缩、重协商) |
| 前向保密 | 不支持 | ✅ 依赖临时密钥(如ECDHE),会话密钥独立 |
| 行业支持 | 已被禁用(RFC 7568) | 成为现代标准(如HTTP/2、QUIC强制要求) |
3. 实际应用中的关键点
- 弃用SSL:
所有现代浏览器(Chrome、Firefox)已禁用SSL,仅支持TLS 1.2+。PCI DSS等合规标准要求禁用TLS 1.0/1.1。 - 术语混淆:
“SSL证书”是历史名称,实际用于TLS协议。证书本身(X.509)兼容两者,但需服务器配置支持TLS。 - 检测与配置:
• 工具:openssl s_client或SSL Labs测试验证协议版本。
• 服务器配置示例(Nginx):nginx ssl_protocols TLSv1.2 TLSv1.3; # 禁用SSLv3、TLS 1.0/1.1 ssl_ciphers ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384;
4. TLS 1.3的革命性改进
- 简化握手:合并消息步骤,1-RTT完成握手,0-RTT恢复会话(需防重放攻击)。
- 算法精简:仅保留AEAD加密(如AES-GCM)和椭圆曲线(如X25519),弃用RSA密钥交换。
- 强制前向保密:所有密钥交换必须使用临时密钥(如ECDHE),彻底杜绝历史密钥泄露风险。
5. 回答示例
“TLS是SSL的继任协议,核心区别有三点:
- 安全性:TLS禁用RC4、MD5等弱算法,强制前向保密(如ECDHE),即使服务器私钥泄露,历史通信仍安全。
- 性能:TLS 1.3将握手从2-RTT优化到1-RTT,并支持0-RTT快速恢复连接。
- 行业合规:现代标准(如PCI DSS、HTTP/2)要求使用TLS 1.2+,所有主流浏览器已禁用SSL。
尽管‘SSL证书’一词仍在用,但它实际服务于TLS协议,且配置需明确禁用SSLv3等不安全版本。”
一、TLS与SSL的扩展知识点(应对追问)
1. 前向保密的意义
• 场景举例:
假设某银行的服务器私钥在2024年泄露,若该银行使用TLS 1.3+ECDHE:
◦ 每次会话的临时密钥已销毁,攻击者无法解密历史交易数据(如2023年的转账记录)。
◦ 若使用传统RSA密钥交换,攻击者可用私钥解密所有历史预主密钥,导致数据全面泄露。
• 结论:前向保密是金融、医疗等敏感场景的合规性要求(如GDPR、PCI DSS)。
2. 协议降级攻击防御(TLS_FALLBACK_SCSV)
• 攻击原理:中间人强制客户端使用低版本协议(如SSL 3.0),利用旧协议漏洞解密通信。
• 防御机制:
◦ 客户端在握手时发送TLS_FALLBACK_SCSV信号,表明这是因降级触发的重连。
◦ 服务器检测到此信号后,若客户端本可支持更高版本协议(如TLS 1.3),则拒绝连接。
• 示例:Chrome浏览器在检测到降级攻击时显示ERR_SSL_VERSION_OR_CIPHER_MISMATCH。
3. HSTS(HTTP Strict Transport Security)
• 作用:通过HTTP响应头Strict-Transport-Security: max-age=31536000; includeSubDomains,强制浏览器仅通过HTTPS(TLS)访问网站,防止SSL剥离攻击。
• 预加载(HSTS Preload List):将域名提交到浏览器厂商的HSTS预加载列表(如Google列表),首次访问即强制HTTPS。
• 实际应用:主流网站(如GitHub、PayPal)均启用HSTS。
二、RSA与ECDHE的区别(简明对比)
| 维度 | RSA | ECDHE |
|---|---|---|
| 核心角色 | 非对称加密/签名算法 | 基于椭圆曲线的临时密钥交换算法 |
| 前向保密 | ❌ 不支持(若用于密钥交换) | ✅ 强制支持(临时密钥销毁后无法回溯) |
| 性能开销 | 高(长密钥加解密慢,如2048位) | 低(短密钥,如256位ECC≈3072位RSA强度) |
| 密钥交换流程 | 客户端生成预主密钥,用RSA公钥加密传输 | 双方交换临时公钥,计算共享密钥(无需传输) |
| 安全性依赖 | 大数分解难题 | 椭圆曲线离散对数问题(ECDLP) |
| 现代应用 | 主要用于证书签名(如RSA-SHA256) | TLS 1.3强制用于密钥交换 |
三、RSA与ECDHE的区别(详细说明)
1. 功能角色
- RSA:
• 密钥交换(传统模式):客户端生成预主密钥 → 用服务器RSA公钥加密 → 传输给服务器(密钥交换与身份认证绑定)。
• 数字签名:验证服务器身份(如证书中的RSA签名)。 - ECDHE:
• 密钥协商:双方基于临时椭圆曲线参数生成共享密钥(预主密钥),与身份认证解耦(需额外签名算法如RSA或ECDSA)。
• 前向保密:每次会话使用独立临时密钥,会话结束后销毁。
2. 安全性对比
- RSA密钥交换的隐患:
• 若服务器RSA私钥泄露,攻击者可解密所有历史通信的预主密钥(无前向保密)。
• 例如,2014年心脏出血漏洞(Heartbleed)导致私钥泄露,使用RSA密钥交换的网站历史数据全盘暴露。 - ECDHE的优势:
• 临时密钥仅存续于单次会话,私钥泄露不影响历史数据。
• 256位ECC密钥安全性≈3072位RSA,抗量子计算能力更强(目前标准下)。
3. 性能差异
- RSA计算开销:
• RSA-2048解密比加密慢约10倍,密钥越长性能越差(如4096位RSA密钥握手延迟显著)。
• 不适合移动设备或高并发服务器(如每秒千次握手)。 - ECDHE优化:
• 椭圆曲线运算速度快,256位ECC密钥签名速度比RSA-2048快约5倍。
• 例如,WhatsApp使用ECDHE-X25519实现每秒百万次密钥协商。
4. 实际配置建议
- 禁用RSA密钥交换:
# Nginx配置示例(仅允许ECDHE)
ssl_ciphers 'ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384';
ssl_prefer_server_ciphers on;
- 优先选择ECDSA证书:
ECDSA签名算法(如secp384r1)比RSA更高效,且与ECDHE天然适配。
四、总结回答示例
“RSA和ECDHE的核心区别在于密钥交换机制和安全性:
- RSA通过公钥加密预主密钥,但缺乏前向保密,私钥泄露会导致历史通信暴露。
- ECDHE基于临时椭圆曲线密钥生成共享密钥,每次会话独立,支持前向保密。
性能上,ECDHE密钥更短、计算更快,适合高并发场景。现代TLS 1.3已废弃RSA密钥交换,强制使用ECDHE等前向保密算法。”
结合扩展知识点,可进一步体现对协议细节和行业实践的理解,增强面试表现。
Redis的内存处理机制
Redis 的内存处理机制是其高性能的核心之一,主要通过以下策略实现高效内存管理与优化:
1. 灵活的数据结构编码优化
Redis 为每种数据类型设计多种内部编码,根据数据规模和内容自动选择最省内存的存储方式:
- 字符串(String):
• 普通字符串(RAW编码)或整数(INT编码,节省内存)。 - 哈希(Hash):
• ziplist(压缩列表):元素较少时(默认hash-max-ziplist-entries 512)使用连续内存存储,无指针开销。
• hashtable(哈希表):元素较多时转换为哈希表,提高查询效率。
# 示例:存储用户信息时,若字段数 ≤ 512且值大小 ≤ 64字节,自动使用ziplist
HSET user:1000 name "Alice" age 30 email "alice@example.com"
- 列表(List):
• ziplist:元素少且小值时使用。
• linkedlist(双向链表) 或 quicklist(3.2+版本):组合ziplist和链表,平衡内存与性能。 - 集合(Set):
• intset(整数集合):元素全为整数且数量少时(默认set-max-intset-entries 512)。
• hashtable:非整数或元素过多时。 - 有序集合(Sorted Set):
• ziplist:元素少时按分值顺序紧凑存储。
• skiplist(跳跃表)+ hashtable:元素多时组合结构,支持快速范围查询。
2. 内存分配与碎片管理
- 内存分配器:
Redis 默认使用 jemalloc(而非 libc),因其高效的内存块分配策略,显著减少内存碎片。
# 启动时指定内存分配器(通常无需修改)
./redis-server --malloc-lib jemalloc
- 碎片整理:
• 自动碎片回收:通过CONFIG SET activedefrag yes启用后台碎片整理。
• 手动触发:执行MEMORY PURGE(依赖jemalloc)释放未用内存。
3. 过期数据清理机制
- 惰性删除(Lazy Expire):
访问键时检查是否过期,若过期则立即删除。避免主动扫描开销,但可能导致内存未被及时释放。 - 定期删除(Active Expire):
每隔100ms随机抽取一定数量(由hz配置控制)的键检查过期,逐步清理。
4. 内存淘汰策略(Eviction Policies)
当内存达到 maxmemory 限制时,根据策略淘汰数据:
- volatile-lru:从设置了过期时间的键中淘汰最近最少使用的(LRU)。
- allkeys-lru:从所有键中淘汰LRU,适合缓存场景。
- volatile-lfu(4.0+):淘汰最不经常使用的(LFU),基于频率统计。
- volatile-ttl:优先淘汰剩余存活时间(TTL)最短的键。
- noeviction(默认):禁止淘汰,写入操作返回错误。
# 配置淘汰策略(示例设置为allkeys-lru)
CONFIG SET maxmemory-policy allkeys-lru
5. 内存优化技巧
- 缩短键名:键名尽量简短(如
u:1000代替user:1000:profile)。 - 使用Hash代替多个String:存储对象时,Hash比多个String节省内存(减少键元数据开销)。
- 压缩存储:对长文本/二进制数据使用客户端压缩(如gzip),存入Redis后解压使用。
- 分片(Sharding):将大数据集分散到多个Redis实例,避免单实例内存压力。
6. 监控与诊断工具
- 内存统计:
INFO memory # 查看内存使用详情,如碎片率(mem_fragmentation_ratio)
MEMORY USAGE key # 查看指定键的内存占用
- 大键分析:
使用redis-cli --bigkeys扫描内存中大键,优化或拆分。 - 内存溢出处理:
若used_memory接近maxmemory,调整淘汰策略或扩容。
总结
Redis 通过 动态编码优化、高效内存分配、智能淘汰策略 及 碎片管理 等机制,在保证性能的同时最大化内存利用率。实际应用中需结合监控数据调整 maxmemory、淘汰策略和数据结构配置,以应对不同场景的内存挑战。
排查慢sql
在面试中回答如何排查慢SQL时,可以按照以下结构化步骤进行阐述,以展现系统化的排查思路和专业知识:
1. 确认问题并收集数据
- 开启慢查询日志
检查数据库的慢查询日志是否开启(如MySQL的slow_query_log),并设置合理的阈值(long_query_time,通常从1秒开始)。通过日志定位具体是哪些SQL语句执行缓慢。 - 使用分析工具
利用工具解析慢查询日志,例如:
•mysqldumpslow(MySQL内置)。
•pt-query-digest(Percona Toolkit),生成报告并排序执行时间、频率高的SQL。
2. 分析SQL执行计划
- 使用
EXPLAIN命令
对目标SQL执行EXPLAIN或EXPLAIN FORMAT=JSON,关注以下字段:
• type:扫描类型(ALL全表扫描需优化)。
• key:是否使用索引。
• rows:预估扫描行数。
• Extra:额外信息(如Using temporary、Using filesort可能需优化)。 - 索引有效性检查
• 确认查询条件字段是否有合适索引(SHOW INDEX FROM table)。
• 检查索引选择性(区分度低的字段如性别,索引效果差)。
3. 检查数据库状态与资源
- 监控实时负载
•SHOW FULL PROCESSLIST:查看当前运行的查询,确认是否有阻塞(如长时间运行的查询或锁等待)。
•SHOW ENGINE INNODB STATUS:分析InnoDB状态,检查锁竞争或死锁。 - 服务器资源瓶颈
• CPU/内存:通过top、htop确认是否资源饱和。
• 磁盘IO:监控工具(如iostat)确认是否存在高IO等待。
4. 优化SQL与索引
- 重写SQL语句
• 避免SELECT *,减少数据传输。
• 拆分复杂查询,优化子查询或JOIN(如改用INNER JOIN并确保关联字段有索引)。
• 分页优化:避免大OFFSET,改用WHERE id > {last_id}。 - 索引优化
• 添加缺失索引,注意联合索引的最左匹配原则。
• 删除冗余索引,减少写操作开销。
• 处理索引失效场景(如隐式类型转换、函数操作字段)。
5. 检查配置与统计信息
- 数据库配置调优
• 确认innodb_buffer_pool_size是否足够大(通常设为物理内存的70-80%)。
• 调整连接数(max_connections避免过高导致资源争抢)。 - 更新统计信息
使用ANALYZE TABLE更新表的统计信息,确保优化器生成高效执行计划。
6. 架构与业务层优化
- 缓存与读写分离
• 引入缓存(如Redis),减少重复查询。
• 读写分离,分散主库压力。 - 分库分表
针对大数据量表,考虑水平拆分或按时间归档历史数据。
7. 复验与持续监控
- 验证优化效果
优化后重新执行SQL,对比执行时间,确认是否达到预期。 - 建立监控告警
使用Prometheus+Grafana等工具监控数据库性能,设置慢查询阈值告警。
总结回答示例
“排查慢SQL时,我会首先通过慢查询日志定位问题SQL,结合EXPLAIN分析执行计划,检查索引使用情况。同时观察数据库实时状态和资源使用,排除锁竞争或硬件瓶颈。根据分析结果优化SQL语句或索引,必要时调整配置或引入缓存、分库分表等架构方案。最后通过测试和监控确保优化效果。”
通过以上步骤,能够系统化地覆盖慢SQL排查的关键点,展现全面的问题解决能力。
排查OutOfMemory问题
排查OutOfMemory(OOM)问题需要系统性地分析内存使用情况并定位根本原因。以下是详细的步骤指南:
1. 确认OOM类型与错误信息
- 查看日志和错误堆栈
• 确定是哪种OOM错误(例如Java中的OutOfMemoryError: Java heap space或OutOfMemoryError: Metaspace)。
• 检查是否有明确的代码位置触发错误。 - 区分内存区域
• 堆内存(Heap):对象实例存储区域。
• 非堆内存(Non-Heap):如元空间(Metaspace)、线程栈、直接内存(Direct Buffer)等。
2. 监控与分析内存使用
- 实时监控工具
• JVM:使用jstat、jconsole、VisualVM或Prometheus + Grafana监控堆/非堆内存变化。
• 系统级:top、htop、vmstat观察物理内存和交换分区使用。 - 生成和分析堆转储(Heap Dump)
• 通过JVM参数(-XX:+HeapDumpOnOutOfMemoryError)自动生成堆转储。
• 使用工具(如Eclipse MAT、JProfiler)分析堆转储,定位大对象或内存泄漏点。
3. 检查内存配置
- JVM参数调优
• 堆内存:调整-Xmx(最大堆)和-Xms(初始堆),例如:java -Xmx4g -Xms4g -jar app.jar• 元空间:调整-XX:MetaspaceSize和-XX:MaxMetaspaceSize(默认无限制)。
• 直接内存:检查-XX:MaxDirectMemorySize(NIO使用)。 - 容器环境(如Docker/K8s)
• 确认容器的内存限制(--memory)是否合理,避免容器因内存不足被OOM Killer终止。
4. 定位内存泄漏
- 分析对象引用链
• 在堆转储中查找占用内存最大的对象(如char[]、HashMap$Node等)。
• 查看这些对象的GC Root引用路径,确认是否有预期外的强引用(如静态集合类缓存未清理)。 - 代码审查
• 检查是否有未关闭的资源(如数据库连接、文件流)。
• 避免静态集合类无限增长(例如全局缓存未设置过期策略)。 - 第三方库问题
• 升级存在已知内存泄漏问题的库版本。
• 使用工具(如Java Agent)跟踪第三方库的内存分配。
5. 优化内存使用
- 减少对象创建
• 复用对象(如对象池、线程局部变量)。
• 避免在循环中频繁创建临时对象(如字符串拼接改用StringBuilder)。 - 调整数据加载策略
• 分页读取大数据集,避免一次性加载到内存。
• 使用流式处理(Streaming)替代全量缓存(如数据库结果集逐行处理)。 - 优化数据结构
• 使用更节省内存的数据结构(如Trove库替代JDK集合)。
• 压缩存储数据(如使用byte[]替代字符串)。
6. 垃圾回收(GC)调优
- 选择合适的GC算法
• 低延迟场景:G1 GC(-XX:+UseG1GC)或ZGC(-XX:+UseZGC)。
• 高吞吐场景:Parallel GC(-XX:+UseParallelGC)。 - 调整GC参数
• 增大新生代比例(-XX:NewRatio)。
• 调整GC触发阈值(如-XX:InitiatingHeapOccupancyPercent)。
• 启用GC日志分析(-Xlog:gc*)。
7. 非堆内存排查
- 元空间(Metaspace)溢出
• 检查是否动态生成大量类(如反射、CGLIB代理)。
• 调整-XX:MaxMetaspaceSize并监控类加载情况。 - 直接内存溢出
• 排查NIO操作(如ByteBuffer.allocateDirect())是否未释放。
• 使用-XX:MaxDirectMemorySize限制直接内存。 - 线程栈溢出
• 检查线程数是否过多(jstack查看线程状态)。
• 调整线程栈大小(-Xss),但需谨慎避免内存浪费。
8. 系统与架构优化
- 分布式架构
• 拆分单体应用为微服务,分散内存压力。
• 使用外部缓存(如Redis)替代本地缓存。 - 限流与降级
• 通过熔断机制(如Hystrix)防止突发流量压垮服务。
• 限制大查询或批量操作的内存占用。
9. 复现与测试
- 压力测试
• 使用JMeter、Gatling模拟高并发场景,观察内存使用情况。
• 对比优化前后的内存消耗。 - 持续监控
• 在生产环境部署APM工具(如SkyWalking、New Relic)实时跟踪内存指标。
总结回答示例
“排查OOM问题需要结合日志分析、堆转储检查和内存监控。首先确认是堆内存还是非堆内存溢出,通过工具定位大对象或泄漏点。检查JVM配置是否合理,优化代码减少内存占用,并调整GC策略。对于容器环境需验证资源限制,架构上可引入缓存和分布式方案降低单节点压力。”
通过以上步骤,可以系统化地定位并解决OOM问题,确保应用稳定运行。
手撕:两两交换链表节点
//这个当时边界判断有问题,引入虚拟头节点的作用是在三个节点的范围内两两交换后两个节点,这样也可以避免循环引用问题。
public ListNode swapPairs(ListNode head) {
ListNode dummyHead = new ListNode(0);
dummyHead.next=head;
ListNode temp=dummyHead;
while(temp.next!=null&&temp.next.next!=null){
ListNode node1=temp.next;
ListNode node2=temp.next.next;
ListNode temp1=node2.next;
temp.next=node2;
temp.next.next=node1;
temp.next.next.next=temp1;
temp=node1;
}
return dummyHead.next;
}
using tempetory 临时表问题
在排查慢SQL时,若通过EXPLAIN发现Extra字段显示Using temporary,表明该查询需要创建临时表来完成操作(如排序、分组或连接)。以下是优化此类问题的系统性思路及具体方法:
1. 确认问题原因
- 临时表的常见触发场景:
•GROUP BY和ORDER BY的列不一致或未使用索引。
•DISTINCT、UNION或子查询导致中间结果存储。
• 复杂的JOIN操作(如未使用索引的关联)。
2. 优化策略与实施步骤
(1) 优化查询结构
- 统一
GROUP BY和ORDER BY的列
若两者列不一致,MySQL可能需先分组再排序,导致临时表。
优化示例:
-- 原查询(GROUP BY和ORDER BY列不一致)
SELECT category_id, SUM(amount)
FROM orders
GROUP BY category_id
ORDER BY created_at;
-- 优化后:统一排序和分组列(或移除不必要的排序)
SELECT category_id, SUM(amount)
FROM orders
GROUP BY category_id
ORDER BY category_id; -- 或删除ORDER BY
- 避免非必要的
DISTINCT或UNION
检查是否可以重写逻辑,减少中间结果集。
(2) 添加合适的索引
- 为
GROUP BY/ORDER BY/JOIN列创建复合索引
使操作能直接通过索引完成,避免临时表。
示例:
-- 原查询(无索引导致临时表)
SELECT user_id, COUNT(*)
FROM logs
GROUP BY user_id;
-- 添加索引优化
ALTER TABLE logs ADD INDEX idx_user_id (user_id);
- 使用覆盖索引(Covering Index)
索引包含所有查询字段,避免回表。
示例:
-- 查询需要user_id和amount字段
SELECT user_id, SUM(amount)
FROM orders
GROUP BY user_id;
-- 创建覆盖索引
ALTER TABLE orders ADD INDEX idx_user_amount (user_id, amount);
(3) 调整数据库配置
- 增大临时表内存阈值
通过调整tmp_table_size和max_heap_table_size,使临时表尽量存储在内存中。
配置建议:
tmp_table_size = 64M
max_heap_table_size = 64M
- 监控临时表使用情况
执行SHOW GLOBAL STATUS LIKE 'Created_tmp_%tables';,若Created_tmp_disk_tables值过高,需优化查询或增加内存。
(4) 重写复杂子查询或JOIN
- 将子查询转换为JOIN
减少中间临时表的生成。
示例:
-- 原查询(子查询导致临时表)
SELECT *
FROM users
WHERE id IN (SELECT user_id FROM orders WHERE amount > 100);
-- 优化为JOIN
SELECT users.*
FROM users
JOIN orders ON users.id = orders.user_id
WHERE orders.amount > 100
GROUP BY users.id; -- 去重
- 使用派生表(Derived Table)优化
结合业务逻辑,分步处理中间结果。
(5) 减少临时表的数据量
- 仅选择必要字段
避免SELECT *,减少临时表存储的数据量。
示例:
-- 原查询
SELECT *
FROM orders
GROUP BY user_id;
-- 优化后:仅选择必要字段
SELECT user_id, COUNT(*)
FROM orders
GROUP BY user_id;
3. 验证优化效果
- 重新执行
EXPLAIN
检查Extra字段是否消除Using temporary。 - 观察查询性能
通过SHOW PROFILES或监控工具确认执行时间减少。
4. 面试回答示例
回答思路:
- 定位问题:通过
EXPLAIN发现Using temporary,说明查询需创建临时表。 - 分析原因:常见于未索引的
GROUP BY/ORDER BY、复杂子查询或JOIN。 - 优化步骤:
• 优化查询结构,统一GROUP BY和ORDER BY列。
• 添加复合索引或覆盖索引。
• 调整tmp_table_size提升内存利用率。
• 重写子查询为JOIN,减少中间结果集。 - 验证效果:重新
EXPLAIN并监控性能。
示例回答:
“当发现EXPLAIN的Extra字段有Using temporary时,我会首先检查查询中是否有GROUP BY或ORDER BY未用索引,或者复杂子查询。例如,若GROUP BY和ORDER BY列不一致,可能需创建临时表排序。优化方法包括:
- 为这些列添加复合索引,使排序和分组直接通过索引完成;
- 调整查询逻辑,统一
GROUP BY和ORDER BY列; - 若无法避免临时表,增大
tmp_table_size让更多临时表存于内存。
同时,重写子查询为JOIN或减少SELECT字段也能有效降低临时表的数据量。”
总结
优化临时表的核心是减少中间结果集的数据量和避免磁盘I/O。通过索引优化、查询重构和配置调整,可显著提升性能。需结合EXPLAIN结果和实际业务场景灵活选择策略。
redis的事务原理,内存机制
Redis 的事务机制提供了一种将多个命令打包、按顺序一次性执行的机制,但与传统数据库(如 MySQL)的事务相比,Redis 的事务不支持回滚(Rollback),且设计理念更偏向于简单和高效。以下是 Redis 事务的核心机制和关键点:
1. 事务的基本流程
Redis 事务通过以下三个命令实现:
MULTI:标记事务开始,后续命令会按顺序进入队列(但不会立即执行)。EXEC:执行事务队列中的所有命令。DISCARD:取消事务,清空队列中的命令。
示例:
MULTI -- 开始事务
SET key1 "A" -- 命令入队
SET key2 "B" -- 命令入队
EXEC -- 执行事务
2. 事务的原子性
- 执行原子性:所有命令在
EXEC时一次性按顺序执行,不会被其他客户端命令打断。 - 错误处理:
• 语法错误(如命令不存在):在EXEC执行前会直接报错,事务不会执行。
• 运行时错误(如对字符串执行INCR):错误命令会失败,但后续命令继续执行,不会回滚。
示例:
MULTI
SET key1 "Hello"
INCR key1 -- 对字符串执行 INCR 会失败
SET key2 "World"
EXEC
-- 结果:key1 被设置为 "Hello",key2 被设置为 "World",INCR 失败但事务继续执行。
3. 乐观锁(WATCH)
WATCH命令:用于监控一个或多个键,如果在事务执行前这些键被其他客户端修改,则事务会被取消(类似乐观锁)。UNWATCH:取消对所有键的监控。
使用场景:确保事务执行期间监控的键未被修改(如余额扣减)。
WATCH balance -- 监控 balance 键
MULTI
DECRBY balance 100 -- 扣减余额
INCRBY debt 100 -- 增加债务
EXEC -- 如果 balance 在 WATCH 后被其他客户端修改,事务失败
4. 事务的特点
- 无隔离性:事务中的命令在
EXEC前不会实际执行,但其他客户端可能修改事务中涉及的键。 - 批量执行:适合需要原子性批量执行多个命令的场景。
- 不支持回滚:设计哲学是“保持简单高效”,错误由开发者自行处理。
5. 适用场景
- 批量执行命令(如初始化数据、批量更新)。
- 结合
WATCH实现简单并发控制(如库存扣减、抢购)。
6. 与 Pipeline 的区别
- 事务:保证原子性,所有命令一起执行。
- Pipeline:将多个命令打包发送以减少网络开销,但不保证原子性。
总结:
Redis 的事务机制通过 MULTI/EXEC 实现命令的批量原子执行,但缺乏回滚功能,需结合 WATCH 实现乐观锁控制。它适合简单场景下的批量操作,对复杂事务需依赖应用层逻辑补偿。
豹趣科技
工程化问题的看法
在协同工作的情况下,编码的可维护性,可靠性,可读性。