面经(1)

面经(1) 1.hashmap的get与put流程put 流程· 计算 hash(key)高位异或低位减少碰撞· 若数组为空则 resize() 初始化· 索引定位(n-1) hash· 若槽位为空则直接插入否则检查首节点· key 相同则覆盖· 不同则按链表或红黑树插入· 链表长度 ≥8 且数组长度 ≥64 时转为红黑树· 插入后 modCount检查 size threshold 则扩容get 流程· 计算 hash(key)定位槽位· 若槽位非空则依次查找先检查首节点hash 和 key 匹配否则按红黑树getTreeNode或链表遍历查找· 找到返回 value否则返回 null扩容机制JDK 1.8· 触发条件· 首次 put 初始化默认容量 16· size threshold容量 × 负载因子 0.75· 链表长度 ≥8 但数组长度 64 时优先扩容避免树化· 过程新容量 旧容量 × 2新阈值也翻倍· 迁移优化· 利用 (e.hash oldCap) 判断新索引0 则位置不变否则 原索引 oldCap· 链表迁移保持原顺序尾插法避免死循环· 红黑树迁移后若某分支节点数 ≤6 则退化为链表· 线程不安全多线程下可能出现数据覆盖建议用 ConcurrentHashMap红黑树解决哈希冲突严重时的性能退化· 树化条件链表长度 ≥8 且 数组长度 ≥64否则优先扩容· 退化条件红黑树节点数 ≤6 时转回链表· 性质自平衡二叉查找树保证最坏情况 O(log n)· 优点插入、删除、查询均衡比 AVL 树调整开销小适合 HashMap 的频繁操作· 节点结构TreeNode 继承 LinkedHashMap.Entry包含 parent、left、right、prev 和颜色标志2.乐观锁以及aba问题以及如何兜底乐观锁通常用版本号或时间戳实现更新数据时校验当前版本号是否与读到的版本一致一致则更新并将版本号1否则重试。ABA问题是T1将A改为B又改回AT2在中间时段读取到A并通过CAS修改可能错误地认为数据没变但实际上已被改动过。这在金融、库存等敏感场景会引发严重问题。兜底方案使用带版本号的原子类如Java的AtomicStampedReference同时维护引用和int戳记或AtomicMarkableReference维护引用和boolean标记能区分A→B→A的变化。全局递增版本号每次更新不仅改数据还把版本号严格递增时间戳不够可靠若系统时钟回拨仍可能ABA版本号始终变大回退版本号不可能。数据库实现建议表加version字段更新时where version oldVersion且更新语句set version version1。由于数据库的隔离级别如可重复读和行锁机制同一个事务内不会误判ABA问题天然被版本号递增解决。业务重试 异步补偿队列同步重试策略· 适用读多写少的乐观锁冲突场景如库存扣减。· 建议指数退避第1次等待10ms第2次20ms第3次40ms最多3~5次。· 前端配合返回特定状态码让用户手动刷新或自动重试。异步补偿队列当同步重试全部失败或者业务不允许阻塞重试如高并发秒杀可以将请求写入消息队列如RocketMQ、Kafka。· 消费者取到消息后再次执行乐观锁更新可重试多次。· 最终仍失败则转入死信队列人工介入或执行预先定义的补偿动作比如记录失败库存流水由定时任务对账修复。· 补偿动作要幂等如扣减库存前检查“是否已处理过该订单”。兜底效果将瞬时冲突转化为最终一致避免直接拒绝用户请求。审计日志 数据修复机制适用场景金融、计费等绝对不能出错的数据。即使乐观锁重试全部失败也不能丢失操作意图。实现方式· 单独建一张 change_log 表记录每次数据变更前的旧值、新值、版本号、操作时间、操作人等。· 更新数据时先写日志后更新可以在同一本地事务中或基于binlog。· 若更新失败包括乐观锁冲突日志里有一条“尝试记录”标记为FAILED。修复流程定时任务扫描失败的变更记录尝试根据当前版本号重新执行更新。若仍失败且业务允许则降级为“人工修复”展示新旧数据差异提供界面让管理员确认后强制更新可绕过乐观锁但要二次审计。利用日志可回滚任意时刻状态保证可追溯。3.事件状态流转新增删除替换状态应该怎么做方案一外部化配置 状态机引擎核心思想将状态机的拓扑结构状态、事件、转移规则从代码中抽离存储到外部配置中心如 Nacos、Apollo或关系数据库。应用启动时加载配置构建状态机实例运行时监听配置变更动态重建状态机并替换旧实例从而实现状态的增、删、改而不重启服务。技术选型建议· Spring StateMachine与 Spring 生态融合好支持动态构建 StateMachine 对象。· Squirrel StateMachine轻量、无侵入同样支持运行时定义。· 自研简单引擎若业务不复杂基于 MapStateEvent, State 存储规则配合配置刷新。详细实现步骤定义配置数据模型将状态、事件、转移规则以 JSON 或数据库表形式存储。// 示例config.json{states:[DRAFT,SUBMITTED,APPROVED,REJECTED],events:[SUBMIT,APPROVE,REJECT],transitions:[{from:DRAFT,event:SUBMIT,to:SUBMITTED},{from:SUBMITTED,event:APPROVE,to:APPROVED},{from:SUBMITTED,event:REJECT,to:REJECTED}]}数据库表设计动态配置· state_config 表id, config_version, states_json, events_json, transitions_json, enabled, create_time。· state_change_log 表审计operator, action_type(ADD/DEL/REPLACE), old_config, new_config, create_time。构建动态状态机工厂使用 Spring StateMachine 的 StateMachineBuilder 动态构建。ComponentpublicclassDynamicStateMachineFactory{privatefinalMapString,StateMachineString,StringmachineCachenewConcurrentHashMap();privatevolatileStateMachineConfigcurrentConfig;PostConstructpublicvoidinit(){loadConfigFromNacos();// 初次加载buildStateMachine();}// 监听配置变更Nacos ListenerpublicvoidonConfigChange(StringnewJson){StateMachineConfignewConfigparseConfig(newJson);// 版本比对若变化则重建if(!newConfig.equals(currentConfig)){synchronized(this){StateMachineString,StringnewMachinebuildMachine(newConfig);machineCache.put(default,newMachine);// 替换currentConfignewConfig;// 可选优雅关闭旧机器释放资源}log.info(StateMachine rebuilt due to config change);}}privateStateMachineString,StringbuildMachine(StateMachineConfigconfig){StateMachineBuilder.BuilderString,StringbuilderStateMachineBuilder.builder();// 添加状态for(Stringstate:config.getStates()){builder.configureStates().withStates().initial(config.getInitialState()).states(newHashSet(config.getStates()));}// 添加转移for(Transitiont:config.getTransitions()){builder.configureTransitions().withExternal().source(t.getFrom()).target(t.getTo()).event(t.getEvent());}returnbuilder.build();}}动态新增状态操作示例· 在配置中心Nacos修改 JSON增加一个新状态 “CANCELLED”并增加从 DRAFT 到 CANCELLED 的 CANCEL 事件转移。· 发布配置 → 应用监听器触发 onConfigChange → 重建状态机 → 新流转规则生效。· 对于数据库存储提供后台管理接口POST /admin/state/add插入新规则后调用 refresh() 重建。删除状态与替换的注意事项· 删除状态必须先确保所有转移规则中不包含该状态作为 from 或 to否则重建时会校验失败。建议在修改配置前做合法性检查如要删除 APPROVED先确认没有 fromAPPROVED 的转移若有则先删除或修改这些转移。· 替换状态例如将 APPROVED 重命名为 PASSED本质是修改 states 列表中的名称并将所有转移中的旧状态名替换为新名。需同步更新数据库中的业务数据历史订单的状态字段值属于数据迁移问题一般通过脚本离线处理或提供灰度迁移工具。运行时一致性保证· 新老事务隔离使用 ThreadLocal 或按业务主键如订单号绑定状态机版本号。简单做法在动态重建时旧的状态机实例不立刻销毁而是通过引用计数等待正在使用它的请求完成类似读写锁。Spring StateMachine 本身默认是无状态的引擎状态存储在上下文中替换实例不影响已运行流程。· 持久化状态如果状态机状态需要持久化如长流程建议将状态机的当前状态和上下文都存到业务表。即使状态机定义变了已有流程仍按旧规则完成新的流程用新规则。配置变更时可以增加版本字段 rule_version每个流程实例在创建时记录当时的规则版本。审计与回滚· 每次配置变更记录操作人、变更前后 JSON、时间存到 state_change_log。· 配置中心支持版本管理如 Nacos 自带可一键回滚到上一个版本应用监听回滚后再次重建。优缺点分析优点 缺点不重启即可调整流转逻辑灵活度高 引入外部组件配置中心/数据库复杂度增加适合多环境、多租户动态配置 需要处理并发构建和旧机器释放可以复用成熟框架的功能如监听器、动作、守卫 状态定义与代码解耦后部分编译期检查丢失方案二自研动态状态矩阵核心思想不使用外部状态机框架而是将状态转移规则存储在内存中的一个 Map 结构里并通过后台接口动态修改这个 Map。业务代码中直接调用状态转移服务查询 Map 获取下一个状态并执行校验与动作。数据结构设计// 复合Key当前状态 事件publicclassTransitionKey{privateStringstate;// 当前状态privateStringevent;// 触发事件// equals hashCode}// 转移规则值publicclassTransitionValue{privateStringtargetState;// 目标状态privateListActionbeforeActions;// 前置动作privateListActionafterActions;// 后置动作privateConditionguardCondition;// 守卫条件可选}核心存储ServicepublicclassDynamicStateMatrix{// 主规则表当前状态事件 - 下一状态及动作privatevolatileMapTransitionKey,TransitionValuematrixnewConcurrentHashMap();// 辅助索引记录每个状态作为源的所有规则便于删除状态时快速清理privatevolatileMapString,SetTransitionKeysourceIndexnewConcurrentHashMap();// 对外提供状态转移方法publicStateContexttransit(StringcurrentState,Stringevent,MapString,Objectpayload){TransitionKeykeynewTransitionKey(currentState,event);TransitionValuevaluematrix.get(key);if(valuenull){thrownewNoTransitionException();}// 执行守卫if(value.getGuardCondition()!null!value.getGuardCondition().test(payload)){thrownewGuardFailedException();}// 执行前置动作for(Actionaction:value.getBeforeActions()){action.execute(payload);}// 返回新状态StringnewStatevalue.getTargetState();// 执行后置动作for(Actionaction:value.getAfterActions()){action.execute(payload);}returnnewStateContext(currentState,event,newState,payload);}}动态增删改接口实现新增状态PostMapping(/admin/transition/add)publicvoidaddTransition(RequestBodyAddTransitionRequestreq){TransitionKeykeynewTransitionKey(req.getFromState(),req.getEvent());TransitionValuevaluenewTransitionValue(req.getToState(),req.getActions());synchronized(this){matrix.put(key,value);sourceIndex.computeIfAbsent(req.getFromState(),k-ConcurrentHashMap.newKeySet()).add(key);}// 可选持久化到数据库saveToDb(req);}· 注意新增状态不需要提前声明状态列表动态矩阵允许任意字符串作为状态名。删除状态PostMapping(/admin/state/delete)publicvoiddeleteState(RequestParamStringstate){synchronized(this){// 1. 查找所有以该状态作为源状态的规则SetTransitionKeyfromRulessourceIndex.getOrDefault(state,Collections.emptySet());if(!fromRules.isEmpty()){thrownewIllegalStateException(State state is source of transitions: fromRules);}// 2. 查找所有以该状态作为目标状态的规则需要反向索引SetTransitionKeytoRulestargetIndex.getOrDefault(state,Collections.emptySet());if(!toRules.isEmpty()){// 可以选择阻断删除或者自动删除这些规则thrownewIllegalStateException(State state is target of transitions: toRules);}// 3. 删除规则实际上没有直接删除状态的概念删除所有相关的规则即可// 但通常还需标记该状态为不可用并清理数据}}· 补充反向索引 MapString, Set targetIndex在添加规则时同步维护。替换状态重命名· 例如将状态 “PENDING” 替换为 “WAITING”。· 步骤扫描 matrix 中所有 TransitionKey 的 state 字段等于 “PENDING” 的修改为 “WAITING”。扫描所有 TransitionValue 的 targetState 等于 “PENDING” 的修改为 “WAITING”。重建索引。注意业务数据库中已存在的实体记录的 status 字段需要批量迁移通常提供后台任务逐步更新。运行时一致性及并发控制· 使用 volatile 引用 synchronized 进行写操作确保修改规则时整个矩阵替换是原子操作。也可以采用 CopyOnWrite 模式创建新 Map填充新规则最后一次性替换 matrix 引用。· 对于正在进行的流转请求由于引用替换是原子的旧请求可能仍然使用旧矩阵因为拿到的是替换前的引用但这恰好保证了单次请求内的规则一致不会出现半路规则变更导致状态错乱。若需要更严格的隔离可以引入版本号将版本存入事务上下文。持久化与审计· 每次通过接口修改规则时同步写入数据库 transition_rule 表带 version 或 is_active 标志并插入审计日志。· 应用重启后从数据库加载最新规则到内存矩阵。优缺点分析优点 缺点极轻量无第三方依赖实现简单透明 需要自己处理守卫条件、动作链、历史记录等功能动态修改实时生效性能极高纯内存Map 缺乏图形化监视和状态机可视化完全可控易于针对业务定制 对于复杂嵌套状态子状态、并行状态几乎无法支持总结与选型建议对比维度 方案一外部化配置引擎 方案二自研动态矩阵复杂度 中等需要集成框架配置中心 低只有集合操作功能丰富度 高支持守卫、动作、子状态、历史状态等 低仅支持基础转移动态能力 优秀配置变更触发重建 优秀直接修改内存Map适合场景 大型系统状态20流转复杂有专门运维团队 中小型系统状态15流转简单需快速迭代对业务数据影响 需处理版本兼容性 同样需要处理状态字段迁移最终建议· 若状态流转规则经常变化每周超过2次且团队规模较大 → 选方案一并采用 Nacos Spring StateMachine。· 若只是偶尔调整每月1次且系统简单 → 选方案二搭配简单后台管理界面即可。· 无论哪种方案都务必对删除状态做依赖检查并建立配置变更的审计日志否则运行时可能出现空转或数据残留。4.bean希望在预生产有生产没有有哪些方案 方案一使用 Profile 注解最常用可以在配置类或 Bean 定义上使用 Profile并根据 Spring 激活的环境配置来决定是否加载。核心是让 Bean 仅在非生产环境激活。示例代码ComponentProfile(!prod)// 当 prod 未激活时此Bean才会被创建publicclassYourPreProdBean{// 你的业务逻辑}如果你的预发布环境 Profile 名为 pre也可以写成 Profile(“pre”)让 Bean 仅在 pre 激活时加载。 方案二使用 ConditionalOnProperty除了 ProfileConditionalOnProperty 也很常用。它能根据配置项的值实现更精细的控制适合将业务逻辑与通用的 Profile 名称解耦。示例代码ComponentConditionalOnProperty(nameyour.bean.enabled,// 配置项名称havingValuepre,// 期望值是 pre 时才加载matchIfMissingfalse// 配置项缺失时不加载)publicclassYourPreProdBean{// 你的业务逻辑}使用时在预发布环境的配置文件中设置 your.bean.enabledpre生产环境不配置或设置其他值即可。 方案三其他备选方案· 编程式的 Conditional当条件非常复杂时可以自己实现 Condition 接口灵活度最高。· 构建时 Maven/Gradle Profile通过 Maven 的 在打包阶段就排除掉特定环境的代码。虽然能保证生产包绝对干净但环境隔离不够灵活。· 配置文件占位符这种方法主要用于切换依赖的 Bean而非控制 Bean 本身是否加载。5.有一个外部配置可能几个月才会变更一次存他的时候用什么数据结构以及读写的时候要注意什么针对“外部配置几个月才变更一次如何存储及读写注意事项”的完整结论最佳数据结构AtomicReference· 用不可变对象如 record、final 字段类承载所有配置字段· 用 AtomicReference 持有该对象的唯一实例读写操作要点· 读直接 get() 获得当前快照无锁、极快、线程安全· 写从远程拉取新配置 → 验证 → 构建完整新对象 → 原子替换set 或 compareAndSet· 绝对禁止逐个字段修改避免读线程看到部分更新· 可用轻量锁防止并发写低频操作无影响其他注意事项· 可见性AtomicReference 已保证无需额外 volatile· 初始兜底应用启动时必须内建默认配置或本地缓存文件防止远端故障· 变更通知替换配置后遍历回调列表通知业务模块如有需要· 定时拉取建议每小时或每天异步检查一次而非实时监听为什么不推荐其他结构方案 问题synchronized 普通变量 每次读都要加锁性能差ReadWriteLock 仍有锁开销不如原子引用轻量ConcurrentHashMap 适合键值集合难以保证整体配置的原子替换可能读到部分字段更新CopyOnWriteArrayList 语义错误列表装单个对象、内存/性能冗余每次写复制数组读需get(0)、完全不是为此场景设计一句话总结用 AtomicReference 持有不可变配置对象读无锁写时整体原子替换CopyOnWriteArrayList 是列表专用工具不适用单个配置的存储。