1、消息队列如何保证消息不丢?
生产者端:关键业务同步发送并检查返回值,失败重试。RabbitMQ 开 Publisher Confirm,Kafka 设acks=all。RocketMQ 用同步发送,同步发送会阻塞等待 Broker 响应。兜底是本地消息表 + 定时补偿,这是最终一致性的起点。
Broker 端:RabbitMQ 交换机、队列、消息三者持久化,配合 confirm 确认落盘,再用镜像或仲裁队列做冗余;Kafka 靠多副本,配replication.factor>=3、min.insync.replicas>=2、关掉 unclean 选举,防止落后副本被选为主导致数据截断,ISR 全写才算成功;RocketMQ 核心业务同步刷盘 + 同步复制,主从都落盘才返回,这是用性能换可靠性。
消费者端:关自动 ack,业务执行完再手动确认。可重试错误走重试队列,耗尽进死信队列 + 告警 + 人工介入;不可重试错误落库 + 告警,不能只落库否则等于换个地方丢。手动 ack 必然带来重复消费,所以消费端必须幂等,用生产端生成的全局唯一 ID 配合 Redis SETNX 或数据库唯一索引去重。
边界:这套机制只能把丢失概率降到极低,做不到绝对零丢失——副本全挂、磁盘损坏、误删都防不住。工程上追求的是可接受的低概率 + 业务兜底。
2、什么是进程和线程?进程和线程的区别?
进程是操作系统进行资源分配的基本单位。一个进程就是一段程序的一次运行实例,它拥有独立的地址空间、文件描述符、内存等资源。进程之间是隔离的,一个进程崩了通常不影响另一个。
线程是CPU调度和执行的基本单位,是进程内部的一条执行流。一个进程可以包含多个线程,多个线程共享进程的地址空间和资源,但各自有独立的程序计数器、栈和寄存器。
区别:第一,进程有独立的地址空间;线程共享所属进程的资源,只独享栈和寄存器这类私有数据。第二,创建和销毁进程开销大,要分配地址空间、页表;线程轻量得多。切换也一样,进程切换要换页表、刷 TLB,线程切换主要换栈和寄存器,快很多。第三,进程间通信要靠 IPC,比如管道、消息队列、共享内存、socket,因为地址空间隔离;线程直接读写共享内存就行,但正因如此要加锁保证同步,否则会有竞态。第四,进程隔离性好,一个崩了不影响别人;线程共享地址空间,一个线程野指针可能把整个进程搞崩,所以多线程程序对并发安全要求更高。
3、进程间通信方式有哪些?
第一,管道(Pipe)。分匿名管道和命名管道。匿名管道只能用于有亲缘关系的进程,比如父子进程,本质是内核里的一块缓冲区,半双工,数据读完就没了。命名管道(FIFO)有文件名,可以用于无亲缘关系的进程。特点是简单,但只能传字节流,容量有限。
第二,消息队列(Message Queue)。内核维护的消息链表,进程可以按类型收发消息。相比管道,它能传结构化消息、支持优先级、生命周期随内核不随进程。缺点是数据要在内核和用户态之间拷贝,大流量下性能一般。
第三,共享内存(Shared Memory)。多个进程映射同一块物理内存,直接读写,是最快的 IPC 方式,因为省掉了拷贝。但共享内存本身不提供同步,必须配合信号量或互斥锁使用,否则会有竞态。
第四,信号量(Semaphore)。它主要不是传数据,而是做同步和互斥,控制多个进程对共享资源的访问。常和共享内存搭配用。
第五,信号(Signal)。一种异步通知机制,比如SIGKILL、SIGTERM。它只能传一个信号编号,不能传数据,主要用于事件通知,比如进程终止、定时器到期。
第六,Socket。最通用的一种,既能本机通信(Unix Domain Socket),也能跨主机通信(TCP/UDP Socket)。跨机场景基本只能靠它。
选型上我一般这么看:传大量数据、追求性能,用共享内存 + 信号量;简单传字节流、有亲缘关系,用管道;要结构化消息、解耦,用消息队列;只是事件通知,用信号;跨机器,用Socket。另外现代语言里很多用 Unix Domain Socket 做本机高性能通信,比 TCP 回环还快。
4、为什么 MySQL 采用 B+ 树作为索引?
MySQL 选 B+ 树,本质上是因为数据在磁盘上,磁盘 I/O 极慢,B+ 树能把一次查询的磁盘 I/O 压到 3~4 次,同时还支持范围查询和排序。
先说为什么不用别的结构。哈希索引等值查询是 O(1),但数据是无序散列的,范围查询、排序、前缀匹配都做不了,只能全表扫。红黑树、AVL 树虽然是 O(log n),但每个节点只存一个键,千万级数据树高会到二三十层,每下一层基本就是一次磁盘 I/O,太慢。B 树非叶子节点也存数据,导致单页能放的键变少,树更高,而且数据散落在各层,范围查询要来回回溯父节点,效率差。
B+ 树赢在两点。第一,非叶子节点只存索引键和子节点指针,不存真实数据,一个 16KB 的页能塞下上千个键,树被压得很矮,千万级数据通常只要 3 层,查询稳定 3 次 I/O。第二,所有数据都在叶子节点,且叶子之间用双向链表串起来,范围查询定位到起点后顺着链表顺序扫就行,天然契合 ORDER BY、GROUP BY、分页这些场景。
5、如何评估 / 判断索引是否生效?常见索引失效场景有哪些?
判断索引是否生效,最直接的办法就是EXPLAIN看执行计划。先看key,为 NULL 就是没走索引;再看type,理想状态是const、ref、range,出现ALL就是全表扫描,index是全索引扫描,基本也没用好。rows和filtered能看出扫描代价,Extra里出现Using index说明命中覆盖索引不用回表,出现Using filesort或Using temporary说明排序分组没吃上索引。MySQL 8.0.18+ 可以用EXPLAIN ANALYZE直接看真实耗时和行数,比估算值更准。
常见失效场景主要有这些。对索引列套函数或做运算,比如DATE(create_time) = '2025-01-01',破坏了 B+ 树的有序性,改成范围查询就能解决。隐式类型转换,varchar 字段传数字进去,MySQL 会把索引列转数字,等于对列做了运算,索引直接废掉,这是最隐蔽的坑。联合索引不遵循最左前缀,跳了最左列或者中间用了范围查询,后面的列就用不上。LIKE 左模糊%abc没法从 B+ 树前缀定位,右模糊abc%没问题。OR 条件里只要有一列没索引,优化器可能直接全表扫,拆成UNION ALL各自走索引更稳。不等于、NOT IN、IS NOT NULL 这类反向条件,如果匹配数据量太大,优化器算下来觉得回表成本高于全表扫,会主动放弃索引。索引选择性太低,比如性别只有两个值,查出来占全表 20%~30% 以上,优化器也会放弃。最后,大量增删改后统计信息过期,优化器可能误判,跑一下ANALYZE TABLE刷新就行。
SELECT *本身不会让索引失效,但如果回表成本太高,优化器会权衡后选择全表扫,所以尽量用覆盖索引减少回表。
6、如何监控并优化慢 SQL?
监控定位主要靠慢查询日志和实时状态。生产环境开启slow_query_log,long_query_time一般设 1 秒,核心链路可以压到 0.5 秒,再配合min_examined_row_limit过滤小表查询。 日志分析用mysqldumpslow看概览,真正排查用pt-query-digest,按 SQL 指纹聚合,优先盯总耗时最高、扫描行数远大于返回行数的那类 SQL。 线上突发时直接跑SHOW FULL PROCESSLIST,看Threads_running是否飙升、有没有长事务或锁等待。
定位到 SQL 后跑EXPLAIN,先看type,至少要到range,核心业务争取ref,出现ALL必须处理。key为空说明索引没生效;rows很大说明扫描代价高;Extra里Using filesort、Using temporary、Using index分别对应排序/临时表/覆盖索引,能直接判断优化方向。 MySQL 8.0 还可以用EXPLAIN ANALYZE看真实耗时分布。
优化时优先改 SQL 和索引。索引列不要套函数或做运算,比如DATE(create_time) = ?改成范围查询;varchar 传参要和字段类型一致,避免隐式转换;联合索引遵循最左前缀,等值列放前面,范围列和排序列放后面。 深分页用游标分页或延迟关联替代大 OFFSET;SELECT *改成明确字段,尽量走覆盖索引减少回表;子查询能改 JOIN 就改 JOIN;大事务拆成小批次。 如果执行计划没问题但依然慢,再看innodb_buffer_pool_size、缓冲池命中率、锁竞争和统计信息是否过期。
上线前必须重新EXPLAIN确认执行计划改善,再在测试环境压测对比耗时和扫描行数;大表加索引用pt-osc或gh-ost避免阻塞,优化后定期ANALYZE TABLE刷新统计信息。
慢 SQL 治理不是加索引就完事,核心是“少扫描、少回表、少排序、少锁等待”,再配上持续的慢查询监控闭环。
7、事务隔离级别有哪些?
四种隔离级别是:读未提交、读已提交、可重复读、串行化。
读未提交能读到别人没提交的数据,脏读、不可重复读、幻读全可能发生,生产基本不用。读已提交只能读到已提交的数据,解决了脏读,但同一事务里两次读同一行可能不一样,也就是不可重复读,Oracle 默认用这个级别。可重复读是 MySQL InnoDB 的默认级别,事务内多次读结果一致,解决了脏读和不可重复读,理论上还可能有幻读,但 InnoDB 靠 MVCC 加 Next-Key 锁在大部分场景下也挡住了幻读。串行化最严格,所有读写都串行执行,三种问题全解决,但并发最差,几乎没人用。
关键区分点是:脏读是读到未提交的修改,不可重复读是同一行被改了,幻读是范围内多了或少了行。实现上,RC 每次查询都生成新的 ReadView,RR 只在事务第一次查询时生成一次并全程复用,这就是两者在"可重复读"上的本质差异。