3、状态存储模型:内存、数据库与分布式缓存
状态存储,说白了就是你的状态数据到底放在哪。这个问题看似简单,但选错了存储模型,后面性能调优会非常痛苦。我见过太多项目,一开始图省事全放内存,结果一重启数据全丢;或者一上来就上 Redis,结果发现大部分状态根本不需要那么高的性能。
我个人习惯把状态存储分成三个层次:内存、数据库、分布式缓存。每个层次都有自己的适用场景和坑。咱们一个一个聊。
3.1 内存状态存储
内存存储,就是直接把状态放在进程的堆内存里。Java 里用 HashMap、ConcurrentHashMap,Go 里用 sync.Map,C++ 里用 unordered_map。优点是快,延迟通常在纳秒级。缺点也明显——进程重启就没了。
适用场景:单机会话状态、临时计算结果、配置缓存、不需要持久化的中间状态。
我在项目中遇到过一个问题:一个网关服务用 ConcurrentHashMap 存储用户会话,平时跑得好好的。结果有一次线上发布,滚动重启时大量用户掉线。为什么?因为重启后内存里的会话全丢了,用户必须重新登录。
解决方案其实很简单:内存只存热数据,冷数据定期刷到外部存储。或者用带持久化的内存数据库,比如 Redis 的 AOF/RDB。
// 一个简单的内存状态存储示例(Java)
public class InMemoryStateStore<K, V> {
private final ConcurrentHashMap<K, V> store = new ConcurrentHashMap<>();
public void put(K key, V value) {
store.put(key, value);
}
public V get(K key) {
return store.get(key);
}
// 注意:这个方法在分布式环境下有问题
public void remove(K key) {
store.remove(key);
}
}
避坑指南:我曾经在内存状态存储里直接存大对象,结果 GC 频繁触发,服务响应时间从 2ms 飙升到 200ms。后来改用对象池 + 软引用才解决。记住:内存不是无限的,大对象要谨慎。
3.2 数据库状态存储
数据库存储,就是把状态持久化到关系型数据库(MySQL、PostgreSQL)或 NoSQL 数据库(MongoDB、Cassandra)。优点是数据不丢,支持复杂查询。缺点是慢——一次磁盘 I/O 通常要几毫秒到几十毫秒。
你想想看,如果你的状态访问频率很高,每次都查数据库,那性能肯定扛不住。我见过一个项目,用户每次请求都查 MySQL 获取权限状态,结果 QPS 刚过 1000 数据库 CPU 就 100% 了。
| 特性 | 内存存储 | 数据库存储 | 分布式缓存 |
|---|---|---|---|
| 延迟 | 纳秒级 | 毫秒级 | 微秒级 |
| 持久化 | 无 | 强 | 可选(RDB/AOF) |
| 容量 | 受限于内存 | 大(TB级) | 受限于内存 |
| 一致性 | 单机强一致 | ACID | 最终一致 |
| 典型场景 | 临时状态 | 持久化状态 | 高频访问状态 |
数据库存储的关键是索引设计。状态查询通常按 key 查,所以主键或唯一索引要设计好。另外,连接池也很重要——我见过有人用默认的 HikariCP 配置,结果连接数不够,请求排队等连接,延迟直接爆炸。
-- 一个典型的状态表设计(MySQL)
CREATE TABLE user_state (
user_id VARCHAR(64) PRIMARY KEY,
state_data JSON NOT NULL,
version INT NOT NULL DEFAULT 1,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);
-- 查询时用主键,避免全表扫描
SELECT state_data FROM user_state WHERE user_id = ?;
小技巧:如果状态数据频繁更新,可以考虑用 UPDATE ... WHERE version = ? 做乐观锁,避免丢失更新。我在一个订单状态服务里用这个方案,并发冲突率从 15% 降到了 0.5%。
3.3 分布式缓存状态存储(Redis / Memcached)
分布式缓存,是内存存储和数据库存储的中间方案。它把数据放在内存里,但分布在多台机器上。Redis 和 Memcached 是主流选择。
我个人更倾向 Redis,原因有三:
- 数据结构丰富:String、Hash、List、Set、Sorted Set,能直接表达复杂状态
- 持久化可选:RDB 快照 + AOF 日志,重启不丢数据
- 高可用:主从 + Sentinel + Cluster,挂了自动切换
Memcached 呢?它更简单,纯内存,不支持持久化。但它的多线程模型在纯缓存场景下性能更好。我记得有一次压测,Memcached 的 QPS 比 Redis 单实例高了 30%。不过现在 Redis 6/7 也支持多线程 I/O 了,差距在缩小。
核心原则:分布式缓存不是数据库。不要把所有的状态都塞进去,只缓存高频访问且可以容忍短暂不一致的状态。
3.3.1 Redis 状态存储模式
Redis 里存状态,我常用的几种模式:
- String + JSON:把整个状态对象序列化成 JSON 字符串存进去。简单,但更新部分字段要全量读写。
- Hash:用 Hash 存状态对象的每个字段。可以单独更新某个字段,节省带宽。
- Sorted Set:用于带过期时间的状态,比如会话超时管理。
// Redis Hash 存储用户状态(Java + Jedis)
public class RedisStateStore {
private final JedisPool jedisPool;
public void updateField(String userId, String field, String value) {
try (Jedis jedis = jedisPool.getResource()) {
// 只更新一个字段,避免全量读写
jedis.hset("user:state:" + userId, field, value);
}
}
public Map<String, String> getAllFields(String userId) {
try (Jedis jedis = jedisPool.getResource()) {
return jedis.hgetAll("user:state:" + userId);
}
}
}
避坑指南:我曾经在 Redis 里存了上亿个 key,结果内存爆了。后来发现很多 key 设置了 TTL 但没生效——因为 TTL 只对顶级 key 有效,Hash 里的 field 不会自动过期。解决方案是用 EXPIRE 给整个 key 设过期时间,或者用 Redis 7 的 FIELD TTL 功能。
3.3.2 缓存策略:旁路缓存 vs 穿透缓存
状态访问模式里,缓存策略直接影响性能。我常用的两种:
| 策略 | 读流程 | 写流程 | 一致性 |
|---|---|---|---|
| 旁路缓存 | 先查缓存,miss 则查 DB 并回填缓存 | 先写 DB,再删缓存 | 最终一致 |
| 穿透缓存 | 只查缓存,miss 则返回空 | 只写缓存,异步写 DB | 弱一致 |
旁路缓存适合读多写少的状态。穿透缓存适合写频繁且可以丢数据的状态,比如用户点击计数。
嗯,这里要注意:旁路缓存有个经典问题——缓存击穿。当某个热点 key 过期,大量请求同时打到 DB,DB 可能扛不住。我习惯用 互斥锁 或 永不过期 + 异步刷新 来解决。
// 旁路缓存 + 互斥锁防止缓存击穿(伪代码)
public State getState(String key) {
State state = cache.get(key);
if (state != null) return state;
// 加锁,只让一个线程去查 DB
synchronized (key.intern()) {
state = cache.get(key); // 双重检查
if (state != null) return state;
state = db.query(key);
cache.set(key, state, 300); // 5分钟过期
return state;
}
}
3.4 三种存储模型的协同
实际项目中,很少只用一种存储模型。我常用的架构是三级缓存:
- L1:本地内存(Caffeine/Guava Cache)—— 纳秒级,存最热的数据
- L2:分布式缓存(Redis)—— 微秒级,存次热的数据
- L3:数据库(MySQL)—— 毫秒级,存全量数据
这样设计的好处是:大部分请求在 L1 就返回了,只有少量请求穿透到 L2 和 L3。我做过一个压测,三级缓存比只用数据库的 QPS 提升了 20 倍。
小技巧:L1 缓存要设置合理的过期时间,太短命中率低,太长数据不一致。我一般设 1-5 秒,配合 Redis 的发布订阅做主动失效。
3.5 选型建议
最后,给几个实用的选型建议:
- 状态需要持久化吗? 需要 → 数据库或 Redis(开启 AOF);不需要 → 内存或 Memcached
- 状态访问频率高吗? 高 → 加缓存层(本地内存 + Redis);低 → 直接查数据库
- 状态可以容忍不一致吗? 可以 → 用最终一致的缓存策略;不可以 → 用数据库强一致
- 状态数据量大吗? 大 → 数据库分片 + Redis 只缓存热点;小 → 全放 Redis 或内存
我的经验:大多数项目,用 Redis + MySQL 的组合就能覆盖 90% 的场景。Redis 扛读,MySQL 扛写和持久化。别一上来就搞三级缓存,复杂度会翻倍。先跑起来,再根据监控数据优化。