面试大杂烩
Java
基础
JVM调优
为什么要进行 JVM 调优?
- 更低的延迟 (Lower Latency):减少 GC 停顿时间(Stop-The-World),提高系统响应速度,尤其是对实时性要求高的应用。
- 更高的吞吐量 (Higher Throughput):降低 GC 开销,让 CPU 更多时间处理业务逻辑,提高单位时间内的处理能力。
- 更高的可用性 (Higher Availability):避免因内存溢出(OOM)导致的程序崩溃,保证系统稳定运行。
- 更小的内存占用 (Smaller Memory Footprint):在满足需求的前提下,合理使用内存,节省硬件成本。
调优的“铁三角”:核心参数与选择
JVM 调优绝大多数工作围绕着内存和垃圾回收器展开
1.堆内存 (Heap Memory) 相关
这是最核心的调整区域。
-Xms 和 -Xmx
-Xms:堆内存的初始大小。建议设置为和 -Xmx 相同,以避免运行时动态调整堆大小带来的额外性能开销。
-Xmx:堆内存的最大大小。这是最重要的参数之一。设置太小会导致频繁 GC 甚至 OOM;设置太大会延长 Full GC 的停顿时间,并可能影响本机其他进程。
示例:-Xms4g -Xmx4g (设置堆初始和最大大小为 4GB)
-XX:NewRatio 和 -XX:SurvivorRatio
-XX:NewRatio:设置年轻代(Young Generation)和老年代(Old Generation)的比例。例如,-XX:NewRatio=2 表示老年代是年轻代的 2 倍(即堆的 1/3 是年轻代,2/3 > 是老年代)。对于大量产生临时对象的应用(如Web应用),可以适当增大年轻代(即减小这个比值)。
-XX:SurvivorRatio:设置 Eden 区与一个 Survivor 区的比例。例如,-XX:SurvivorRatio=8 表示 Eden 区是一个 Survivor 区的 8 倍(即年轻代的 8/10 是 Eden,两个 Survivor 各占 1/> 10)。通常不需要调整,除非有非常明确的诊断数据。
-Xmn
直接设置年轻代的大小(例如 -Xmn1g)。优先级高于 -XX:NewRatio。通常更直观。2.垃圾回收器 (Garbage Collector) 选择与参数
选择正确的 GC 比调优其参数更重要。JDK 8 以后,G1GC 已成为默认选择。
串行回收器 (Serial GC)
-XX:+UseSerialGC
适用于单核小型应用,Client 模式默认。
并行回收器 (Parallel GC / Throughput GC)
-XX:+UseParallelGC / -XX:+UseParallelOldGC
目标:最大化吞吐量。适合后台运算、科学计算等,对停顿不敏感的应用。
相关参数:-XX:ParallelGCThreads(设置并行 GC 线程数)
并发标记清除回收器 (CMS GC) - 已废弃 (Deprecated)
-XX:+UseConcMarkSweepGC
目标:降低停顿时间。JDK 9 开始被标记为废弃,JDK 14 中移除。不推荐在新项目中使用。
G1 回收器 (Garbage-First GC) - JDK 9+ 默认
-XX:+UseG1GC
目标:在延迟和吞吐量之间取得平衡。适用于大堆(>4G)、低停顿要求的应用。
核心参数:
-XX:MaxGCPauseMillis:设置期望的最大停顿时间目标(例如 -XX:MaxGCPauseMillis=200)。G1 会尽力但不保证达到这个目标。这是 G1 最重要的调优参数。
-XX:InitiatingHeapOccupancyPercent(IHOP):触发并发标记周期的堆占用阈值(默认 45%)。如果老年代增长过快,可以适当调低此值。
ZGC / Shenandoah - 下一代低延迟回收器
-XX:+UseZGC (JDK 15+ 可投入生产) / -XX:+UseShenandoahGC (非Oracle JDK)
目标:亚毫秒级(<1ms)的超低停顿,适用于超大堆(TB 级别)和极致延迟要求的场景。
目前兼容性和稳定性仍在持续提升中,是未来的方向。3.其他重要参数
-XX:+HeapDumpOnOutOfMemoryError 和 -XX:HeapDumpPath
发生 OOM 时自动生成堆转储(Heap Dump)文件,用于事后分析,强烈建议开启。
示例:-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/path/to/heapdump.hprof
-XX:MetaspaceSize 和 -XX:MaxMetaspaceSize
设置元空间(取代永久代 PermGen)的初始和最大大小。如果类加载很多,需要适当调大(默认较小,且只受本机内存限制)。
示例:-XX:MetaspaceSize=256m -XX:MaxMetaspaceSize=256m
-Xss
设置每个线程的栈大小。默认通常为 1M。如果系统线程数非常多,可以适当减小(如 -Xss256k)来节省内存,但可能增加栈溢出风险。
JDK1.8新特性
| 特性 | 主要目的 | 核心优点 |
|---|---|---|
| Lambda 表达式 | 实现函数式编程 | 代码简洁,避免匿名内部类模板代码 |
| 函数式接口 | 为 Lambda 提供类型 | 定义明确的目标类型,内置常用接口 |
| Stream API | 处理集合数据 | 声明式编程,内部迭代,易于并行 |
| 方法引用 | 简化 Lambda | 语法更紧凑,可读性更强 |
| 接口默认/静态方法 | 扩展接口而不破坏现有代码 | 增强接口能力,支持库的平滑演进 |
| 新日期时间 API | 替代老旧的 Date/Calendar | 清晰、不可变、线程安全 |
| Optional | 优雅处理 null | 减少 NPE,明确表达可能缺失的值 |
线程同步的主要机制
- synchronized 关键字
synchronized 是 Java 中最基本、最常用的同步机制,它提供了一种内置的锁机制来保证原子性。
a) 同步实例方法
锁是当前实例对象 (this)。
public class SynchronizedExample {
private int count = 0;
// 同步实例方法
public synchronized void increment() {
count++;
}
public synchronized int getCount() {
return count;
}
}
b) 同步静态方法
锁是当前类的 Class 对象(如 SynchronizedExample.class)。
public class SynchronizedExample {
private static int count = 0;
// 同步静态方法
public static synchronized void increment() {
count++;
}
}
c) 同步代码块
可以更细粒度地控制锁的范围,并可以指定任意对象作为锁。
public class SynchronizedExample {
private int count = 0;
// 专门创建一个对象来作为锁是一种好习惯,尤其是不想用this做锁时
private final Object lock = new Object();
public void increment() {
// 同步代码块,使用lock对象作为锁
synchronized (lock) {
count++;
}
// 其他不需要同步的代码可以放在外面,提高性能
}
// 也可以用this作为锁,效果和同步实例方法一样
public void decrement() {
synchronized (this) {
count--;
}
}
}
使用修改后的例子:
public class SafeExample implements Runnable {
private static int count = 0;
// 静态变量需要静态锁
private static final Object staticLock = new Object();
@Override
public void run() {
for (int i = 0; i < 100000; i++) {
// 使用同步代码块保护count++
synchronized (staticLock) {
count++;
}
}
}
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(new SafeExample());
Thread t2 = new Thread(new SafeExample());
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("count的最终值是: " + count); // 现在结果总是200000
}
}
- Lock 接口 (java.util.concurrent.locks.Lock)
java.util.concurrent.locks 包提供了更灵活的锁操作。最常用的是 ReentrantLock(可重入锁)。
优点:
尝试非阻塞获取锁:tryLock() 方法可以尝试获取锁,如果获取失败不会一直阻塞。
可中断的获取锁:lockInterruptibly() 方法可以在等待锁时响应中断。
超时获取锁:tryLock(long time, TimeUnit unit) 可以设置超时时间。
可以创建多个条件变量(Condition):比 wait()/notify() 更精细的线程通信。
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class LockExample {
private static int count = 0;
// 创建ReentrantLock实例
private static final Lock lock = new ReentrantLock();
public static void main(String[] args) throws InterruptedException {
Runnable task = () -> {
for (int i = 0; i < 100000; i++) {
lock.lock(); // 获取锁
try {
count++;
} finally {
lock.unlock(); // 释放锁必须放在finally块中,确保一定会执行
}
}
};
Thread t1 = new Thread(task);
Thread t2 = new Thread(task);
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("count的最终值是: " + count);
}
}
synchronized 与 Lock 的对比
特性 synchronized Lock (ReentrantLock) 实现层次 JVM 层面的关键字,内置 JDK 层面的接口,通过代码实现 锁的释放 自动释放(代码块/方法执行完毕或发生异常) 必须手动调用 unlock()(通常在finally中) 灵活性 较差,只能以块结构的方式获取和释放锁 非常灵活,可以非阻塞、可中断、超时获取锁 性能 在早期版本较差,JDK 6 后进行了大量优化,现在性能很好 在高竞争环境下可能表现更好 读写分离 不支持 支持(ReadWriteLock) 条件变量 单一,通过 wait(), notify(), notifyAll() 多个,通过 Condition await(), signal(), signalAll() 公平性 非公平锁 可选(构造函数可指定创建公平或非公平锁)
Java集合
ArrayList
特性 描述 优点 缺点 随机访问 通过索引获取元素极快 (get(int index)) 查询性能高 (O(1)) - 尾部添加 在列表末尾添加元素平均很快 平均摊销时间复杂度为 O(1) 偶尔触发扩容,会导致性能抖动 中间操作 在指定位置插入或删除元素 - 性能差 (O(n)),需要移动元素 内存占用 底层是数组 相比 LinkedList,每个元素占用的内存更少(无需存储节点指针) 使用建议:
首选场景:当你的主要操作是随机访问或遍历,而很少在列表中间进行插入和删除操作时。
初始化:如果能够预估数据量的大小,最好在构造时指定初始容量(new ArrayList<>(1000)),这样可以避免多次扩容,提升性能。
避免在循环中中间操作:尽量避免在 ArrayList 的前端或中间进行大量的添加或删除操作,否则性能会急剧下降。这种情况下,LinkedList 可能更合适。
HashMap
底层实现
JDK 1.7 及之前:数组 + 链表
JDK 1.8 及之后:数组 + 链表 + 红黑树详细步骤
- 计算哈希值 (hash(Object key))
并非直接使用 key.hashCode()。JDK 8 进行了优化:将哈希码的高16位与低16位进行异或操作:(h = key.hashCode()) ^ (h >>> 16)。
目的:掺入高位特征,减少哈希冲突。因为计算下标时 (n-1) & hash 只用到低位的比特,如果高位变化很大而低位不变,容易冲突。- 计算数组下标
通过 i = (table.length - 1) & hash 计算键值对应该存放在数组的哪个位置(哪个“桶”里)。
为什么用 & 而不是 %? 因为数组长度 n 总是 2 的幂,(n-1) & hash 等价于 hash % n,但位运算 & 的效率远高于取模 %。- 处理哈希冲突
如果计算出的桶位置是空的,直接放入新节点。
如果不为空(发生哈希冲突),则遍历该桶中的链表或树:
如果 key 已存在(hash 值相等且 (key == e.key || key.equals(e.key))):用新 value 覆盖旧 value。
如果 key 不存在:将新节点插入到链表末尾(JDK 1.7是头插法,JDK 1.8改为尾插法,避免了多线程下扩容时可能引起的死循环)。- 判断是否树化
插入新节点后,如果链表的长度大于等于 8 (TREEIFY_THRESHOLD),则会触发树化检查。
如果此时整个哈希桶数组的容量大于等于 64 (MIN_TREEIFY_CAPACITY),则将该链表转换为红黑树。
如果容量小于 64,则优先进行扩容(resize()),因为扩容本身也能缩短链表长度。
CurrentHashMap
底层实现:数组 + 链表 + 红黑树
如何保证线程安全
CAS (Compare-And-Swap):无锁算法,用于实现乐观锁。在无竞争的情况下非常高效。常用于初始化数组、插入新节点(当桶为空时)等场景。
synchronized:用于锁定当前要操作的桶的第一个节点(头节点)。锁粒度非常小,只锁住单个桶。只要多个线程不操作同一个桶,它们就可以并发执行。
volatile:用于修饰 Node 的 value 和 next 指针,以及核心的 table 数组引用。保证了内存的可见性,确保一个线程的修改能立即被其他线程看到。具体步骤
- 计算哈希:使用 spread 方法计算 key 的 hash,保证为正数。
- 循环尝试:整个 put 操作在一个无限循环 for (Node<K,V>\[] tab = table;;) 中进行,直到成功插入才 break。
- 初始化 table (Lazy-load):如果 table 为空,先调用 initTable() 初始化。这里使用 CAS 操作来保证只有一个线程能执行初始化。
- 定位桶:通过 (n - 1) & hash 找到 key 对应的桶,获取头节点 f。
- CAS 插入(无锁):如果桶 f 为 null,说明是第一次插入。使用 CAS 操作 tabAt(tab, i) 将新节点放入桶中。如果 CAS 成功则插入完成;如果失败(被其他线程抢先),则循环重试。
- 同步块插入(加锁):如果桶 f 不为 null,则用 synchronized 锁住这个头节点 f。
这里检查是否正在扩容(f.hash == MOVED),如果是,则当前线程会先协助扩容 (helpTransfer),扩容完再重试。- 遍历链表或树:
链表:遍历,如果 key 存在则覆盖 value;否则插入到链表尾部。
红黑树:调用红黑树的插入方法。
判断是否需要树化:插入链表后,如果链表长度 >= 8,则调用 treeifyBin 尝试树化。树化前会检查当前数组容量是否 >= 64,如果不够,会优先选择扩容。
框架
Spring
常用注解
- 核心注解 (IoC & DI)
注解 说明 @Component 通用注解,用于标记任何一个类为 Spring 组件(Bean)。Spring 会自动扫描并创建其实例。 @Repository 标注在 DAO 层(数据访问层) 的类上。是 @Component 的特化,同时会将平台特定的持久化异常转换为 Spring 的统一异常。 @Service 标注在 业务服务层(Service层) 的类上。是 @Component 的特化,表示一个业务逻辑组件。 @Controller 标注在 Web 控制层(MVC) 的类上。负责处理请求,通常与 @RequestMapping 结合使用。 @RestController @Controller 和 @ResponseBody 的组合注解。用于 RESTful Web 服务,返回的数据直接写入 HTTP 响应体(如 JSON/XML),而不是视图页面。 @Configuration 标记一个类为配置类,相当于一个 XML 配置文件,内部会包含多个 @Bean 方法的定义。 @Bean 在 @Configuration 或 @Component 类的方法上使用。定义了一个由 Spring IoC 容器管理的 Bean,方法名默认为 Bean 的名称。 @Autowired 自动依赖注入。可以用于字段、Setter 方法、构造方法上。Spring 会按类型(byType)自动装配合适的 Bean。 @Qualifier 与 @Autowired 配合使用,当有多个相同类型的 Bean 时,通过名称(byName)来指定要注入的具体 Bean。 @Value 注入属性值。可以注入外部属性(如来自 .properties 文件)或表达式(SpEL)的结果。例如:@Value("${database.url}")。 @Scope 指定 Bean 的作用域,如 singleton(默认,单例)、prototype(原型,每次注入都新建)、request、session、application 等。 @Lazy 延迟初始化,表示这个 Bean 在第一次被请求时才会被创建,而不是在容器启动时。 @Primary 当有多个相同类型的 Bean 时,被标注 @Primary 的 Bean 将作为自动装配时的首选候选者。
- Web MVC 注解
注解 说明 @RequestMapping 通用请求映射。可以标注在类或方法上,将 HTTP 请求映射到 MVC 控制器的方法。可通过 method、path 等属性细化。 @GetMapping @RequestMapping(method = RequestMethod.GET) 的快捷方式。 @PostMapping @RequestMapping(method = RequestMethod.POST) 的快捷方式。 @PutMapping @RequestMapping(method = RequestMethod.PUT) 的快捷方式。 @DeleteMapping @RequestMapping(method = RequestMethod.DELETE) 的快捷方式。 @PatchMapping @RequestMapping(method = RequestMethod.PATCH) 的快捷方式。 @RequestParam 用于获取 URL 查询参数或表单字段的值并绑定到方法参数。例如:@RequestParam("id") Long userId。 @PathVariable 用于获取 RESTful 风格 URL 中的模板变量。例如:/user/{id} 对应 @PathVariable("id") Long id。 @RequestBody 将 HTTP 请求体(如 JSON 数据) 反序列化并绑定到方法参数对象上。 @ResponseBody 将方法返回值直接写入 HTTP 响应体,而不是渲染一个视图。通常用于返回 JSON/XML 数据。 @ModelAttribute 1. 用于方法参数:从模型中获取属性。2. 用于方法:在 @RequestMapping 方法执行前,将返回值添加到模型。 @CookieValue 将 HTTP 请求中的 Cookie 值绑定到方法参数。 @RequestHeader 将 HTTP 请求头信息绑定到方法参数。 @ResponseStatus 标注在方法或异常类上,指定 HTTP 响应的状态码。例如:@ResponseStatus(HttpStatus.NOT_FOUND)。 @ControllerAdvice 全局异常处理。定义一个类,其中的 @ExceptionHandler、@InitBinder、@ModelAttribute 方法会应用到所有控制器。 @ExceptionHandler 标注在方法上,用于处理特定类型的异常,通常在一个 @Controller 或 @ControllerAdvice 类中。 @CrossOrigin 启用跨域请求。可以标注在控制器类或方法上,为请求处理方式启用跨源资源共享(CORS)。
- 数据访问/事务注解
注解 说明 @Transactional 声明式事务管理。标注在类或方法上,定义方法的事务属性(如传播行为、隔离级别、回滚规则等)。
- 切面编程 (AOP) 注解
注解 说明 @Aspect 声明一个类是切面,包含通知(Advice)和切点(Pointcut)。 @Before 前置通知:在目标方法执行之前执行。 @After 后置通知:在目标方法执行之后(无论是否发生异常)执行。 @AfterReturning 返回通知:在目标方法成功执行并返回后执行。 @AfterThrowing 异常通知:在目标方法抛出异常后执行。 @Around 环绕通知:最强大的通知类型,可以在目标方法执行前后自定义行为,甚至可以决定是否执行目标方法。 @Pointcut 定义切点表达式,声明一个可重用的切点。
- Spring Boot
注解 说明 @SpringBootApplication 核心注解,是 @SpringBootConfiguration, @EnableAutoConfiguration, @ComponentScan 三个注解的组合。通常放在主启动类上。 @EnableAutoConfiguration 启用自动配置。Spring Boot 会根据类路径中的 Jar 包,自动配置应用程序。 @SpringBootConfiguration 标记类为配置类,是 @Configuration 的另一种形式。 @ConfigurationProperties 将外部配置文件(如 application.properties)中的属性批量绑定到一个 Java Bean 上。 @ConditionalOnClass 条件注解,当类路径下存在指定的类时,配置才生效。 @ConditionalOnProperty 条件注解,当指定的配置属性具有特定值时,配置才生效。
事务的传播机制
- 7种传播机制
传播行为类型 说明 外部不存在事务 外部存在事务 适用场景 REQUIRED (默认) 支持当前事务。如果不存在,则新建一个。 新建一个事务 加入当前事务 最常用的场景。例如,新增用户和新增日志应该在同一事务中。 REQUIRES_NEW 新建一个事务。如果当前存在事务,则挂起当前事务。 新建一个事务 挂起外部事务,新建内部事务。两个事务互不干扰。 内外事务完全独立,内层事务成功与否不应影响外层。例如,日志记录(即使失败也不应回滚主业务)。 SUPPORTS 支持当前事务。如果不存在,则以非事务方式执行。 以非事务方式运行 加入当前事务 方法可以“随大流”,有事务就用,没有也行。查询操作有时会使用。 NOT_SUPPORTED 以非事务方式执行。如果当前存在事务,则挂起当前事务。 以非事务方式运行 挂起外部事务,以非事务方式运行内部方法。 强制非事务执行。例如,执行一些不需要事务的统计计算。 MANDATORY 强制要求存在当前事务。如果不存在,则抛出异常。 抛出异常 IllegalTransactionStateException 加入当前事务 方法必须在一个已存在的事务中被调用,否则就是编程错误。 NEVER 以非事务方式执行。如果当前存在事务,则抛出异常。 以非事务方式运行 抛出异常 IllegalTransactionStateException 方法绝对不能在任何事务中运行,用于检查事务泄露。 NESTED 嵌套事务。如果当前存在事务,则在当前事务的嵌套事务中执行。如果不存在,则行为同 REQUIRED。 新建一个事务 在外部事务中创建一个保存点(Savepoint)。内部事务的回滚只影响保存点之后的操作,不影响外部事务之前的操作。但外部事务的回滚会回滚整个嵌套事务。 复杂的业务场景,允许部分操作回滚而不影响全局。例如,电商下单时,扣库存和扣款是主事务,为每个商品创建订单项可以作为嵌套事务,某个商品失败只回滚该商品的操作。
- 重点机制示例
- REQUIRED (默认)
这是最常用的模式。
场景:methodB 有事务,调用 methodA(REQUIRED)。
行为:methodA 会加入到 methodB 的事务中。它们属于同一个事务。任何一个方法发生异常,整个事务都会回滚。
@Transactional(propagation = Propagation.REQUIRED) // 默认值,可省略
public void methodB() {
// 一些数据库操作...
methodA(); // 调用内层方法
// 更多数据库操作...
}
@Transactional(propagation = Propagation.REQUIRED)
public void methodA() {
// 数据库操作...
}
- REQUIRES_NEW
场景:methodB 有事务,调用 methodA(REQUIRES_NEW)。
行为:Spring 会挂起 methodB 的事务,为 methodA 创建一个全新、独立的事务。两个事务拥有独立的连接和隔离级别。
如果 methodA 完成提交,然后 methodB 之后发生异常并回滚,methodA 的操作不会回滚。
如果 methodA 发生异常并回滚,默认情况下会抛出异常并导致 methodB 的事务也回滚(除非 methodB 捕获并处理了这个异常)。
@Transactional
public void methodB() {
// 操作1 (属于事务B)
try {
methodA(); // 调用内层方法
} catch (Exception e) {
// 捕获异常,防止事务B因A的异常而回滚
}
// 操作2 (属于事务B)
}
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void methodA() {
// 操作A (属于独立的新事务A)
// 如果这里异常,事务A回滚,并且异常会抛给methodB
}
- NESTED (基于保存点)
场景:methodB 有事务,调用 methodA(NESTED)。
行为:Spring 会在外部事务(methodB 的事务)中创建一个保存点(Savepoint)。
如果 methodA 成功执行,则其操作会随着外部事务一起提交。
如果 methodA 执行失败并回滚,则只会回滚到保存点的状态,不会影响保存点之前 methodB 所做的操作。
如果 methodB 在 methodA 之后发生异常,则整个事务(包括 methodA 的操作)都会回滚。
NESTED 和 REQUIRES_NEW 的区别:
REQUIRES_NEW:完全独立的新事务,互不干扰。
NESTED:是外部事务的子集,其提交依赖于外部事务的最终提交。
注意:NESTED 需要底层数据库支持保存点功能(如 MySQL, PostgreSQL),否则会降级为 REQUIRED。
常见问题
Q1: Spring事务失效的常见情况
数据库引擎不支持事务(最根本的前提)
场景:如果你使用的是 MySQL,并且表使用的是 MyISAM 引擎,那么事务会完全失效,因为 MyISAM 本身就不支持事务。
解决方案:将表引擎改为 InnoDB。方法非 public 修饰
场景:@Transactional 注解标注在了一个 protected、private 或包权限的方法上。
原因:Spring AOP 代理默认是基于 CGLIB 的,而 CGLIB 无法代理非 public 方法。基于接口的 JDK 动态代理也同样如此。
解决方案:确保被 @Transactional 注解的方法都是 public 的。自调用(同类调用)★ 最常见 & 最隐蔽 ★
场景:在同一个类中,一个非事务方法 A() 调用了本类的事务方法 B()。
@Service
public class UserService {
public void A() {
// ... 一些操作
this.B(); // 自调用,事务失效!
// ...
}
@Transactional
public void B() {
// 数据库操作
}
}
原因:A() 方法中的 this.B() 中的 this 是目标对象本身,而不是 Spring 注入的代理对象。因此调用不会经过代理,事务逻辑自然不会生效。
解决方案:
(推荐)将方法 B() 抽取到另一个 Service 中,然后通过 @Autowired 注入这个新的 Service 再调用。
(不推荐)通过 AopContext 获取当前代理对象(需要开启 expose-proxy)。
@Service
public class UserService {
public void A() {
// 获取当前代理对象并调用其方法
((UserService) AopContext.currentProxy()).B();
}
@Transactional
public void B() { ... }
}
配置中需要开启:@EnableAspectJAutoProxy(exposeProxy = true)
- 异常类型不对或被捕获
场景1:抛出的是非 RuntimeException 或 Error。
原因:@Transactional 默认只回滚 RuntimeException 和 Error。如果抛出的是 IOException、SQLException 等受检异常(Checked Exception),事务不会回滚。
解决方案:使用 @Transactional(rollbackFor = Exception.class) 指定需要回滚的异常类型。
场景2:异常被方法内部 catch 吞掉了。
原因:代理对象只有在接收到异常时才会触发回滚逻辑。如果你在方法内 try-catch 了异常却没有重新抛出,代理对象就感知不到异常,自然会提交事务。
解决方案:如果希望事务回滚,必须在 catch 块中重新抛出一个运行时异常,或者通过 TransactionAspectSupport.currentTransactionStatus().setRollbackOnly() 手动回滚。
@Transactional
public void method() {
try {
// 数据库操作
} catch (Exception e) {
// 只打印日志,没有抛出
e.printStackTrace(); // 事务失效!
// throw new RuntimeException(e); // 必须重新抛出
}
}
未被 Spring 容器管理
场景:你给一个类加了 @Transactional,但这个类没有被 Spring 扫描到(即不是 @Component, @Service, @Repository 等),或者你直接 new 了一个对象来调用方法。
原因:只有 Spring 容器管理的 Bean 才会被代理。
解决方案:确保类已被 Spring 扫描并管理。propagation 传播属性设置不当
场景:内层方法设置了 Propagation.NOT_SUPPORTED, Propagation.NEVER 等传播行为。
原因:这是由传播行为的定义决定的,并非“失效”,而是符合预期的行为。
解决方案:根据业务需求正确配置传播行为。
@Transactional
public void A() {
// 有事务
B(); // B方法不支持事务,会挂起A的事务,以非事务运行
}
@Transactional(propagation = Propagation.NOT_SUPPORTED)
public void B() {
// 非事务执行,操作立即提交,无法回滚
}
多线程调用
场景:在方法内开启新线程进行数据库操作。
原因:事务信息(如数据库连接)是存储在 ThreadLocal 中的,不同线程拥有不同的 ThreadLocal,因此新线程无法共享原线程的事务上下文。
解决方案:避免在多线程中处理同一事务,或将多线程操作合并到主线程中。错误配置(如切面顺序)
场景:如果同时使用了自定义的 AOP 切面,并且切面的优先级比事务切面高,且在切面中吞掉了异常。
原因:自定义切面先执行,如果它捕获了异常,事务切面就接收不到异常信号。
解决方案:调整切面顺序,使用 @Order 注解确保事务切面有更高的优先级(更小的 order 值)。总结
数据库:用的是 InnoDB 引擎吗?
注解:方法是否是 public 的?
调用:是自调用吗?(检查调用链,是否通过代理对象调用)
异常:抛出的异常类型对吗?异常被捕获了吗?
管理:这个类被 Spring 管理了吗?(@Service 等注解加了没?扫描路径对了吗?)
配置:@EnableTransactionManagement 开启了吗?(Spring Boot 项目默认已开启)
传播行为:检查 propagation 设置是否符合预期。
SpringBoot
自动装配
- 启动: Spring Boot 应用启动,执行 main 方法中的 SpringApplication.run(...)。
- 触发入口注解: 扫描到被 @SpringBootApplication 注解的入口类。
- 启用自动装配: @SpringBootApplication 中的 @EnableAutoConfiguration 注解生效。
- 加载候选配置: @EnableAutoConfiguration 通过 @Import 导入了 AutoConfigurationImportSelector。该类从 META-INF/spring.factories 文件中读取所有自动配置类的全类名。
- 过滤与条件判断: 遍历这些候选配置类,根据其上的 @ConditionalOnXxx 条件注解进行筛选,最终确定哪些配置类需要被加载。
- 执行配置类: 生效的自动配置类 (XXXAutoConfiguration) 被加载,它们内部的 @Bean 方法开始执行,向 IoC 容器中添加组件。
- 自定义配置优先: 这些配置类中的 @Bean 方法通常带有 @ConditionalOnMissingBean 条件,如果用户已经在自己的配置中定义了相同类型的 Bean,则自动配置将不会生效,确保了用户自定义的配置优先于自动配置。
常见问题
Q1: 自动装配 vs 传统配置 (spring.factories vs @Import)
你可能会有疑问,为什么不用传统的 @Import 直接导入所有配置类,而要用 spring.factories 这种看似繁琐的方式?
答案:解耦和可扩展性。
@Import 是写死在代码里的,需要编译时就知道要导入哪些类。
spring.factories 是一种SPI (Service Provider Interface) 机制。它允许第三方 Jar 包(例如 mybatis-spring-boot-starter)通过在自己的 Jar 包中创建一个 META-INF/spring.factories 文件,并将自己的自动配置类写在里面,从而被 Spring Boot 发现和加载。这使得自动装配对整个生态体系都是可扩展的,而不需要修改 Spring Boot 本身的源代码。
SpringCloud
常用组件
核心与必备组件
组件名 功能描述 对比 Spring Cloud Netflix 官方文档 Nacos 服务注册发现 和 配置管理 的二合一平台。是整个体系的基石。 Eureka + Config https://nacos.io Spring Cloud Alibaba Nacos Discovery 服务发现组件,用于将微服务注册到 Nacos Server,并能从 Nacos 发现其他服务。 Spring Cloud Netflix Eureka https://github.com/alibaba/spring-cloud-alibaba/wiki/Nacos-discovery Spring Cloud Alibaba Nacos Config 配置管理组件,用于从 Nacos Server 拉取应用的外部配置,并支持配置动态刷新。 Spring Cloud Config https://github.com/alibaba/spring-cloud-alibaba/wiki/Nacos-config
服务治理与容错组件
组件名 功能描述 对比 Spring Cloud Netflix 官方文档 Spring Cloud Alibaba Sentinel 流量控制、熔断降级、系统负载保护 的轻量级组件。提供可视化控制台。 Hystrix https://github.com/alibaba/spring-cloud-alibaba/wiki/Sentinel Spring Cloud LoadBalancer 客户端负载均衡器。通常与 Nacos Discovery 结合使用,实现从服务列表中选择实例进行调用。 Ribbon (Spring Cloud 官方组件) Spring Cloud OpenFeign 声明式的 REST 客户端。只需定义一个接口并添加注解,就能实现对其他服务的 HTTP 调用。集成了负载均衡和熔断器。 Feign (Spring Cloud 官方组件)
分布式事务组件
组件名 功能描述 对比 Spring Cloud Netflix 官方文档 Seata 分布式事务解决方案。提供了 AT、TCC、Saga 和 XA 多种事务模式,其中 AT 模式无侵入,使用简单。 Apache ShardingSphere, LCN https://seata.io Spring Cloud Alibaba Seata 对 Seata 的集成,让你在 Spring Cloud 项目中方便地使用 Seata。 https://github.com/alibaba/spring-cloud-alibaba/wiki/Seata
其他常用组件
组件名 功能描述 Spring Cloud Gateway API 网关,是所有流量的入口,负责路由转发、权限校验、限流、熔断、日志监控等。它是 Spring Cloud 官方二代网关,替代了 Netflix Zuul。 Spring Cloud Stream 用于构建消息驱动微服务的框架。可以很方便地与 RocketMQ(阿里系)、Kafka 等消息中间件集成。 RocketMQ 阿里开源的分布式消息中间件,常用于异步解耦、流量削峰、数据同步等场景。Spring Cloud Alibaba 对其有良好集成。
中间件
Redis
数据类型
主数据类型
- String: 字符串
- List: 列表
- Hash: 哈希表
- Set: 集合
- Sorted Set: 有序集合
扩展类型
- Bitmaps: 位图(基于 String)
- HyperLogLog: 基数统计(基于 String)
- Geospatial: 地理空间(基于 Sorted Set)
新类型
- Stream: 流(5.0版本引入)
总结
数据类型 特性 适用场景 String 二进制安全,可计数 缓存,计数器,分布式锁 List 有序,可重复,双向操作 消息队列,最新列表 Hash 键值对集合,适合存储对象 存储对象信息,购物车 Set 无序,唯一,集合运算 共同好友,抽奖,去重 Sorted Set 唯一,有序(按分数排序) 排行榜,延迟任务,带权重队列 Bitmaps 位操作,极省空间 签到统计,用户活跃度 HyperLogLog 基数估算,固定大小 大规模UV统计 Geospatial 地理位置计算 附近的人,地点 Stream 持久化消息流,消费者组 可靠的消息队列,事件溯源
常见问题
Q1: Sorted Set实现原理
核心结构:哈希表 + 跳跃表
一个 Sorted Set 在 Redis 内部的完整表示包含两个部分:
一个字典 (Hash Table)
一个跳跃表 (Skip List)
- 字典 (Dict)
作用:实现 O(1) 复杂度的根据成员 (member) 查找分数 (score) 的操作。
结构:
Key:存储的是集合的成员 (member)。
Value:存储的是该成员对应的分数 (score)。
要它? 如果只用跳跃表,根据成员查分数需要 O(log N) 的时间(因为要在跳跃表中遍历查找成员)。字典的引入将这个操作优化到了 O(1),对于像 ZSCORE 这样的命令至关重要。- 跳跃表 (Skip List)
作用:维护一个按分数 (score) 排序的成员列表,支持高效的范围查询(如 ZRANGE, ZRANK)和插入删除操作。
为什么需要它? 字典本身是无序的,无法直接支持基于分数的排序和范围查询。而跳跃表在这种场景下性能非常好,平均时间复杂度为 O(log N)。
Rabbitmq
常见问题
Q1: 脑裂
- 1.什么是脑裂?
脑裂 是指在高可用集群中,由于网络分区或节点暂时性故障,导致集群中的成员无法正常通信。此时,原本的一个集群分裂成了两个或多个独立的部分,这些部分都认为自己是唯一存活的集群,并且可能同时对外提供服务(如写入操作),从而导致数据不一致、冲突和混乱的现象。
在 RabbitMQ 的语境下,脑裂特指镜像队列 同时存在于两个无法通信的节点分区中,并且两个分区都可能有生产者继续向队列写入消息,导致同一队列的数据出现分叉。
- 2.RabbitMQ 如何处理和防止脑裂?
RabbitMQ 提供了三种主要的策略来处理网络分区(从而避免脑裂),核心在于优先保证数据一致性,而非可用性。
策略一:pause-minority 模式(默认推荐)
原理: 节点会根据自己对集群状态的认知,判断自己是否属于“少数派”(分区中的节点数少于或等于总节点数的一半)。如果判断自己是少数派,它会主动暂停(pause)自己。
一个被暂停的节点会:
断开所有客户端的连接。
停止处理消息(既不能生产也不能消费)。
实际上停止服务,不再认为自己是一个主节点。
效果: 在上面的例子中:
分区 A (node2, node3): 有 2 个节点,是多数派(超过 3/2=1.5),所以继续运行。
分区 B (node1): 只有 1 个节点,是少数派,它会主动暂停自己。
结果: 最终只有多数派分区(分区 A)能提供服务,完全避免了脑裂。网络恢复后,暂停的节点(node1)会重新加入集群,并从新的主节点同步所有错过的数据。
优点: 强一致性,保证数据不会出错0。
缺点: 牺牲了部分可用性(少数派分区完全不可用)。对于偶数节点的集群,可能会出现两个分区节点数相同(都是少数派)而导致整个集群都暂停的情况,因此建议集群使用奇数个节点(3, 5, 7...)。策略二:pause-if-all-down 模式
原理: 只有当节点无法连接到所有指定的节点时,它才会暂停。你需要配置一个节点列表(nodes),该模式只关心是否能连接到这些特定节点,而不是计算集群多数派。
适用场景: 适用于跨机房部署等场景,你希望只有当整个机房都失联时才暂停,而不是根据简单的数量多数来判断。
风险: 如果配置不当,比 pause-minority 更容易引发脑裂。策略三:autoheal 模式
原理: 这种模式不那么保守。当网络分区恢复后,RabbitMQ 会选择一个连接客户端最多的分区继续运行,并重启其他分区中的节点,让它们以从节点身份重新加入。
过程: 在分区期间,多个分区可能都在运行(存在脑裂风险)。但一旦网络恢复,系统会通过“牺牲”一个分区的方式来修复(heal)数据不一致。
优点: 在分区期间提供了更好的可用性。
缺点: 分区期间可能发生脑裂,导致数据不一致。恢复时,被放弃的分区中的所有数据将会丢失。这是一个优先保证可用性(AP) 的策略。
- 3.如何配置和选择策略
策略是通过 cluster_partition_handling 配置项设置的。
配置文件示例 (rabbitmq.conf):
# 使用 pause-minority 模式(推荐)
cluster_partition_handling = pause_minority
# 使用 pause-if-all-down 模式
cluster_partition_handling = pause_if_all_down
cluster_partition_handling.pause_if_all_down.recover = heal
cluster_partition_handling.pause_if_all_down.nodes.1 = rabbit@node1
cluster_partition_handling.pause_if_all_down.nodes.2 = rabbit@node2
# 使用 autoheal 模式
cluster_partition_handling = autoheal
- 4.如何监控和恢复
监控: 使用 RabbitMQ 管理界面或 rabbitmqctl cluster_status 命令监控集群状态。网络分区是严重事件,应该触发警报。
检测分区: 管理界面的 Overview 页面上方会有明显的红色警告提示 “Partition detected”。
恢复: 当网络问题修复后,被暂停的节点会自动尝试重新加入集群。通常不需要手动干预。你可以使用 rabbitmqctl forget_cluster_node 命令手动移除故障节点(如果需要)。
Q2: 消息的幂等性
- 为什么会产生重复消息
1.生产者重复发送:
生产者发送消息后,Broker 可能因网络抖动等原因未及时返回 ack 确认。
生产者会触发重试机制,从而可能发出两条一模一样的消息。2.Broker 重复投递:
消费者处理完消息后,在给 Broker 返回 ack 之前突然断开连接(如进程崩溃、网络中断)。
Broker 因未收到 ack,会将消息重新置为 Ready 状态,并投递给其他消费者(或等待当前消费者重连后再次投递)。3.消费者重复处理:
消费者处理成功,但在输出结果(如修改数据库、调用下游接口)后,自身崩溃,未能发送 ack。
消费者重启后,会再次收到同一条消息。
- 如何保证消息幂等性
方案一:唯一业务ID(最推荐、最通用)
这是最常用且有效的方案。
消息体设计:在生产者端,为每一条业务消息生成一个全局唯一的ID(例如 UUID、雪花算法ID、业务字段组合如“订单ID+操作类型”)。这个ID需要代表唯一的一笔业务。
{
"msg_id": "202409020001", // 唯一消息ID
"order_id": "123456", // 业务ID
"amount": 100,
"action": "add_points"
}
消费者处理流程:
第一步:SELECT:在执行业务逻辑之前,先拿着这个唯一消息ID msg_id 去数据库中查询(例如一张 message_processed 表)。
第二步:判断:
如果查到了记录:说明这条消息已经被成功处理过了,直接返回成功(ack消息),不做任何业务操作。
如果没查到记录:说明这是条新消息,继续执行业务操作。
第三步:INSERT + 业务操作:将业务操作和插入 message_processed 记录放在同一个数据库事务中。这是关键!
事务成功:业务做了,记录也插了,消息被消费。
事务失败:业务回滚,记录也没插入,消息会重试。
message_processed 表设计:
字段名 类型 说明
id bigint 自增主键
msg_id varchar(128) 唯一消息ID,需要加唯一索引
create_time datetime 创建时间
优点:
通用性强,适用于所有业务场景。
可靠性高,依靠数据库的事务和唯一索引保证。
缺点:
增加了数据库的写入压力(每次消费都要写一条记录)。
需要创建一张额外的表。方案二:利用数据库唯一键约束(方案一的特化版)
如果业务逻辑本身就是在向数据库插入一条记录,而这条记录的某个字段可以天然作为唯一标识(例如订单ID),那么可以利用数据库的唯一索引来避免重复插入。
例子:订单创建消息
消息内容:{“order_id”: "123456", "user_id": "1001", ...}
消费者逻辑:INSERT INTO orders (order_id, user_id, ...) VALUES ("123456", "1001", ...);
如果消息重复,第二次插入时,因为 order_id 字段有唯一索引,插入会失败,从而避免了创建两个相同的订单。
优点:
无需额外表,利用现有业务表即可。
实现简单。
缺点:
适用范围窄,只适用于“新增”操作,不适用于更新、删除或复杂逻辑。
插入失败可能意味着其他错误,需要做好异常区分。方案三:乐观锁(适用于更新操作)
如果业务是更新操作(如扣减库存、更新状态),可以使用乐观锁。
例子:更新订单状态
消息内容:{“order_id”: "123456", "new_status": "PAID", "version": 1}
消费者逻辑:执行SQL:
UPDATE orders
SET status = 'PAID', version = version + 1
WHERE order_id = '123456' AND version = 1;
检查 UPDATE 操作影响的行数 (affected rows):
如果为 1:成功,是第一次处理。
如果为 0:失败,要么版本号不对(消息是旧的),要么已经处理过了(当前版本号已经不是1了)。
优点:
不需要额外表。
是处理并发更新的标准模式。
缺点:
需要改造业务表,增加版本号字段。
消息体中需要携带版本号信息。方案四:分布式锁(复杂场景)
在非常复杂的业务场景下,可以先尝试获取一个分布式锁(基于 Redis 或 Zookeeper),锁的Key就是消息的唯一ID。获取到锁的线程才能处理业务,处理完后释放锁。后续的重复消息由于获取不到锁,会处理失败或直接放弃。
优点:
概念清晰。
缺点:
性能开销大,引入新的中间件,增加了系统复杂性。
通常不推荐作为首选,除非业务逻辑本身就需要强互斥。
Sentinel
数据库
MySQL
MySQL存储引擎
InnoDB(默认存储引擎)
从 MySQL 5.5 版本开始,InnoDB 成为默认的存储引擎。
核心特性:
支持事务(ACID):这是它最重要的特性。支持 COMMIT、ROLLBACK 和崩溃恢复能力,确保数据完整性。
行级锁:只在需要时锁定特定的行,而不是整个表。这极大地提高了在高并发读写负载下的性能和可扩展性。
外键约束:支持 FOREIGN KEY,保证数据的一致性和参照完整性。
MVCC(多版本并发控制):通过保存数据的快照来提高并发性能,允许非阻塞读操作。
适用场景:
绝大多数场景:除非有特殊需求,否则 InnoDB 是最安全、最通用的选择。
需要事务的应用(如银行交易、订单系统)。
高并发读写、需要行级锁的应用。
需要外键来保证数据完整性的应用。
不适用场景:
对全文索引有极高要求(虽然 MySQL 5.6+ 的 InnoDB 也支持全文索引,但可能不如专业的搜索引擎)。
只读或读多写极少,且对查询速度有极致要求,可以牺牲一些功能来换取极致的空间和性能(这时可考虑 MyISAM 或列式存储引擎)。MyISAM(MySQL 5.5 之前的默认引擎)
注意:MySQL 8.0 已不再支持 MyISAM 的数据字典元数据校验,意味着它正逐渐被淘汰。
核心特性:
表级锁:对表进行写操作时(如 UPDATE、INSERT),会锁定整个表。这导致它在高并发写操作下性能很差。
不支持事务:发生故障后无法安全恢复,可能丢失数据或需要修复表。
全文索引:在早期版本中,MyISAM 的全文索引比 InnoDB 的更成熟(但现在差距已缩小)。
高速读:如果表主要是用于读(如 SELECT COUNT()),且并发写很少,它的速度可能非常快。
支持 AUTO_INCREMENT on secondary columns.
适用场景:
旧的、不再更新的应用程序。
只读或读远多于写的表,且对并发性要求不高。
数据仓库、报表类应用,进行大量的 COUNT() 查询(因为它将行数存储在元数据中,查询极快)。
不适用场景:
现代 Web 应用:因为需要事务和高并发写入。
任何需要数据可靠性和崩溃恢复的场景。Memory
核心特性:
将所有数据存储在RAM 中,速度极快。
表结构在服务器重启后保留,但所有数据都会丢失。
默认使用哈希索引,也支持 B-tree 索引。
不支持 TEXT 或 BLOB 等变长数据类型。
适用场景:
用于存储临时、非关键的会话数据。
用于缓存中间结果集的查找表。
需要极快访问速度,且数据丢失也无所谓的场景。
不适用场景:
任何需要持久化存储的数据。Archive
核心特性:
为大量很少被引用的历史、归档数据的存储和检索而优化。
插入速度非常快,并很好地压缩数据(磁盘 I/O 更少)。
只支持 INSERT 和 SELECT 操作,不支持 DELETE、UPDATE 和索引(在行上)。
支持行级锁。
适用场景:
日志记录、审计数据等需要大量存储但很少更新的归档数据。CSV
核心特性:
以逗号分隔值的格式将数据存储在文本文件中。
不支持索引。
可以直接用文本编辑器打开查看。
适用场景:
快速导出数据到 CSV 格式,或从 CSV 文件导入数据。
与其他需要读写 CSV 文件的应用程序交换数据。其他引擎
Blackhole:接受数据但不存储,就像“黑洞”。常用于复制架构或记录日志。
Federated:访问远程 MySQL 服务器上的表,而不是本地存储数据。注意:该引擎默认禁用,且有诸多限制,不建议使用。
Merge(MRG_MyISAM):将多个结构相同的 MyISAM 表逻辑上组合成一个表,适用于数据仓库。
列式存储引擎:
MyRocks:由 Facebook 开发,基于 RocksDB,为高压缩率和写入密集型负载而优化。
ColumnStore:为大规模数据仓库和分析查询(OLAP)设计。总结
特性 InnoDB MyISAM Memory Archive 存储限制 64TB 256TB RAM 无 事务 支持 不支持 不支持 不支持 锁粒度 行级锁 表级锁 表级锁 行级锁 外键 支持 不支持 不支持 不支持 全文索引 支持 不支持 不支持 不支持 MVCC 支持 不支持 不支持 不支持 压缩 支持 是(只读) 否 极高压缩 适用场景 绝大多数事务型应用 只读报表、Web(旧) 临时表、缓存 日志归档
- 如何选择?
默认选择 InnoDB:对于 99% 的新项目,直接使用 InnoDB。它提供了事务安全性和高并发能力,这是现代应用的基础。
需要全文搜索:优先使用 InnoDB 的全文索引。如果不能满足,可以考虑集成专业的搜索引擎(如 Elasticsearch 或 Solr),而不是用 MyISAM。
临时数据/缓存:考虑使用 Memory 引擎。
日志/归档:考虑使用 Archive 引擎。
数据仓库/只读报表:如果表完全是静态的(只读),且需要频繁地 COUNT(*),可以测试一下 MyISAM 的性能是否仍有优势,但需谨慎评估风险。更好的选择是使用列式存储引擎或专门的 OLAP 数据库。
高并发下MySQL优化
层面一:SQL语句与索引优化(成本最低,效果最显著)
这是优化的第一步,也是最关键的一步。80%的性能问题源于糟糕的SQL和索引。
避免低效SQL:
避免使用 SELECT *:只获取需要的字段,减少网络传输和内存消耗。
避免大事务:尽量将大事务拆分为小事务,及时提交,减少锁的持有时间。
避免函数操作 on 索引列:如 WHERE YEAR(create_time) = 2023 会导致索引失效。应改为 WHERE create_time BETWEEN '2023-01-01' AND '2023-12-31'。
避免类型转换:WHERE user_id = '123'(user_id 是整数)会导致类型转换,索引失效。
高效使用索引:
确保查询都使用到索引:使用 EXPLAIN 分析每一条核心SQL的执行计划,关注 type 字段(至少达到 range 级别,理想是 ref 或 const)。
创建覆盖索引:索引包含了查询所需的所有字段,无需回表,极大提升性能。例如 SELECT id, name FROM users WHERE status = 'active',可以建立索引 (status, name, id)。
注意联合索引的最左前缀原则:索引 (a, b, c) 可以用于查询 a=?,a=? AND b=?, a=? AND b=? AND c=?,但不能用于 b=? 或 c=?。
使用前缀索引:对于 TEXT/VARCHAR 等长字段,不必对整个字段建索引,可以使用 column_name(prefix_length)。
索引不是越多越好:索引会降低写操作(INSERT/UPDATE/DELETE)的速度,并占用额外空间。需要平衡读写比例。
利用批量操作:
批量插入:使用 INSERT INTO table VALUES (v1), (v2), (v3)... 代替多条单行插入。
批量更新:尽量减少循环逐条更新。层面二:数据库设计优化
良好的设计是应对高并发的基础。
选择合适的存储引擎:
InnoDB:绝对的主流选择。支持行级锁、外键、事务(ACID),写并发性能远优于MyISAM。除非有特殊理由,否则一律使用 InnoDB。
范式与反范式的权衡:
范式化:减少数据冗余,更新操作更快更简单。
反范式化:适当的数据冗余(如将常用字段冗余到主表),用空间换时间,减少表关联查询,适合读多写少的场景。例如,在订单列表里直接冗余“用户名”,而不是每次都要 JOIN 用户表。
数据类型优化:
使用最简单、最小的数据类型。例如,用 INT 而不是 VARCHAR 存储IP地址(使用 INET_ATON() 和 INET_NTOA() 转换)。
避免使用 NULL,尽量用默认值(如空字符串、0)。NULL 会使索引和值比较变得更复杂。
分库分表(sharding):
当单表数据量过大(如千万级)时,B+树深度增加,查询性能下降。
水平分表:将一张表的数据按某种规则(如用户ID哈希、时间范围)拆分到多个结构相同的表中。这是解决大数据量和高并发的主要手段。
垂直分表:将一张宽表(有很多列)按访问频率拆分成多个小表(如“热点字段表”和“冷数据表”)。
分库:在分表的基础上,将表分布到不同的物理数据库实例上,进一步分散压力。
工具:可以借助 ShardingSphere、MyCat 等中间件,但对应用侵入性较强,增加了系统复杂度。层面三:架构优化(从单机到分布式)
这是应对极高并发的主要手段。
引入缓存(Cache) - 重中之重!
原则:缓存是数据库的前置屏障,绝大部分的读请求不应该到达数据库。
策略:
客户端缓存:浏览器缓存。
应用层缓存:本地缓存(如 Caffeine/Ehcache)或分布式缓存(如 Redis/Memcached)。将热点数据(如用户信息、商品信息、秒杀库存)放在缓存中。
数据库缓存:MySQL 自身的 query_cache(8.0已移除),效果一般,不推荐过多依赖。
注意缓存一致性和缓存穿透、击穿、雪崩问题。
读写分离(Read/Write Splitting):
基于主从复制(Master-Slave Replication)实现。
一主多从:主库(Master)负责处理写操作(事务),从库(Slave)负责处理读操作。
优点:分摊读压力,提升读性能。从库还可以用于备份和故障转移。
挑战:主从同步有延迟,需要业务能容忍短暂的数据不一致(如评论、点赞等场景适合,金融余额等场景不适合)。
消息队列(MQ)削峰填谷:
对于高并发的写操作(如秒杀下单、日志记录),可以将请求先写入消息队列(如 Kafka/RabbitMQ/RocketMQ)。
后端服务以自己能处理的速度从队列中消费消息,再写入数据库。
优点:避免流量洪峰直接冲垮数据库,将异步的、非核心的操作与主流程解耦。层面四:MySQL配置与硬件优化
硬件优化:
SSD硬盘:必须的。将机械硬盘(HDD)换成固态硬盘(SSD),IOPS提升几个数量级,是性价比最高的硬件升级。
内存:扩大内存,使 innodb_buffer_pool_size 足够大,能容纳整个工作数据集(working set),让查询尽可能在内存中完成,避免磁盘IO。
关键配置调优 (my.cnf):
innodb_buffer_pool_size:最重要的参数。通常设置为可用物理内存的 50%-70%。
innodb_log_file_size:redo log 大小。更大的 log file 可以减少磁盘刷写。通常设置为 1G-4G。
max_connections:最大连接数。设置过高可能导致内存耗尽,需要配合连接池使用。
innodb_flush_log_at_trx_commit:
=1(默认):完全ACID,每次事务提交都刷盘,最安全,但性能最差。
=2:每次事务提交只写 OS 缓存,每秒刷一次盘。性能更好,但宕机可能丢失1秒数据。
=0:每秒写日志和刷盘。性能最好,但最不安全。
根据业务对一致性和性能的要求权衡。很多金融业务用1,而一些能容忍少量数据丢失的场景(如日志、评论)可以用2。
sync_binlog:控制二进制日志(binlog)刷盘策略。类似上一条,=1 最安全,=0 或 >1 性能更好。
连接池:
无论是应用端(如 HikariCP, Druid)还是数据库端(如 ProxySQL),使用连接池可以避免频繁创建和销毁连接的开销。
关注公众号
低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。
持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。
转载内容版权归作者及来源网站所有,本站原创内容转载请注明来源。
-
上一篇
MySQL数据库在高并发下的优化方案
1. 概述 高并发场景下,数据库极易成为整个应用系统的瓶颈,导致响应变慢、超时甚至宕机。本方案旨在从多个层面(SQL、数据库配置、架构、硬件等)提供优化策略,以提升 MySQL 在高并发环境下的性能、稳定性和扩展性。 2. SQL 语句及索引优化 (最有效、成本最低) 2.1 避免慢查询 使用 EXPLAIN:分析所有核心 SQL 的执行计划(EXPLAIN SELECT ...),重点关注以下字段: type:至少达到 range 级别,理想是 ref、eq_ref 或 const。避免 ALL(全表扫描)。 key:是否使用了正确的索引。 rows:预估扫描的行数,尽可能少。 Extra:...
-
下一篇
Crontab安装和使用
安装crontab sh 复制代码 # 安装crontab yum install cronie -y 常用命令 如果使用service则使用下列命令 sh 复制代码 # 启动 service crond start # 停止 service crond stop # 重启 service crond restart # 运行状态 service crond status # 重新载入配置 service crond reload 如果使用systemctl则使用下列命令 sh 复制代码 # 启动 systemctl start crond # 停止 systemctl stop crond ...
相关文章
文章评论
共有0条评论来说两句吧...

微信收款码
支付宝收款码