一篇看懂:缓存模型 → 实战代码 → 高阶玩法 → 暗黑黑科技 所有示例基于 Spring Boot 3 & Redis 7,复制即可跑。
一、概念速览:4 种缓存模式一张图
| 模式 | 谁来写缓存 | 一致性 | 适用场景 |
|---|---|---|---|
| Cache Aside | 应用自己 | 中等 | 读多写少,最常用 |
| Read Through | Redis 插件 | 较好 | 读多,缓存层透明 |
| Write Through | 同步写 DB+缓存 | 高 | 写多,强一致 |
| Write Behind | 先写缓存,异步刷盘 | 最高吞吐 | 计数器、秒杀 |
二、Spring Boot 3 快速落地(Cache Aside)
1.依赖 & 配置
XML
spring:
redis:
host: localhost
port: 6379
cache:
type: redis # 开启 Spring Cache
redis:
time-to-live: 3600s
key-prefix: demo
use-key-prefix: true
2.启动类加 @EnableCaching
3.业务代码(零侵入)
java
@Service
public class ProductService {
@Cacheable(value = "product", key = "#id")
public Product getProduct(Long id) {
return dao.selectById(id); // 缓存未命中才走 DB
}
@CacheEvict(value = "product", key = "#id")
public void updateProduct(Long id, Product p) {
dao.updateById(id, p);
}
}
效果:第一次查 DB,后续 1h 内直接走 Redis;更新时自动失效。
三、进阶:手写 RedisTemplate 模板(细粒度控制)
java
@Configuration
public class RedisConfig {
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory f) {
RedisTemplate<String, Object> t = new RedisTemplate<>();
t.setConnectionFactory(f);
// 序列化
GenericJackson2JsonRedisSerializer json = new GenericJackson2JsonRedisSerializer();
t.setKeySerializer(RedisSerializer.string());
t.setValueSerializer(json);
t.setHashKeySerializer(RedisSerializer.string());
t.setHashValueSerializer(json);
return t;
}
}
工具类封装:
java
@Component
public class RedisUtil {
@Resource
private RedisTemplate<String, Object> redisTemplate;
public void set(String k, Object v, long seconds) {
redisTemplate.opsForValue().set(k, v, Duration.ofSeconds(seconds));
}
public <T> T get(String k, Class<T> clazz) {
return (T) redisTemplate.opsForValue().get(k);
}
public boolean exists(String k) {
return Boolean.TRUE.equals(redisTemplate.hasKey(k));
}
}
使用:
java
String key = "product:" + id;
Product p = redisUtil.get(key, Product.class);
if (p == null) {
p = dao.selectById(id);
redisUtil.set(key, p, 3600);
}
四、高并发三板斧
- 分布式锁(防超卖)
java
public boolean deductStock(Long id, int num) {
String lockKey = "lock:stock:" + id;
String uuid = Thread.currentThread().getId() + ":" + System.nanoTime();
Boolean ok = redisTemplate.opsForValue() .setIfAbsent(lockKey, uuid, 10, TimeUnit.SECONDS);
if (!Boolean.TRUE.equals(ok)) return false;
try {
int stock = Integer.parseInt(redisTemplate.opsForValue().get("stock:" + id));
if (stock < num) return false;
redisTemplate.opsForValue().decrement("stock:" + id, num);
return true;
} finally {
// 用 Lua 保证"自己的锁自己解"
String script = "if redis.call('get',KEYS[1])==ARGV[1] then " + "return redis.call('del',KEYS[1]) else return 0 end";
redisTemplate.execute(new DefaultRedisScript<>(script, Long.class), Collections.singletonList(lockKey), uuid);
}
}
- 布隆过滤器(防穿透)
java
RBloomFilter<String> bloomFilter = redisson.getBloomFilter("user-filter");
bloomFilter.tryInit(1_000_000, 0.01); // 100w 容量,1% 误判
// 初始化合法 ID
userIds.forEach(id -> bloomFilter.add(id));
// 查询时先过过滤器
if (!bloomFilter.contains(requestUserId)) {
throw new RuntimeException("非法 ID");
}
- 雪崩 + 击穿双保险
-
随机 TTL:ttl = base + Random(0~600) 打散过期
-
热点永不过期 + 异步刷新: 设置物理过期 = 逻辑过期 + 30min,后台线程提前 10min 更新缓存 。
五、黑科技:把缓存玩成"中间件"
1. 双写一致性 → 基于 Canal 的异步最终一致
-
MySQL 开 binlog
-
Canal 监听变更 → MQ → 应用更新 Redis 结果:应用只写 DB,缓存零耦合,延迟 <1s。
2. 大 Key 实时扫描 + 自动拆分
java
// 利用 MEMORY USAGE 命令扫描
public List<BigKeyInfo> scanBigKeys() {
Cursor<byte[]> cursor = redisTemplate.executeWithStickyConnection( conn -> conn.scan(ScanOptions.scanOptions().count(1000).build()));
List<BigKeyInfo> big = new ArrayList<>();
while (cursor.hasNext()) {
String k = new String(cursor.next());
long mem = redisTemplate.execute( (RedisCallback<Long>) con -> con.memoryUsage(k.getBytes()));
if (mem > 1024 * 1024) big.add(new BigKeyInfo(k, mem));
} return big;
}
发现后 按日期/哈希拆表 或改用 redis.hset 分桶 。
3. Pipeline 批量打榜(万次读写 1s 完成)
java
List<Object> pipe = redisTemplate.executePipelined((RedisCallback<Object>) conn -> {
for (int i = 0; i < 10000; i++) {
conn.setEx(("key" + i).getBytes(), 300, ("val" + i).getBytes());
} return null;
});
单条 1ms → 批量 0.1μs,QPS 提升 100+ 倍 。
4. Redis + Lua 实现秒杀库存扣减(原子 + 0 超卖)
java
-- stock.lua
local stock = tonumber(redis.call('get', KEYS[1]))
if stock <= 0 then return -1 end
redis.call('decr', KEYS[1])
return stock
Java 侧仅一次 redisTemplate.execute(new DefaultRedisScript<>(script, Long.class), keys),网络 RTT 最小化。
5. 二级缓存(本地 Caffeine + Redis)
思路:L1 进程级 μs,L2 全局共享;L1 miss → L2 → DB,L2 变更发广播清 L1。 实现:Spring Cache 支持多提供者,开箱即用。
六、常见翻车现场 & 急救方案
| 问题 | 现象 | 急救药 |
|---|---|---|
| 缓存雪崩 | 瞬时 QPS 全打 DB | 随机 TTL + 热点永不过期 + 异步刷新 |
| 缓存穿透 | 非法 ID 持续请求 | 布隆过滤器 + 空值缓存 |
| 缓存击穿 | 热点 Key 失效瞬间 | 互斥锁(setIfAbsent)+ 逻辑过期 |
| 大 Key | 阻塞主从同步 | 扫描拆分,用 hash/分桶 |
| 双写不一致 | DB≠缓存 | 延迟双删 / Canal 异步最终一致 |
七、总结脑图(文字版)
XML
Java-Redis 缓存
├─ 4 种模式:Aside / Read|Write Through / Write Behind
├─ Spring 落地:@Cacheable + RedisTemplate
├─ 高并发三件套:分布式锁 + 布隆过滤器 + 雪崩/击穿防护
└─ 黑科技:Canal 异步一致 / 大 Key 自动拆 / Pipeline 批量 / Lua 0 超卖 / 二级缓存
收藏这篇,面试、调优、高并发都能打!