1. final、finalize、finally之间的区别?
在Java中,final、finalize和finally虽然名字相似,但作用完全不同。以下是它们的区别:
1. final(关键字)
- 作用:用于修饰类、方法、变量,表示“不可变”。
- 修饰类:类不能被继承(如
String类)。 - 修饰方法:方法不能被子类重写(但可以被继承)。
- 修饰变量:变量值(或引用)只能赋值一次,成为常量。
- 基本类型:值不可变。
- 引用类型:引用不可变(但对象内部状态可能可变)。
- 修饰类:类不能被继承(如
示例:
final int MAX = 100; // 常量
final List<String> list = new ArrayList<>(); // 引用不可变,但list内容可修改
2. finalize()(方法)
- 作用:定义在
Object类中的方法,当对象被垃圾回收器回收时,可能会被调用(不保证一定执行)。 - 问题:
- 执行时机不确定,可能导致资源泄漏。
- 性能差,影响垃圾回收效率。
- 现状:Java 9 后标记为
@Deprecated,不推荐使用。建议用AutoCloseable(如try-with-resources)或PhantomReference替代。
示例:
@Override
protected void finalize() throws Throwable {
// 释放资源的逻辑(不推荐)
}
3. finally(代码块)
- 作用:与
try-catch配合使用,无论是否发生异常,finally中的代码都会执行(除非 JVM 退出或线程中断)。 - 典型用途:释放资源(如关闭文件、数据库连接等)。
示例:
try {
// 可能抛出异常的代码
} catch (Exception e) {
// 异常处理
} finally {
// 必须执行的代码(如关闭资源)
}
关键区别总结
| final | finalize() | finally | |
|---|---|---|---|
| 类型 | 关键字 | Object 类的方法 | 异常处理代码块 |
| 作用 | 控制不可变性 | 对象回收前的清理(已弃用) | 确保代码必然执行 |
| 场景 | 类、方法、变量 | 垃圾回收阶段 | try-catch 异常处理 |
常见混淆点
final修饰引用变量时,引用不可变,但对象内部状态可能可变(如final List可以增删元素)。finally不是绝对会执行:例如在try中调用System.exit(0)或 JVM 崩溃时不会执行。finalize()已被废弃,避免依赖它释放关键资源。
通过理解三者的不同用途和设计目的,可以避免因名称相似而导致的误用。
2. 一个任务需要依赖另外两个任务执行完成之后再执行,怎么设计?
在Java中,可以通过多种方式实现任务依赖调度。以下是几种常见且高效的解决方案:
1. 使用 CompletableFuture(推荐)
Java 8引入的CompletableFuture提供了简洁的异步编程模型,适合处理多任务依赖。
// 定义任务A和任务B
CompletableFuture<Void> taskA = CompletableFuture.runAsync(() -> doTaskA());
CompletableFuture<Void> taskB = CompletableFuture.runAsync(() -> doTaskB());
// 等待A和B都完成后执行任务C
CompletableFuture.allOf(taskA, taskB)
.thenRun(() -> doTaskC())
.join(); // 若需阻塞等待最终结果(可选)
优点:
- 非阻塞异步执行,资源利用率高。
- 支持链式调用和异常处理。
- 可灵活组合多个任务(如
thenApply、thenCombine等)。
2. 使用 CountDownLatch
适用于需要显式同步的场景,通过计数器控制任务完成。
CountDownLatch latch = new CountDownLatch(2);
// 提交任务A和任务B到线程池
executor.submit(() -> {
try {
doTaskA();
} finally {
latch.countDown();
}
});
executor.submit(() -> {
try {
doTaskB();
} finally {
latch.countDown();
}
});
// 等待A和B完成
latch.await();
// 执行任务C
doTaskC();
优点:
- 控制精准,适用于需要手动触发完成的场景。
- 简单直观,适合少量任务。
3. 使用 Future 和线程池
通过Future.get()阻塞等待任务结果。
ExecutorService executor = Executors.newFixedThreadPool(2);
Future<?> futureA = executor.submit(() -> doTaskA());
Future<?> futureB = executor.submit(() -> doTaskB());
// 阻塞等待任务A和B完成
futureA.get();
futureB.get();
// 执行任务C
doTaskC();
executor.shutdown();
优点:
- 结合线程池管理资源,避免频繁创建线程。
- 支持获取任务返回值。
4. 使用 ExecutorService 和 invokeAll
批量提交任务并等待所有任务完成。
List<Callable<Void>> tasks = Arrays.asList(
() -> { doTaskA(); return null; },
() -> { doTaskB(); return null; }
);
ExecutorService executor = Executors.newCachedThreadPool();
executor.invokeAll(tasks); // 阻塞直到所有任务完成
executor.submit(() -> doTaskC());
executor.shutdown();
优点:
- 适合批量任务管理。
- 代码简洁,无需手动同步。
对比总结
| 方法 | 适用场景 | 特点 |
|---|---|---|
CompletableFuture | 异步编程、复杂任务依赖 | 非阻塞、链式调用、代码简洁(推荐Java 8+) |
CountDownLatch | 显式同步、简单任务协调 | 需手动控制计数器,适合少量任务 |
Future.get() | 需要获取任务结果或异常处理 | 阻塞调用,可能影响性能 |
invokeAll | 批量提交任务并统一管理 | 自动等待所有任务完成 |
选择建议:
- 现代Java项目优先使用
CompletableFuture,其异步特性和链式调用能简化代码并提升性能。 - 若需强同步控制(如旧代码维护),可选用
CountDownLatch或Future.get()。 - 对于批量任务,
invokeAll提供便捷的管理方式。
3. 如果有一个任务失败了,你如何处理异常?
作为后端Java程序员,处理任务失败的异常需要结合代码健壮性、可维护性和用户体验来设计。以下是系统的异常处理策略:
1. 基础异常处理流程
(1) 明确异常类型
- 受检异常(Checked Exception):必须显式捕获或声明抛出(如
IOException)。 - 非受检异常(Unchecked Exception):通常是代码逻辑错误(如
NullPointerException、IllegalArgumentException)。 - 自定义异常:定义业务相关的异常(如
OrderNotFoundException)。
(2) 分层捕获与处理
- Controller层:捕获并封装为对用户友好的错误响应。
- Service层:处理业务逻辑异常,必要时抛出。
- DAO层:处理数据访问异常(如
SQLException),向上层抛出业务相关异常。
示例:
// Service层抛出自定义异常
public void processOrder(Order order) {
if (order == null) {
throw new BusinessException("订单不能为空");
}
// 业务逻辑...
}
// Controller层捕获并处理
@PostMapping("/order")
public ResponseEntity<?> createOrder(@RequestBody Order order) {
try {
orderService.processOrder(order);
return ResponseEntity.ok("订单创建成功");
} catch (BusinessException e) {
return ResponseEntity.status(400).body(e.getMessage());
}
}
2. 关键处理原则
(1) 不吞没异常(Swallowing Exceptions)
- 错误做法:
catch后不做任何处理。 - 正确做法:记录日志、抛出或转换为业务异常。
// 错误示例:吞没异常
try {
// 可能抛出异常的代码
} catch (Exception e) {
// 没有日志或处理!
}
// 正确示例:记录日志并抛出
try {
// 可能抛出异常的代码
} catch (IOException e) {
log.error("文件读取失败", e);
throw new BusinessException("系统内部错误");
}
(2) 资源释放
- 使用
try-with-resources(Java 7+)确保资源自动关闭。 - 适用于
AutoCloseable接口的实现类(如Connection、FileInputStream)。
// 自动关闭资源
try (FileInputStream fis = new FileInputStream("file.txt");
BufferedReader reader = new BufferedReader(new InputStreamReader(fis))) {
// 读取文件
} catch (IOException e) {
log.error("文件处理失败", e);
}
(3) 事务回滚
- Spring事务管理:在
@Transactional方法中,默认对RuntimeException回滚。 - 需显式配置对受检异常的回滚:
@Transactional(rollbackFor = BusinessException.class) public void updateOrder() throws BusinessException { // 业务逻辑... }
3. 高级处理策略
(1) 全局异常处理(Spring Boot)
- 使用
@ControllerAdvice+@ExceptionHandler统一处理异常,返回标准化错误响应。
@ControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(BusinessException.class)
public ResponseEntity<ErrorResponse> handleBusinessException(BusinessException ex) {
ErrorResponse error = new ErrorResponse(ex.getMessage(), 400);
return ResponseEntity.status(400).body(error);
}
@ExceptionHandler(Exception.class)
public ResponseEntity<ErrorResponse> handleGenericException(Exception ex) {
ErrorResponse error = new ErrorResponse("系统内部错误", 500);
log.error("未捕获的异常", ex);
return ResponseEntity.status(500).body(error);
}
}
// 错误响应DTO
@Data
class ErrorResponse {
private String message;
private int code;
// 构造方法省略...
}
(2) 重试机制
- 对短暂性故障(如网络抖动)使用重试策略。
- 工具库:Spring Retry、Resilience4j。
@Retryable(maxAttempts = 3, backoff = @Backoff(delay = 1000))
public void callExternalService() {
// 调用可能失败的外部服务
}
(3) 降级与熔断
- 使用 Hystrix 或 Sentinel 实现熔断,避免级联故障。
- 示例:服务调用失败时返回默认值或缓存数据。
4. 日志与监控
- 日志规范:
- 记录异常堆栈(
log.error("描述", e),而非e.getMessage())。 - 使用唯一错误码(如
ERR_ORDER_001)便于追踪。
- 记录异常堆栈(
- 监控报警:
- 集成 Prometheus + Grafana 监控异常频率。
- 配置 ELK 日志分析系统。
5. 常见错误场景与处理
| 场景 | 处理方式 |
|---|---|
| 第三方API调用失败 | 重试 + 熔断 + 降级 |
| 数据库连接超时 | 检查连接池配置(如 HikariCP 的 connectionTimeout) |
| 空指针异常 | 使用 Optional、Objects.requireNonNull 防御性编程 |
| 并发修改异常 | 使用线程安全集合(如 ConcurrentHashMap)或加锁 |
总结
- 核心目标:保证系统可用性、数据一致性、用户体验友好。
- 处理链:
预防 → 捕获 → 记录 → 恢复/降级 → 监控。 - 避免:直接暴露底层异常细节(如SQL错误)给用户。
通过分层处理、全局拦截、重试熔断等综合策略,可以构建一个健壮的后端异常处理体系。
4. 你在使用CompletableFuture的时候为什么要自定义线程池?
在使用 CompletableFuture 时,自定义线程池是一个重要的优化手段,原因如下:
1. 避免公共线程池的资源竞争
- 默认行为:
CompletableFuture默认使用ForkJoinPool.commonPool(),这是一个全局共享的线程池。 - 问题:如果所有异步任务都依赖它,可能导致:
- 资源争用:多个任务竞争同一池的线程,高并发时性能下降。
- 饥饿风险:如果某些任务长时间占用线程(如阻塞 I/O),其他任务可能无法及时执行。
- 示例:假设你的应用同时处理 HTTP 请求和后台计算,所有任务共用
commonPool,可能导致关键请求被延迟。
2. 隔离任务类型,优化资源分配
- 场景差异:
- CPU 密集型任务:线程数通常设为 CPU 核数,避免过多线程导致上下文切换。
- I/O 密集型任务(如网络请求、数据库操作):需要更多线程,避免线程因 I/O 等待而闲置。
- 自定义线程池:可以为不同类型任务分配独立线程池,例如:
// CPU 密集型任务池(线程数 = CPU 核数) ExecutorService cpuExecutor = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors()); // I/O 密集型任务池(线程数更大,可复用线程) ExecutorService ioExecutor = Executors.newCachedThreadPool();
3. 精细化控制线程池参数
- 核心参数:通过自定义线程池,可以调整:
- 核心线程数(
corePoolSize):长期保留的线程数。 - 队列容量(
workQueue):任务队列大小,避免内存溢出。 - 拒绝策略(
RejectedExecutionHandler):自定义任务满时的处理逻辑(如丢弃、重试)。
- 核心线程数(
- 示例:
ThreadPoolExecutor customExecutor = new ThreadPoolExecutor( 4, // corePoolSize 8, // maxPoolSize 60, TimeUnit.SECONDS, // 空闲线程存活时间 new LinkedBlockingQueue<>(100), // 任务队列容量 new ThreadFactoryBuilder().setNameFormat("custom-pool-%d").build(), // 线程命名 new ThreadPoolExecutor.CallerRunsPolicy() // 队列满时由调用线程执行任务 );
4. 避免 ForkJoinPool 的局限性
- 工作窃取(Work-Stealing)机制:
commonPool使用ForkJoinPool,其线程会从其他队列窃取任务。- 优点:适合计算密集型任务的分治策略(如递归任务拆分)。
- 缺点:对于大量短期异步任务(如微服务调用),任务拆分和窃取可能引入额外开销。
- 自定义线程池:选择
ThreadPoolExecutor可能更高效,尤其是任务间无父子依赖时。
5. 避免阻塞公共池导致系统级问题
- 风险:如果某个
CompletableFuture任务因同步阻塞(如Thread.sleep或锁竞争)占用commonPool线程,所有依赖该池的任务都会受影响。 - 解决方案:将阻塞任务隔离到独立的线程池,例如使用
newCachedThreadPool弹性扩缩容。
如何使用自定义线程池?
在提交任务时显式指定 Executor:
ExecutorService customExecutor = Executors.newFixedThreadPool(4);
CompletableFuture.supplyAsync(() -> {
// 异步任务逻辑
return result;
}, customExecutor).thenApplyAsync(result -> {
// 后续处理(使用同一线程池)
return processedResult;
}, customExecutor);
总结
| 场景 | 默认线程池 (commonPool) | 自定义线程池 |
|---|---|---|
| 资源竞争 | 高并发时性能下降 | 隔离任务,减少竞争 |
| 任务类型 | 适合计算密集型 | 支持 I/O 密集型 + 混合场景 |
| 参数控制 | 固定配置(依赖 JVM 默认) | 按需调整核心线程数、队列、拒绝策略 |
| 阻塞任务影响 | 全局阻塞风险 | 局部隔离,避免级联故障 |
核心原则:根据任务特性(CPU/IO 密集型、优先级、资源需求)选择合适的线程池,避免一刀切的默认配置。
5. 内置线程池有哪些?
Java 提供了多种内置线程池,主要通过 Executors 工具类创建。这些线程池在底层均基于 ThreadPoolExecutor 或 ScheduledThreadPoolExecutor 实现,适用于不同场景。以下是常见的内置线程池及其核心特性:
1. FixedThreadPool(固定大小线程池)
- 定义:
Executors.newFixedThreadPool(int nThreads),创建固定线程数的线程池,核心线程数等于最大线程数。 - 参数:
使用无界队列LinkedBlockingQueue(默认容量为Integer.MAX_VALUE)。 - 特点:
- 线程数固定,任务超出核心线程数时进入队列等待。
- 线程空闲时不会被回收,长期占用资源。
- 潜在问题:若任务堆积速度远大于处理速度,可能导致内存溢出(OOM)。
- 适用场景:
适合处理长期稳定的并发任务,如 Web 服务的请求处理。
2. CachedThreadPool(可缓存线程池)
- 定义:
Executors.newCachedThreadPool(),核心线程数为 0,最大线程数为Integer.MAX_VALUE,空闲线程存活时间为 60 秒。 - 参数:
使用SynchronousQueue(无容量队列,任务直接提交给线程处理,不存储任务)。 - 特点:
- 线程可无限扩容,适用于短时异步任务。
- 空闲线程超时自动回收,减少资源占用。
- 潜在问题:任务提交过快可能导致线程数激增,引发 CPU 或内存耗尽。
- 适用场景:
短生命周期、高吞吐量的任务,如 HTTP 短连接请求。
3. SingleThreadExecutor(单线程池)
- 定义:
Executors.newSingleThreadExecutor(),核心线程数和最大线程数均为 1。 - 参数:
使用LinkedBlockingQueue(无界队列)。 - 特点:
- 所有任务按提交顺序串行执行(FIFO)。
- 线程异常终止后会自动创建新线程替代。
- 潜在问题:与
FixedThreadPool类似,任务堆积可能导致 OOM。
- 适用场景:
需严格顺序执行的任务,如日志记录、单线程任务调度。
4. ScheduledThreadPool(定时/周期性任务线程池)
- 定义:
Executors.newScheduledThreadPool(int corePoolSize),支持延迟执行或周期性任务。 - 参数:
使用DelayedWorkQueue(优先级队列,按延迟时间排序任务)。 - 特点:
- 核心线程数固定,最大线程数为
Integer.MAX_VALUE。 - 提供
schedule()、scheduleAtFixedRate()等方法实现定时任务。 - 优势:替代
Timer,更安全且支持多线程并发调度。
- 核心线程数固定,最大线程数为
- 适用场景:
定时任务(如缓存刷新)或周期性任务(如心跳检测)。
其他线程池(补充)
- SingleThreadScheduledExecutor:
单线程版本的ScheduledThreadPool,保证任务顺序执行。 - WorkStealingPool(Java 8+):
Executors.newWorkStealingPool(),基于 Fork/Join 框架,适合并行处理可分治的任务(如递归计算)。 - ForkJoinPool(Java 7+):
支持任务拆分与合并,常用于大数据处理或递归任务。
内置线程池的潜在问题
- 资源耗尽风险:
如FixedThreadPool和SingleThreadExecutor使用无界队列,任务堆积易导致 OOM;CachedThreadPool线程数无上限,可能耗尽 CPU 或内存。 - 实际开发建议:
推荐通过ThreadPoolExecutor自定义线程池,显式设置有界队列、合理的拒绝策略,以及线程工厂,避免资源失控。
总结
| 线程池类型 | 核心参数 | 适用场景 | 风险提示 |
|---|---|---|---|
| FixedThreadPool | 固定线程数 + 无界队列 | 稳定并发任务 | 任务堆积导致 OOM |
| CachedThreadPool | 动态扩容 + 同步队列 | 短时高吞吐任务 | 线程数激增耗尽资源 |
| SingleThreadExecutor | 单线程 + 无界队列 | 顺序执行任务 | 同 FixedThreadPool |
| ScheduledThreadPool | 固定核心线程 + 延迟队列 | 定时/周期性任务 | 周期性任务堆积可能延迟 |
如需完整源码示例或拒绝策略配置,可参考 Java 官方文档 或相关技术博客。
6. 为什么不直接使用内置线程池?
作为Java后端开发,不推荐直接使用Executors提供的内置线程池(如FixedThreadPool、CachedThreadPool等),主要是因为其默认参数设计存在资源耗尽风险,而实际生产环境需要更精细化的线程池配置。以下是具体原因和解决方案:
一、内置线程池的核心问题
1. 资源耗尽风险
-
FixedThreadPool / SingleThreadExecutor
使用无界队列LinkedBlockingQueue(默认容量Integer.MAX_VALUE),当任务提交速度远大于处理速度时,队列无限堆积,可能导致内存溢出(OOM)。// 隐患示例:队列无界,任务堆积可能引发OOM ExecutorService executor = Executors.newFixedThreadPool(10); for (int i = 0; i < 1_000_000; i++) { executor.submit(() -> { // 长时间任务... }); } -
CachedThreadPool
最大线程数为Integer.MAX_VALUE,且使用SynchronousQueue(每个任务必须立即分配线程),高并发场景下可能瞬间创建大量线程,导致CPU/内存资源耗尽。
2. 缺乏容错机制
- 默认拒绝策略为
AbortPolicy(直接抛出异常),无法灵活应对突发流量或服务降级。 - 缺少任务队列监控、线程池动态调参等能力。
3. 可维护性差
- 内置线程池的参数(如核心线程数、队列类型)在创建后无法动态调整。
- 问题排查困难:无界队列的任务堆积难以定位。
二、生产环境推荐方案:自定义线程池
使用ThreadPoolExecutor构造函数手动配置线程池,核心参数如下:
public ThreadPoolExecutor(
int corePoolSize, // 核心线程数(长期保留的线程)
int maximumPoolSize, // 最大线程数(临时扩容上限)
long keepAliveTime, // 空闲线程存活时间
TimeUnit unit, // 时间单位
BlockingQueue<Runnable> workQueue, // 任务队列(推荐有界队列)
ThreadFactory threadFactory, // 线程工厂(命名线程便于监控)
RejectedExecutionHandler handler // 拒绝策略
)
1. 关键配置示例
// 示例:自定义线程池(有界队列 + 合理拒绝策略)
ThreadPoolExecutor executor = new ThreadPoolExecutor(
10, // corePoolSize
50, // maximumPoolSize
60, TimeUnit.SECONDS,
new ArrayBlockingQueue<>(1000), // 有界队列(容量1000)
new CustomThreadFactory("order-pool"), // 自定义线程命名
new CustomRejectedPolicy() // 自定义拒绝策略(如降级、记录日志)
);
2. 优势对比
| 维度 | 内置线程池 | 自定义线程池 |
|---|---|---|
| 队列类型 | 默认无界(Fixed/Single)或同步队列(Cached) | 显式指定有界队列(如ArrayBlockingQueue) |
| 线程数控制 | 固定或无限扩展 | 可动态调整核心/最大线程数 |
| 拒绝策略 | 仅支持默认策略(如AbortPolicy) | 支持自定义策略(降级、重试、日志等) |
| 可监控性 | 无线程命名,难以定位问题 | 通过线程工厂命名,结合监控工具追踪 |
| 资源保护 | 易引发OOM或资源耗尽 | 通过队列容量和拒绝策略实现流量整形 |
三、生产环境最佳实践
1. 参数调优原则
- 核心线程数:根据业务类型调整(CPU密集型:
NCPU + 1;IO密集型:2*NCPU)。 - 队列容量:结合系统承载能力和任务平均耗时设定(避免过大或过小)。
- 拒绝策略:根据业务容忍度选择:
- Log and Retry:记录日志后重试(需保证幂等性)。
- Fallback:返回默认值或降级结果。
- CallerRunsPolicy:由提交任务的线程执行(天然限流)。
2. 监控与治理
- 线程池指标:监控活跃线程数、队列大小、拒绝任务数等(集成Micrometer + Prometheus)。
- 动态调参:借助美团动态线程池框架或Spring的
ThreadPoolTaskExecutor实现运行时调整参数。 - 线程命名:通过
ThreadFactory为线程命名,便于日志排查。
3. 代码示例:整合Spring
@Configuration
public class ThreadPoolConfig {
@Bean("taskExecutor")
public ThreadPoolTaskExecutor taskExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(10);
executor.setMaxPoolSize(50);
executor.setQueueCapacity(1000);
executor.setThreadNamePrefix("async-task-");
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
executor.initialize();
return executor;
}
}
四、阿里巴巴开发规约警示
《阿里巴巴Java开发手册》明确禁止使用Executors创建线程池,强制要求通过ThreadPoolExecutor构造函数创建,原因包括:
- 无界队列风险:
FixedThreadPool和SingleThreadExecutor的任务队列默认长度Integer.MAX_VALUE,堆积可能导致OOM。 - 线程数失控:
CachedThreadPool允许创建的线程数最大为Integer.MAX_VALUE,高并发下导致资源耗尽。
五、总结
- 避免使用内置线程池:主要因默认参数设计不符合生产环境要求。
- 自定义线程池核心价值:
- 资源可控:通过有界队列和合理拒绝策略防止系统崩溃。
- 灵活扩展:支持动态调整参数和自定义拒绝逻辑。
- 易于维护:线程命名、监控指标提升问题排查效率。
通过精细化配置和监控,可以构建高可用、易维护的线程池体系,保障后端服务的稳定性。
7. 线程池的参数有哪些?最大线程数和核心线程数有什么区别?
在Java中,线程池的参数和核心线程数与最大线程数的区别是面试中的高频问题。以下是清晰的结构化回答:
线程池的核心参数
ThreadPoolExecutor构造函数包含以下关键参数:
-
corePoolSize(核心线程数)
- 线程池中长期保持的最小线程数,即使这些线程处于空闲状态。
- 默认情况下,核心线程不会被回收(除非设置
allowCoreThreadTimeOut(true))。
-
maximumPoolSize(最大线程数)
- 线程池允许创建的最大线程数。当任务队列已满且当前线程数小于最大线程数时,线程池会创建新线程处理任务。
-
keepAliveTime(线程空闲存活时间)
- 当线程数超过核心线程数时,空闲线程在终止前等待新任务的最长时间。
- 仅作用于超出核心线程数的临时线程。
-
unit(存活时间单位)
keepAliveTime的时间单位(如TimeUnit.SECONDS)。
-
workQueue(任务队列)
- 用于保存待执行任务的阻塞队列(如
LinkedBlockingQueue、ArrayBlockingQueue)。 - 队列容量直接影响线程池的扩缩容逻辑。
- 用于保存待执行任务的阻塞队列(如
-
threadFactory(线程工厂)
- 用于创建新线程的工厂,可自定义线程名称、优先级等(便于监控和调试)。
-
handler(拒绝策略)
- 当线程池和队列均满载时,处理新提交任务的策略(如
AbortPolicy、CallerRunsPolicy)。
- 当线程池和队列均满载时,处理新提交任务的策略(如
核心线程数 vs. 最大线程数
核心区别
| 维度 | 核心线程数(corePoolSize) | 最大线程数(maximumPoolSize) |
|---|---|---|
| 定义 | 线程池长期维持的最小线程数 | 线程池允许创建的最大线程数上限 |
| 默认行为 | 空闲时不会被回收(除非显式配置) | 超出核心数的线程空闲超时后会被回收 |
| 任务处理优先级 | 优先使用核心线程处理任务 | 队列满后才会创建新线程(直到达到最大线程数) |
| 资源占用 | 长期占用资源 | 动态调整,资源按需分配 |
线程池处理任务的流程
-
任务提交时:
- 如果当前线程数 < 核心线程数 → 立即创建新线程执行任务。
- 如果当前线程数 ≥ 核心线程数 → 任务进入队列等待。
- 如果队列已满且当前线程数 < 最大线程数 → 创建新线程处理任务。
- 如果队列已满且当前线程数 ≥ 最大线程数 → 触发拒绝策略。
-
示例场景:
- 参数:
corePoolSize=5,maximumPoolSize=10,队列容量=100。 - 任务提交:200个任务同时到达。
- 处理流程:
- 前5个任务由核心线程直接处理。
- 接下来100个任务进入队列。
- 队列满后,创建5个新线程(总线程数=10)处理后续5个任务。
- 剩余90个任务触发拒绝策略(如抛出异常或丢弃任务)。
- 参数:
参数配置最佳实践
-
CPU密集型任务(如计算、数据处理):
- 核心线程数 ≈ CPU核心数(
Runtime.getRuntime().availableProcessors())。 - 最大线程数可等于核心线程数(避免过多线程切换开销)。
- 核心线程数 ≈ CPU核心数(
-
IO密集型任务(如网络请求、数据库操作):
- 核心线程数可设置为
2 * CPU核心数。 - 最大线程数适当增大(如
CPU核心数 * (1 + 平均等待时间/计算时间))。
- 核心线程数可设置为
-
队列选择:
- 有界队列(如
ArrayBlockingQueue):防止资源耗尽,但需合理设置容量。 - 无界队列(如
LinkedBlockingQueue):可能导致内存溢出,不推荐生产使用。
- 有界队列(如
常见误区
- 误区1:认为最大线程数越大越好。
→ 线程过多会导致频繁上下文切换,降低性能(需根据任务类型调整)。 - 误区2:忽略队列容量影响。
→ 队列容量过小会频繁触发拒绝策略,过大则可能内存溢出。 - 误区3:依赖默认拒绝策略(
AbortPolicy)。
→ 生产环境需自定义策略(如降级、重试、日志记录)。
总结
- 核心线程数是线程池的“基线资源”,确保系统有基本处理能力。
- 最大线程数是线程池的“弹性上限”,应对突发流量。
- 合理配置参数需结合任务类型(CPU/IO密集)、系统资源(CPU、内存)、队列策略及拒绝策略。
8. MySQL中select语句执行流程
在 MySQL 中,一条 SELECT 语句的执行流程可以分为 Server 层和存储引擎层两个主要部分。以下是详细的执行流程(以 InnoDB 引擎为例):
1. 连接器(Connector)
- 作用:管理客户端连接、身份认证和权限校验。
- 流程:
- 客户端发起连接请求,连接器验证用户名、密码和权限。
- 若验证通过,建立连接并分配线程(长连接会复用线程,避免频繁创建销毁)。
- 若未显式关闭连接,连接会保持到超时(由
wait_timeout参数控制)。
2. 查询缓存(Query Cache,MySQL 8.0 已移除)
- 作用:缓存
SELECT语句及其结果(以 Key-Value 形式存储,Key 是 SQL 语句,Value 是结果集)。 - 流程:
- 检查当前 SQL 是否命中缓存。
- 若命中且用户有权限,直接返回缓存结果。
- 若未命中或表发生更新(INSERT/UPDATE/DELETE),缓存失效。
- 注意:查询缓存在高并发或频繁更新的场景下效率极低,MySQL 8.0 已移除该功能。
3. 分析器(Parser)
- 作用:对 SQL 进行词法分析和语法分析,生成抽象语法树(AST)。
- 流程:
- 词法分析:解析 SQL 中的关键字(如
SELECT、FROM)、表名、列名等。 - 语法分析:检查 SQL 是否符合 MySQL 语法规则(如缺少关键字会抛出
You have an error in your SQL syntax)。
- 词法分析:解析 SQL 中的关键字(如
4. 优化器(Optimizer)
- 作用:生成最优执行计划(如选择索引、JOIN 顺序等)。
- 关键决策:
- 索引选择:基于成本模型(Cost-Based Optimization)选择使用哪个索引。
- JOIN 顺序:确定多表 JOIN 的最佳顺序。
- 子查询优化:可能将子查询转换为 JOIN 操作。
- 输出:最终生成执行计划(可通过
EXPLAIN查看)。
5. 执行器(Executor)
- 作用:根据优化器的执行计划调用存储引擎接口,获取并返回数据。
- 流程:
- 权限校验:检查用户是否有权访问目标表(即使连接器已校验,此处仍需二次确认)。
- 调用存储引擎:打开表,根据执行计划逐行读取数据。
- 返回结果:将结果集返回给客户端(可能通过网络或缓存)。
6. 存储引擎(Storage Engine,以 InnoDB 为例)
- 作用:负责数据的存储和检索。
- 核心流程:
- 缓冲池(Buffer Pool):优先从内存中的缓冲池读取数据页(Page),减少磁盘 IO。
- 索引检索:
- 若使用主键索引(聚簇索引),直接通过 B+ 树定位到数据行。
- 若使用二级索引,先通过二级索引找到主键值,再回表(回表查询)到聚簇索引获取完整数据。
- 事务与 MVCC:
- 根据事务隔离级别(如
READ COMMITTED或REPEATABLE READ)和 ReadView,通过 Undo Log 判断数据版本可见性。
- 锁机制:根据隔离级别,可能对数据行加锁(如
SELECT ... FOR UPDATE)。
7. 日志模块(辅助流程)
- Binlog(Server 层日志):记录所有数据变更操作(
SELECT不写入 Binlog)。 - Undo Log(存储引擎层日志):用于事务回滚和 MVCC 多版本控制。
- Redo Log(存储引擎层日志):保证事务的持久性(
SELECT不触发 Redo Log 写入)。
流程图总结
客户端 → 连接器 → 查询缓存(8.0 前)→ 分析器 → 优化器 → 执行器 → 存储引擎(InnoDB)
↓
返回结果集
性能优化关注点
- 索引设计:避免全表扫描,减少回表查询。
- 缓冲池调优:合理设置
innodb_buffer_pool_size,提升内存命中率。 - 执行计划分析:通过
EXPLAIN观察是否使用预期索引。 - 避免大事务:长事务可能导致 Undo Log 膨胀和锁竞争。
常见面试题扩展
-
如何判断一条查询是否走了索引?
答:使用EXPLAIN查看key字段显示的索引名称。 -
什么是回表查询?
答:通过二级索引找到主键后,再通过主键索引查询完整数据的过程。 -
MVCC 如何实现可重复读?
答:在事务开始时生成 ReadView,后续读取基于该视图判断数据可见性。
9. select * 会导致索引失效吗?
在 MySQL 中,SELECT * 本身不会直接导致索引失效,但它的使用可能会间接影响查询性能或索引的覆盖能力。以下是详细分析:
1. 索引失效的根本原因
索引是否失效主要取决于以下因素:
- WHERE 条件是否命中索引列:如果查询条件中的字段没有索引,优化器无法使用索引。
- 索引列是否参与计算或函数操作:例如
WHERE YEAR(create_time) = 2023,即使create_time有索引,也会失效。 - 是否违反最左前缀原则:复合索引未按定义顺序使用最左列时可能失效。
- 数据分布导致全表扫描更高效:当索引列区分度低(如性别列),优化器可能选择全表扫描。
SELECT * 不涉及上述条件,因此不会直接导致索引失效。
2. SELECT * 的潜在性能问题
尽管索引可能被使用,但 SELECT * 会带来以下问题:
(1) 回表查询(针对二级索引)
- 场景:如果查询使用了二级索引(非聚簇索引),但
SELECT *需要返回所有列:-- 假设 age 列有二级索引,但其他列(name, address)不在索引中 SELECT * FROM users WHERE age = 25; - 流程:
- 通过二级索引找到
age=25的主键值(索引覆盖)。 - 根据主键值回表到聚簇索引中查找完整的行数据(包含
name,address等列)。
- 通过二级索引找到
- 影响:回表操作会增加磁盘 I/O,降低查询效率。
(2) 覆盖索引失效
- 覆盖索引:如果索引包含查询所需的所有列,则无需回表。
- 问题:
SELECT *要求返回所有列,除非索引是聚簇索引(默认主键索引)或复合索引包含所有列,否则无法利用覆盖索引。 - 示例:
-- 复合索引 (age, name) CREATE INDEX idx_age_name ON users(age, name); -- 走覆盖索引(无需回表) SELECT age, name FROM users WHERE age = 25; -- 无法覆盖索引(需回表获取 address) SELECT * FROM users WHERE age = 25;
3. 优化建议
(1) 避免 SELECT *
- 明确指定所需列:减少数据传输量和回表开销。
-- 只查询需要的列 SELECT id, age, name FROM users WHERE age = 25;
(2) 使用覆盖索引
- 设计复合索引时包含查询所需的所有列:
-- 覆盖索引 (age, name, address) CREATE INDEX idx_age_name_address ON users(age, name, address); -- 直接通过索引返回数据,无需回表 SELECT age, name, address FROM users WHERE age = 25;
(3) 结合 EXPLAIN 分析
- 使用
EXPLAIN查看执行计划,确认是否使用索引:EXPLAIN SELECT * FROM users WHERE age = 25;- 观察
key(使用的索引)和Extra列(若显示Using index,表示覆盖索引生效)。
- 观察
4. 总结
| 场景 | 索引是否失效 | 性能影响 |
|---|---|---|
SELECT * + WHERE 命中索引列 | 否 | 可能需回表,性能下降 |
SELECT * + WHERE 未命中索引列 | 是(因无索引) | 全表扫描,性能差 |
SELECT 具体列 + 覆盖索引 | 否 | 无需回表,性能最优 |
核心结论:
SELECT *不直接导致索引失效,但可能因回表操作降低性能。- 最佳实践是明确指定所需列并合理设计覆盖索引。
10. 索引失效的原因有哪些?
在数据库查询优化中,索引失效是一个常见问题,可能导致查询性能显著下降。以下是索引失效的主要原因及详细解释:
1. 不符合最左前缀原则(联合索引失效)
- 场景:联合索引(如
INDEX(A, B, C))的查询条件未包含最左列。 - 示例:
-- 有效使用索引:条件包含最左列A SELECT * FROM table WHERE A = 1 AND B = 2; -- 索引失效:条件未包含A,直接使用B和C SELECT * FROM table WHERE B = 2 AND C = 3; - 原因:B+树索引按列顺序构建,跳过最左列时无法利用索引的有序性。
2. 对索引列使用函数或计算
- 场景:在WHERE条件中对索引列进行运算或函数操作。
- 示例:
-- 索引失效:对date_column使用YEAR函数 SELECT * FROM table WHERE YEAR(date_column) = 2023; -- 索引失效:对列进行数学运算 SELECT * FROM table WHERE column + 10 > 100; - 原因:索引存储的是原始值,运算后的值无法直接匹配索引结构。
3. 隐式类型转换
- 场景:索引列与查询条件的数据类型不一致。
- 示例:
-- 索引列是VARCHAR,但条件使用数值类型(隐式转换) SELECT * FROM table WHERE str_column = 123; -- 等效于 CAST(str_column AS INT) = 123 - 原因:数据库需将索引列强制转换为条件类型,导致索引无法匹配。
4. 使用OR连接非索引列
- 场景:OR条件中包含未索引的列。
- 示例:
-- 假设indexed_column有索引,non_indexed_column无索引 SELECT * FROM table WHERE indexed_column = 'A' OR non_indexed_column = 'B'; - 结果:优化器可能放弃使用索引,直接全表扫描。
5. LIKE以通配符开头
- 场景:模糊查询的通配符(
%)在开头。 - 示例:
-- 索引失效:无法利用B+树的前缀匹配特性 SELECT * FROM table WHERE column LIKE '%value%'; -- 有效:通配符仅在末尾 SELECT * FROM table WHERE column LIKE 'value%';
6. 数据区分度过低
- 场景:索引列的值重复率极高(如性别列)。
- 示例:
-- 假设gender列仅有'男'/'女'两种值 SELECT * FROM table WHERE gender = '男'; - 原因:优化器认为全表扫描比索引回表更高效。
7. 范围查询后的索引列失效
- 场景:在联合索引中,范围查询(
>,<,BETWEEN)后的列无法使用索引。 - 示例:
-- 联合索引(A, B) SELECT * FROM table WHERE A > 10 AND B = 5; - 结果:A列使用范围查询后,B列无法继续利用索引。
8. 查询返回大量数据
- 场景:查询结果集超过表数据的20%~30%。
- 示例:
-- 若表有100万行,返回30万行数据 SELECT * FROM table WHERE status = 'active'; - 原因:优化器认为全表扫描比多次回表更高效。
9. 统计信息不准确
- 场景:表的统计信息未及时更新(如数据分布变化后未执行
ANALYZE TABLE)。 - 结果:优化器可能误判索引的成本,选择全表扫描。
10. 强制类型转换或字符集不匹配
- 场景:索引列与查询条件的字符集不一致。
- 示例:
-- 假设utf8mb4列与utf8条件比较 SELECT * FROM table WHERE utf8mb4_column = 'value' COLLATE utf8_general_ci;
11. 使用NOT或!=操作符
- 场景:否定条件可能导致全表扫描。
- 示例:
SELECT * FROM table WHERE column != 'value';
12. 索引列允许NULL值
- 场景:大量NULL值存在时,
IS NULL或IS NOT NULL可能无法有效使用索引。 - 示例:
SELECT * FROM table WHERE nullable_column IS NULL;
总结与优化建议
| 场景 | 优化方法 |
|---|---|
| 联合索引跳过最左列 | 调整查询条件或索引顺序 |
| 函数操作索引列 | 避免在WHERE中对索引列运算 |
| 隐式类型转换 | 确保条件与索引列类型一致 |
| 区分度过低 | 考虑是否必要建索引 |
| 大量数据返回 | 使用分页或缩小查询范围 |
| 统计信息不准确 | 定期执行ANALYZE TABLE更新统计信息 |
核心原则:
- 通过
EXPLAIN分析执行计划,确认索引使用情况。 - 避免过度索引,仅对高频查询且区分度高的列建索引。
- 联合索引设计需兼顾查询条件的顺序和频率。
11. 数据库和缓存一致性如何保证?
在分布式系统中,保证数据库和缓存的一致性是一个复杂但关键的问题。以下是常见策略及其详细实现步骤和注意事项:
1. 缓存更新策略
(1) Cache-Aside(旁路缓存)
- 流程:
- 读操作:
- 先读缓存,命中则返回。
- 未命中则读数据库,写入缓存后返回。
- 写操作:
- 先更新数据库,再删除缓存(避免并发导致脏数据)。
- 读操作:
- 优点:简单易实现,适合读多写少场景。
- 缺点:
- 短暂不一致:在删除缓存后、下次读请求前,可能出现数据库与缓存不一致。
- 并发问题:若写操作删缓存后,读操作在数据库更新前加载旧数据到缓存(需结合延迟双删或锁缓解)。
(2) Write-Through(直写模式)
- 流程:
- 应用同时更新缓存和数据库(原子性需保证)。
- 优点:强一致性,缓存与数据库同步更新。
- 缺点:
- 性能开销大(每次写都需操作缓存和数据库)。
- 不适用于高并发写入场景。
(3) Write-Behind(异步回写)
- 流程:
- 先更新缓存,异步批量更新数据库。
- 优点:写性能高,适合写多场景。
- 缺点:
- 数据丢失风险(缓存宕机导致未持久化数据丢失)。
- 一致性弱,需容忍延迟。
2. 一致性保障方案
(1) 延迟双删
- 场景:解决Cache-Aside并发问题。
- 步骤:
- 更新数据库。
- 删除缓存。
- 延迟一定时间(如500ms)后再次删除缓存。
- 目的:清除可能因并发读操作导致的旧缓存。
- 代码示例:
public void updateData(Data data) { // 1. 更新数据库 database.update(data); // 2. 删除缓存 cache.delete(data.getId()); // 3. 提交延迟任务,二次删除 executor.schedule(() -> cache.delete(data.getId()), 500, TimeUnit.MILLISECONDS); }
(2) 分布式锁
- 场景:防止并发写操作导致的数据不一致。
- 步骤:
- 写操作前获取锁。
- 更新数据库并删除缓存。
- 释放锁。
- 实现:使用Redis的RedLock或ZooKeeper实现。
- 代码示例:
public void updateDataWithLock(Data data) { String lockKey = "lock:data:" + data.getId(); try { if (redisLock.tryLock(lockKey, 3, TimeUnit.SECONDS)) { database.update(data); cache.delete(data.getId()); } } finally { redisLock.unlock(lockKey); } }
(3) 监听数据库变更(最终一致性)
- 场景:通过数据库的Binlog或触发器同步缓存。
- 工具:
- Canal:监听MySQL Binlog,解析后更新缓存。
- Debezium:通用数据库变更捕获工具。
- 流程:
- 数据库更新后,Binlog记录变更。
- 中间件解析Binlog,发送消息到MQ(如Kafka)。
- 消费者更新缓存。
- 优点:解耦业务代码,保证最终一致性。
- 缺点:系统复杂度高,需维护消息队列和消费者。
3. 容错与降级策略
(1) 缓存过期时间
- 设置合理的缓存过期时间(如30分钟),即使不一致也能自动恢复。
- 权衡:过期时间越短,一致性越强,但数据库压力越大。
(2) 降级机制
- 缓存故障时,直接读数据库并返回,记录日志告警。
- 代码示例:
public Data getData(String id) { try { Data data = cache.get(id); if (data == null) { data = database.get(id); cache.set(id, data, 30, TimeUnit.MINUTES); } return data; } catch (CacheException e) { // 降级:直接查数据库 return database.get(id); } }
4. 高并发场景优化
(1) 热点数据永不过期
- 对热点数据设置永不过期,通过异步线程定期更新。
- 示例:
public Data getHotData(String id) { Data data = cache.get(id); if (data == null) { data = database.get(id); cache.set(id, data); // 不设置过期时间 // 异步更新缓存 executor.submit(() -> { Data latestData = database.get(id); cache.set(id, latestData); }); } return data; }
(2) 互斥锁(Mutex Lock)
- 防止缓存击穿:多个线程同时查询同一个未命中缓存的Key时,仅允许一个线程查数据库。
- 代码示例:
public Data getDataSafely(String id) { Data data = cache.get(id); if (data == null) { String lockKey = "mutex:" + id; if (redis.setnx(lockKey, "1", 10, TimeUnit.SECONDS)) { try { data = database.get(id); cache.set(id, data, 30, TimeUnit.MINUTES); } finally { redis.delete(lockKey); } } else { // 等待其他线程加载完成 Thread.sleep(100); return cache.get(id); } } return data; }
5. 方案对比与选型
| 方案 | 一致性强度 | 性能影响 | 复杂度 | 适用场景 |
|---|---|---|---|---|
| Cache-Aside | 最终一致性 | 低 | 低 | 读多写少,容忍短暂不一致 |
| Write-Through | 强一致性 | 高 | 中 | 写少,强一致性要求 |
| Write-Behind | 最终一致性 | 高 | 高 | 写多,允许异步持久化 |
| 监听Binlog | 最终一致性 | 中 | 高 | 业务解耦,大数据量同步 |
总结
- 读场景优先:使用Cache-Aside结合延迟双删或互斥锁。
- 写场景优化:高频写可考虑Write-Behind或Binlog监听。
- 一致性要求:强一致性需牺牲性能(如分布式锁),最终一致性更适合高并发。
- 容错设计:缓存降级、过期时间、热点数据永不过期是必备手段。
12. Redisson分布式锁实现
Redisson 是一个基于 Redis 的 Java 客户端,提供了丰富的分布式服务功能,分布式锁是其核心特性之一。以下是 Redisson 分布式锁的实现原理、关键机制和使用示例:
1. 核心实现原理
(1) 加锁流程
Redisson 通过 Lua 脚本在 Redis 中执行原子操作,确保加锁过程的原子性。
核心命令:
-- 参数:KEYS[1](锁名称), ARGV[1](锁超时时间), ARGV[2](客户端唯一标识 + 线程ID)
if (redis.call('exists', KEYS[1]) == 0) then
redis.call('hincrby', KEYS[1], ARGV[2], 1);
redis.call('pexpire', KEYS[1], ARGV[1]);
return nil;
end;
if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then
redis.call('hincrby', KEYS[1], ARGV[2], 1);
redis.call('pexpire', KEYS[1], ARGV[1]);
return nil;
end;
return redis.call('pttl', KEYS[1]);
- 唯一标识:使用
UUID + 线程ID作为客户端标识,支持可重入。 - 可重入性:通过 Hash 结构记录锁的持有次数(计数器)。
- 超时时间:默认 30 秒,通过 Watchdog 机制自动续期。
(2) 锁续期(Watchdog 机制)
- 作用:防止业务执行时间超过锁超时时间,导致锁提前释放。
- 流程:
- 加锁成功后,启动一个后台线程(Watchdog)。
- 每隔
锁超时时间 / 3(默认 10 秒),检查客户端是否仍持有锁。 - 若持有锁,则重置锁的超时时间为 30 秒。
- 代码触发:仅在未显式设置
leaseTime(锁超时时间)时启用 Watchdog。
(3) 释放锁
-- 参数:KEYS[1](锁名称), KEYS[2](解锁消息Channel), ARGV[1](释放消息), ARGV[2](超时时间), ARGV[3](客户端唯一标识 + 线程ID)
if (redis.call('hexists', KEYS[1], ARGV[3]) == 0) then
return nil;
end;
local counter = redis.call('hincrby', KEYS[1], ARGV[3], -1);
if (counter > 0) then
redis.call('pexpire', KEYS[1], ARGV[2]);
return 0;
else
redis.call('del', KEYS[1]);
redis.call('publish', KEYS[2], ARGV[1]);
return 1;
end;
return nil;
- 原子性释放:通过 Lua 脚本保证检查和删除操作的原子性。
- 发布解锁消息:通知其他等待线程可以尝试获取锁。
2. 关键特性
(1) 可重入性
- 实现:通过 Hash 结构存储锁名称、客户端标识和计数器。
- 示例:
RLock lock = redisson.getLock("myLock"); lock.lock(); try { // 可重入:同一线程再次加锁 lock.lock(); // 业务逻辑... } finally { lock.unlock(); lock.unlock(); // 需多次解锁直至计数器归零 }
(2) 锁等待与公平性
- 非公平锁:默认模式,抢占式获取锁。
- 公平锁:按请求顺序分配锁,避免饥饿。
RLock fairLock = redisson.getFairLock("fairLock"); fairLock.lock();
(3) 联锁(MultiLock)与红锁(RedLock)
- 联锁:同时获取多个锁,全部成功才算加锁成功。
RLock lock1 = redisson.getLock("lock1"); RLock lock2 = redisson.getLock("lock2"); RedissonMultiLock multiLock = new RedissonMultiLock(lock1, lock2); multiLock.lock(); - 红锁:基于 Redis 多节点的分布式锁算法(需部署多个独立 Redis 实例)。
Config config = new Config(); config.useClusterServers().addNodeAddress("redis://node1:6379", "redis://node2:6379"); RedissonClient redisson = Redisson.create(config); RLock redLock = redisson.getRedLock(lock1, lock2, lock3); redLock.lock();
3. 使用示例
(1) 基础加锁与解锁
// 获取锁对象
RLock lock = redisson.getLock("anyLock");
try {
// 加锁(默认30秒超时,启用Watchdog)
lock.lock();
// 执行业务逻辑
// ...
} finally {
// 释放锁
lock.unlock();
}
(2) 尝试加锁(非阻塞)
if (lock.tryLock()) {
try {
// 加锁成功,执行业务逻辑
} finally {
lock.unlock();
}
} else {
// 加锁失败,处理其他逻辑
}
(3) 超时等待加锁
// 最多等待100秒,锁超时30秒后自动释放
boolean isLocked = lock.tryLock(100, 30, TimeUnit.SECONDS);
if (isLocked) {
try {
// ...
} finally {
lock.unlock();
}
}
4. 注意事项
(1) 避免锁超时未释放
- 原因:未调用
unlock()或业务逻辑阻塞导致 Watchdog 无法续期。 - 建议:
- 确保
lock.unlock()在finally块中调用。 - 避免在锁内执行耗时过长的操作。
- 确保
(2) 锁续期失败
- 场景:Redis 节点故障或网络分区。
- 处理:使用 RedLock 算法(需部署多节点)提高容错性。
(3) 锁误释放
- 原因:不同客户端使用相同锁名称,或未验证锁持有者。
- 解决:依赖 Redisson 的客户端唯一标识(UUID + 线程ID)防止误删。
5. 对比其他方案
| 方案 | 优点 | 缺点 |
|---|---|---|
| Redisson | 功能丰富(可重入、Watchdog)、易集成 | 依赖 Redis 可用性 |
| ZooKeeper | CP 模型强一致性 | 性能较低,部署复杂 |
| 数据库锁 | 无需额外中间件 | 性能差,不适合高并发 |
总结
- 核心机制:Lua 脚本保证原子性、Watchdog 自动续期、可重入设计。
- 适用场景:分布式环境下资源争用控制(如秒杀、定时任务调度)。
- 最佳实践:结合 RedLock 多节点部署提升可靠性,避免长事务锁持有。