1. 线程池拒绝策略 — 满了怎么办?
📌 真实业务场景:秒杀系统异步下单
电商秒杀活动,1000 QPS 的下单请求涌入。为保护数据库,Controller 不直接写库,而是把订单任务丢到线程池异步处理。
当线程池满了(核心线程 + 队列 + 最大线程全占满),新的下单请求该怎么处理?这就是拒绝策略的决策点。
Controller 接收请求
→
pool.submit(订单任务)
→
核心线程(8)
→
队列(200)
→
最大线程(16)
→
❗拒绝策略触发
线程池处理链路:8核心线程 → 200队列 → 额外8线程 → 共224并发位 → 超出部分由拒绝策略决定去向
💭 线程池拒绝策略(线程+队列都满时):
AbortPolicy = 直接抛出 RejectedExecutionException,调用方必须处理(默认策略)
CallerRunsPolicy = 让提交任务的线程自己执行该任务,变相限流(线程池不接,但任务不丢)
DiscardPolicy = 静默丢弃新提交的任务,不抛异常,生产环境慎用(问题难排查)
DiscardOldestPolicy = 丢弃队列中最早的任务,把新任务入队(牺牲最早任务容纳最新)
专业定义
AbortPolicy = ThreadPoolExecutor 默认拒绝策略,触发时抛出 RejectedExecutionException,让调用方感知失败并做降级处理。适用于核心业务链路,宁可报错也不能静默丢数据
专业定义
CallerRunsPolicy = 由提交任务的线程自己执行被拒绝的任务,不抛异常、不丢任务,本质是一种负反馈限流机制:调用者忙于执行任务就会放慢提交速度
专业定义
DiscardPolicy = 直接静默丢弃被拒绝的任务,不抛异常、不记录日志。仅适用于日志/监控等可丢失场景;生产环境必须配合监控指标,否则丢失不可感知
四种拒绝策略对比
| 策略 | 行为 | ✅ 什么时候用 | ❌ 什么时候不用 |
AbortPolicy (默认) |
抛出 RejectedExecutionException |
关键任务不能丢、需要感知失败、下单/支付等核心流程 |
不能接受异常中断的场景、日志等可丢失的任务 |
| CallerRunsPolicy |
由提交任务的线程自己执行 |
削峰填谷(放慢提交速度)、不能抛异常、想自动限流 |
调用者线程不能阻塞(如Netty的IO线程)、任务执行时间极长 |
| DiscardPolicy |
静默丢弃,不通知 |
日志写入、监控上报等可丢失场景、消费者场景 |
关键业务、需要知道丢弃了什么、审计场景 |
| DiscardOldestPolicy |
丢弃队列最老的任务 |
实时数据(股价/心跳)、老数据没意义的场景 |
任务有先后顺序要求、历史数据有价值 |
❌ 错误写法:用默认 AbortPolicy,直接崩
// 📁 config/ThreadPoolConfig.java
@Configuration
public class ThreadPoolConfig {
// ❌ 用了默认 AbortPolicy,秒杀高峰直接 500!
@Bean
public ThreadPoolExecutor seckillThreadPool() {
return new ThreadPoolExecutor(
8, // 核心线程
16, // 最大线程
60, TimeUnit.SECONDS, // 空闲存活
new ArrayBlockingQueue<>(200) // 队列200
// ❌ 没指定拒绝策略 → 默认 AbortPolicy
// 当并发 > 224 (16+200+8) 时,submit() 抛异常
);
}
}
// 📁 controller/SeckillController.java
@RestController
public class SeckillController {
@Autowired
private ThreadPoolExecutor seckillThreadPool;
@Autowired
private SeckillOrderService seckillOrderService;
@PostMapping("/seckill/{goodsId}")
public Result<String> seckill(@PathVariable Long goodsId, Long userId) {
seckillThreadPool.submit(() -> {
seckillOrderService.executeSeckill(goodsId, userId);
});
return Result.ok("秒杀成功,订单创建中");
}
// ❌ 秒杀高峰时,submit() 抛 RejectedExecutionException
// → Controller 没有catch → 全局异常处理器返回500
// → 用户看到"系统繁忙",以为是系统挂了,疯狂重试!
}
// 📁 service/SeckillOrderService.java
@Service
public class SeckillOrderService {
@Autowired
private StringRedisTemplate redisTemplate;
@Autowired
private OrderMapper orderMapper;
public void executeSeckill(Long goodsId, Long userId) {
// 1. Redis 原子扣库存
Long stock = redisTemplate.opsForValue().decrement("seckill:stock:" + goodsId);
if (stock == null || stock < 0) {
throw new BizException("库存不足");
}
// 2. 写入 DB 订单
Order order = new Order(goodsId, userId, "PAID");
orderMapper.insert(order);
}
}
✅ 正确写法:自定义拒绝策略 + 降级
// 📁 handler/OrderRejectHandler.java — 自定义拒绝策略
@Component
public class OrderRejectHandler implements RejectedExecutionHandler {
@Autowired
private OrderRetryMapper orderRetryMapper; // 重试队列表
@Override
public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
// 1. 记录日志(知道丢了什么、什么时候、线程池什么状态)
log.warn("秒杀任务被拒绝! 活跃={}, 队列={}, 已完成={}",
executor.getActiveCount(),
executor.getQueue().size(),
executor.getCompletedTaskCount());
// 2. 从 Runnable 中提取订单信息,存入DB重试队列
if (r instanceof SeckillTask) {
SeckillTask task = (SeckillTask) r;
OrderRetry retry = new OrderRetry(task.getGoodsId(), task.getUserId());
orderRetryMapper.insert(retry); // 定时任务稍后重试
}
// 3. 注意:不抛异常!Controller 层不会 500
// 用户体验:返回"排队中,请稍后查看订单"
}
}
// 📁 config/ThreadPoolConfig.java — 正确配置
@Configuration
public class ThreadPoolConfig {
@Autowired
private OrderRejectHandler orderRejectHandler;
@Bean("seckillThreadPool")
public ThreadPoolExecutor seckillThreadPool() {
return new ThreadPoolExecutor(
8, // 核心线程:CPU核心数
16, // 最大线程:2倍核心
60, TimeUnit.SECONDS, // 空闲线程60s回收
new ArrayBlockingQueue<>(200), // 有界队列!不要用无界
new ThreadFactoryBuilder() // 命名线程,方便排查
.setNameFormat("seckill-pool-%d").build(),
orderRejectHandler // ✅ 自定义拒绝策略
);
}
}
// 📁 controller/SeckillController.java — 正确写法
@RestController
public class SeckillController {
@Autowired @Qualifier("seckillThreadPool")
private ThreadPoolExecutor seckillThreadPool;
@Autowired
private SeckillOrderService seckillOrderService;
@PostMapping("/seckill/{goodsId}")
public Result<String> seckill(@PathVariable Long goodsId, Long userId) {
try {
seckillThreadPool.submit(
new SeckillTask(goodsId, userId, seckillOrderService)
);
return Result.ok("秒杀成功,订单创建中");
} catch (Exception e) {
// submit() 内部会把拒绝异常包装成 ExecutionException
// 但自定义Handler不抛异常,这里不会进来
log.error("秒杀提交失败", e);
return Result.fail("系统繁忙,请重试");
}
}
}
// SeckillTask:可序列化的任务包装类(方便拒绝时提取参数)
@AllArgsConstructor
public class SeckillTask implements Runnable {
private final Long goodsId;
private final Long userId;
private final SeckillOrderService service;
@Override
public void run() {
service.executeSeckill(goodsId, userId);
}
// getter 省略,供 RejectHandler 提取参数用
}
DiscardPolicy 的坑:日志上报场景
// ❌ 用了 DiscardPolicy,但没加监控 — 丢了多少日志完全不知道
@Bean("logPool")
public ThreadPoolExecutor logThreadPool() {
return new ThreadPoolExecutor(
2, 4, 30, TimeUnit.SECONDS,
new LinkedBlockingQueue<>(500),
new ThreadPoolExecutor.DiscardPolicy() // 静默丢弃
);
// ❌ 队列满后,日志静默消失
// 线上出问题排查时,发现关键日志完全缺失!
}
// ✅ 改进:即使用 DiscardPolicy,也要加监控
@Bean("logPool")
public ThreadPoolExecutor logThreadPool() {
ThreadPoolExecutor pool = new ThreadPoolExecutor(
2, 4, 30, TimeUnit.SECONDS,
new LinkedBlockingQueue<>(500),
(r, executor) -> { // ✅ 自定义策略
// 不阻塞,不抛异常,但记录丢弃次数
logRejectedCounter.increment(); // Prometheus 计数器
log.warn("日志丢弃,当前队列={},累计丢弃={}",
executor.getQueue().size(),
logRejectedCounter.count());
}
);
return pool;
}
⚠️ 踩坑警告
① AbortPolicy 抛异常的位置:submit() 会把异常包装在 Future 里,execute() 直接抛。如果用 submit() 又不调 future.get(),异常会被吞掉,线程池看起来正常但任务全失败了!
② CallerRunsPolicy 的陷阱:如果调用者是 Tomcat 的请求处理线程,拒绝后请求线程自己去执行任务 → Tomcat 线程被占满 → 整个服务无法接收新请求!
③ DiscardPolicy 无感知:静默丢弃是最危险的,一定要配合监控指标,否则线上出问题时日志全丢,排查无从下手
④ Executors.newFixedThreadPool 的坑:底层用的是无界队列 LinkedBlockingQueue(Integer.MAX_VALUE),队列永远不满,拒绝策略永远不触发 → 任务堆积 → OOM!
2. CompletableFuture vs @Async — 异步选型
需要异步
→
需要编排多任务?
是 → CompletableFuture
否 → @Async
💭 类比
CompletableFuture = 外卖配送链:下单 → 备菜 → 配送 → 送达,每一步可以编排,可以组合
@Async = 叫同事帮忙:"你先忙,那个事帮我处理下",说完就走,不用管他怎么做的
专业定义
CompletableFuture = JDK 8 引入的可编排异步编程框架,支持 supplyAsync/runAsync 提交异步任务,通过 thenCompose/thenCombine/allOf 链式编排,可以拿到返回值并精确控制异常和超时
专业定义
@Async = Spring 框架注解,基于 AOP 代理将标注方法异步执行;调用者不需等待方法完成即返回,但要求 ①@EnableAsync 启用 ②跨类调用(走代理)③指定线程池
决策表
| 需求 | CF | @Async |
| 需要编排多个异步任务(串行/并行/组合) | ✅ thenCompose/thenCombine/allOf | ❌ 做不到 |
| 需要异步任务的返回值 | ✅ SupplyAsync + get/join | ⚠️ Future<T> 但无法编排 |
| 需要精确的异常处理链 | ✅ exceptionally / handle | ❌ 只能try-catch |
| 需要超时控制 | ✅ orTimeout / completeOnTimeout | ❌ 没有内置超时 |
| 简单的 fire-and-forget | ⚠️ 杀鸡用牛刀 | ✅ 加个注解就行 |
| 不需要返回值,发个通知/记个日志 | 可以但没必要 | ✅ 最简单 |
📋 场景背景:用户注册功能
业务流程:用户提交注册信息 → 创建账号 → 查用户画像(RPC) → 发欢迎邮件(RPC) → 发新人优惠券(RPC)
性能要求:接口RT < 200ms
问题:最初用同步串行写法,3个RPC各自耗时约100ms,总计约300ms+DB写入50ms ≈ 350ms,超出要求
❌ 写法1:同步串行(最差,新人最常写)
问题:3个RPC串行调用,前一个完成才开始下一个,总耗时 = 所有RPC时间之和
实际表现:RT ≈ 50ms(DB) + 100ms(画像) + 100ms(邮件) + 100ms(优惠券) = 350ms ❌ 超标
// 📁 service/UserService.java — 注册主逻辑
@Service
public class UserService {
@Autowired private UserMapper userMapper;
@Autowired private ProfileService profileService; // RPC查画像
@Autowired private EmailService emailService; // RPC发邮件
@Autowired private CouponService couponService; // RPC发优惠券
public User register(UserReq req) {
// ① 写入数据库(50ms)
User user = new User(req.getPhone(), req.getName());
userMapper.insert(user);
// ② 查用户画像(100ms)—— 必须等①完成
Profile profile = profileService.getProfile(user.getId());
// ③ 发欢迎邮件(100ms)—— 必须等②完成,但其实跟②没有依赖关系!
emailService.sendWelcome(user.getEmail());
// ④ 发新人优惠券(100ms)—— 必须等③完成,但其实跟③也没有依赖关系!
couponService.grantNewUserCoupon(user.getId());
return user;
}
}
为什么差? ②③④互相之间没有依赖关系(发邮件不需要等画像查完),但串行写法让它们排队执行
串行时间线:
DB写入 ████ (50ms)
查画像 ██████████ (100ms)
发邮件 ██████████ (100ms)
发优惠券 ██████████ (100ms)
总计 ≈ 350ms
✅ 写法2:CompletableFuture 并行编排(推荐)
关键洞察:②查画像、③发邮件、④发优惠券 三者互不依赖 → 可以并行执行
并行后:总耗时 ≈ max(100ms, 100ms, 100ms) = 100ms + DB 50ms = 150ms ✅ 达标
// 📁 config/AsyncConfig.java — 先配一个线程池给CF用
@Configuration
public class AsyncConfig {
// 给 CompletableFuture.supplyAsync / runAsync 传的线程池
@Bean("taskExecutor")
public Executor taskExecutor() {
ThreadPoolTaskExecutor exec = new ThreadPoolTaskExecutor();
exec.setCorePoolSize(8); // 核心线程数
exec.setMaxPoolSize(16); // 最大线程数
exec.setQueueCapacity(200); // 等待队列容量
exec.setThreadNamePrefix("cf-pool-"); // 线程名前缀(排查用)
exec.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy()); // 满了让调用者自己跑
exec.initialize();
return exec;
}
}
// 📁 service/UserService.java — 用 CompletableFuture 重写
@Service
public class UserService {
@Autowired private UserMapper userMapper;
@Autowired private ProfileService profileService;
@Autowired private EmailService emailService;
@Autowired private CouponService couponService;
@Autowired
@Qualifier("taskExecutor")
private Executor taskExecutor; // ← 注入上面配的线程池
public User register(UserReq req) {
// ① 同步写DB(必须先完成,后续步骤依赖user.getId())
User user = new User(req.getPhone(), req.getName());
userMapper.insert(user); // 50ms
// ②③④ 三个不互相依赖的RPC → 并行执行
CompletableFuture<Profile> profileFuture = CompletableFuture
.supplyAsync(() -> profileService.getProfile(user.getId()), taskExecutor);
CompletableFuture<Void> emailFuture = CompletableFuture
.runAsync(() -> emailService.sendWelcome(user.getEmail()), taskExecutor);
CompletableFuture<Void> couponFuture = CompletableFuture
.runAsync(() -> couponService.grantNewUserCoupon(user.getId()), taskExecutor);
// 等三个全部完成,带3秒超时(防止某个RPC卡死)
CompletableFuture.allOf(profileFuture, emailFuture, couponFuture)
.orTimeout(3, TimeUnit.SECONDS)
.join(); // 阻塞等待全部完成
// 把画像结果塞进user返回
user.setProfile(profileFuture.join());
return user;
}
}
并行时间线:
DB写入 ████ (50ms)
查画像 ██████████ (100ms) ┐
发邮件 ██████████ (100ms) ├ 三个同时跑!最慢的100ms
发优惠券 ██████████ (100ms) ┘
总计 ≈ 50 + 100 = 150ms(比串行快一倍多)
// 📁 service/ProfileService.java — 被调用的RPC(供参考,看清调用关系)
@Service
public class ProfileService {
@Autowired private ProfileFeignClient profileFeignClient; // Feign调用用户中心
// 普通同步方法,耗时约100ms(网络往返+远端处理)
public Profile getProfile(Long userId) {
return profileFeignClient.getById(userId);
}
}
// 📁 service/EmailService.java
@Service
public class EmailService {
@Autowired private EmailFeignClient emailFeignClient;
public void sendWelcome(String email) {
emailFeignClient.sendTemplate(email, "WELCOME");
}
}
// 📁 service/CouponService.java
@Service
public class CouponService {
@Autowired private CouponFeignClient couponFeignClient;
public void grantNewUserCoupon(Long userId) {
couponFeignClient.grant(userId, "NEW_USER_100");
}
}
// 📁 controller/UserController.java — 入口,看清完整调用链
@RestController
@RequestMapping("/api/users")
public class UserController {
@Autowired private UserService userService;
@PostMapping("/register")
public Result<User> register(@RequestBody @Valid UserReq req) {
User user = userService.register(req); // ← 调用上面的register方法
return Result.success(user);
}
}
/* 完整调用链:
* 前端POST /api/users/register
* → UserController.register()
* → UserService.register()
* → userMapper.insert() (同步, 50ms)
* → CompletableFuture 并行:
* ├→ ProfileService.getProfile() (100ms)
* ├→ EmailService.sendWelcome() (100ms)
* └→ CouponService.grantNewUserCoupon() (100ms)
* → allOf().join() 等全部完成
* ← 返回User
* ← 返回Result
*/
✅ 写法3:@Async 适合"发完就不管"的场景
专业总结@Async = Spring 通过 AOP 动态代理实现的方法级异步。被注解的方法在调用时,Spring 拦截调用,将方法体提交到指定线程池执行,调用者立即返回。适合 fire-and-forget(发完即弃)场景。底层原理:CGLIB/JDK 动态代理 + AsyncExecutionInterceptor + 线程池执行,因此同类内部调用(this.method())绕过代理会导致注解失效。
如果需求变简单:注册成功后只发一封邮件通知就行,不需要等结果
这时候用 @Async 比 CompletableFuture 更简单 —— 加个注解,调用者完全不用管异步细节
⚠️ 坑1:忘了加 @EnableAsync
专业总结@EnableAsync 是 Spring 开启异步代理的总开关(通过 AsyncAnnotationBeanPostProcessor 注册)。不加它,所有 @Async 注解被当作普通注解忽略,方法仍同步执行。不报错、不警告、静默失效,是最隐蔽的坑。
// ❌ 没有加 @EnableAsync → @Async 注解就是摆设!
@SpringBootApplication
public class OrderApplication {
public static void main(String[] args) {
SpringApplication.run(OrderApplication.class, args);
}
}
// ✅ 正确:加 @EnableAsync
@SpringBootApplication
@EnableAsync // ← 必须加!
public class OrderApplication {
public static void main(String[] args) {
SpringApplication.run(OrderApplication.class, args);
}
}
⚠️ 坑2:同类内部调用 this.method()
专业总结@Async/@Transactional/@Cacheable 等 Spring 注解都基于 AOP 动态代理。同类内部调用(this.xxx())直接走原始对象方法,绕过代理,所有注解全部失效。解决:将被注解的方法拆到独立 Bean 中,通过 @Autowired 注入后调用,让 Spring 代理介入。
// ❌ 错误:this.sendEmail() 绕过Spring代理,@Async不生效!
// 📁 service/UserService.java
@Service
public class UserService {
@Autowired private UserMapper userMapper;
public User register(UserReq req) {
User user = new User(req.getPhone(), req.getName());
userMapper.insert(user);
// ❌ this. 调用 → 绕过代理 → @Async 不生效 → 同步执行!
this.sendWelcomeEmail(user.getEmail());
return user;
}
@Async // 这里想要异步,但被 this. 调用时完全无效
public void sendWelcomeEmail(String email) {
emailFeignClient.send(email, "WELCOME");
}
}
// ──────────────────────────────────────────────
// ✅ 正确:@Async方法拆到独立Bean,通过注入调用
// 📁 service/EmailService.java(独立类)
@Service
public class EmailService {
@Autowired private EmailFeignClient emailFeignClient;
@Async("asyncExecutor") // ← 指定线程池名
public void sendWelcomeEmail(String email) {
emailFeignClient.send(email, "WELCOME");
}
}
// 📁 service/UserService.java(调用方)
@Service
public class UserService {
@Autowired private EmailService emailService; // ← 注入独立Bean
public User register(UserReq req) {
User user = new User(req.getPhone(), req.getName());
userMapper.insert(user);
// ✅ 通过注入的emailService调用 → 走代理 → 真正异步!
emailService.sendWelcomeEmail(user.getEmail());
return user;
}
}
⚠️ 坑3:不指定线程池 → SimpleAsyncTaskExecutor
专业总结@Async 不指定线程池时,Spring 使用 SimpleAsyncTaskExecutor,它的特点是每次调用都新建线程,不复用。高并发下线程数无上限暴涨 → 线程创建/销毁开销大 + 可能 OOM。必须指定 ThreadPoolTaskExecutor 才有线程复用和队列控制。
// ❌ 不指定线程池 → SimpleAsyncTaskExecutor → 每次new线程!
@Service
public class EmailService {
@Async // ← 没有指定线程池名
public void sendEmail(String email) {
emailFeignClient.send(email, "WELCOME");
}
}
// 结果:1000个请求 = 1000个线程 = 线程爆炸 / OOM
// ──────────────────────────────────────────────
// ✅ 正确:定义线程池 + @Async 指定名称
// 📁 config/AsyncConfig.java
@Configuration
@EnableAsync
public class AsyncConfig {
@Bean("asyncExecutor")
public Executor asyncExecutor() {
ThreadPoolTaskExecutor exec = new ThreadPoolTaskExecutor();
exec.setCorePoolSize(4); // 平时保持4个线程
exec.setMaxPoolSize(8); // 最多8个线程
exec.setQueueCapacity(100); // 排队最多100个任务
exec.setThreadNamePrefix("async-"); // 看日志好排查
exec.setRejectedExecutionHandler(
new ThreadPoolExecutor.CallerRunsPolicy());
exec.initialize();
return exec;
}
}
// 📁 service/EmailService.java(指定线程池)
@Service
public class EmailService {
@Async("asyncExecutor") // ← 指定用上面定义的线程池
public void sendEmail(String email) {
emailFeignClient.send(email, "WELCOME");
}
}
📊 最终对比总结
| 写法 | 总耗时 | 代码复杂度 | 适用场景 |
| ❌ 同步串行 |
~350ms |
最简单 |
任务之间有依赖、或不在乎性能 |
| ✅ CompletableFuture |
~150ms |
中等(需理解编排API) |
多个不依赖任务并行 + 需要等结果 + 需要编排/异常处理 |
| ✅ @Async |
~50ms(不等异步任务) |
最简单(加注解) |
fire-and-forget:发通知/记日志,不需要等结果 |
一句话记忆
• 需要编排多个任务(串行→并行→组合)→ CompletableFuture
• 需要不等结果直接返回(发通知/记日志)→ @Async
• @Async 必须:① 加 @EnableAsync ② 不同类调用 ③ 指定线程池
面试金句CompletableFuture 本质是「可组合的异步回调」——可以拿到返回值、支持链式编排(thenCompose/thenCombine)、支持异常兜底(exceptionally)、支持超时控制(orTimeout),适合需要编排多任务并等待结果的场景。@Async 本质是「基于AOP代理的声明式异步」——调用者完全不需要处理异步细节,适合不需要返回值的 fire-and-forget 场景。两者底层都需要线程池:CF 通过 supplyAsync 第二参数传入,@Async 通过注解 value 指定 Bean 名。
3. 锁选型 — 从 synchronized 到分布式锁
📌 真实业务场景:商品库存扣减
用户下单扣库存,如果两个请求同时读到 stock=1,各自判断 stock>0,各自扣减,结果 stock 变成 -1(超卖)。
单体架构用 synchronized 就行,但部署多个实例后,synchronized 只锁当前 JVM,跨实例无效 → 需要分布式锁。
💭 类比
synchronized = 公共厕所:有人就等,不知道等多久,不能中途离开
ReentrantLock = 银行取号:可以不排队走人(tryLock)、可以公平叫号、可以精确唤醒某个人(Condition)
StampedLock = 图书馆:大家都能看(乐观读),有人要改的时候才锁住
Redis分布式锁 = 跨楼层门禁:整栋楼统一的锁,不管你在哪个JVM
专业定义
synchronized = JVM 内置关键字锁(monitor),基于对象头 Mark Word 实现,JDK 6 后支持偏向锁→轻量级锁→重量级锁的锁升级。仅限单 JVM 内互斥,不可中断、不可超时、不可尝试获取
专业定义
ReentrantLock = 基于 AQS(AbstractQueuedSynchronizer)实现的可重入独占锁,支持 tryLock 非阻塞获取、lockInterruptibly 可中断、fair 公平队列、Condition 精确唤醒。需手动 lock/unlock,必须配合 try-finally
专业定义
Redis 分布式锁 = 基于 SET key value NX EX seconds 原子命令实现的跨 JVM 互斥锁;解锁必须用 Lua 脚本(先 GET 验证值再 DEL)保证原子性,防止误删他人锁;生产环境推荐 Redisson 的 watchdog 自动续期
决策表
| 锁类型 | ✅ 什么时候用 | ❌ 什么时候不用 | 类比 |
| synchronized |
单JVM简单互斥、不需要tryLock、不需要公平性、锁粒度粗 |
需要tryLock非阻塞、需要公平队列、需要精确唤醒、需要可中断 |
公共厕所 |
| ReentrantLock |
需要tryLock(非阻塞获取)、需要公平锁、需要Condition精确唤醒、需要可中断的锁 |
简单互斥(synchronized更简洁)、不需要高级特性 |
银行取号 |
| StampedLock |
读多写少、乐观读场景、性能要求极高 |
写多读少、不需要乐观读、代码复杂度增加不值得 |
图书馆 |
| Redis分布式锁 |
跨JVM互斥、分布式系统、多服务共享资源 |
单JVM(用本地锁)、对一致性要求极高(用Redlock/Zookeeper) |
跨楼层门禁 |
| 不需要锁 |
CAS/Atomic够用、无状态服务、并发量极低、用并发集合即可 |
— |
— |
专业总结synchronized 是 JVM 级别的对象监视器锁(monitor lock),通过 monitorenter/monitorexit 字节码指令实现。在单 JVM 内有效,但多实例部署时每个 JVM 有独立的锁空间,无法实现跨进程互斥。
// 🔗 调用链(集群部署 = 2个以上实例):
// OrderController.createOrder()
// → InventoryService.deductStock(goodsId, quantity)
// → [方案A] synchronized(this) ← 单JVM有效,集群下无效!
// → [方案B] Redis SET NX EX 分布式锁 ← 集群下有效
// 📁 service/InventoryService.java
@Service
public class InventoryService {
@Autowired
private InventoryMapper inventoryMapper;
// ❌ synchronized 只锁当前 JVM!
// 部署2个实例后:实例A和实例B各有自己的锁,互不感知
// 同一商品的两个请求分别打到 A 和 B → 两个synchronized各自通过 → 超卖!
public synchronized boolean deductStock(Long goodsId, int quantity) {
Inventory inv = inventoryMapper.selectByGoodsId(goodsId);
if (inv.getStock() < quantity) {
return false; // 库存不足
}
// 💥 集群环境下:两个实例同时读到 stock=1,各自扣减,stock变-1
inv.setStock(inv.getStock() - quantity);
inventoryMapper.update(inv);
return true;
}
}
❌ 错误:Redis锁不设过期时间 + 没 finally
// 📁 service/DistributedLockService.java
@Service
public class DistributedLockService {
@Autowired
private StringRedisTemplate redisTemplate;
// ❌ 问题1:没设过期时间 → 如果程序崩溃,锁永远不释放 → 死锁
// ❌ 问题2:没 try-finally → 如果业务异常,锁不释放 → 下一个请求永远等
public boolean tryLockBad(String key) {
Boolean locked = redisTemplate.opsForValue()
.setIfAbsent(key, "locked"); // ❌ 没设过期!
return locked != null && locked;
}
public void unlockBad(String key) {
redisTemplate.delete(key); // ❌ 无论谁加的锁都能删 → 可能删别人的锁
}
}
// 📁 service/InventoryService.java — 用了上面的坏锁
public boolean deductStockBad(Long goodsId, int quantity) {
String lockKey = "lock:inventory:" + goodsId;
if (!lockService.tryLockBad(lockKey)) {
throw new BizException("系统繁忙,请重试");
}
// ❌ 如果这里抛异常,unlockBad 不会被调用 → 锁泄漏!
Inventory inv = inventoryMapper.selectByGoodsId(goodsId);
if (inv.getStock() < quantity) {
lockService.unlockBad(lockKey); // 每个return前都要手动解锁,容易漏
return false;
}
inv.setStock(inv.getStock() - quantity);
inventoryMapper.update(inv);
lockService.unlockBad(lockKey); // ❌ 删的可能是别人的锁(锁已过期被别人抢到)
return true;
}
专业总结Redis 分布式锁通过 SET key value NX EX 原子命令实现互斥。NX 保证不存在才创建(互斥),EX 保证自动过期(防死锁)。解锁必须用 Lua 脚本先 GET 比较唯一标识再 DEL,保证「检查+删除」的原子性,防止误删别人的锁。
✅ 正确:Redis分布式锁(SET NX EX + 唯一标识 + Lua解锁)
// 📁 service/DistributedLockService.java — 生产级实现
@Service
public class DistributedLockService {
@Autowired
private StringRedisTemplate redisTemplate;
private static final String UNLOCK_SCRIPT =
"if redis.call('get', KEYS[1]) == ARGV[1] then " +
" return redis.call('del', KEYS[1]) " +
"else return 0 end";
// ✅ SET key value NX EX seconds → 原子操作,不存在才设置+自动过期
public String tryLock(String key, long expireSeconds) {
String lockValue = UUID.randomUUID().toString(); // 唯一标识,防止删别人的锁
Boolean locked = redisTemplate.opsForValue()
.setIfAbsent(key, lockValue, expireSeconds, TimeUnit.SECONDS);
return locked != null && locked ? lockValue : null;
}
// ✅ Lua脚本解锁:先验证值再删除,保证原子性
public boolean unlock(String key, String lockValue) {
Long result = redisTemplate.execute(
new DefaultRedisCallback<>(connection ->
connection.eval(UNLOCK_SCRIPT.getBytes(), ReturnType.INTEGER,
1, key.getBytes(), lockValue.getBytes())),
true);
return result != null && result == 1;
}
}
// 📁 service/InventoryService.java — 正确使用
@Service
public class InventoryService {
@Autowired private DistributedLockService lockService;
@Autowired private InventoryMapper inventoryMapper;
public boolean deductStock(Long goodsId, int quantity) {
String lockKey = "lock:inventory:" + goodsId;
String lockValue = lockService.tryLock(lockKey, 10); // 10秒自动过期
if (lockValue == null) {
throw new BizException("系统繁忙,请重试");
}
try { // ✅ 必须 try-finally
Inventory inv = inventoryMapper.selectByGoodsId(goodsId);
if (inv.getStock() < quantity) {
return false; // 不需要在这里手动解锁
}
inv.setStock(inv.getStock() - quantity);
inventoryMapper.update(inv);
return true;
} finally {
lockService.unlock(lockKey, lockValue); // ✅ 不管成功失败,都会解锁
}
}
}
// 📁 controller/OrderController.java
@RestController
public class OrderController {
@Autowired private InventoryService inventoryService;
@PostMapping("/order")
public Result<String> createOrder(Long goodsId, int quantity) {
boolean ok = inventoryService.deductStock(goodsId, quantity);
if (!ok) {
return Result.fail("库存不足");
}
// 创建订单...
return Result.ok("下单成功");
}
}
⭐ 进阶:Redisson 分布式锁(生产推荐 — 自带看门狗自动续期)
// 📁 config/RedissonConfig.java — Redisson 配置
@Configuration
public class RedissonConfig {
@Bean(destroyMethod = "shutdown")
public RedissonClient redissonClient() {
Config config = new Config();
config.useSingleServer()
.setAddress("redis://127.0.0.1:6379")
.setConnectionPoolSize(32);
return Redisson.create(config);
}
}
// 📁 service/RedissonLockService.java — Redisson 分布式锁
@Service
public class RedissonLockService {
@Autowired
private RedissonClient redissonClient;
// ✅ Redisson 方式:tryLock 不传 leaseTime → 启动看门狗(watchdog)!
// 看门狗默认 30 秒过期,每 10 秒自动续期一次(= 30s / 3)
// 业务执行多久都不会提前过期,直到 unlock 后看门狗自动停止
public boolean deductStockWithRedisson(Long goodsId, int quantity) {
RLock lock = redissonClient.getLock("lock:inventory:" + goodsId);
try {
// 参数1: waitTime=0(拿不到锁立刻返回,不等待)
// 参数2: 不传 leaseTime → 启动看门狗自动续期!
if (!lock.tryLock(0, TimeUnit.SECONDS)) {
throw new BizException("系统繁忙,请重试");
}
// ===== 业务逻辑 =====
// 不管这里执行多久(GC停顿、网络超时),
// 看门狗都在后台自动续期,锁不会提前过期!
Inventory inv = inventoryMapper.selectByGoodsId(goodsId);
if (inv.getStock() < quantity) return false;
inv.setStock(inv.getStock() - quantity);
inventoryMapper.update(inv);
return true;
} finally {
// ✅ 先检查是否还持有锁,再 unlock
// 防止业务超时后锁被 Redis 自动释放,此时 unlock 会报错
if (lock.isHeldByCurrentThread()) {
lock.unlock(); // unlock 后看门狗自动停止
}
}
}
}
// ═══════════════════════════════════════════════════════
// 🔍 Redisson 看门狗内部原理(源码简化版,框架帮你做的)
// ═══════════════════════════════════════════════════════
//
// 1. tryLock() 成功后,Redisson 内部启动一个 Netty 定时任务
// 2. 每隔 10 秒(= 过期时间 30s / 3)执行续期逻辑:
// IF Redis 中 key 还属于当前线程 THEN
// EXPIRE key 30 ← 续期到 30 秒!
// ELSE
// 停止看门狗 ← 锁已不属于我,停止续期
// END IF
// 3. lock.unlock() → 看门狗任务自动取消
//
// ⚠️ 坑1:tryLock(waitTime, leaseTime, unit) 指定了 leaseTime
// → 看门狗不会启动!只有不传 leaseTime 才启动
// ⚠️ 坑2:unlock 前必须 isHeldByCurrentThread() 检查
// → 否则锁过期后被别人抢走,你 unlock 会报异常
面试金句Redisson 看门狗(Watchdog)解决了手动 SET NX EX 的致命问题:业务执行超过过期时间导致锁提前释放。当你调用 tryLock() 不指定 leaseTime 时,Redisson 自动启动后台任务,每隔 10 秒(= 默认30s/3)检查锁是否仍属于当前线程,是则 EXPIRE 续期到 30 秒。业务执行多久锁就续多久,直到 unlock 后自动停止。
手动 SET NX EX vs Redisson 对比
| 维度 | 手动 SET NX EX | Redisson(生产推荐) |
| 过期时间 | 写死(如10秒),业务超时就出事 | 看门狗自动续期,业务多久都不怕 |
| 解锁原子性 | 需要手写 Lua 脚本(GET比较+DEL) | 框架内置,直接 unlock() 即可 |
| 可重入性 | ❌ 不支持(同线程重复加锁会死锁) | ✅ 支持(Hash 结构记录重入次数) |
| 等待机制 | ❌ 只能自己写 sleep + 重试 | ✅ 支持 waitTime 公平排队等待 |
| 代码量 | 50+ 行(加锁+解锁+Lua+异常处理) | 3 行(getLock + tryLock + unlock) |
| 适用场景 | 简单场景、不想引第三方依赖 | 生产环境、需要高可靠分布式锁 |
💡 为什么资料里先讲手动 SET NX EX?
因为面试官经常追问"你自己怎么实现分布式锁?",手动写法是基本功,必须答得出来。Redisson 是加分项——在答出手动实现后,补充"生产环境推荐 Redisson,因为它解决了看门狗续期、可重入、等待机制等问题",体现你的深度。
ReentrantLock + Condition 精确唤醒(适合生产者-消费者)
// 📁 service/BoundedBuffer.java — 内部消息队列缓冲区
// 场景:订单服务往缓冲区放消息,物流服务从中取消息
public class BoundedBuffer<T> {
private final ReentrantLock lock = new ReentrantLock();
private final Condition notFull = lock.newCondition(); // 生产者等
private final Condition notEmpty = lock.newCondition(); // 消费者等
private final Object[] items = new Object[100];
private int putIdx, takeIdx, count;
// 生产者:放数据
public void put(T item) throws InterruptedException {
lock.lock();
try {
while (count == items.length)
notFull.await(); // 满了:生产者等待
items[putIdx] = item;
if (++putIdx == items.length) putIdx = 0;
count++;
notEmpty.signal(); // ✅ 精确唤醒消费者(不是所有线程)
} finally {
lock.unlock(); // ✅ try-finally 保证解锁
}
}
// 消费者:取数据
@SuppressWarnings("unchecked")
public T take() throws InterruptedException {
lock.lock();
try {
while (count == 0)
notEmpty.await(); // 空了:消费者等
T item = (T) items[takeIdx];
if (++takeIdx == items.length) takeIdx = 0;
count--;
notFull.signal(); // ✅ 精确唤醒生产者
return item;
} finally {
lock.unlock();
}
}
}
⚠️ 锁的常见坑
① synchronized 和 ReentrantLock 不要混用在同一资源上,两者互不感知
② StampedLock 不支持重入!不要在持有锁时再获取同一把锁,会死锁
③ Redis锁的过期时间:太短→业务没执行完锁就过期了,别人拿到锁→并发问题;太长→宕机后等待时间长。推荐设为业务预期耗时的2-3倍,配合Redisson看门狗(watchdog)自动续期
④ 解锁必须验证:直接 del key 可能删掉别人的锁(你的锁过期后别人新加了一把)→ 必须用 Lua 脚本先 get 比较再 del
⑤ 能用 Atomic/CAS 就别上锁 — AtomicInteger 代替 synchronized count++
4. 集合选型 — ArrayList 不是万能的
📌 真实业务场景:用户权限校验
每次API请求都要校验用户是否有权限访问某个资源。用户角色列表从DB查出来后存在内存中。
用List存→每次校验都要遍历(O(n));用HashSet存→O(1)直接判断。
当用户量大、请求量高时,集合选型直接影响接口性能。
💭 类比
ArrayList = 电影院座位:编号固定,按号找快(随机访问O(1)),但中间插个座得挪半排人(插入O(n))
LinkedList = 排队:中间插队容易(插入O(1)),但找人要一个个数(随机访问O(n))
HashMap = 字典:按拼音直接翻到(O(1)),不需要顺序
TreeMap = 按价格排序的商品目录:自动排序,范围查找方便
LinkedHashMap = 按时间排序的聊天记录:插入顺序/访问顺序
专业定义
ArrayList = 基于动态数组实现的 List,随机访问 get(i) 时间复杂度 O(1),尾部插入均摊 O(1),中间插入/删除 O(n)(需数组搬移)。内部数组容量按 1.5 倍扩容,适合读多写少场景
专业定义
HashMap = 基于哈希表 + 链表/红黑树实现的 Map,put/get 平均 O(1)。JDK 8 链表长度 ≥8 且数组长度 ≥64 时转红黑树(O(logn)),非线程安全,多线程扩容可能导致数据丢失或死循环
专业定义
ConcurrentHashMap = 线程安全的 HashMap,JDK 8 基于 CAS + synchronized 锁单个桶节点(锁粒度为 Node 数组槽位),读写不互斥,支持高并发场景下的安全访问
专业定义
LinkedHashMap = HashMap 的有序子类,通过维护双向链表记录插入/访问顺序。accessOrder=true 时按访问顺序排序,配合 removeEldestEntry() 可一行实现 LRU 缓存淘汰策略
决策表
| 场景 | ✅ 用什么 | ❌ 不要用 | 原因 |
| 随机访问多(get(i)) | ArrayList | LinkedList | ArrayList O(1) vs LinkedList O(n) |
| 尾部增删 | ArrayList | — | ArrayList 尾部 O(1) |
| 头部/中间频繁插入删除 | LinkedList(极少场景) | ArrayList | ArrayList 头部插入 O(n) |
| 键值对查找 | HashMap | List遍历 | HashMap O(1) vs List O(n) |
| 需要插入/访问顺序 | LinkedHashMap | HashMap | HashMap 无序 |
| 需要自动排序/范围查询 | TreeMap | HashMap+手动排序 | TreeMap O(logn) 有序 |
| 并发写 | ConcurrentHashMap | HashMap+synchronized | 锁粒度更细,性能更好 |
| LRU缓存 | LinkedHashMap(accessOrder=true) | 手动实现 | removeEldestEntry一行搞定 |
| 去重 | HashSet / LinkedHashSet | List.contains | Set O(1) vs List O(n) |
❌ 错误:用 List.contains 做权限校验
// 📁 service/PermissionService.java
@Service
public class PermissionService {
@Autowired
private RoleMapper roleMapper;
// ❌ 每次请求都查DB + 用List判断
public boolean hasPermission(Long userId, String resource) {
// 1. 每次请求都查DB(应该缓存)
List<String> roles = roleMapper.findRolesByUserId(userId);
// 2. ❌ List.contains() 是 O(n) 遍历!
// 如果用户有50个角色,最坏情况遍历50次
if (roles.contains("ADMIN")) {
return true;
}
// 3. ❌ 再遍历一次检查权限资源
for (String role : roles) {
List<String> perms = roleMapper.findPermsByRole(role); // 每个角色又查DB!
if (perms.contains(resource)) {
return true;
}
}
return false;
// 💥 结果:一个请求 N 次DB查询 + N 次 O(n) 遍历
// 1000 QPS 下 = 1000 * 50 * DB查询 → 接口耗时 500ms+
}
}
✅ 正确:用 HashSet O(1) 查找 + 缓存
// 📁 service/PermissionService.java — 正确实现
@Service
public class PermissionService {
@Autowired
private RoleMapper roleMapper;
// ✅ 本地缓存:userId → 权限Set,5分钟过期
private final Cache<Long, Set<String>> permCache = Caffeine.newBuilder()
.expireAfterWrite(5, TimeUnit.MINUTES)
.maximumSize(10000)
.build();
public boolean hasPermission(Long userId, String resource) {
// 1. 从缓存获取权限集合(Hit率99%+ → 不查DB)
Set<String> permissions = permCache.get(userId, this::loadPermissionsFromDB);
// 2. ✅ HashSet.contains() 是 O(1)!
return permissions.contains(resource);
}
// 从DB加载,存入HashSet
private Set<String> loadPermissionsFromDB(Long userId) {
List<String> roles = roleMapper.findRolesByUserId(userId);
if (roles.contains("ADMIN")) {
return Set.of("*"); // ADMIN 拥有所有权限
}
List<String> perms = roleMapper.findPermsByRoleIds(roles); // ✅ 批量查,一次DB
return new HashSet<>(perms); // ✅ 转成HashSet
}
}
// 效果:缓存命中时 → 1次HashSet.contains() → O(1) → 耗时 <1ms
❌ vs ✅:并发场景下 HashMap vs ConcurrentHashMap
// 📁 service/UserRoleCache.java — 多线程并发读写角色缓存
// ❌ 用 HashMap:多线程put可能导致数据丢失(HashMap扩容时链表成环 → 死循环)
@Service
public class UserRoleCacheBad {
private final Map<Long, String> cache = new HashMap<>(); // ❌ 非线程安全!
// 定时刷新线程 → 写;请求线程 → 读 → 并发修改 → JDK7 死循环 / JDK8 数据丢失
@Scheduled(fixedRate = 60000)
public void refresh() {
cache.clear();
cache.putAll(loadFromDB()); // ❌ 多线程同时put → 可能丢数据
}
public String getRole(Long userId) {
return cache.get(userId); // ❌ 可能读到不一致状态
}
}
// ❌ 用 synchronized 包装:虽然线程安全,但锁粒度太粗,读写互斥
private final Map<Long, String> cache =
Collections.synchronizedMap(new HashMap<>()); // ❌ 读也要锁 → 并发吞吐量低
// ✅ 用 ConcurrentHashMap:分段锁/CAS,读写不互斥
@Service
public class UserRoleCache {
private final ConcurrentHashMap<Long, String> cache = new ConcurrentHashMap<>();
@Scheduled(fixedRate = 60000)
public void refresh() {
cache.clear(); // ✅ 线程安全
loadFromDB().forEach(cache::put); // ✅ 多线程put不丢数据
}
public String getRole(Long userId) {
return cache.get(userId); // ✅ 无锁读,不影响写入
}
}
ArrayList 遍历修改陷阱 + 正确写法
// 📁 service/DataCleanService.java — 定时清理无效数据
// ❌ 遍历时删除 → ConcurrentModificationException
public void cleanBad(List<User> users) {
for (User u : users) { // 增强 for 循环底层用 Iterator
if (u.getStatus() == 0) {
users.remove(u); // ❌ Iterator检测到modCount变化 → 抛异常!
}
}
}
// ✅ 写法1:Iterator.remove()
public void cleanGood1(List<User> users) {
Iterator<User> it = users.iterator();
while (it.hasNext()) {
if (it.next().getStatus() == 0) {
it.remove(); // ✅ Iterator自己的remove,同步更新modCount
}
}
}
// ✅ 写法2:Java 8 removeIf(最简洁)
public void cleanGood2(List<User> users) {
users.removeIf(u -> u.getStatus() == 0); // ✅ 一行搞定
}
subList 陷阱 + LRU 缓存实现
// ❌ subList 返回的是视图(view),不是副本!
List<String> list = new ArrayList<>(Arrays.asList("A", "B", "C", "D"));
List<String> sub = list.subList(0, 2); // 返回的是原List的视图
sub.clear(); // 清除子列表
System.out.println(list); // ["C", "D"] ← 原列表也被改了!
// ✅ 需要独立副本时:new ArrayList(list.subList(0, 2))
// 📁 config/CacheConfig.java — LinkedHashMap 实现 LRU 缓存
public class LRUCache<K, V> extends LinkedHashMap<K, V> {
private final int maxSize;
public LRUCache(int maxSize) {
super(maxSize, 0.75f, true); // accessOrder=true → 访问后移到末尾
this.maxSize = maxSize;
}
@Override
protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
return size() > maxSize; // 超过容量自动删除头部(最老的)
}
}
// 使用示例
LRUCache<String, String> cache = new LRUCache<>(3);
cache.put("a", "1"); cache.put("b", "2"); cache.put("c", "3");
cache.get("a"); // 访问"a","a"移到最新
cache.put("d", "4"); // 超过容量3,删除最老的"b"
System.out.println(cache); // {c=3, a=1, d=4}
5. @Transactional — 7种悄悄失效的场景
💭 类比
@Transactional = 银行转账:要么都成功,要么都回滚
同类内部调用 = 走后门:不走正规流程,制度管不到
catch 吞异常 = 出了事不上报:领导不知道,当然不会处理
checked 异常默认不回滚 = 只有杀人(运行时异常)才报警,偷东西(受检异常)不管
专业定义
@Transactional = Spring 基于 AOP 代理实现声明式事务注解,通过 TransactionInterceptor 拦截方法调用,在方法前 begin transaction、方法后 commit、异常时 rollback。默认只回滚 RuntimeException 和 Error,需 rollbackFor=Exception.class 才回滚受检异常
专业定义
事务传播行为(Propagation) = 当一个事务方法调用另一个事务方法时,决定如何处理事务上下文的 7 种策略。REQUIRED(默认)= 有则加入无则新建;REQUIRES_NEW = 总是新建独立事务,挂起外层;NESTED = 通过 savepoint 实现嵌套
7 种失效场景
| # | 失效场景 | 原因 | ✅ 修复方式 |
| 1 |
方法不是 public |
Spring AOP 只拦截 public 方法 |
改为 public |
| 2 |
同类内部调用 |
this.xxx() 不走代理 |
注入自己 / AopContext.currentProxy() |
| 3 |
异常被 catch 吞了 |
没抛出代理,代理不知道出错了 |
catch 后再 throw / 手动回滚 |
| 4 |
checked 异常默认不回滚 |
默认只回滚 RuntimeException |
rollbackFor = Exception.class |
| 5 |
数据库引擎不支持 |
MyISAM 不支持事务 |
用 InnoDB |
| 6 |
传播行为用错 |
REQUIRES_NEW 嵌套导致回滚异常 |
理解7种传播行为 |
| 7 |
未开启事务管理 |
Spring Boot 自动开启,但纯 Spring 需要 @EnableTransactionManagement |
加 @EnableTransactionManagement |
7 种传播行为
| 传播行为 | 含义 | ✅ 什么时候用 |
| REQUIRED(默认) | 有事务就加入,没有就新建 | 90%的场景,最常用 |
| REQUIRES_NEW | 总是新建事务,挂起当前事务 | 独立事务(日志记录,不受外层回滚影响) |
| NESTED | 嵌套事务(savepoint) | 子事务可独立回滚,但不影响外层 |
| SUPPORTS | 有事务就加入,没有就非事务执行 | 查询方法(可事务可非事务) |
| NOT_SUPPORTED | 非事务执行,挂起当前事务 | 不需要事务的操作(发通知) |
| MANDATORY | 必须在事务中,否则抛异常 | 强制要求调用方有事务 |
| NEVER | 必须在非事务中,否则抛异常 | 不允许在事务中执行 |
代码:失效场景 vs 正确写法
// 📁 service/OrderService.java
@Service
public class OrderService {
@Autowired private OrderMapper orderMapper;
@Autowired private OrderService self; // 注入自己,解决同类调用
// ❌ 失效1:同类内部调用
public void createOrderBad(Order order) {
orderMapper.insert(order); // 无事务
this.deductStock(order); // ❌ this调用不走代理!
}
@Transactional
public void deductStock(Order order) { ... }
// ✅ 修复1:通过注入的代理对象调用
public void createOrderGood(Order order) {
orderMapper.insert(order);
self.deductStock(order); // ✅ 走代理!
}
// ❌ 失效2:catch 吞异常
@Transactional
public void transferBad() {
try {
accountMapper.deduct(fromId, amount);
accountMapper.add(toId, amount);
} catch (Exception e) {
log.error("转账失败", e);
// ❌ 异常被吞了,代理不知道!不会回滚!
}
}
// ✅ 修复2:catch 后再 throw
@Transactional
public void transferGood() {
try {
accountMapper.deduct(fromId, amount);
accountMapper.add(toId, amount);
} catch (Exception e) {
log.error("转账失败", e);
throw new RuntimeException(e); // ✅ 重新抛出
}
}
// ❌ 失效3:checked 异常默认不回滚
@Transactional // 默认只回滚 RuntimeException
public void importBad() throws IOException {
fileService.process(); // 抛 IOException → 不回滚!
}
// ✅ 修复3:rollbackFor = Exception.class
@Transactional(rollbackFor = Exception.class)
public void importGood() throws IOException {
fileService.process(); // ✅ 任何异常都回滚
}
}
6. 异常处理 — 怎么设计才不挖坑
💭 类比
全局异常处理(@RestControllerAdvice) = 公司前台:所有投诉统一到这里,分类处理
空catch = 把投诉信扔了:问题被掩盖
catch Exception太宽 = 把所有投诉都按同一种方式处理:杀鸡用牛刀
自定义业务异常 = 投诉分类标签:不同类型走不同流程
专业定义
@RestControllerAdvice = Spring 提供的全局异常处理注解,结合 @ExceptionHandler 实现统一的异常→HTTP响应映射。拦截 Controller 抛出的异常,按异常类型匹配对应的 handler 方法,避免异常直接暴露给前端
专业定义
自定义业务异常 = 继承 RuntimeException 并关联错误码枚举的异常类,用于区分"业务规则不满足"(用户可理解)和"系统内部错误"(需排查)。全局处理器对 BizException 返回具体错误码,对 Exception 返回通用提示
异常处理原则
| 原则 | ✅ 正确 | ❌ 错误 |
| 空catch | 至少 log.error 记录 | catch (Exception e) {} 空实现 |
| catch范围 | catch 具体异常(IOException/SQLException) | catch Exception 一锅端 |
| 异常信息 | log.error("msg", e) 带堆栈 | e.getMessage() 只有消息没堆栈 |
| 业务异常 | 自定义 BizException + 错误码 | throw new RuntimeException("用户不存在") |
| 异常当流程 | 用 if-else 判断 | 用异常控制业务流程 |
完整代码:全局异常处理 + 自定义异常 + 错误码
// 1. 📁 enums/ErrorCode.java — 错误码枚举
public enum ErrorCode {
USER_NOT_FOUND(40001, "用户不存在"),
INSUFFICIENT_BALANCE(40002, "余额不足"),
ORDER_EXPIRED(40003, "订单已过期"),
PARAM_INVALID(40004, "参数校验失败"),
SYSTEM_ERROR(50000, "系统繁忙");
private final int code;
private final String message;
// getter, constructor 省略
}
// 2. 📁 exception/BizException.java — 自定义业务异常
public class BizException extends RuntimeException {
private final ErrorCode errorCode;
public BizException(ErrorCode errorCode) {
super(errorCode.getMessage());
this.errorCode = errorCode;
}
}
// 3. 📁 handler/GlobalExceptionHandler.java — 全局异常处理器
@RestControllerAdvice
public class GlobalExceptionHandler {
// 业务异常:返回具体错误码
@ExceptionHandler(BizException.class)
public Result<?> handleBiz(BizException e) {
log.warn("业务异常: code={}, msg={}", e.getErrorCode().getCode(), e.getMessage());
return Result.fail(e.getErrorCode().getCode(), e.getMessage());
}
// 参数校验异常
@ExceptionHandler(MethodArgumentNotValidException.class)
public Result<?> handleValidation(MethodArgumentNotValidException e) {
String msg = e.getBindingResult().getFieldErrors().stream()
.map(f -> f.getField() + ": " + f.getDefaultMessage())
.collect(Collectors.joining("; "));
return Result.fail(ErrorCode.PARAM_INVALID.getCode(), msg);
}
// 未知异常:返回通用错误(不暴露内部细节)
@ExceptionHandler(Exception.class)
public Result<?> handleUnknown(Exception e) {
log.error("未知异常", e); // 带完整堆栈
return Result.fail(ErrorCode.SYSTEM_ERROR.getCode(), "系统繁忙");
}
}
// 4. 📁 service/UserService.java — 使用示例
@Service
public class UserService {
public User getUser(Long id) {
User user = userMapper.selectById(id);
if (user == null) {
throw new BizException(ErrorCode.USER_NOT_FOUND); // ✅
}
return user;
}
}
7. 设计模式 — 什么时候用什么模式
📌 真实业务场景:电商支付方式选择
电商系统支持微信支付、支付宝、银行卡、余额支付四种方式。每新增一种支付方式,如果用 if-else,代码会越来越臃肿,每次改动都要改核心支付逻辑。策略模式让新增支付方式 = 新增一个类,核心逻辑零修改。
💭 类比
策略模式 = 出行选交通:飞机/高铁/自驾,按需选
工厂模式 = 对象创建:调用方只传参数,工厂决定实例化哪个具体类
观察者模式 = YouTube订阅:UP主发视频,粉丝收到通知
模板方法 = 考试:题目固定,答案可变
责任链 = 快递分拣:一站一站传
专业定义
策略模式(Strategy) = 定义一系列算法,将每个算法封装成独立的策略类,使它们可以互相替换。新增算法 = 新增策略类,调用方零修改。配合 Spring 的 List<接口> 注入可实现策略自动注册
专业定义
工厂模式(Factory) = 将对象的创建逻辑与使用逻辑分离,由工厂类根据输入条件决定实例化哪个实现类。Spring 的 BeanFactory / FactoryBean 是工厂模式的框架级实现
专业定义
模板方法模式(Template Method) = 在父类定义算法骨架(固定流程),将可变步骤声明为抽象方法由子类实现。通过 final 方法防止子类修改流程顺序,实现"不变流程 + 可变步骤"
决策表
| 你遇到的问题 | ✅ 用什么模式 | Spring中的例子 |
| 同一操作多种算法/规则(支付/折扣/排序) | 策略模式 | ResourceResolver |
| 创建逻辑复杂,需要根据条件创建不同对象 | 工厂模式 | BeanFactory |
| 事件通知,解耦发布者和订阅者 | 观察者模式 | ApplicationEvent |
| 固定流程,可变步骤(审批/数据处理) | 模板方法 | AbstractApplicationContext.refresh() |
| 多个处理器依次处理(过滤器/审批链) | 责任链 | FilterChain / InterceptorChain |
| 简单 if-else 就够 | 不用模式 | — |
❌ 错误:if-else 堆叠支付逻辑
// 📁 service/PaymentService.java — 每加一种支付就要改这里
@Service
public class PaymentService {
@Autowired private WechatPayClient wechatClient;
@Autowired private AlipayClient alipayClient;
@Autowired private AccountMapper accountMapper;
public PayResult processPayment(Order order, String payType) {
// ❌ 第1种:微信支付
if ("WECHAT".equals(payType)) {
WxPayRequest req = new WxPayRequest();
req.setAppId("wx123");
req.setOutTradeNo(order.getOrderNo());
req.setAmount(order.getAmount().multiply(new BigDecimal("100")).intValue());
WxPayResponse resp = wechatClient.unifiedOrder(req);
return new PayResult(resp.getCodeUrl());
}
// ❌ 第2种:支付宝
else if ("ALIPAY".equals(payType)) {
AlipayTradeRequest req = new AlipayTradeRequest();
req.setSubject(order.getProductName());
req.setOutTradeNo(order.getOrderNo());
req.setTotalAmount(order.getAmount().toString());
AlipayTradeResponse resp = alipayClient.tradePay(req);
return new PayResult(resp.getPayUrl());
}
// ❌ 第3种:余额支付
else if ("BALANCE".equals(payType)) {
Account acc = accountMapper.selectByUserId(order.getUserId());
if (acc.getBalance().compareTo(order.getAmount()) < 0) {
throw new BizException("余额不足");
}
acc.setBalance(acc.getBalance().subtract(order.getAmount()));
accountMapper.update(acc);
return new PayResult("SUCCESS");
}
// ❌ 新增"银行卡支付" → 又要加一个 else if → 方法越来越长
// ❌ 修改微信支付参数 → 要在这个几百行方法里找 → 容易改错其他分支
throw new BizException("不支持的支付方式");
}
}
// 📁 controller/PaymentController.java
@RestController
public class PaymentController {
@Autowired private PaymentService paymentService;
@PostMapping("/pay")
public Result<PayResult> pay(Long orderId, String payType) {
return Result.ok(paymentService.processPayment(orderId, payType));
}
}
✅ 正确:策略模式 + Spring Map注入
// 📁 strategy/PaymentStrategy.java — 策略接口
public interface PaymentStrategy {
String getPayType(); // 支持的支付类型
PayResult pay(Order order); // 执行支付
}
// 📁 strategy/WechatPayStrategy.java — 微信支付策略
@Service
public class WechatPayStrategy implements PaymentStrategy {
@Autowired private WechatPayClient wechatClient;
@Override public String getPayType() { return "WECHAT"; }
@Override public PayResult pay(Order order) {
WxPayRequest req = new WxPayRequest();
req.setAppId("wx123");
req.setOutTradeNo(order.getOrderNo());
req.setAmount(order.getAmount().multiply(new BigDecimal("100")).intValue());
WxPayResponse resp = wechatClient.unifiedOrder(req);
return new PayResult(resp.getCodeUrl());
}
}
// 📁 strategy/AlipayStrategy.java — 支付宝策略
@Service
public class AlipayStrategy implements PaymentStrategy {
@Autowired private AlipayClient alipayClient;
@Override public String getPayType() { return "ALIPAY"; }
@Override public PayResult pay(Order order) {
AlipayTradeRequest req = new AlipayTradeRequest();
req.setSubject(order.getProductName());
req.setOutTradeNo(order.getOrderNo());
req.setTotalAmount(order.getAmount().toString());
AlipayTradeResponse resp = alipayClient.tradePay(req);
return new PayResult(resp.getPayUrl());
}
}
// 📁 strategy/BalancePayStrategy.java — 余额支付策略
@Service
public class BalancePayStrategy implements PaymentStrategy {
@Autowired private AccountMapper accountMapper;
@Override public String getPayType() { return "BALANCE"; }
@Override
@Transactional(rollbackFor = Exception.class) // ✅ 每个策略可以有自己的事务需求
public PayResult pay(Order order) {
Account acc = accountMapper.selectByUserId(order.getUserId());
if (acc.getBalance().compareTo(order.getAmount()) < 0) {
throw new BizException("余额不足");
}
acc.setBalance(acc.getBalance().subtract(order.getAmount()));
accountMapper.update(acc);
return new PayResult("SUCCESS");
}
}
// 📁 factory/PaymentStrategyFactory.java — 策略工厂
@Service
public class PaymentStrategyFactory {
private final Map<String, PaymentStrategy> strategyMap;
// ✅ Spring启动时自动把所有PaymentStrategy实现注入List
@Autowired
public PaymentStrategyFactory(List<PaymentStrategy> strategies) {
// 转 Map:getPayType() → 策略实例
this.strategyMap = strategies.stream()
.collect(Collectors.toMap(PaymentStrategy::getPayType, s -> s));
// 新增支付方式时:只需新建一个 @Service 类,这里自动收录!
}
public PaymentStrategy getStrategy(String payType) {
PaymentStrategy strategy = strategyMap.get(payType);
if (strategy == null) {
throw new BizException("不支持的支付方式: " + payType);
}
return strategy;
}
}
// 📁 service/PaymentService.java — 核心逻辑只此一行
@Service
public class PaymentService {
@Autowired private PaymentStrategyFactory strategyFactory;
public PayResult processPayment(Order order, String payType) {
PaymentStrategy strategy = strategyFactory.getStrategy(payType); // ✅ 一行搞定
return strategy.pay(order);
}
}
// ✅ 新增"银行卡支付" = 新建 UnionPayStrategy.java,加@Service → 完事!
// 不需要改 PaymentService、PaymentController、Factory 中任何一行代码
模板方法模式:数据导入流程
// 📁 service/DataImporter.java — 抽象模板
// 场景:系统支持Excel/CSV/JSON三种导入,流程一样:校验→解析→处理→保存→通知
public abstract class DataImporter {
// 模板方法:固定流程(final 防止子类覆盖流程顺序)
public final ImportResult importData(MultipartFile file) {
validate(file); // 1. 校验(公共逻辑)
List<Map<String, Object>> rows = parse(file); // 2. 解析(可变:Excel/CSV/JSON)
List<Map<String, Object>> filtered = process(rows); // 3. 处理(可变:去重/转换)
save(filtered); // 4. 保存(可变:不同表)
notifyComplete(); // 5. 通知(公共逻辑)
return new ImportResult(filtered.size());
}
// 公共逻辑(子类不能改)
private void validate(MultipartFile file) {
if (file.isEmpty()) throw new BizException("文件为空");
if (file.getSize() > 10 * 1024 * 1024) throw new BizException("文件超过10MB");
}
private void notifyComplete() {
log.info("导入完成,发送通知...");
messageService.send("数据导入完成");
}
// 可变步骤:由子类实现
protected abstract List<Map<String, Object>> parse(MultipartFile file);
protected abstract List<Map<String, Object>> process(List<Map<String, Object>> rows);
protected abstract void save(List<Map<String, Object>> rows);
}
// 📁 service/ExcelImporter.java — Excel导入
@Service
public class ExcelImporter extends DataImporter {
@Override
protected List<Map<String, Object>> parse(MultipartFile file) {
return EasyExcel.read(file.getInputStream()).sheet().doReadSync();
}
@Override
protected List<Map<String, Object>> process(List<Map<String, Object>> rows) {
return rows.stream().filter(r -> r.get("name") != null).collect(Collectors.toList());
}
@Override
protected void save(List<Map<String, Object>> rows) {
userMapper.batchInsert(rows);
}
}
8. Spring 注解 — 容易混淆的对比
💭 类比
@Component = 员工(通用角色)
@Service = 业务专员(语义更明确)
@Repository = 数据专员(自带异常转换)
@Controller = 前台接待(处理外部请求)
@Configuration = 部门规章(CGLIB代理,不能随便改)
@Autowired = 按岗位招人(byType)
@Resource = 按名字招人(byName)
专业定义
@Configuration vs @Component = @Configuration 标注的类会被 CGLIB 生成代理子类,拦截 @Bean 方法调用,确保多次调用返回同一个单例实例。@Component 不走代理,@Bean 方法间相互调用会创建新对象
专业定义
@Value vs @ConfigurationProperties = @Value 用于注入单个配置项(${key}),不支持批量绑定和校验;@ConfigurationProperties 按 prefix 批量绑定整个配置组到 POJO,配合 @Validated 支持 JSR-303 校验
注解对比表
| 对比组 | 左 | 右 | 区别 | ✅ 什么时候用哪个 |
| 语义注解 |
@Component |
@Service / @Repository / @Controller |
功能一样,语义不同。@Repository自带异常转换(PersistenceExceptionTranslator) |
按职责选:DAO用@Repository、业务用@Service、控制器用@Controller、工具用@Component |
| 配置类 |
@Configuration |
@Component |
@Configuration 用CGLIB代理,保证@Bean方法调用返回同一实例;@Component不会 |
有@Bean方法的配置类用@Configuration,不要用@Component |
| 注入方式 |
@Autowired |
@Resource |
@Autowired byType(Spring);@Resource byName(JSR-250,Java标准) |
Spring项目优先@Autowired;需要按名字用@Resource;多实现配合@Qualifier |
| 多bean歧义 |
@Qualifier |
@Primary |
@Qualifier 注入时指定;@Primary 全局默认 |
单个注入指定用@Qualifier;某个实现是默认的用@Primary |
| 配置注入 |
@Value |
@ConfigurationProperties |
@Value 单个值;@ConfigurationProperties 批量绑定+校验 |
1-2个值用@Value;多个配置项用@ConfigurationProperties |
| 映射 |
@RequestMapping |
@GetMapping / @PostMapping |
@RequestMapping 通用;@GetMapping等是快捷方式,限定了HTTP方法 |
优先用@GetMapping/@PostMapping等快捷方式,更明确 |
代码:@Configuration vs @Component 的坑
// 📁 config/BadConfig.java
// ❌ 用 @Component:每次调用 myBean() 都会 new 一个新对象!
@Component
public class BadConfig {
@Bean
public DataSource dataSource() {
return new HikariDataSource(...);
}
@Bean
public JdbcTemplate jdbcTemplate() {
return new JdbcTemplate(dataSource()); // ❌ 又调了一次 dataSource()!
// @Component 下会创建第二个 DataSource 实例!
}
}
// 📁 config/GoodConfig.java
// ✅ 用 @Configuration:CGLIB代理保证单例
@Configuration
public class GoodConfig {
@Bean
public DataSource dataSource() {
return new HikariDataSource(...);
}
@Bean
public JdbcTemplate jdbcTemplate() {
return new JdbcTemplate(dataSource()); // ✅ 返回同一个实例
}
}
代码:@Value vs @ConfigurationProperties
// 📁 config/OssConfigBad.java — 对象存储配置
// ❌ @Value:多个配置项很痛苦
@Component
public class OssConfigBad {
@Value("${oss.endpoint}")
private String endpoint;
@Value("${oss.accessKey}")
private String accessKey;
@Value("${oss.secretKey}")
private String secretKey;
@Value("${oss.bucketName}")
private String bucketName;
// 每加一个配置就要加一行 @Value
}
// 📁 config/OssProperties.java
// ✅ @ConfigurationProperties:批量绑定+校验
@ConfigurationProperties(prefix = "oss")
@Validated
public class OssProperties {
@NotBlank
private String endpoint;
@NotBlank
private String accessKey;
@NotBlank
private String secretKey;
private String bucketName = "default"; // 默认值
// getter/setter
}
全栈资料库 · 场景决策 · Java后端篇