高阶题 · 数据库优化 / 性能优化 / 架构设计
3 题 · 平均 3.75/5 · 高阶题
| 场景 | 时序 | 不一致原因 |
|------|------|---------|
| **并发读写** | T1: A 读缓存 miss → T2: B 更新 DB + 删缓存 → T3: A 回源 DB(读到旧值)→ T4: A 写缓存(旧值) | A 的回源和 B 的删除并发,A 用旧值覆盖了缓存 |
| **主从延迟** | T1: B 更新主库 + 删缓存 → T2: A 读缓存 miss → T3: A 回源从库(主从延迟,读到旧值)→ T4: A 写缓存(旧值) | 从库数据延迟,回源读到旧数据 |
| **删除缓存失败** | T1: B 更新 DB 成功 → T2: B 删缓存失败 → T3: A 读缓存(旧值) | 缓存删除失败,A 读到旧缓存 |
| 根因 | 说明 | 发生概率 |
|------|------|---------|
| **删除缓存失败** | 网络问题、Redis 宕机 | 低(< 0.1%) |
| **并发读写覆盖** | 回源读到旧值,覆盖新缓存 | 中(0.1-1%) |
| **主从延迟** | 回源从库读到旧数据 | 中(取决于延迟) |
| **TTL 过期 + 并发** | 缓存过期时正好有更新 | 低 |
---
| 方案 | 一致性 | 复杂度 | 性能 | 适用场景 |
|------|--------|--------|------|---------|
| **Cache Aside(先更新 DB,再删缓存)** | 最终一致 | 低 | 高 | **通用场景** |
| **双删(更新 DB → 删缓存 → 延迟再删)** | 最终一致 | 中 | 中 | 高并发、强一致要求 |
| **Canal/binlog 监听** | 最终一致 | 高 | 高 | 大规模、解耦 |
| **读写锁 + 强一致** | 强一致 | 高 | 低 | 金融、支付 |
**推荐:双删 + Canal 兜底**
```java
@Service
public class ProductService {
@Autowired
private ProductDao productDao;
@Autowired
private StringRedisTemplate redisTemplate;
@Autowired
private Scheduler scheduler;
@Transactional
public void updatePrice(Long productId, BigDecimal newPrice) {
String key = "product:" + productId;
// 1. 第一次删除缓存(提前删除,减少不一致窗口)
redisTemplate.delete(key);
// 2. 更新 DB
productDao.updatePrice(productId, newPrice);
// 3. 延迟第二次删除(解决并发读写问题)
// 延迟时间 > 读写事务耗时(通常 1-5 秒)
scheduler.schedule(() -> {
redisTemplate.delete(key);
log.info("Second delete cache: {}", key);
}, 3, TimeUnit.SECONDS);
}
}
```
**双删时序**:
```
T1: 请求 B → 更新价格(¥99 → ¥89)
T2: B → 第一次删缓存
T3: B → 更新 DB
T4: 请求 A → 查缓存 → miss
T5: A → 回源 DB → 读到 ¥89(B 已更新)
T6: A → 写缓存(¥89)
T7: B → 延迟 3 秒 → 第二次删缓存
T8: 用户 C → 查缓存 → miss → 回源 DB → ¥89 → 正确!
```
```java
@RocketMQMessageListener(topic = "product_change", consumerGroup = "cache_group")
public class ProductCacheConsumer implements RocketMQListener<Message> {
@Override
public void onMessage(Message message) {
BinlogEvent event = JSON.parseObject(message.getBody(), BinlogEvent.class);
if (event.getTable().equals("product")) {
String key = "product:" + event.getProductId();
redisTemplate.delete(key);
log.info("Delete cache by binlog: {}", key);
}
}
}
```
**Canal 架构**:
```
MySQL 主库 → binlog → Canal Server → RocketMQ → 缓存消费者 → Redis 删缓存
↓
订单消费者 → 业务处理
```
**优点**:
- 业务代码无侵入(不需要在更新代码里写删缓存逻辑)
- 解耦(DB 变更和缓存删除分开)
- 可靠(binlog 不会丢,MQ 可重试)
**缺点**:
- 延迟(binlog → MQ → 消费者,通常 1-3 秒)
- 部署复杂(需要 Canal + MQ)
---
**问题**:请求查一个不存在的数据(如 productId=999999),缓存永远 miss,每次都打 DB。
**方案 A:空值缓存**
```java
public Product getProduct(Long productId) {
String key = "product:" + productId;
Product product = redisTemplate.opsForValue().get(key);
if (product != null) {
return product;
}
// 缓存 miss,查 DB
product = productDao.selectById(productId);
if (product == null) {
// 数据不存在,缓存空值(短 TTL)
redisTemplate.opsForValue().set(key, new Product(), 60, TimeUnit.SECONDS);
return null;
}
// 数据存在,正常缓存
long ttl = 1800 + ThreadLocalRandom.current().nextLong(600);
redisTemplate.opsForValue().set(key, product, ttl, TimeUnit.SECONDS);
return product;
}
```
**方案 B:布隆过滤器**
```java
public class ProductBloomFilter {
// 预估 100 万商品,误判率 0.01%
private final BloomFilter<CharSequence> bloomFilter = BloomFilter.create(
new Funnel<CharSequence>() {
@Override
public int hashCode(CharSequence input) {
return input.toString().hashCode();
}
@Override
public void writeInto(HashCodeOutputStream out, CharSequence value) {
out.writeBytes(value.toString().getBytes(StandardCharsets.UTF_8));
}
},
1_000_000,
0.0001
);
public void addProduct(Long productId) {
bloomFilter.put(String.valueOf(productId));
}
public boolean mightContain(Long productId) {
return bloomFilter.mightContain(String.valueOf(productId));
}
}
// 查询时先过布隆过滤器
public Product getProduct(Long productId) {
// 1. 布隆过滤器判断(不存在直接返回)
if (!bloomFilter.mightContain(productId)) {
return null;
}
// 2. 查缓存
String key = "product:" + productId;
Product product = redisTemplate.opsForValue().get(key);
if (product != null) return product;
// 3. 查 DB
product = productDao.selectById(productId);
if (product != null) {
bloomFilter.addProduct(productId);
redisTemplate.opsForValue().set(key, product, 1800, TimeUnit.SECONDS);
}
return product;
}
```
**方案对比**:
| 方案 | 优点 | 缺点 | 适用场景 |
|------|------|------|---------|
| **空值缓存** | 简单、无额外依赖 | 浪费内存(缓存空值) | 数据量小、穿透少 |
| **布隆过滤器** | 不占缓存内存、拦截率高 | 有误判率、需要维护 | 数据量大、穿透多 |
**问题**:1 万个商品缓存同时过期,瞬间打垮 DB。
**方案:随机 TTL + 热点 key 永不过期**
```java
public void setCache(String key, Object value) {
// 1. 基础 TTL + 随机偏移(防同时过期)
long baseTtl = 1800;
long randomTtl = ThreadLocalRandom.current().nextLong(600);
long ttl = baseTtl + randomTtl;
redisTemplate.opsForValue().set(key, value, ttl, TimeUnit.SECONDS);
}
// 2. 热点 key 永不过期(本地缓存兜底)
@Scheduled(fixedDelay = 3600000)
public void refreshHotKeys() {
Set<String> hotKeys = getHotKeys();
for (String key : hotKeys) {
redisTemplate.expire(key, 2, TimeUnit.HOURS);
}
}
```
**问题**:某个商品被 10 万 QPS 访问,Redis 单 key 压力过大。
**方案:多级缓存(本地 + Redis)**
```java
@Service
public class ProductService {
// L1: 本地缓存(5 秒过期)
private final Cache<Long, Product> localCache = Caffeine.newBuilder()
.maximumSize(10_000)
.expireAfterWrite(Duration.ofSeconds(5))
.build();
// L2: Redis
@Autowired
private StringRedisTemplate redisTemplate;
public Product getProduct(Long productId) {
// 1. L1 本地缓存
Product product = localCache.getIfPresent(productId);
if (product != null) {
return product;
}
// 2. L2 Redis
String key = "product:" + productId;
product = redisTemplate.opsForValue().get(key);
if (product != null) {
localCache.put(productId, product);
return product;
}
// 3. L3 DB
product = productDao.selectById(productId);
if (product != null) {
redisTemplate.opsForValue().set(key, product, 1800, TimeUnit.SECONDS);
localCache.put(productId, product);
}
return product;
}
}
```
**多级缓存架构**:
```
L1: 本地缓存(Caffeine)→ 1ms,命中率 60%
L2: Redis → 5ms,命中率 30%
L3: DB → 50ms,命中率 10%
加权平均延迟 = 0.6×1 + 0.3×5 + 0.1×50 = 8.6ms
```
---
| 场景 | 时序 | 不一致原因 |
|------|------|---------|
| **并发读写** | T1: A 读缓存 miss → T2: B 更新 DB + 删缓存 → T3: A 回源 DB(读到旧值)→ T4: A 写缓存(旧值) | A 的回源和 B 的删除并发,A 用旧值覆盖了缓存 |
| **主从延迟** | T1: B 更新主库 + 删缓存 → T2: A 读缓存 miss → T3: A 回源从库(主从延迟,读到旧值)→ T4: A 写缓存(旧值) | 从库数据延迟,回源读到旧数据 |
| **删除缓存失败** | T1: B 更新 DB 成功 → T2: B 删缓存失败 → T3: A 读缓存(旧值) | 缓存删除失败,A 读到旧缓存 |
| 根因 | 说明 | 发生概率 |
|------|------|---------|
| **删除缓存失败** | 网络问题、Redis 宕机 | 低(< 0.1%) |
| **并发读写覆盖** | 回源读到旧值,覆盖新缓存 | 中(0.1-1%) |
| **主从延迟** | 回源从库读到旧数据 | 中(取决于延迟) |
| **TTL 过期 + 并发** | 缓存过期时正好有更新 | 低 |
---
| 方案 | 一致性 | 复杂度 | 性能 | 适用场景 |
|------|--------|--------|------|---------|
| **Cache Aside(先更新 DB,再删缓存)** | 最终一致 | 低 | 高 | **通用场景** |
| **双删(更新 DB → 删缓存 → 延迟再删)** | 最终一致 | 中 | 中 | 高并发、强一致要求 |
| **Canal/binlog 监听** | 最终一致 | 高 | 高 | 大规模、解耦 |
| **读写锁 + 强一致** | 强一致 | 高 | 低 | 金融、支付 |
**推荐:双删 + Canal 兜底**
```java
@Service
public class ProductService {
@Autowired
private ProductDao productDao;
@Autowired
private StringRedisTemplate redisTemplate;
@Autowired
private Scheduler scheduler;
@Transactional
public void updatePrice(Long productId, BigDecimal newPrice) {
String key = "product:" + productId;
// 1. 第一次删除缓存(提前删除,减少不一致窗口)
redisTemplate.delete(key);
// 2. 更新 DB
productDao.updatePrice(productId, newPrice);
// 3. 延迟第二次删除(解决并发读写问题)
// 延迟时间 > 读写事务耗时(通常 1-5 秒)
scheduler.schedule(() -> {
redisTemplate.delete(key);
log.info("Second delete cache: {}", key);
}, 3, TimeUnit.SECONDS);
}
}
```
**双删时序**:
```
T1: 请求 B → 更新价格(¥99 → ¥89)
T2: B → 第一次删缓存
T3: B → 更新 DB
T4: 请求 A → 查缓存 → miss
T5: A → 回源 DB → 读到 ¥89(B 已更新)
T6: A → 写缓存(¥89)
T7: B → 延迟 3 秒 → 第二次删缓存
T8: 用户 C → 查缓存 → miss → 回源 DB → ¥89 → 正确!
```
```java
@RocketMQMessageListener(topic = "product_change", consumerGroup = "cache_group")
public class ProductCacheConsumer implements RocketMQListener<Message> {
@Override
public void onMessage(Message message) {
BinlogEvent event = JSON.parseObject(message.getBody(), BinlogEvent.class);
if (event.getTable().equals("product")) {
String key = "product:" + event.getProductId();
redisTemplate.delete(key);
log.info("Delete cache by binlog: {}", key);
}
}
}
```
**Canal 架构**:
```
MySQL 主库 → binlog → Canal Server → RocketMQ → 缓存消费者 → Redis 删缓存
↓
订单消费者 → 业务处理
```
**优点**:
- 业务代码无侵入(不需要在更新代码里写删缓存逻辑)
- 解耦(DB 变更和缓存删除分开)
- 可靠(binlog 不会丢,MQ 可重试)
**缺点**:
- 延迟(binlog → MQ → 消费者,通常 1-3 秒)
- 部署复杂(需要 Canal + MQ)
---
**问题**:请求查一个不存在的数据(如 productId=999999),缓存永远 miss,每次都打 DB。
**方案 A:空值缓存**
```java
public Product getProduct(Long productId) {
String key = "product:" + productId;
Product product = redisTemplate.opsForValue().get(key);
if (product != null) {
return product;
}
// 缓存 miss,查 DB
product = productDao.selectById(productId);
if (product == null) {
// 数据不存在,缓存空值(短 TTL)
redisTemplate.opsForValue().set(key, new Product(), 60, TimeUnit.SECONDS);
return null;
}
// 数据存在,正常缓存
long ttl = 1800 + ThreadLocalRandom.current().nextLong(600);
redisTemplate.opsForValue().set(key, product, ttl, TimeUnit.SECONDS);
return product;
}
```
**方案 B:布隆过滤器**
```java
public class ProductBloomFilter {
// 预估 100 万商品,误判率 0.01%
private final BloomFilter<CharSequence> bloomFilter = BloomFilter.create(
new Funnel<CharSequence>() {
@Override
public int hashCode(CharSequence input) {
return input.toString().hashCode();
}
@Override
public void writeInto(HashCodeOutputStream out, CharSequence value) {
out.writeBytes(value.toString().getBytes(StandardCharsets.UTF_8));
}
},
1_000_000,
0.0001
);
public void addProduct(Long productId) {
bloomFilter.put(String.valueOf(productId));
}
public boolean mightContain(Long productId) {
return bloomFilter.mightContain(String.valueOf(productId));
}
}
// 查询时先过布隆过滤器
public Product getProduct(Long productId) {
// 1. 布隆过滤器判断(不存在直接返回)
if (!bloomFilter.mightContain(productId)) {
return null;
}
// 2. 查缓存
String key = "product:" + productId;
Product product = redisTemplate.opsForValue().get(key);
if (product != null) return product;
// 3. 查 DB
product = productDao.selectById(productId);
if (product != null) {
bloomFilter.addProduct(productId);
redisTemplate.opsForValue().set(key, product, 1800, TimeUnit.SECONDS);
}
return product;
}
```
**方案对比**:
| 方案 | 优点 | 缺点 | 适用场景 |
|------|------|------|---------|
| **空值缓存** | 简单、无额外依赖 | 浪费内存(缓存空值) | 数据量小、穿透少 |
| **布隆过滤器** | 不占缓存内存、拦截率高 | 有误判率、需要维护 | 数据量大、穿透多 |
**问题**:1 万个商品缓存同时过期,瞬间打垮 DB。
**方案:随机 TTL + 热点 key 永不过期**
```java
public void setCache(String key, Object value) {
// 1. 基础 TTL + 随机偏移(防同时过期)
long baseTtl = 1800;
long randomTtl = ThreadLocalRandom.current().nextLong(600);
long ttl = baseTtl + randomTtl;
redisTemplate.opsForValue().set(key, value, ttl, TimeUnit.SECONDS);
}
// 2. 热点 key 永不过期(本地缓存兜底)
@Scheduled(fixedDelay = 3600000)
public void refreshHotKeys() {
Set<String> hotKeys = getHotKeys();
for (String key : hotKeys) {
redisTemplate.expire(key, 2, TimeUnit.HOURS);
}
}
```
**问题**:某个商品被 10 万 QPS 访问,Redis 单 key 压力过大。
**方案:多级缓存(本地 + Redis)**
```java
@Service
public class ProductService {
// L1: 本地缓存(5 秒过期)
private final Cache<Long, Product> localCache = Caffeine.newBuilder()
.maximumSize(10_000)
.expireAfterWrite(Duration.ofSeconds(5))
.build();
// L2: Redis
@Autowired
private StringRedisTemplate redisTemplate;
public Product getProduct(Long productId) {
// 1. L1 本地缓存
Product product = localCache.getIfPresent(productId);
if (product != null) {
return product;
}
// 2. L2 Redis
String key = "product:" + productId;
product = redisTemplate.opsForValue().get(key);
if (product != null) {
localCache.put(productId, product);
return product;
}
// 3. L3 DB
product = productDao.selectById(productId);
if (product != null) {
redisTemplate.opsForValue().set(key, product, 1800, TimeUnit.SECONDS);
localCache.put(productId, product);
}
return product;
}
}
```
**多级缓存架构**:
```
L1: 本地缓存(Caffeine)→ 1ms,命中率 60%
L2: Redis → 5ms,命中率 30%
L3: DB → 50ms,命中率 10%
加权平均延迟 = 0.6×1 + 0.3×5 + 0.1×50 = 8.6ms
```
---
| 场景 | 时序 | 不一致原因 |
|------|------|---------|
| **并发读写** | T1: A 读缓存 miss → T2: B 更新 DB + 删缓存 → T3: A 回源 DB(读到旧值)→ T4: A 写缓存(旧值) | A 的回源和 B 的删除并发,A 用旧值覆盖了缓存 |
| **主从延迟** | T1: B 更新主库 + 删缓存 → T2: A 读缓存 miss → T3: A 回源从库(主从延迟,读到旧值)→ T4: A 写缓存(旧值) | 从库数据延迟,回源读到旧数据 |
| **删除缓存失败** | T1: B 更新 DB 成功 → T2: B 删缓存失败 → T3: A 读缓存(旧值) | 缓存删除失败,A 读到旧缓存 |
| 根因 | 说明 | 发生概率 |
|------|------|---------|
| **删除缓存失败** | 网络问题、Redis 宕机 | 低(< 0.1%) |
| **并发读写覆盖** | 回源读到旧值,覆盖新缓存 | 中(0.1-1%) |
| **主从延迟** | 回源从库读到旧数据 | 中(取决于延迟) |
| **TTL 过期 + 并发** | 缓存过期时正好有更新 | 低 |
---
| 方案 | 一致性 | 复杂度 | 性能 | 适用场景 |
|------|--------|--------|------|---------|
| **Cache Aside(先更新 DB,再删缓存)** | 最终一致 | 低 | 高 | **通用场景** |
| **双删(更新 DB → 删缓存 → 延迟再删)** | 最终一致 | 中 | 中 | 高并发、强一致要求 |
| **Canal/binlog 监听** | 最终一致 | 高 | 高 | 大规模、解耦 |
| **读写锁 + 强一致** | 强一致 | 高 | 低 | 金融、支付 |
**推荐:双删 + Canal 兜底**
```java
@Service
public class ProductService {
@Autowired
private ProductDao productDao;
@Autowired
private StringRedisTemplate redisTemplate;
@Autowired
private Scheduler scheduler;
@Transactional
public void updatePrice(Long productId, BigDecimal newPrice) {
String key = "product:" + productId;
// 1. 第一次删除缓存(提前删除,减少不一致窗口)
redisTemplate.delete(key);
// 2. 更新 DB
productDao.updatePrice(productId, newPrice);
// 3. 延迟第二次删除(解决并发读写问题)
// 延迟时间 > 读写事务耗时(通常 1-5 秒)
scheduler.schedule(() -> {
redisTemplate.delete(key);
log.info("Second delete cache: {}", key);
}, 3, TimeUnit.SECONDS);
}
}
```
**双删时序**:
```
T1: 请求 B → 更新价格(¥99 → ¥89)
T2: B → 第一次删缓存
T3: B → 更新 DB
T4: 请求 A → 查缓存 → miss
T5: A → 回源 DB → 读到 ¥89(B 已更新)
T6: A → 写缓存(¥89)
T7: B → 延迟 3 秒 → 第二次删缓存
T8: 用户 C → 查缓存 → miss → 回源 DB → ¥89 → 正确!
```
```java
@RocketMQMessageListener(topic = "product_change", consumerGroup = "cache_group")
public class ProductCacheConsumer implements RocketMQListener<Message> {
@Override
public void onMessage(Message message) {
BinlogEvent event = JSON.parseObject(message.getBody(), BinlogEvent.class);
if (event.getTable().equals("product")) {
String key = "product:" + event.getProductId();
redisTemplate.delete(key);
log.info("Delete cache by binlog: {}", key);
}
}
}
```
**Canal 架构**:
```
MySQL 主库 → binlog → Canal Server → RocketMQ → 缓存消费者 → Redis 删缓存
↓
订单消费者 → 业务处理
```
**优点**:
- 业务代码无侵入(不需要在更新代码里写删缓存逻辑)
- 解耦(DB 变更和缓存删除分开)
- 可靠(binlog 不会丢,MQ 可重试)
**缺点**:
- 延迟(binlog → MQ → 消费者,通常 1-3 秒)
- 部署复杂(需要 Canal + MQ)
---
**问题**:请求查一个不存在的数据(如 productId=999999),缓存永远 miss,每次都打 DB。
**方案 A:空值缓存**
```java
public Product getProduct(Long productId) {
String key = "product:" + productId;
Product product = redisTemplate.opsForValue().get(key);
if (product != null) {
return product;
}
// 缓存 miss,查 DB
product = productDao.selectById(productId);
if (product == null) {
// 数据不存在,缓存空值(短 TTL)
redisTemplate.opsForValue().set(key, new Product(), 60, TimeUnit.SECONDS);
return null;
}
// 数据存在,正常缓存
long ttl = 1800 + ThreadLocalRandom.current().nextLong(600);
redisTemplate.opsForValue().set(key, product, ttl, TimeUnit.SECONDS);
return product;
}
```
**方案 B:布隆过滤器**
```java
public class ProductBloomFilter {
// 预估 100 万商品,误判率 0.01%
private final BloomFilter<CharSequence> bloomFilter = BloomFilter.create(
new Funnel<CharSequence>() {
@Override
public int hashCode(CharSequence input) {
return input.toString().hashCode();
}
@Override
public void writeInto(HashCodeOutputStream out, CharSequence value) {
out.writeBytes(value.toString().getBytes(StandardCharsets.UTF_8));
}
},
1_000_000,
0.0001
);
public void addProduct(Long productId) {
bloomFilter.put(String.valueOf(productId));
}
public boolean mightContain(Long productId) {
return bloomFilter.mightContain(String.valueOf(productId));
}
}
// 查询时先过布隆过滤器
public Product getProduct(Long productId) {
// 1. 布隆过滤器判断(不存在直接返回)
if (!bloomFilter.mightContain(productId)) {
return null;
}
// 2. 查缓存
String key = "product:" + productId;
Product product = redisTemplate.opsForValue().get(key);
if (product != null) return product;
// 3. 查 DB
product = productDao.selectById(productId);
if (product != null) {
bloomFilter.addProduct(productId);
redisTemplate.opsForValue().set(key, product, 1800, TimeUnit.SECONDS);
}
return product;
}
```
**方案对比**:
| 方案 | 优点 | 缺点 | 适用场景 |
|------|------|------|---------|
| **空值缓存** | 简单、无额外依赖 | 浪费内存(缓存空值) | 数据量小、穿透少 |
| **布隆过滤器** | 不占缓存内存、拦截率高 | 有误判率、需要维护 | 数据量大、穿透多 |
**问题**:1 万个商品缓存同时过期,瞬间打垮 DB。
**方案:随机 TTL + 热点 key 永不过期**
```java
public void setCache(String key, Object value) {
// 1. 基础 TTL + 随机偏移(防同时过期)
long baseTtl = 1800;
long randomTtl = ThreadLocalRandom.current().nextLong(600);
long ttl = baseTtl + randomTtl;
redisTemplate.opsForValue().set(key, value, ttl, TimeUnit.SECONDS);
}
// 2. 热点 key 永不过期(本地缓存兜底)
@Scheduled(fixedDelay = 3600000)
public void refreshHotKeys() {
Set<String> hotKeys = getHotKeys();
for (String key : hotKeys) {
redisTemplate.expire(key, 2, TimeUnit.HOURS);
}
}
```
**问题**:某个商品被 10 万 QPS 访问,Redis 单 key 压力过大。
**方案:多级缓存(本地 + Redis)**
```java
@Service
public class ProductService {
// L1: 本地缓存(5 秒过期)
private final Cache<Long, Product> localCache = Caffeine.newBuilder()
.maximumSize(10_000)
.expireAfterWrite(Duration.ofSeconds(5))
.build();
// L2: Redis
@Autowired
private StringRedisTemplate redisTemplate;
public Product getProduct(Long productId) {
// 1. L1 本地缓存
Product product = localCache.getIfPresent(productId);
if (product != null) {
return product;
}
// 2. L2 Redis
String key = "product:" + productId;
product = redisTemplate.opsForValue().get(key);
if (product != null) {
localCache.put(productId, product);
return product;
}
// 3. L3 DB
product = productDao.selectById(productId);
if (product != null) {
redisTemplate.opsForValue().set(key, product, 1800, TimeUnit.SECONDS);
localCache.put(productId, product);
}
return product;
}
}
```
**多级缓存架构**:
```
L1: 本地缓存(Caffeine)→ 1ms,命中率 60%
L2: Redis → 5ms,命中率 30%
L3: DB → 50ms,命中率 10%
加权平均延迟 = 0.6×1 + 0.3×5 + 0.1×50 = 8.6ms
```
---