☕ Java 后端场景决策

什么时候用 / 什么时候不用 / 边界在哪里 — 8 大核心场景全解析

线程池拒绝策略 CF vs @Async 锁选型 集合选型 @Transactional 异常处理 设计模式 Spring注解

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,直接崩

📁 Project
📂 src/main/java
📂 controller
📄 SeckillController.java
📂 service
📄 SeckillOrderService.java
📂 config
📄 ThreadPoolConfig.java ❌
📂 handler
📄 OrderRejectHandler.java
// 📁 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); } }

✅ 正确写法:自定义拒绝策略 + 降级

📁 Project
📂 src/main/java
📂 controller
📄 SeckillController.java
📂 service
📄 SeckillOrderService.java
📂 config
📄 ThreadPoolConfig.java ✅
📂 handler
📄 OrderRejectHandler.java ✅
// 📁 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 的坑:日志上报场景

📁 Project
📂 src/main/java
📂 config
📄 ThreadPoolConfig.java ❌→✅
// ❌ 用了 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 ❌ 超标
📁 Project
📂 src/main/java
📂 controller
📄 UserController.java
📂 service
📄 UserService.java
📄 EmailService.java
📄 CouponService.java
📄 ProfileService.java
📂 config
📄 AsyncConfig.java
📄 OrderApplication.java
// 📁 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 ✅ 达标
📁 Project
📂 src/main/java
📂 controller
📄 UserController.java
📂 service
📄 UserService.java
📄 EmailService.java
📄 CouponService.java
📄 ProfileService.java
📂 config
📄 AsyncConfig.java
📄 OrderApplication.java
// 📁 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; } }
📁 Project
📂 src/main/java
📂 controller
📄 UserController.java
📂 service
📄 UserService.java
📄 EmailService.java
📄 CouponService.java
📄 ProfileService.java
📂 config
📄 AsyncConfig.java
📄 OrderApplication.java
// 📁 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(比串行快一倍多)
📁 Project
📂 src/main/java
📂 controller
📄 UserController.java
📂 service
📄 UserService.java
📄 ProfileService.java
📄 EmailService.java
📄 CouponService.java
📂 config
📄 AsyncConfig.java
📄 OrderApplication.java
// 📁 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"); } }
📁 Project
📂 src/main/java
📂 controller
📄 UserController.java
📂 service
📄 UserService.java
📄 EmailService.java
📄 CouponService.java
📄 ProfileService.java
📂 config
📄 AsyncConfig.java
📄 OrderApplication.java
// 📁 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 注解被当作普通注解忽略,方法仍同步执行。不报错、不警告、静默失效,是最隐蔽的坑。
📁 Project
📂 src/main/java
📄 OrderApplication.java
// ❌ 没有加 @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 代理介入。
📁 Project
📂 src/main/java
📂 service
📄 UserService.java
📄 EmailService.java
// ❌ 错误: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 才有线程复用和队列控制。
📁 Project
📂 src/main/java
📂 service
📄 EmailService.java
📂 config
📄 AsyncConfig.java
// ❌ 不指定线程池 → 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 有独立的锁空间,无法实现跨进程互斥。
📁 Project
📂 src/main/java/com.example.inventory
📂 controller
📄 OrderController.java
📂 service
📄 InventoryService.java
📄 DistributedLockService.java
📂 config
📄 RedisConfig.java
// 🔗 调用链(集群部署 = 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

📁 Project
📂 src/main/java/com.example.inventory
📂 controller
📄 OrderController.java
📂 service
📄 InventoryService.java
📄 DistributedLockService.java
📂 config
📄 RedisConfig.java
// 📁 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解锁)

📁 Project
📂 src/main/java/com.example.inventory
📂 controller
📄 OrderController.java
📂 service
📄 InventoryService.java
📄 DistributedLockService.java
📂 config
📄 RedisConfig.java
// 📁 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 分布式锁(生产推荐 — 自带看门狗自动续期)

📁 Project
📂 src/main/java/com.example.inventory
📂 controller
📄 OrderController.java
📂 service
📄 InventoryService.java
📄 DistributedLockService.java
📄 RedissonLockService.java
📂 config
📄 RedissonConfig.java
// 📁 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 EXRedisson(生产推荐)
过期时间写死(如10秒),业务超时就出事看门狗自动续期,业务多久都不怕
解锁原子性需要手写 Lua 脚本(GET比较+DEL)框架内置,直接 unlock() 即可
可重入性❌ 不支持(同线程重复加锁会死锁)✅ 支持(Hash 结构记录重入次数)
等待机制❌ 只能自己写 sleep + 重试✅ 支持 waitTime 公平排队等待
代码量50+ 行(加锁+解锁+Lua+异常处理)3 行(getLock + tryLock + unlock)
适用场景简单场景、不想引第三方依赖生产环境、需要高可靠分布式锁
💡 为什么资料里先讲手动 SET NX EX?
因为面试官经常追问"你自己怎么实现分布式锁?",手动写法是基本功,必须答得出来。Redisson 是加分项——在答出手动实现后,补充"生产环境推荐 Redisson,因为它解决了看门狗续期、可重入、等待机制等问题",体现你的深度。

ReentrantLock + Condition 精确唤醒(适合生产者-消费者)

📁 Project
📂 src/main/java/com.example.inventory
📂 controller
📄 OrderController.java
📂 service
📄 InventoryService.java
📄 DistributedLockService.java
📄 BoundedBuffer.java
📂 config
📄 RedisConfig.java
// 📁 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))ArrayListLinkedListArrayList O(1) vs LinkedList O(n)
尾部增删ArrayListArrayList 尾部 O(1)
头部/中间频繁插入删除LinkedList(极少场景)ArrayListArrayList 头部插入 O(n)
键值对查找HashMapList遍历HashMap O(1) vs List O(n)
需要插入/访问顺序LinkedHashMapHashMapHashMap 无序
需要自动排序/范围查询TreeMapHashMap+手动排序TreeMap O(logn) 有序
并发写ConcurrentHashMapHashMap+synchronized锁粒度更细,性能更好
LRU缓存LinkedHashMap(accessOrder=true)手动实现removeEldestEntry一行搞定
去重HashSet / LinkedHashSetList.containsSet O(1) vs List O(n)

❌ 错误:用 List.contains 做权限校验

📁 Project
📂 src/main/java
📂 controller
📄 ResourceController.java
📂 service
📄 PermissionService.java ❌
📄 UserRoleCache.java
📂 config
📄 CacheConfig.java
// 📁 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) 查找 + 缓存

📁 Project
📂 src/main/java
📂 controller
📄 ResourceController.java
📂 service
📄 PermissionService.java ✅
📄 UserRoleCache.java
📂 config
📄 CacheConfig.java
// 📁 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

📁 Project
📂 src/main/java
📂 service
📄 UserRoleCache.java ❌→✅
// 📁 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 遍历修改陷阱 + 正确写法

📁 Project
📂 src/main/java
📂 service
📄 DataCleanService.java
// 📁 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 缓存实现

📁 Project
📂 src/main/java
📂 config
📄 CacheConfig.java
// ❌ 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 正确写法

📁 Project
📂 src/main/java
📂 service
📄 OrderService.java ❌→✅
// 📁 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 判断用异常控制业务流程

完整代码:全局异常处理 + 自定义异常 + 错误码

📁 Project
📂 src/main/java
📂 enums
📄 ErrorCode.java
📂 exception
📄 BizException.java
📂 handler
📄 GlobalExceptionHandler.java
📂 service
📄 UserService.java
// 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 堆叠支付逻辑

📁 Project
📂 src/main/java
📂 controller
📄 PaymentController.java
📂 service
📄 PaymentService.java ❌
📂 strategy
📄 (不存在)
📂 factory
📄 (不存在)
// 📁 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注入

📁 Project
📂 src/main/java
📂 controller
📄 PaymentController.java
📂 service
📄 PaymentService.java ✅
📂 strategy
📄 PaymentStrategy.java
📄 WechatPayStrategy.java
📄 AlipayStrategy.java
📄 BalancePayStrategy.java
📂 factory
📄 PaymentStrategyFactory.java
// 📁 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 中任何一行代码

模板方法模式:数据导入流程

📁 Project
📂 src/main/java
📂 service
📄 DataImporter.java
📄 ExcelImporter.java
// 📁 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 的坑

📁 Project
📂 src/main/java
📂 config
📄 BadConfig.java ❌
📄 GoodConfig.java ✅
// 📁 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

📁 Project
📂 src/main/java
📂 config
📄 OssConfigBad.java ❌
📄 OssProperties.java ✅
// 📁 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后端篇