Files
JChargePointProtocol/docs/缓存架构.md
2025-10-28 14:39:06 +08:00

378 lines
16 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 缓存架构
<cite>
**本文档引用的文件**
- [CachedVersionedEntityRepository.java](file://jcpp-app/src/main/java/sanbing/jcpp/app/dal/repository/impl/CachedVersionedEntityRepository.java)
- [VersionedCache.java](file://jcpp-infrastructure-cache/src/main/java/sanbing/jcpp/infrastructure/cache/VersionedCache.java)
- [VersionedCaffeineCache.java](file://jcpp-infrastructure-cache/src/main/java/sanbing/jcpp/infrastructure/cache/VersionedCaffeineCache.java)
- [VersionedRedisCache.java](file://jcpp-infrastructure-cache/src/main/java/sanbing/jcpp/infrastructure/cache/VersionedRedisCache.java)
- [JCPPCaffeineCacheConfiguration.java](file://jcpp-infrastructure-cache/src/main/java/sanbing/jcpp/infrastructure/cache/JCPPCaffeineCacheConfiguration.java)
- [JCPPRedisCacheConfiguration.java](file://jcpp-infrastructure-cache/src/main/java/sanbing/jcpp/infrastructure/cache/JCPPRedisCacheConfiguration.java)
- [PileCacheKey.java](file://jcpp-app/src/main/java/sanbing/jcpp/app/service/cache/pile/PileCacheKey.java)
- [PileCacheEvictEvent.java](file://jcpp-app/src/main/java/sanbing/jcpp/app/service/cache/pile/PileCacheEvictEvent.java)
- [PileRepositoryImpl.java](file://jcpp-app/src/main/java/sanbing/jcpp/app/dal/repository/impl/PileRepositoryImpl.java)
- [Pile.java](file://jcpp-app/src/main/java/sanbing/jcpp/app/dal/entity/Pile.java)
</cite>
## 目录
1. [引言](#引言)
2. [多级缓存架构设计](#多级缓存架构设计)
3. [缓存一致性机制](#缓存一致性机制)
4. [缓存读写流程](#缓存读写流程)
5. [缓存键设计](#缓存键设计)
6. [缓存失效策略](#缓存失效策略)
7. [性能监控与优化](#性能监控与优化)
8. [结论](#结论)
## 引言
JChargePointProtocol系统采用多级缓存架构来优化充电桩相关数据的访问性能。该架构结合了Caffeine本地缓存和Redis分布式缓存的优势在保证高性能的同时确保数据一致性。本文档深入分析这一缓存架构的设计原理、实现机制和最佳实践。
## 多级缓存架构设计
```mermaid
graph TD
Client[客户端请求] --> ReadCache[读取缓存]
ReadCache --> CheckCaffeine{检查Caffeine<br>本地缓存}
CheckCaffeine --> |命中| ReturnFromCaffeine[返回Caffeine数据]
CheckCaffeine --> |未命中| CheckRedis{检查Redis<br>分布式缓存}
CheckRedis --> |命中| ReturnFromRedis[返回Redis数据]
CheckRedis --> |未命中| QueryDB[查询数据库]
QueryDB --> UpdateRedis[更新Redis缓存]
UpdateRedis --> UpdateCaffeine[更新Caffeine缓存]
UpdateCaffeine --> ReturnFromDB[返回数据库数据]
WriteOperation[写入操作] --> UpdateDB[更新数据库]
UpdateDB --> DeleteRedis[删除Redis缓存]
DeleteRedis --> DeleteCaffeine[删除Caffeine缓存]
DeleteCaffeine --> Complete[操作完成]
style ReadCache fill:#f9f,stroke:#333
style WriteOperation fill:#f9f,stroke:#333
```
**图示来源**
- [PileRepositoryImpl.java](file://jcpp-app/src/main/java/sanbing/jcpp/app/dal/repository/impl/PileRepositoryImpl.java#L35-L45)
- [VersionedCaffeineCache.java](file://jcpp-infrastructure-cache/src/main/java/sanbing/jcpp/infrastructure/cache/VersionedCaffeineCache.java#L30-L50)
- [VersionedRedisCache.java](file://jcpp-infrastructure-cache/src/main/java/sanbing/jcpp/infrastructure/cache/VersionedRedisCache.java#L80-L100)
**本节来源**
- [JCPPCaffeineCacheConfiguration.java](file://jcpp-infrastructure-cache/src/main/java/sanbing/jcpp/infrastructure/cache/JCPPCaffeineCacheConfiguration.java)
- [JCPPRedisCacheConfiguration.java](file://jcpp-infrastructure-cache/src/main/java/sanbing/jcpp/infrastructure/cache/JCPPRedisCacheConfiguration.java)
### Caffeine与Redis组合的优势
JChargePointProtocol采用Caffeine本地缓存和Redis分布式缓存的组合设计主要基于以下考虑
1. **性能分层**Caffeine作为第一级缓存提供微秒级的访问速度减少对远程Redis的访问压力。
2. **高可用性**即使Redis服务暂时不可用本地缓存仍能提供一定程度的服务能力。
3. **网络开销优化**:避免频繁的网络往返,特别适合高并发场景下的充电桩状态查询。
4. **资源利用**充分利用应用服务器的内存资源同时通过Redis实现多实例间的数据共享。
Caffeine缓存通过`JCPPCaffeineCacheConfiguration`配置支持基于权重的最大容量限制和写后过期策略。Redis缓存通过
`JCPPRedisCacheConfiguration`配置,提供了连接池管理、序列化配置和分布式环境下的缓存管理。
## 缓存一致性机制
```mermaid
classDiagram
class VersionedCache {
+get(K key)
+put(K key, V value)
+evict(K key)
+evict(K key, Integer version)
+getVersion(V value)
}
class VersionedCaffeineCache {
-doGet(K key)
-doPut(K key, V value, Integer version)
-wrapValue(V value, Integer version)
}
class VersionedRedisCache {
-SET_VERSIONED_VALUE_LUA_SCRIPT
-doPut(rawKey, value, version, expiration, connection)
-init()
}
class HasVersion {
+getVersion()
+setVersion(Integer version)
}
class Pile {
-UUID id
-String pileCode
-Integer version
}
class PileCacheKey {
-UUID pileId
-String pileCode
-isVersioned()
}
VersionedCache <|-- VersionedCaffeineCache
VersionedCache <|-- VersionedRedisCache
HasVersion <|-- Pile
VersionedCacheKey <|-- PileCacheKey
VersionedCaffeineCache --> JCPPPair : "包装版本和值"
VersionedRedisCache --> LUA : "使用Lua脚本"
Pile --> PileCacheKey : "通过ID或编码创建键"
```
**图示来源**
- [VersionedCache.java](file://jcpp-infrastructure-cache/src/main/java/sanbing/jcpp/infrastructure/cache/VersionedCache.java)
- [VersionedCaffeineCache.java](file://jcpp-infrastructure-cache/src/main/java/sanbing/jcpp/infrastructure/cache/VersionedCaffeineCache.java)
- [VersionedRedisCache.java](file://jcpp-infrastructure-cache/src/main/java/sanbing/jcpp/infrastructure/cache/VersionedRedisCache.java)
- [Pile.java](file://jcpp-app/src/main/java/sanbing/jcpp/app/dal/entity/Pile.java#L60)
- [PileCacheKey.java](file://jcpp-app/src/main/java/sanbing/jcpp/app/service/cache/pile/PileCacheKey.java)
**本节来源**
- [CachedVersionedEntityRepository.java](file://jcpp-app/src/main/java/sanbing/jcpp/app/dal/repository/impl/CachedVersionedEntityRepository.java)
- [VersionedCache.java](file://jcpp-infrastructure-cache/src/main/java/sanbing/jcpp/infrastructure/cache/VersionedCache.java)
### CachedVersionedEntityRepository实现机制
`CachedVersionedEntityRepository`是缓存一致性机制的核心实现,其工作原理如下:
1. **版本号机制**:实体类(如`Pile`)实现`HasVersion`接口,包含`version`字段,每次更新时版本号递增。
2. **条件更新**:在向缓存写入数据时,只有当新数据的版本号大于缓存中现有数据的版本号时,才会执行更新操作。
3. **原子性保证**Redis层面使用Lua脚本确保"检查版本-更新值"操作的原子性,避免并发更新导致的数据不一致。
`VersionedRedisCache`通过Lua脚本实现了版本控制逻辑
- 获取当前缓存值的版本号前8字节
- 比较新版本号与当前版本号
- 仅当新版本号更大时才更新缓存
这种设计有效避免了脏读问题,确保了缓存与数据库之间的一致性。
## 缓存读写流程
```mermaid
sequenceDiagram
participant Client as 客户端
participant Repo as PileRepositoryImpl
participant Caffeine as Caffeine缓存
participant Redis as Redis缓存
participant DB as 数据库
Client->>Repo : findPileByCode(pileCode)
Repo->>Caffeine : get(PileCacheKey)
Caffeine-->>Repo : 缓存命中/未命中
alt Caffeine命中
Repo-->>Client : 返回数据
else Caffeine未命中
Repo->>Redis : get(PileCacheKey)
Redis-->>Repo : 缓存命中/未命中
alt Redis命中
Repo->>Caffeine : put(数据)
Repo-->>Client : 返回数据
else Redis未命中
Repo->>DB : selectByCode(pileCode)
DB-->>Repo : 返回实体
Repo->>Redis : put(实体, version)
Repo->>Caffeine : put(实体)
Repo-->>Client : 返回实体
end
end
Note over Client,Repo : 读取流程Caffeine → Redis → DB
Client->>Repo : updatePile(pile)
Repo->>DB : update(pile)
DB-->>Repo : 更新成功
Repo->>Repo : 发布PileCacheEvictEvent
Repo->>Caffeine : evict(相关缓存键)
Repo->>Redis : evict(相关缓存键)
Repo-->>Client : 操作完成
Note over Client,Repo : 写入流程更新DB → 删除两级缓存
```
**图示来源**
- [PileRepositoryImpl.java](file://jcpp-app/src/main/java/sanbing/jcpp/app/dal/repository/impl/PileRepositoryImpl.java#L35-L45)
- [VersionedCaffeineCache.java](file://jcpp-infrastructure-cache/src/main/java/sanbing/jcpp/infrastructure/cache/VersionedCaffeineCache.java#L30-L50)
- [VersionedRedisCache.java](file://jcpp-infrastructure-cache/src/main/java/sanbing/jcpp/infrastructure/cache/VersionedRedisCache.java#L80-L100)
**本节来源**
- [PileRepositoryImpl.java](file://jcpp-app/src/main/java/sanbing/jcpp/app/dal/repository/impl/PileRepositoryImpl.java)
- [AbstractCachedEntityRepository.java](file://jcpp-app/src/main/java/sanbing/jcpp/app/dal/repository/impl/AbstractCachedEntityRepository.java)
### 读取流程
缓存的读取遵循"先本地,后远程"的原则:
1. 首先尝试从Caffeine本地缓存中获取数据
2. 如果本地缓存未命中则查询Redis分布式缓存
3. 如果Redis缓存也未命中则访问数据库获取数据
4. 将数据库查询结果依次写入Redis和Caffeine缓存供后续请求使用
这种分层查询策略最大限度地利用了本地缓存的高性能优势,同时保证了数据的最终一致性。
### 写入流程
写入操作采用"先数据库,后缓存"的策略:
1. 首先更新数据库中的数据
2. 发布缓存失效事件(如`PileCacheEvictEvent`
3. 在事件处理中删除两级缓存中的相关数据
这种"写后失效"Write-Through with Cache-Aside模式确保了数据的一致性避免了缓存与数据库之间的数据偏差。
## 缓存键设计
```mermaid
classDiagram
class PileCacheKey {
-UUID pileId
-String pileCode
+PileCacheKey(UUID pileId)
+PileCacheKey(String pileCode)
+toString()
+isVersioned()
}
class VersionedCacheKey {
+isVersioned()
}
PileCacheKey --> VersionedCacheKey : 实现
note right of PileCacheKey
缓存键设计原则:
1. 支持多种查询方式
2. 版本控制标识
3. 序列化兼容性
end
```
**图示来源**
- [PileCacheKey.java](file://jcpp-app/src/main/java/sanbing/jcpp/app/service/cache/pile/PileCacheKey.java)
**本节来源**
- [PileCacheKey.java](file://jcpp-app/src/main/java/sanbing/jcpp/app/service/cache/pile/PileCacheKey.java)
### PileCacheKey设计原则
`PileCacheKey`的设计体现了以下核心原则:
1. **多维度查询支持**同时支持通过充电桩ID`pileId`)和桩编码(`pileCode`)两种方式创建缓存键,满足不同场景的查询需求。
2. **版本控制标识**:通过`isVersioned()`方法区分是否需要版本控制,当通过`pileId`查询时不需要版本控制,而通过`pileCode`
查询时需要版本控制。
3. **简洁的toString实现**`toString()`方法优先返回`pileId`,不存在时返回`pileCode`,确保缓存键的可读性和一致性。
4. **序列化兼容**:使用`@Serial`注解和`serialVersionUID`确保序列化兼容性,避免版本升级导致的问题。
这种设计既满足了功能需求,又考虑了性能和可维护性。
## 缓存失效策略
```mermaid
flowchart TD
Start([缓存失效触发]) --> ActiveEviction["主动失效策略"]
Start --> PassiveEviction["被动失效策略"]
ActiveEviction --> EventDriven["事件驱动失效"]
EventDriven --> PileEvictEvent["PileCacheEvictEvent"]
PileEvictEvent --> HandleEvict["handleEvictEvent处理"]
HandleEvict --> EvictCaffeine["删除Caffeine缓存"]
HandleEvict --> EvictRedis["删除Redis缓存"]
PassiveEviction --> TTL["TTL过期"]
TTL --> CaffeineTTL["Caffeine TTL配置"]
CaffeineTTL --> expireAfterWrite["expireAfterWrite"]
TTL --> RedisTTL["Redis TTL配置"]
RedisTTL --> cacheTtl["cacheTtl参数"]
EvictCaffeine --> End([缓存失效完成])
EvictRedis --> End
expireAfterWrite --> End
cacheTtl --> End
style ActiveEviction fill:#f96,stroke:#333
style PassiveEviction fill:#69f,stroke:#333
```
**图示来源**
- [PileCacheEvictEvent.java](file://jcpp-app/src/main/java/sanbing/jcpp/app/service/cache/pile/PileCacheEvictEvent.java)
- [PileRepositoryImpl.java](file://jcpp-app/src/main/java/sanbing/jcpp/app/dal/repository/impl/PileRepositoryImpl.java#L40-L48)
- [JCPPCaffeineCacheConfiguration.java](file://jcpp-infrastructure-cache/src/main/java/sanbing/jcpp/infrastructure/cache/JCPPCaffeineCacheConfiguration.java#L60-L70)
- [VersionedRedisCache.java](file://jcpp-infrastructure-cache/src/main/java/sanbing/jcpp/infrastructure/cache/VersionedRedisCache.java#L50-L60)
**本节来源**
- [PileCacheEvictEvent.java](file://jcpp-app/src/main/java/sanbing/jcpp/app/service/cache/pile/PileCacheEvictEvent.java)
- [PileRepositoryImpl.java](file://jcpp-app/src/main/java/sanbing/jcpp/app/dal/repository/impl/PileRepositoryImpl.java#L40-L48)
### 主动失效
主动失效通过事件驱动机制实现:
- 当充电桩数据被修改或删除时,发布`PileCacheEvictEvent`事件
- `PileRepositoryImpl`中的`@TransactionalEventListener`监听并处理该事件
- 在事件处理方法`handleEvictEvent`中,删除与该充电桩相关的所有缓存键
这种设计确保了数据变更后缓存能够及时失效,避免了脏数据的传播。
### 被动失效
被动失效依赖于TTLTime To Live机制
- Caffeine缓存通过`expireAfterWrite`配置写后过期时间
- Redis缓存通过`cacheTtl`参数配置过期时间
- 当缓存项超过设定的生存时间后自动失效
被动失效作为主动失效的补充,提供了额外的安全保障,防止因事件机制故障导致的缓存数据长期不一致。
## 性能监控与优化
```mermaid
graph LR
Metrics[性能监控指标] --> HitRate["缓存命中率"]
Metrics --> Latency["访问延迟"]
Metrics --> Memory["内存使用"]
Metrics --> QPS["查询吞吐量"]
HitRate --> CaffeineHit["Caffeine命中率"]
HitRate --> RedisHit["Redis命中率"]
Latency --> LocalLatency["本地缓存延迟"]
Latency --> RemoteLatency["远程缓存延迟"]
Memory --> CaffeineMem["Caffeine内存"]
Memory --> RedisMem["Redis内存"]
QPS --> ReadQPS["读取QPS"]
QPS --> WriteQPS["写入QPS"]
Optimization[优化建议] --> SizeTuning["容量调优"]
Optimization --> TTLTuning["TTL调优"]
Optimization --> PatternAnalysis["访问模式分析"]
Optimization --> Monitoring["监控告警"]
SizeTuning --> MaxSize["设置合理的maxSize"]
TTLTuning --> OptimalTTL["根据业务需求设置TTL"]
PatternAnalysis --> Hotspot["识别热点数据"]
Monitoring --> Alert["设置命中率告警"]
style HitRate fill:#ff6,stroke:#333
style Optimization fill:#6f6,stroke:#333
```
**图示来源**
- [JCPPCaffeineCacheConfiguration.java](file://jcpp-infrastructure-cache/src/main/java/sanbing/jcpp/infrastructure/cache/JCPPCaffeineCacheConfiguration.java#L60-L70)
- [CacheSpecs.java](file://jcpp-infrastructure-cache/src/main/java/sanbing/jcpp/infrastructure/cache/CacheSpecs.java)
**本节来源**
- [JCPPCaffeineCacheConfiguration.java](file://jcpp-infrastructure-cache/src/main/java/sanbing/jcpp/infrastructure/cache/JCPPCaffeineCacheConfiguration.java)
- [CacheSpecsMap.java](file://jcpp-infrastructure-cache/src/main/java/sanbing/jcpp/infrastructure/cache/CacheSpecsMap.java)
### 性能监控指标
建议监控以下关键性能指标:
1. **缓存命中率**衡量缓存有效性的核心指标应分别监控Caffeine和Redis的命中率。
2. **访问延迟**:记录从各级缓存获取数据的响应时间,识别性能瓶颈。
3. **内存使用**监控Caffeine和Redis的内存占用情况防止内存溢出。
4. **查询吞吐量**:统计单位时间内的缓存读写操作数量。
### 优化建议
1. **容量调优**根据实际内存资源和访问模式合理设置Caffeine的`maxSize`和Redis的内存限制。
2. **TTL调优**根据业务数据的变更频率设置合适的TTL值平衡数据新鲜度和缓存效率。
3. **访问模式分析**:通过监控识别热点数据,考虑对热点数据进行特殊处理或预加载。
4. **监控告警**:设置缓存命中率的告警阈值,当命中率异常下降时及时排查问题。
## 结论
JChargePointProtocol的多级缓存架构通过Caffeine和Redis的有机结合在性能和数据一致性之间取得了良好平衡。
`CachedVersionedEntityRepository`
通过版本号机制有效解决了缓存一致性问题,避免了脏读现象。缓存键的精心设计支持了多种查询场景,而主动与被动相结合的失效策略确保了数据的及时更新。通过合理的性能监控和持续优化,该缓存架构能够有效支撑系统的高并发访问需求。