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;
private final Object lock = new Object();
public void increment() {
synchronized (lock) {
count++;
}
}
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++) {
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);
}
}
- 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;
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();
}
}
};
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
常用注解
| 注解 |
说明 |
| @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 将作为自动装配时的首选候选者。 |
| 注解 |
说明 |
| @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 |
声明式事务管理。标注在类或方法上,定义方法的事务属性(如传播行为、隔离级别、回滚规则等)。 |
| 注解 |
说明 |
| @Aspect |
声明一个类是切面,包含通知(Advice)和切点(Pointcut)。 |
| @Before |
前置通知:在目标方法执行之前执行。 |
| @After |
后置通知:在目标方法执行之后(无论是否发生异常)执行。 |
| @AfterReturning |
返回通知:在目标方法成功执行并返回后执行。 |
| @AfterThrowing |
异常通知:在目标方法抛出异常后执行。 |
| @Around |
环绕通知:最强大的通知类型,可以在目标方法执行前后自定义行为,甚至可以决定是否执行目标方法。 |
| @Pointcut |
定义切点表达式,声明一个可重用的切点。 |
| 注解 |
说明 |
| @SpringBootApplication |
核心注解,是 @SpringBootConfiguration, @EnableAutoConfiguration, @ComponentScan 三个注解的组合。通常放在主启动类上。 |
| @EnableAutoConfiguration |
启用自动配置。Spring Boot 会根据类路径中的 Jar 包,自动配置应用程序。 |
| @SpringBootConfiguration |
标记类为配置类,是 @Configuration 的另一种形式。 |
| @ConfigurationProperties |
将外部配置文件(如 application.properties)中的属性批量绑定到一个 Java Bean 上。 |
| @ConditionalOnClass |
条件注解,当类路径下存在指定的类时,配置才生效。 |
| @ConditionalOnProperty |
条件注解,当指定的配置属性具有特定值时,配置才生效。 |
事务的传播机制
| 传播行为类型 |
说明 |
外部不存在事务 |
外部存在事务 |
适用场景 |
| 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() {
try {
methodA();
} catch (Exception e) {
}
}
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void methodA() {
}
- 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();
}
@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 |
官方文档 |
| 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 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)
新类型
总结
| 数据类型 |
特性 |
适用场景 |
| 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: 脑裂
脑裂 是指在高可用集群中,由于网络分区或节点暂时性故障,导致集群中的成员无法正常通信。此时,原本的一个集群分裂成了两个或多个独立的部分,这些部分都认为自己是唯一存活的集群,并且可能同时对外提供服务(如写入操作),从而导致数据不一致、冲突和混乱的现象。
在 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) 的策略。
策略是通过 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
监控: 使用 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",
"order_id": "123456",
"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),使用连接池可以避免频繁创建和销毁连接的开销。