最近开始面试了,410面试了一家公司 针对自己薄弱的面试题库,深入了解下,也应付下面试。在这里先祝愿大家在现有公司好好沉淀,定位好自己的目标,在自己的领域上发光发热,在自己想要的领域上(技术管理、项目管理、业务管理等)越走越远!希望各位面试都能稳过,待遇都是杠杠的!因为内心还是很慌,所以先整理所有高频的面试题出来,包含一些业务场景及java相关的所有面试。
JAVA基础
1.java的各种锁
乐观锁、悲观锁、公平锁,非公平锁、排他锁、共享锁、重入锁、偏向锁、重量级锁
- 基础锁概念:
- 乐观锁:
- 假设并发冲突很少发生
- 实现方式:版本号机制、CAS操作
- 典型实现:Atomic类、StampedLock的乐观读
- 使用场景:读多写少,冲突概率低
- 悲观锁:
- 假设并发冲突经常发生
- 实现方式:synchronized、ReentrantLock
- 典型特点:先加锁再访问
- 使用场景:写多读少,重读概率高
- 公平锁:
- 按请求顺序分配锁
- 实现:ReentrantLock(true)
- 优点: 避免线程饥饿
- 缺点:吞吐量较低
- 非公平锁:
- 允许线程“插队”获取锁
- 实现:synchronized、ReentrantLock(false)
- 优点:吞吐量高
- 缺点:可能造成线程饥饿
- 排他锁(独占锁)
- 同一个时刻只允许有一个线程持有
- 实现:synchronized、ReentrantLock、ReentrantReadWriteLock.WriteLock
- 共享锁:
- 允许多个线程共同持有
- 实现:ReentrantReadWriteLock.ReadLock、Semaphore
- 乐观锁:
- 高级锁特征
- 重入锁(Reentrant Lock)
- 线程可以重复获取已持有的锁
- 实现:synchronized、ReentrantLock
- 锁技术:每次重入计数+1,完全释放需解锁相同次数
- 锁膨胀过程(JVM优化)
- 无锁状态:初始状态
- 偏向锁:优化单线程重复访问
- 轻量级锁:多线程轻度竞争时自旋等待
- 重量级锁:竞争激烈时转为OS互斥量
- 自旋锁(Spin Lock)
- 线程不立即阻塞,而是循环尝试获取锁
- 减少线程上下文切换开销
- 使用场景:锁持有时间短的场景
- 重入锁(Reentrant Lock)
- 特殊用途锁:
- 分段锁(segment Lock)
- 将数据分段,每段独立加锁
- 典型实现:ConcurrentHashMap(JDK7版本)
- 提高并发度,减少锁竞争
- 邮筒锁(mailbox Lock)
- 用于线程间通信的同步机制
- 类似生产者-消费者模式中的交换区
- 条件锁(condition Lock)
- 基于条件的等待/通知机制
- 实现:condition 接口
- 比Object.wait()/notify更灵活
- 分布式锁:
- 跨JVM进程的锁机制
- 常见实现:redis、zookeeper
- 典型方案:RedLock算法
- 分段锁(segment Lock)
- JUV包中的并发工具
- 信号量(semaphore)
- 控制同时访问特定资源的线程数量
- 可做流量控制
- 倒计时门闩(countDownLath)
- 等待多个线程完成后再继续
- 循环栅栏(cyclicBarrier)
- 让一组线程互相等待到达屏障点
- 相位器(Phaser)
- 更灵活的多阶段同步屏障
- 信号量(semaphore)
- 其他重要概念
- 锁消除(Lock Elimination)
- JVM在即时编译时消除不必要的锁
- 基于逃逸分析判断对象不会逃逸出当前线程
- 锁粗化(Lock coarsening)
- 将多个锁的锁操作合并成一个更大范围的锁
- 减少频繁加锁解锁的开销
- 死锁(Dead Lock)
- 多线程互相等待对方释放锁
- 四个必要条件:互斥条件、请求与保持、不可剥夺、循环等待
- 活锁(live lock)
- 线程不断改变但无法继续执行
- 类似“谦让过度”的情况
- 锁饥饿(Lock starvation)
- 某些线程长期无法获取资源
- 常见于不合理的锁分配策略
- 锁消除(Lock Elimination)
- 锁的选择建议:
- 优先考虑内置锁:简单的synchronized
- 需要灵活时:选择ReentrantLock或者ReentrantReadWriteLock
- 读多写少场景:考虑stampedLock的乐观读
- 高度并发计数:使用Atomic类
- 资源池控制:使用semaphore
- 线程协调:根据场景选择CountDownLatch/CyclicBarrier/Phaser
Q:分布式锁的实现逻辑?
A:分布式锁是解决分布式系统中资源互斥访问的关键技术.
- 分布式锁核心特性要求:
- 互斥性:同一时刻只有一个客户端能持有锁
- 防死锁:持有锁的客户端崩溃后锁能自动释放
- 容错性:部分节点宕机不影响锁服务的可用性
- 可重入:同一客户端多次获取同一把锁
- 高性能:获取/释放的操作要高效
- 主流实现方案:
- 基于数据库实现:
- 实现流程:
- 创建数据库表专门用来做分布式锁,每次分配一个锁就在数据库插入一条数据,释放则删除
- 特点:
- 简单易实现,性能较差(增加了IO开销)
- 需要处理死锁和超时问题
- 可考虑乐观锁版本号机制优化
- 实现流程:
- 基于redis实现
- 实现流程:
- setnx方案:通过redis的setnx获取锁,当返回成功时处理业务逻辑,其底层通过LUA脚本保证原子性
- RedLock算法(Redis官方推荐):尝试获取所有节点的锁,并获取耗时检查
- 特点:
- 性能优异
- 需要处理锁续期问题(看门狗机制-只要客户端还活着即jvm进程未崩溃,就主动续约)
- RedLock需要至少5个Redis主节点
- 网络分区可能出现脑裂问题
- 实现流程:
- 基于zookeeper实现
- 实现流程:
- 创建临时有序节点
- 获取所有子节点
- 判断是否最小节点,是则获取锁成功,否则监听前一个节点阻塞等待
- 特点:
- 可靠性高(cp系统)
- 性能低于Redis方案
- 天然解决锁释放问题(临时节点)
- 实现相对复杂
- 实现流程:
- 基于Etcd的实现
- 实现流程:
- 获取锁(租约机制)
- 保持心跳持续锁
- 释放锁
- 特点:
- 基于Raft协议强一致
- 支持租约自动过期
- 比zk更易实现
- 实现流程:
- 关键性问题解决方案:
- 锁续期问题:
- redis方案:启动后台线程定期延长锁过期时间(Redisson的watchdog机制)
- zookeeper方案:会话心跳自动维持
- 锁误释放问题
- 每个锁绑定唯一客户端标识(UUID)
- 释放时校验标识(Lua脚本保证原子性)
- 锁等待问题:
- 实现公平锁(zk顺序节点)
- 设置合理的等待超时时间
- 集群故障处理
- redis:主从切换可能导致锁丢失(RedLock可缓解)
- zk:半数以上节点存活即可工作
- 锁续期问题:
- 基于数据库实现:
各方案对比
方案 | 一致性 | 性能 | 实现复杂度 | 适用场景 |
---|---|---|---|---|
数据库 | 强 | 低 | 简单 | 低频简单场景 |
Redis | 弱 | 高 | 中等 | 高频、允许偶尔失效 |
RedLock | 较强 | 中 | 复杂 | 高可靠性要求 |
Zookeeper | 强 | 中 | 复杂 | CP系统、分布式协调 |
Etcd | 强 | 中高 | 中等 | Kubernetes环境、CP系统 |
- 最佳实现:
- 始终设置合理的锁超时时间
- 实现锁的可重入人逻辑(如计数机制)
- 添加锁获取失败的重试策略(带退避算法)
- 关键操作记录审计日志
- 生产环境建议使用成熟框架
- java:redisson、curator
- go:etcd/clientv3
- python:python-redis-lock
Q:redlock如何保障redis主从切换时锁丢失问题?
问题描述:普通的redis主从架构中:客户端向主节点申请锁,主节点异步复制锁的信息到从节点中。若主节点崩溃,从节点升级为主节点时,可能尚未接受到锁信息,导致新主节点上没有锁信息,其他客户端可以获取相同的锁,破坏了互斥性
A:RedLock通过多节点独立获取+多数表决机制解决该问题。
- 部署多个完全独立的redis主节点(无主从关系,建议至少部署5个)
- 客户端依次向所有节点申请锁
- 当获取多数节点(N/2+1)的锁时,才算获取成功
- 锁的有效时间包含获取锁的时间消耗
Q:Redisson看门狗机制如何实现业务未处理完成,锁会自动续约?
A:Redisson的看门狗机制是其分布式锁实现的核心特性之一,它解决了业务处理时间超过锁超时时间的问题。
- 核心原理:
- 自动续期机制:当客户端获取锁后,会启动一个后台线程定期检查并延长锁的持有时间
- 健康检查:只要客户端还“活着”(JVM进程未崩溃),锁就不会因为超时被意外释放
- 默认配置:
- 锁默认超时时间:30秒
- 续期检查间隔:超时时间的1/3(默认10秒一次)
Q:什么是死锁,如何避免?
A:死锁(Deadlock)是指两个或多个进程/线程在执行过程中,由于竞争资源或彼此通信而造成的一种互相等待的现象,若无外力干涉,这些进程/线程都将无法继续执行下去
- 死锁的必要提交:
- 互斥条件:资源一次只能由一个进程占用
- 请求与保持条件:进程持有至少一个资源,并等待获取其他被占用资源
- 不可剥夺条件:已分配给进程的资源,不能被其他进程强行夺取
- 循环等待条件:存在一个进程等待的循环链
- 死锁常见场景:
- 数据库死锁
- java多线程死锁
- 预防策略(破坏必要条件)
必要条件 | 破坏方法 |
---|---|
互斥条件 | 使用共享资源(如读写锁) |
请求与保持条件 | 一次性申请所有资源(All-or-Nothing) |
不可剥夺条件 | 允许抢占资源(设置超时/中断) |
循环等待条件 | 资源有序分配法(对所有资源排序,按固定顺序申请) |
- 避免策略(运行时判断)
- 银行家算法:系统在分配资源先计算分配的安全性
- 资源分配图算法:检查图中是否存在环
- 检查与恢复
- 检查机制:
- 定期检查资源分配图
- 使用等待图(wait-for graph)检测环
- 恢复措施:
- 进程终止:强制终止部分死锁进程
- 资源抢占:回滚并抢占部分资源
- 检查机制:
- 解决:
- 统一加锁顺序:通过固定的顺序打破循环等待条件
- 按照固定顺序加锁,避免线程之间交叉申请资源
- 设置超时机制:即使退出等待状态避免僵局
- 设置超时时间,打破占有且等待条件,通过try lock设置超时机制,未获取到锁时,退出等待
- 使用无锁算法和并发工具类,尽量避免显示加锁
- java.util.concurrent包下面的安全容器与工具
- concurrentHashMap
- ConcurrentLinkedQueue
- AtomicInteger
- 尽量避免使用显示加锁
- java.util.concurrent包下面的安全容器与工具
- 减少锁的颗粒度,限制同步范围优先性能
- 尽量减少锁的颗粒度,缩小锁的临界区,减少线程之间的竞争
- 避免锁嵌套
- 尽量减少一个线程持有多个锁,或者多个线程相互金正同一组锁的场景
- 检查死锁
- 通过死锁监控工具(JConsole、visualVM中的线程视图)分析线程状态并排查
- 统一加锁顺序:通过固定的顺序打破循环等待条件
2.JVM垃圾回收原理及如何优化
- 垃圾回收核心概念:
- 可达性分析算法:通过GC Roots对象作为起点,向下搜索引用链,不可达的对象即为垃圾
- GC Roots包括:
- 虚拟机栈中引用的对象
- 方法区中类静态属性引用的对象
- 方法区中常用引用的对象
- 本地方法栈中JNI引用的对象
- 内存分代模型
- JVM将堆内存划分为不同的代际
堆内存结构
┌───────────────────────┐
│ Young Gen │
│ ┌─────┐ ┌─────┐ ┌─────┐│
│ │Eden │ │S0 │ │S1 ││
│ └─────┘ └─────┘ └─────┘│
├───────────────────────┤
│ Old Gen │
└───────────────────────┘
│ Permanent Gen/Metaspace │
└───────────────────────┘
- 主流的垃圾回收器:
- 新生代回收器:
- serial:单线程,复制算法
- parNew:serial的多线程版本
- parallel scavenge:吞吐量有限
- 老年代回收器:
- serial old:单线程,标记-整理算法
- parallel old:parallel scavenge的老年代斑斑
- CMS:低延迟,标记-清楚算法
- G1回收器:
- 面向服务端应用
- 将堆划分为多个Region
- 可预测的停顿时间模型
- ZGC/Shenandoah
- JDK11+引入的超低延迟回收器
- 停顿时间不超过10ms
- 支持TB级堆内存
- 新生代回收器:
- 垃圾回收优化策略:
- 关键JVM参数:
- 设置初始和最大堆大小,老年代和新生代比例,eden/survivor比例,启用G1回收器,启用CMS回收器
- 优化原则:
- 内存分配优化:
- 避免过大的对象直接进入老年代
- 合理设置新生代大小,减少过早晋升
- 监控对象年龄分布,调整晋升阈值
- GC策略选择:
- 吞吐量优先:parallel scavenge+parallel old
- 低延迟优先:CMS/G1/ZGC
- 大内存应用:G1/ZGC/Shenandoah
- 内存分配优化:
- 关键JVM参数:
- 常见问题解决方案:
- 频繁full gc
- 检查内存泄露
- 调整老年代大小
- 优先对象分配模式
- 长时间GC停顿
- 考虑切换到G1或者ZGC
- 减少存活对象数量
- 增加堆内存
- 内存碎片问题:
- 使用标记-整理算法回收器
- 适当减少-xxcmsInitiatingOccupancyFraction值
- 内存碎片问题:
- 定期重启服务
- 使用优化技巧
- 对象池化:复用对象减少GC压力
- 本地缓冲控制:合理使用weak /soft reference
- 集合优化:预估大小避免扩容
- 流处理:及时关闭资源
- 监控工具:
- jstat -gcutil pid
- visualVM
- GCViewer分析GC日志
- Arthas实时诊断
- 使用优化技巧
- 频繁full gc
- 不同场景下优化建议
- web应用
- 推荐G1回收器,关注会话对象生命周期,优化缓冲策略
- 大数据处理
- 增加新生代比例,考虑使用parallel回收器,监控大对象分配
- 金融交易系统:
- 优先考虑ZGC/shenandoah,严格控制停顿时间,减少不可预测的对象分配
- web应用
3.工作中如何使用线程的?
框架级别的,在启动框架时,利用多线程来处理一些批量的任务,比如spring boot在启动的时候通过多线程来过滤自动配置类,当然他的线程是在启动的时候过滤下,后续就关闭了。但是如果我们是对外提供的接口,如果使用线程池,有可能在高并发的场景下,创建大量的线程,从而导致过度的消息系统的资源,甚至拉垮系统,所以我们使用线程池合理的创建执行到销毁来管理系统。
- 基础的使用:通过直接创建线程、线程池来使用
- 典型应用:通过异常处理,主流程不需要等待结果的操作;并行计算,CPU密集型任务拆分;定时任务;生产者-消费者模式
- 线程池类型选择
- CPU密集型:固定大小线程池
- IO密集型:可缓存线程池
- 定时/延迟任务:调度线程池
- 任务优先级管理:自定义ThreadPoolExecutor
- 推荐自定义线程池配置
- 关键参数建议:
- 核心线程数:CPU密集型设为CPU核心数+1,IO密集型可设更高
- 队列容量:根据系统负载设置,避免OOM
- 拒绝策略
- AbortPolicy:默认,抛出异常
- callerRunsPolicy:由调用线程执行
- DiscardOldestPolicy:丢弃最旧任务
- DiscardPolicy:默认丢弃
-
Q:线程常见问题:
- 线程泄露,线程数持续增长不释放
- 解决:
- 确保正确暴毙线程池、使用有界队列、设置合理的线程存活时间
- 死锁预防:防止死锁四步走
- 性能监控:监控线程池状态
Q:使用什么方式创建线程池?为何不适用jdk内置的executors创建线程池?
4.工作中如何使用策略模式+依赖注入?
在工作中总是会有通过某些枚举来判断做什么操作,比如在订单支付接口中,我们需要判断用户提交的是微信支付还是支付宝支付,如果后面支付的方式越来越多,代码会越来越长,那么如何使用策略模式来改造这种现象呢?
原始代码:
public class PaymentService {public void processPayment(String paymentType, BigDecimal amount) {if ("ALIPAY".equals(paymentType)) {// 支付宝支付逻辑System.out.println("处理支付宝支付: " + amount);} else if ("WECHAT".equals(paymentType)) {// 微信支付逻辑System.out.println("处理微信支付: " + amount);} else if ("UNIONPAY".equals(paymentType)) {// 银联支付逻辑System.out.println("处理银联支付: " + amount);} else {throw new IllegalArgumentException("不支持的支付方式");}}
}
上面代码违反了开闭原则,每次新增支付方式需要调整原代码,方法臃肿,随着支付方式越来越多,会越来越长,难以单独测试某中支付方式,支付逻辑与其他代码逻辑耦合。
方式一:策略模式改造:定义策略接口
public interface PaymentStrategy {/*** 支付处理方法* @param amount 支付金额* @return 支付结果*/PaymentResult pay(BigDecimal amount);/*** 是否支持当前支付类型* @param paymentType 支付类型* @return 是否支持*/boolean supports(String paymentType);
}// 支付结果封装
public class PaymentResult {private boolean success;private String message;private String transactionId;// getters/setters
}
方式一:策略模式改造:实现具体策略模式,以支付宝方式为例
public class AlipayStrategy implements PaymentStrategy {@Overridepublic PaymentResult pay(BigDecimal amount) {// 调用支付宝SDK的具体实现System.out.println("支付宝支付处理中,金额: " + amount);PaymentResult result = new PaymentResult();result.setSuccess(true);result.setTransactionId("ALI" + System.currentTimeMillis());return result;}@Overridepublic boolean supports(String paymentType) {return "ALIPAY".equalsIgnoreCase(paymentType);}
}
方式一: 策略模式改造:创建策略工厂
public class PaymentStrategyFactory {private final List<PaymentStrategy> strategies;// 通过构造器注入所有策略public PaymentStrategyFactory(List<PaymentStrategy> strategies) {this.strategies = strategies;}public PaymentStrategy getStrategy(String paymentType) {return strategies.stream().filter(s -> s.supports(paymentType)).findFirst().orElseThrow(() -> new IllegalArgumentException("不支持的支付方式: " + paymentType));}
}
方式一:策略模式改造:改造支付服务
@Service
public class PaymentService {private final PaymentStrategyFactory strategyFactory;// 构造器注入public PaymentService(PaymentStrategyFactory strategyFactory) {this.strategyFactory = strategyFactory;}public PaymentResult processPayment(String paymentType, BigDecimal amount) {PaymentStrategy strategy = strategyFactory.getStrategy(paymentType);return strategy.pay(amount);}
}
方式二:springboot集成优化 自动注册策略bean
@Configuration
public class PaymentConfig {@Beanpublic PaymentStrategyFactory paymentStrategyFactory(List<PaymentStrategy> strategies) {return new PaymentStrategyFactory(strategies);}@Beanpublic PaymentStrategy alipayStrategy() {return new AlipayStrategy();}@Beanpublic PaymentStrategy wechatPayStrategy() {return new WechatPayStrategy();}@Beanpublic PaymentStrategy unionPayStrategy() {return new UnionPayStrategy();}
}
方式二:springboot集成优化 使用枚举优化支付类型
public enum PaymentType {ALIPAY("ALIPAY", "支付宝"),WECHAT("WECHAT", "微信支付"),UNIONPAY("UNIONPAY", "银联支付");private final String code;private final String name;// constructor/getters
}
方式二:springboot集成优化 策略接口改进,使用枚举
public interface PaymentStrategy {PaymentResult pay(BigDecimal amount);// 改为支持PaymentType枚举boolean supports(PaymentType paymentType);
}
方式三:优化方案:策略+模板方法模式结合
public abstract class AbstractPaymentStrategy implements PaymentStrategy {@Overridepublic final PaymentResult pay(BigDecimal amount) {// 1. 参数校验validate(amount);// 2. 执行支付PaymentResult result = doPay(amount);// 3. 记录日志logPayment(result);return result;}protected abstract PaymentResult doPay(BigDecimal amount);private void validate(BigDecimal amount) {if (amount == null || amount.compareTo(BigDecimal.ZERO) <= 0) {throw new IllegalArgumentException("金额必须大于0");}}private void logPayment(PaymentResult result) {// 记录支付日志}
}
方式三:优化方案 策略缓存优化
public class CachedPaymentStrategyFactory {private final Map<PaymentType, PaymentStrategy> strategyCache = new ConcurrentHashMap<>();private final List<PaymentStrategy> strategies;public CachedPaymentStrategyFactory(List<PaymentStrategy> strategies) {this.strategies = strategies;}public PaymentStrategy getStrategy(PaymentType paymentType) {return strategyCache.computeIfAbsent(paymentType, type -> strategies.stream().filter(s -> s.supports(type)).findFirst().orElseThrow(() -> new IllegalArgumentException("不支持的支付方式")));}
}
Spring相关面试题
1.Spring AOP底层实现原理是什么?
- Spring AOP(面向切面编程)的底层实现基于动态代理技术,主要通过两种实现方式:JDK动态代理和CGLIB字节码生成。当目标类实现了接口,Spring默认使用了JDK动态代理,否则使用CGLIB方式,而spring-boot选择了VCGLIB方式来实现。
- JDK动态代理(基于接口)
- 适用条件:目标类实现了至少一个接口
- 代理对象通过JDK直接生成,实现目标类的接口,并通过反射调用目标类
- 特点:
- 运行时生成接口的代理类
- 通过InvocationHandler拦截方法调用
- 性能较好,但只能代理接口方法
- CGLIB字节码生成(基于子类)
- 适用条件:不能使用final
- 适用对象:目标类没有实现接口
- CGLIB通过CFLIB-ASM操作框架生成,继承目标类,通过子类调用父类的方式进行调用目标方法
- 特点:
- 通过ASM库直接生成目标类的子类(Enhancer)
- 可以代理普通类方法(包括非public方法)
- 创建代理对象速度较慢,但执行效率高
- 代理创建流程
- Spring AOP创建代理的核心流程:
- 解析切面配置
- 通过@Aspect注解或者XML配置识别切面
- 解析切入点表达式(Pointcut)
- 创建代理工厂(proxyFactory)
- 选择代理机制
- 生成代理对象
- JDK代理:proxy.newProxyInstance()
- CGLIB:enhance.create()
- Spring AOP创建代理的核心流程:
- 拦截器链执行
- Spring AOP通过责任链模式执行增强逻辑
- 增强类型与顺序执行
- spring aop支持了五种通知类型
- @Aroud环绕通知
- @before前置通知
- 目标方法执行
- @AfterReturing(返回通知,正常返回时执行)
- @After(后置通知,finally块中执行)
- @AfterThrowing(异常通知,抛出异常时执行)
- 性能优化与实现
- Spring对AOP进行了多项优化
- 缓冲机制
- 代理类缓冲DefaultAopProxyFactory
- 拦截器链缓冲AdvisedSupport
- 预过滤
- 预先排除不可能匹配的方法
- 选择性代理
- 支队匹配切入点的方法生成代理逻辑
- 其他方法直接调用目标方法
- 缓冲机制
- Spring对AOP进行了多项优化
- JDK动态代理(基于接口)
- 与AspectJ的关系
特性 | Spring AOP | AspectJ |
---|---|---|
实现方式 | 运行时动态代理 | 编译时/加载时织入 |
性能 | 较好 | 最优(编译期完成) |
功能范围 | 仅方法级别 | 字段、构造器、静态块等 |
依赖 | 仅Spring核心 | 需要AspectJ编译器/织入器 |
适用场景 | 简单切面需求 | 复杂切面需求 |
- 总结:
- Spring AOP的底层实现本质上是基于代理模式的运行时增强,其核心特点是
- 非侵入性:通过代理实现,不修改原始代码
- 灵活性:支持多种通知类型和切入点表达式
- 可扩展性:可与AspectJ部分功能集成
- 性能平衡:通过缓存和优化策略保证运行时效率
- Spring AOP的底层实现本质上是基于代理模式的运行时增强,其核心特点是
spring boot面试相关
1.spring boot 解决跨域5种方式
什么是跨域,当你公司的域名eg:http://mywork.com要到https://baidu.com上去获取东西
此时可以看到域名mywork->baidu不同;协议http->https不同;端口不同;二级域名不同,ip不同等。跨域不一定有异常,跨域异常只有在前端才会发生,因为浏览器有一个同源策略,当发现我们不同域之间的访问是不安全的行为,他会禁止,然后抛出异常。
- jsonp
- 优点是因为他够老,能兼容各种浏览器,无兼容问题
- 缺点只支持get,而且存在安全问题,且前后端都要相对应的去调整接受参数等信息。
- cors
- 前端不需要代码调整,主要靠服务端进行配置,
- cors需要浏览器和服务器同时支持,目前所有浏览器都支持该功能,IE版本不能低于10
- 浏览器一旦发现AJAX请求跨源就会自动添加一些附加的头信息,有时候还会多出一次附加的请求,但用户不会有感知
- 后端使用
CrossOrigin注解,配置:
origins
:允许的源列表、methods
:允许的HTTP方法、maxAge
:预检请求缓存时间(秒)、allowedHeaders
:允许的请求头,这里只支持单独的接口
- 全局cors配置
- 基于webMvcConfigure和Filter配置
- CorsFilter 过滤器
- nginx反向代理
-
Spring Security配置CORS
-
使用Gateway统一处理(微服务架构)
方案 | 适用场景 | 优点 | 缺点 |
---|---|---|---|
@CrossOrigin | 简单项目,少量端点需要跨域 | 简单直观 | 需要每个Controller单独配置 |
全局WebMvc配置 | 统一管理的中型项目 | 一处配置,全局生效 | 无法针对不同路径精细控制 |
Filter配置 | 需要精细控制过滤顺序的项目 | 灵活,可与其他Filter配合 | 配置相对复杂 |
Spring Security | 已使用Spring Security的项目 | 与安全配置统一管理 | 需要了解Security相关知识 |
Gateway统一处理 | 微服务架构,API网关统一入口 | 前端无感知,后端统一处理 | 需要引入Spring Cloud Gateway |
mysql相关
mysql相关面试可以查看本博主其他博客:
面试题之数据库相关-mysql篇-CSDN博客
面试题之数据库-mysql高阶及业务场景设计-CSDN博客
组合面试题
1.如何有效的阻止订单重复提交和支付
理论上只会在用户在下单的这个动作可能因为网络抖动、RPC重试等进行多次下单的操作,其他步骤确认订单只是修改订单状态,跳转支付和确认支付这些不会出现多次支付的问题。所以该题主要是针对用户多次调用下单接口怎么处理即下单接口的幂等性问题。
- 订单重复提交问题
- 前端防重复提交方案
- 按钮置灰等操作
- PRG模式:post/redirect/get模式,用户点击表单时重定向跳转到其他页面。
- token机制,在用户进入订单界面前生成固定的token,前端限制一个token调用时,后端拦截token的一次性,做请求拦截限制
- 请求拦截:通过axios拦截器拦截信息
- 后端接口设计
- 幂等性设计
- 每次请求先配合客户端生成一个唯一id,可由订单id+用户id+确认标识做绑定,同一接口,每次调用的id一致则不生成新的订单,注意标识符的失效时间
- 请求参数中带有时间戳与当前时间对比,若时间太长则默认为重复请求
- 请求状态检查,根据日志查询、用户订单关联查询是否有重复数据
- 数据库唯一约束
- 先获取数据库唯一ID来处理
- redis原子操作
- setnx操作,对同一个订单id+用户id+确认标识做绑定,设置失效时间,进行处理
- 幂等性设计
- 前端防重复提交方案
- 支付方重复方案
- 订单状态机制
- 第三方支付幂等
- 支付结果异步核对
- 分布式系统解决方案
- 分布式锁
- 消息队列去重
- 异常处理机制
- 补偿事务处理
- 人工审核接口
- 监控与报警
- 重复请求监控 设置ip白名单防止恶意操作
- 建议:
- 多层次防御:前端+后端+数据库约束等
- 核心原则:所有写操作必须实现幂等性接口
- 关键数据:订单号、用户ID、时间戳组合防重复
- 状态管理:严格的状态机控制流程
- 补偿机制:自动核对+人工干预双重保险
- 技术选择
场景 | 推荐方案 | 优点 |
---|---|---|
简单单体系统 | 本地锁+数据库唯一约束 | 实现简单 |
分布式高并发 | Redis分布式锁+消息队列 | 扩展性好 |
金融级支付系统 | 状态机+定时核对+人工干预 | 可靠性最高 |
旧系统改造 | 前端Token+后端幂等接口 | 侵入性最小 |
2.RestTemplate 如何优化连接池
- restTemplate默认是没有连接池,他的调用原理是每次都会创建一个HTTP连接,默认使用simpleClientHttpRequestFactory去创建连接,底层通过HttpURLConnection创建连接。在高并发的条件下,会无上限的创建连接,消耗系统资源。所以需要通过连接池来限制最大连接数,当请求的域不是很多且不随机的情况下,还可以复用同一个域的HTTP连接;
- HTTP请求流程:在我们发起一个http请求连接的时候,会对域名解析,连接之前的三次握手,如果是HTTPS还需要传递安全证书,以及请求完成之后的4次挥手。但是我们真正请求和响应只在其中的一小环节,所以我们通过一个连接池就可以对同一个域来建立一个长链接,就无需执行每次的无关业务请求的动作,这样就实现了连接的复用。
- 通过resttemplate来配置连接池的话有HttpClient和OkHttp.
- 具体实现:
- 代码上引入httpclient连接池的包,修改RestTemplate请求工厂,将默认工厂的simpleClientHttpRequestFactory换成HttpClientFactory,在为这个工厂配置请求bean。最后去设置连接池参数信息。设置最大连接数,根据QPS的响应时间平均值来设置,设置某个域的长链接复用,但是如果该值太小,比如设置2,那么只会创建两个长链接,这样后续的接口会进行阻塞等待。
- 在实现连接池的方法时,需要注意以下几点:
- 路由区分:对重要API设置独立的路由连接数
- 异常处理:配置重试机制
- DNS刷新:避免DNS缓冲问题
- 连接存活时间
参数 | 建议值 | 说明 |
---|---|---|
setMaxTotal | 100-500 | 最大连接数,根据服务器配置调整 |
setDefaultMaxPerRoute | 50-100 | 每个路由(host:port)的最大连接数 |
setConnectTimeout | 3000-5000ms | 建立TCP连接的超时时间 |
setSocketTimeout | 5000-10000ms | 数据传输超时时间 |
setConnectionRequestTimeout | 1000-2000ms | 从连接池获取连接的超时时间 |
evictIdleConnections | 30-60s | 空闲连接回收时间 |
3.如何设计秒杀系统?高并发?
查看此博客:面试题之如何设计一个秒杀系统?-CSDN博客
工具类应用
1.git如何撤回已提交的push代码
使用idea集成可视化界面操作
- 已提交 未推送
- 使用idea,找到提交的版本,选择undo commit
- 已提交 已推送
- revert commit->本地代码回滚到提交前的版本->在push一下 将远程仓库代码覆盖上
- 已回滚 代码恢复
- 选择刚才已经回滚的代码->cherry pick还原已经写好的代码