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又改回A,T2在中间时段读取到A并通过CAS修改,可能错误地认为数据没变,但实际上已被改动过。这在金融、库存等敏感场景会引发严重问题。
兜底方案:
- 使用带版本号的原子类:如Java的AtomicStampedReference(同时维护引用和int戳记),或AtomicMarkableReference(维护引用和boolean标记),能区分A→B→A的变化。
- 全局递增版本号:每次更新不仅改数据,还把版本号严格递增(时间戳不够可靠,若系统时钟回拨仍可能ABA),版本号始终变大,回退版本号不可能。
- 数据库实现建议:表加version字段,更新时where version = oldVersion,且更新语句set version = version+1。由于数据库的隔离级别(如可重复读)和行锁机制,同一个事务内不会误判,ABA问题天然被版本号递增解决。
- 业务重试 + 异步补偿队列
同步重试策略
· 适用读多写少的乐观锁冲突场景(如库存扣减)。
· 建议指数退避:第1次等待10ms,第2次20ms,第3次40ms,最多3~5次。
· 前端配合:返回特定状态码,让用户手动刷新或自动重试。
异步补偿队列
当同步重试全部失败,或者业务不允许阻塞重试(如高并发秒杀),可以将请求写入消息队列(如RocketMQ、Kafka)。
· 消费者取到消息后,再次执行乐观锁更新,可重试多次。
· 最终仍失败则转入死信队列,人工介入或执行预先定义的补偿动作(比如记录失败库存流水,由定时任务对账修复)。
· 补偿动作要幂等:如扣减库存前检查"是否已处理过该订单"。
兜底效果
将瞬时冲突转化为最终一致,避免直接拒绝用户请求。
- 审计日志 + 数据修复机制
适用场景
金融、计费等绝对不能出错的数据。即使乐观锁重试全部失败,也不能丢失操作意图。
实现方式
· 单独建一张 change_log 表,记录每次数据变更前的旧值、新值、版本号、操作时间、操作人等。
· 更新数据时先写日志,后更新(可以在同一本地事务中,或基于binlog)。
· 若更新失败(包括乐观锁冲突),日志里有一条"尝试记录",标记为FAILED。
修复流程
- 定时任务扫描失败的变更记录,尝试根据当前版本号重新执行更新。
- 若仍失败且业务允许,则降级为"人工修复":展示新旧数据差异,提供界面让管理员确认后强制更新(可绕过乐观锁,但要二次审计)。
- 利用日志可回滚任意时刻状态,保证可追溯。
3.事件状态流转新增,删除,替换状态应该怎么做
方案一:外部化配置 + 状态机引擎
核心思想
将状态机的拓扑结构(状态、事件、转移规则)从代码中抽离,存储到外部配置中心(如 Nacos、Apollo)或关系数据库。应用启动时加载配置构建状态机实例,运行时监听配置变更,动态重建状态机并替换旧实例,从而实现状态的增、删、改而不重启服务。
技术选型建议
· Spring StateMachine:与 Spring 生态融合好,支持动态构建 StateMachine 对象。
· Squirrel StateMachine:轻量、无侵入,同样支持运行时定义。
· 自研简单引擎(若业务不复杂):基于 Map<State+Event, State> 存储规则,配合配置刷新。
详细实现步骤
- 定义配置数据模型
将状态、事件、转移规则以 JSON 或数据库表形式存储。
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 动态构建。
java
@Component
public class DynamicStateMachineFactory {
private final Map<String, StateMachine<String, String>> machineCache = new ConcurrentHashMap<>();
private volatile StateMachineConfig currentConfig;
@PostConstruct
public void init() {
loadConfigFromNacos(); // 初次加载
buildStateMachine();
}
// 监听配置变更(Nacos Listener)
public void onConfigChange(String newJson) {
StateMachineConfig newConfig = parseConfig(newJson);
// 版本比对,若变化则重建
if (!newConfig.equals(currentConfig)) {
synchronized (this) {
StateMachine<String, String> newMachine = buildMachine(newConfig);
machineCache.put("default", newMachine); // 替换
currentConfig = newConfig;
// 可选:优雅关闭旧机器,释放资源
}
log.info("StateMachine rebuilt due to config change");
}
}
private StateMachine<String, String> buildMachine(StateMachineConfig config) {
StateMachineBuilder.Builder<String, String> builder = StateMachineBuilder.builder();
// 添加状态
for (String state : config.getStates()) {
builder.configureStates().withStates().initial(config.getInitialState())
.states(new HashSet<>(config.getStates()));
}
// 添加转移
for (Transition t : config.getTransitions()) {
builder.configureTransitions()
.withExternal()
.source(t.getFrom())
.target(t.getTo())
.event(t.getEvent());
}
return builder.build();
}
}
- 动态新增状态(操作示例)
· 在配置中心(Nacos)修改 JSON,增加一个新状态 "CANCELLED",并增加从 DRAFT 到 CANCELLED 的 CANCEL 事件转移。
· 发布配置 → 应用监听器触发 onConfigChange → 重建状态机 → 新流转规则生效。
· 对于数据库存储,提供后台管理接口:POST /admin/state/add,插入新规则后调用 refresh() 重建。
- 删除状态与替换的注意事项
· 删除状态:必须先确保所有转移规则中不包含该状态作为 from 或 to,否则重建时会校验失败。建议在修改配置前做合法性检查:如要删除 APPROVED,先确认没有 from=APPROVED 的转移,若有则先删除或修改这些转移。
· 替换状态:例如将 APPROVED 重命名为 PASSED,本质是修改 states 列表中的名称,并将所有转移中的旧状态名替换为新名。需同步更新数据库中的业务数据(历史订单的状态字段值),属于数据迁移问题,一般通过脚本离线处理或提供灰度迁移工具。
- 运行时一致性保证
· 新老事务隔离:使用 ThreadLocal 或按业务主键(如订单号)绑定状态机版本号。简单做法:在动态重建时,旧的状态机实例不立刻销毁,而是通过引用计数等待正在使用它的请求完成(类似读写锁)。Spring StateMachine 本身默认是无状态的引擎(状态存储在上下文中),替换实例不影响已运行流程。
· 持久化状态:如果状态机状态需要持久化(如长流程),建议将状态机的当前状态和上下文都存到业务表。即使状态机定义变了,已有流程仍按旧规则完成,新的流程用新规则。配置变更时可以增加版本字段 rule_version,每个流程实例在创建时记录当时的规则版本。
- 审计与回滚
· 每次配置变更记录操作人、变更前后 JSON、时间,存到 state_change_log。
· 配置中心支持版本管理(如 Nacos 自带),可一键回滚到上一个版本,应用监听回滚后再次重建。
优缺点分析
优点 缺点
不重启即可调整流转逻辑,灵活度高 引入外部组件(配置中心/数据库),复杂度增加
适合多环境、多租户动态配置 需要处理并发构建和旧机器释放
可以复用成熟框架的功能(如监听器、动作、守卫) 状态定义与代码解耦后,部分编译期检查丢失
方案二:自研动态状态矩阵
核心思想
不使用外部状态机框架,而是将状态转移规则存储在内存中的一个 Map 结构里,并通过后台接口动态修改这个 Map。业务代码中直接调用状态转移服务,查询 Map 获取下一个状态,并执行校验与动作。
数据结构设计
java
// 复合Key:当前状态 + 事件
public class TransitionKey {
private String state; // 当前状态
private String event; // 触发事件
// equals & hashCode
}
// 转移规则值
public class TransitionValue {
private String targetState; // 目标状态
private List<Action> beforeActions; // 前置动作
private List<Action> afterActions; // 后置动作
private Condition guardCondition; // 守卫条件(可选)
}
核心存储:
java
@Service
public class DynamicStateMatrix {
// 主规则表:当前状态+事件 -> 下一状态及动作
private volatile Map<TransitionKey, TransitionValue> matrix = new ConcurrentHashMap<>();
// 辅助索引:记录每个状态作为源的所有规则,便于删除状态时快速清理
private volatile Map<String, Set<TransitionKey>> sourceIndex = new ConcurrentHashMap<>();
// 对外提供状态转移方法
public StateContext transit(String currentState, String event, Map<String, Object> payload) {
TransitionKey key = new TransitionKey(currentState, event);
TransitionValue value = matrix.get(key);
if (value == null) {
throw new NoTransitionException();
}
// 执行守卫
if (value.getGuardCondition() != null && !value.getGuardCondition().test(payload)) {
throw new GuardFailedException();
}
// 执行前置动作
for (Action action : value.getBeforeActions()) {
action.execute(payload);
}
// 返回新状态
String newState = value.getTargetState();
// 执行后置动作
for (Action action : value.getAfterActions()) {
action.execute(payload);
}
return new StateContext(currentState, event, newState, payload);
}
}
动态增删改接口实现
- 新增状态
java
@PostMapping("/admin/transition/add")
public void addTransition(@RequestBody AddTransitionRequest req) {
TransitionKey key = new TransitionKey(req.getFromState(), req.getEvent());
TransitionValue value = new TransitionValue(req.getToState(), req.getActions());
synchronized (this) {
matrix.put(key, value);
sourceIndex.computeIfAbsent(req.getFromState(), k -> ConcurrentHashMap.newKeySet()).add(key);
}
// 可选:持久化到数据库
saveToDb(req);
}
· 注意:新增状态不需要提前声明状态列表,动态矩阵允许任意字符串作为状态名。
- 删除状态
java
@PostMapping("/admin/state/delete")
public void deleteState(@RequestParam String state) {
synchronized (this) {
// 1. 查找所有以该状态作为源状态的规则
Set<TransitionKey> fromRules = sourceIndex.getOrDefault(state, Collections.emptySet());
if (!fromRules.isEmpty()) {
throw new IllegalStateException("State " + state + " is source of transitions: " + fromRules);
}
// 2. 查找所有以该状态作为目标状态的规则(需要反向索引)
Set<TransitionKey> toRules = targetIndex.getOrDefault(state, Collections.emptySet());
if (!toRules.isEmpty()) {
// 可以选择阻断删除,或者自动删除这些规则
throw new IllegalStateException("State " + state + " is target of transitions: " + toRules);
}
// 3. 删除规则(实际上没有直接删除状态的概念,删除所有相关的规则即可)
// 但通常还需标记该状态为不可用,并清理数据
}
}
· 补充反向索引 Map<String, 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 仅在非生产环境激活。
示例代码:
java
@Component
@Profile("!prod") // 当 'prod' 未激活时,此Bean才会被创建
public class YourPreProdBean {
// 你的业务逻辑
}
如果你的预发布环境 Profile 名为 pre,也可以写成 @Profile("pre"),让 Bean 仅在 pre 激活时加载。
🔧 方案二:使用 @ConditionalOnProperty
除了 @Profile,@ConditionalOnProperty 也很常用。它能根据配置项的值,实现更精细的控制,适合将业务逻辑与通用的 Profile 名称解耦。
示例代码:
java
@Component
@ConditionalOnProperty(
name = "your.bean.enabled", // 配置项名称
havingValue = "pre", // 期望值是 'pre' 时才加载
matchIfMissing = false // 配置项缺失时不加载
)
public class YourPreProdBean {
// 你的业务逻辑
}
使用时,在预发布环境的配置文件中设置 your.bean.enabled=pre,生产环境不配置或设置其他值即可。
💡 方案三:其他备选方案
· 编程式的 @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 是列表专用工具,不适用单个配置的存储。