一、概述
1.缓存穿透(Cache Penetration)* 缓存穿透(Cache Penetration)是指客户端请求的数据在缓存中和数据库中都不存在,这样缓存永远不会生效,这些请求都会打到数据库,导致数据库压力骤增甚至崩溃。* 触发原因:恶意攻击、参数伪造、业务逻辑漏洞。* 核心问题:大量请求访问数据库中不存在的数据,缓存无法拦截。* 攻击方式:* 攻击者使用不存在的用户id频繁请求;* 这些请求都会直接访问数据库,导致数据库压力过大;* 解决缓存穿透的常见方案:* (1)缓存空值:当查询数据库发现数据不存在时,将空结果(如null)写入缓存,并设置较短的过期时间;* (2)布隆过滤器(Bloom Filter):再缓存层前加布隆过滤器,预先存储所有合法Key的哈希值,查询时先检查布隆过滤器,若返回”不存在“,直接拦截请求,若返回”可能存在“,继续查询缓存/数据库;* (3)互斥锁(Mutex Lock):缓存未命中时,通过互斥锁(如 Redis 的 SETNX)保证只有一个线程查询数据库,其他线程等待回填缓存。* (4)接口层校验:在 API 入口处校验参数合法性,拦截明显无效的请求(如非法 ID 格式、负数等)* (5)热点数据永不过期:对高频访问的热点数据设置永不过期,通过后台线程主动更新缓存。* (6)缓存预热:在系统启动或低峰期,预先加载热点数据到缓存中。* (7)实时监控与限流:监控异常流量(如大量 null 响应),触发限流策略(如令牌桶、漏桶算法),保护数据库。
2.缓存击穿(Cache Breakdown)* 缓存击穿的定义:缓存击穿(Cache Breakdown)是指某个热点key(如爆款商品信息)在缓存中过期后,大量并发请求同时访问数据库(请求数据存在),导致数据库压力骤增。* 触发原因:缓存过期时间到期,且高并发场景下请求集中失效。* 核心问题:单个热点key失效后,大量请求同时访问数据库。* 3.缓存雪崩(Cache Avalanche)* 问题描述:* 大量缓存key在同一时间过期;* 大量请求直接访问数据库,导致数据库崩溃;
二、代码
2.1 controller层
package com.study.sredis.stept001.controller;import cn.hutool.core.lang.UUID;
import cn.hutool.core.thread.ThreadUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONUtil;
import com.alibaba.fastjson.JSON;
import com.google.common.util.concurrent.RateLimiter;
import com.study.sredis.stept001.domain.User;
import com.study.sredis.stept001.service.UserService;
import com.study.sredis.utils.R;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.concurrent.TimeUnit;
@RestController
@RequestMapping("/userRedis")
public class CacheTestController {@Autowiredprivate StringRedisTemplate stringRedisTemplate;@Autowiredprivate UserService userService;private static final String KEY_HOT = "hot:user:";private static final List<String> HOT_ID_LIST = Arrays.asList("1001", "1002", "1003");private static final String KEY_HOT_PRE = "user:cache:hot:";private final RateLimiter rateLimiter = RateLimiter.create(10.0);private static final String KEY_NULL_COUNT = "user:null:count:";private static final String KEY_BLOCK_FLAG = "user:block:"; private static final int MAX_NULL_THRESHOLD = 30; private static final int BLOCK_SECONDS = 60; @PostMapping("/selectById")public R selectById(@RequestBody User user) {User userInfo = userService.getById(user.getId());stringRedisTemplate.opsForValue().set(String.valueOf(userInfo.getId()), userInfo.getUserName());String value = stringRedisTemplate.opsForValue().get(String.valueOf(userInfo.getId()));HashMap<String, User> map = new HashMap<>();map.put(value, userInfo);return R.ok(map);}@PostMapping("/selectByIdNull")public R selectByIdNull(@RequestBody User user) {
String key = String.valueOf(user.getId());String userJson = stringRedisTemplate.opsForValue().get(key);
if (StrUtil.isNotBlank(userJson)) {User user1 = JSONUtil.toBean(userJson, User.class);return R.ok(user1);}
if (userJson != null) {
return R.fail("用户信息不存在");}
User userInfo = userService.getById(user.getId());if (userInfo == null) {stringRedisTemplate.opsForValue().set(key, "", 1000, TimeUnit.MINUTES);
return R.fail("用户信息不存在");}
stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(userInfo), 1000, TimeUnit.MINUTES);return R.ok(userInfo);}@PostMapping("/selectByIdBloomFilter")public R selectByIdBloomFilter(@RequestBody User user) {if (user.getId() == null || user.getId() <= 0) {return R.fail("用户id不合适");}long startTime = System.currentTimeMillis();User userInfo = userService.selectByIdBloomFilter(user);long endTime = System.currentTimeMillis();HashMap<String, Object> result = new HashMap<>();result.put("time", endTime - startTime);if (userInfo != null) {result.put("info", userInfo);return R.ok(result);} else {result.put("info", null);return R.fail(result);}}@PostMapping("/comWithSelectByIdBloomFilter")public R comWithSelectByIdBloomFilter(@RequestBody User user) {if (user.getId() == null || user.getId() <= 0) {return R.fail("用户id不合法");}long startTime = System.currentTimeMillis();User userInfo = userService.selectByIdDirect(user);long endTime = System.currentTimeMillis();HashMap<String, Object> result = new HashMap<>();result.put("time", endTime - startTime);if (userInfo != null) {result.put("info", userInfo);return R.ok(result);} else {result.put("info", null);return R.fail(result);}}@PostMapping("/selectByWithLock")public R selectByWithLock(@RequestBody User user) {if (user.getId() == null || user == null) {return R.fail("用户id不能为空");}long startTime = System.currentTimeMillis();HashMap<String, Object> result = new HashMap<>();try {
User userInfo = userService.selectByIdWithSimpleLock(user); long endTime = System.currentTimeMillis();if (result != null) {result.put("time", endTime - startTime);result.put("info", userInfo);return R.ok(result);} else {result.put("time", endTime - startTime);result.put("info", null);return R.fail(result);}} catch (Exception e) {long endTime = System.currentTimeMillis();result.put("time", endTime - startTime);result.put("info", null);return R.fail(result);}}@PostMapping("/selectByIdWithInterfacter")public R selectByIdWithInterfacter(@RequestBody User user) {
Long id = validId(String.valueOf(user.getId()));if (id == null) {return R.fail("非法id格式");}
User userInfo = userService.getById(user.getId());return user == null ? R.fail("用户不存在") : R.ok(userInfo);}private Long validId(String idRaw) {if (idRaw == null || idRaw.isEmpty()) {return null;}try {long id = Long.parseLong(idRaw.trim());return id > 0L ? id : null;} catch (NumberFormatException e) {return null;}}@PostMapping("/selectByIdWithHot")public R<User> selectByIdWithHot(@RequestBody User user) {String key = KEY_HOT + user.getId();String lockKey = key + ":lock";String lockVal = UUID.fastUUID().toString();int retry = 3; while (retry-- > 0) {String json = stringRedisTemplate.opsForValue().get(key);if (StrUtil.isNotBlank(json)) {return R.ok(JSON.parseObject(json, User.class));}Boolean locked = stringRedisTemplate.opsForValue().setIfAbsent(lockKey, lockVal, 5, TimeUnit.SECONDS);if (Boolean.TRUE.equals(locked)) {try {json = stringRedisTemplate.opsForValue().get(key);if (StrUtil.isNotBlank(json)) {return R.ok(JSON.parseObject(json, User.class));}User userDB = userService.getById(user.getId());if (userDB != null) {stringRedisTemplate.opsForValue().set(key, JSON.toJSONString(userDB));}return userDB == null ? R.fail("用户信息不存在") : R.ok(userDB);} finally {String lua ="if redis.call('get', KEYS[1]) == ARGV[1] then " +" return redis.call('del', KEYS[1]) " +"else return 0 end";stringRedisTemplate.execute(new DefaultRedisScript<>(lua, Long.class),Collections.singletonList(lockKey),lockVal);}}ThreadUtil.sleep(100);}return R.fail("系统繁忙,请稍后再试");}@Scheduled(fixedDelay = 30_000)public void refreshHotCache() {for (String id : HOT_ID_LIST) {String key = KEY_HOT + id;User user = userService.getById(id);if (user != null) {stringRedisTemplate.opsForValue().set(key, user.toString());} else {stringRedisTemplate.delete(key);}}}@PostMapping("/selectByIdWithPreHot")public R selectByIdWithPreHot(@RequestBody User user) {String key = KEY_HOT_PRE + user.getId();String json = stringRedisTemplate.opsForValue().get(key);if(StrUtil.isNotBlank(json)){return R.ok(JSON.parseObject(json,User.class));}return R.fail("用户信息不存在");}@PostMapping("/selectById4")public R selectById4(@RequestBody User user) {if (!rateLimiter.tryAcquire()) {return R.fail("系统繁忙,请稍后再试");}String blockKey = KEY_BLOCK_FLAG + user.getId();if (Boolean.TRUE.equals(stringRedisTemplate.hasKey(blockKey))) {return R.fail("请求过于频繁,请稍后再试");}User userInfo = userService.getById(user.getId());if (userInfo == null) {String countKey = KEY_NULL_COUNT + user.getId();long count = stringRedisTemplate.opsForValue().increment(countKey);stringRedisTemplate.expire(countKey, 60, TimeUnit.SECONDS); if (count >= MAX_NULL_THRESHOLD) {stringRedisTemplate.opsForValue().set(blockKey, "1", BLOCK_SECONDS, TimeUnit.SECONDS);return R.fail("触发保护策略,稍后再试");}return R.fail("用户信息不存在");}return R.ok(userInfo);}
}
2.2 service层
2.2.1 service接口
package com.study.sredis.stept001.service;import com.baomidou.mybatisplus.extension.service.IService;
import com.study.sredis.stept001.domain.User;public interface UserService extends IService<User> {User selectByIdBloomFilter(User user);User selectByIdDirect(User user);public User selectByIdWithLock(User user);public User selectByIdWithSpringLock(User user);public User selectByIdWithSimpleLock(User user);
}
2.2.2 serviceImpl
package com.study.sredis.stept001.service.impl;import cn.hutool.bloomfilter.BloomFilter;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.study.sredis.stept001.domain.User;
import com.study.sredis.stept001.mapper.userMapper;
import com.study.sredis.stept001.service.UserService;
import com.study.sredis.utils.RedisLockUtil;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.integration.redis.util.RedisLockRegistry;
import org.springframework.stereotype.Service;import javax.annotation.PostConstruct;
import java.time.Duration;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;@Service
public class UserServiceImpl extends ServiceImpl<userMapper, User> implements UserService {@Autowiredprivate BloomFilter bloomFilter;@Autowiredprivate userMapper userMapper;@Autowiredprivate RedissonClient redissonClient;@Autowiredprivate RedisTemplate<String, Object> redisTemplate;@Autowiredprivate RedisLockUtil redisLockUtil;private static final String CACHE_PREFIX = "user:";private static final String LOCK_PREFIX = "lock:user";private static final long CACHE_EXPIRE = 300; @Overridepublic User selectByIdBloomFilter(User user) {
if (!bloomFilter.contains("user:" + user.getId())) {return null;}
User userInfo = userMapper.selectById(user.getId());return userInfo;}@Overridepublic User selectByIdDirect(User user) {User userInfo = userMapper.selectById(user.getId());return userInfo;}@Overridepublic User selectByIdWithLock(User user) {String cacheKey = CACHE_PREFIX + user.getId();String lockKey = LOCK_PREFIX + user.getId();
User userInfo = (User) redisTemplate.opsForValue().get(cacheKey);if (userInfo != null) {return "NULL".equals(userInfo) ? null : userInfo;}
RLock lock = redissonClient.getLock(lockKey);try {
boolean locked = lock.tryLock(5, 30, TimeUnit.SECONDS);if (locked) {
userInfo = (User) redisTemplate.opsForValue().get(cacheKey);if (userInfo != null) {return "NULL".equals(userInfo) ? null : userInfo;}
userInfo = userMapper.selectById(user.getId());
if (userInfo != null) {redisTemplate.opsForValue().set(cacheKey, userInfo, CACHE_EXPIRE, TimeUnit.SECONDS);} else {redisTemplate.opsForValue().set(cacheKey, "NULL", 60, TimeUnit.SECONDS); }return userInfo;} else {
Thread.sleep(100); return selectByIdWithLock(user); }} catch (InterruptedException e) {Thread.currentThread().interrupt();throw new RuntimeException("获取锁被中断", e);} finally {
if (lock.isHeldByCurrentThread()) {lock.unlock();}}}@Overridepublic User selectByIdWithSpringLock(User user) {String cacheKey = CACHE_PREFIX + user.getId();String lockKey = LOCK_PREFIX + user.getId();
User userInfo = (User) redisTemplate.opsForValue().get(cacheKey);if (userInfo != null) {return "NULL".equals(userInfo) ? null : userInfo;}
RedisLockRegistry lockRegistry = new RedisLockRegistry(redisTemplate.getConnectionFactory(), "user-lock");Lock lock = lockRegistry.obtain(lockKey);try {
if (lock.tryLock(5, TimeUnit.SECONDS)) {try {
userInfo = (User) redisTemplate.opsForValue().get(cacheKey);if (userInfo != null) {return "NULL".equals(userInfo) ? null : userInfo;}
userInfo = userMapper.selectById(user.getId());
if (userInfo != null) {redisTemplate.opsForValue().set(cacheKey, user, Duration.ofMinutes(5));} else {redisTemplate.opsForValue().set(cacheKey, "NULL", Duration.ofMinutes(1));}return userInfo;} finally {lock.unlock();}} else {
Thread.sleep(100);return selectByIdWithLock(user);}} catch (InterruptedException e) {Thread.currentThread().interrupt();throw new RuntimeException("获取锁被中断", e);}}@Overridepublic User selectByIdWithSimpleLock(User user) {String cacheKey = "user:" + user.getId();String lockKey = "lock:user:" + user.getId();String lockValue = UUID.randomUUID().toString();
User userInfo = (User) redisTemplate.opsForValue().get(cacheKey);if (userInfo != null) {return "NULL".equals(userInfo) ? null : userInfo;}boolean locked = false;try {
for (int i = 0; i < 3; i++) {locked = redisLockUtil.tryLock(lockKey, lockValue, 30, TimeUnit.SECONDS);if (locked) break;Thread.sleep(100);}if (locked) {
userInfo = (User) redisTemplate.opsForValue().get(cacheKey);if (userInfo != null) {return "NULL".equals(userInfo) ? null : userInfo;}
userInfo = userMapper.selectById(user.getId());
if (userInfo != null) {redisTemplate.opsForValue().set(cacheKey, userInfo, Duration.ofMinutes(5));} else {redisTemplate.opsForValue().set(cacheKey, "NULL", Duration.ofMinutes(1));}return userInfo;} else {
return userMapper.selectById(user.getId());}} catch (InterruptedException e) {Thread.currentThread().interrupt();throw new RuntimeException("获取锁被中断", e);} finally {if (locked) {redisLockUtil.releaseLock(lockKey, lockValue);}}}@PostConstructpublic void initBloomFilter() {
List<User> userList = userMapper.selectList(null);for (User user : userList) {bloomFilter.add("user:" + user.getId());}}
}
2.3 依赖
<dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-jdbc</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-jdbc</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-thymeleaf</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><dependency><groupId>org.mybatis.spring.boot</groupId><artifactId>mybatis-spring-boot-starter</artifactId><version>2.2.2</version></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-devtools</artifactId><scope>runtime</scope><optional>true</optional></dependency><dependency><groupId>com.mysql</groupId><artifactId>mysql-connector-j</artifactId><scope>runtime</scope></dependency><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><optional>true</optional></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId></dependency><dependency><groupId>com.baomidou</groupId><artifactId>mybatis-plus-boot-starter</artifactId><version>3.5.5</version></dependency><dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId><version>8.0.28</version></dependency><dependency><groupId>cn.hutool</groupId><artifactId>hutool-all</artifactId><version>5.7.16</version></dependency><dependency><groupId>org.redisson</groupId><artifactId>redisson-spring-boot-starter</artifactId><version>3.24.3</version></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-integration</artifactId></dependency><dependency><groupId>org.springframework.integration</groupId><artifactId>spring-integration-redis</artifactId></dependency><dependency><groupId>com.alibaba</groupId><artifactId>fastjson</artifactId><version>1.2.83</version></dependency><dependency><groupId>com.google.guava</groupId><artifactId>guava</artifactId><version>32.1.3-jre</version></dependency></dependencies>
2.4 工具类
2.4.1 布隆过滤器配置类——BloomFilterConfig
package com.study.sredis.utils;import cn.hutool.bloomfilter.BitMapBloomFilter;
import cn.hutool.bloomfilter.BloomFilter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class BloomFilterConfig {@Beanpublic BloomFilter bloomFilter() {return new BitMapBloomFilter(1000);}
}
2.4.2 缓存预热配置类——CacheWarmRunner
package com.study.sredis.utils;import com.alibaba.fastjson.JSON;
import com.study.sredis.stept001.domain.User;
import com.study.sredis.stept001.service.UserService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;import java.util.Arrays;
import java.util.List;
@Component
@RequiredArgsConstructor
@Slf4j
public class CacheWarmRunner implements ApplicationRunner {private final StringRedisTemplate redisTpl;private final UserService userService;private static final String KEY_HOT = "user:cache:hot:"; private static final List<Long> HOT_ID_LIST = Arrays.asList(1001L, 1002L, 1003L);@Overridepublic void run(ApplicationArguments args) {log.info("====== 缓存预热开始 ======");for (Long id : HOT_ID_LIST) {String key = KEY_HOT + id;if (Boolean.TRUE.equals(redisTpl.hasKey(key))) continue;User user = userService.getById(id);if (user != null) {redisTpl.opsForValue().set(key, JSON.toJSONString(user));log.info("已预热 -> {}", key);}}log.info("====== 缓存预热结束 ======");}
}
2.4.3 redis配置类——RedisConfig
package com.study.sredis.utils;import org.redisson.Redisson;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;@Configuration
public class RedisConfig {@Beanpublic RedisTemplate<String,Object> redisTemplate(RedisConnectionFactory redisConnectionFactory){RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();redisTemplate.setConnectionFactory(redisConnectionFactory);redisTemplate.setKeySerializer(new StringRedisSerializer());redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer());return redisTemplate;}
@Beanpublic RedissonClient redissonClient() {Config config = new Config();config.useSingleServer().setAddress("redis://localhost:6379").setDatabase(0);return Redisson.create(config);}
}
2.4.4 锁配置类——RedisLockUtil
package com.study.sredis.utils;import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;import java.util.concurrent.TimeUnit;
@Component
public class RedisLockUtil {@Autowiredprivate StringRedisTemplate redisTemplate;public boolean tryLock(String key, String value, long expire, TimeUnit timeUnit) {return Boolean.TRUE.equals(redisTemplate.opsForValue().setIfAbsent(key, value, expire, timeUnit));}public boolean releaseLock(String key, String value) {String currentValue = redisTemplate.opsForValue().get(key);if (value.equals(currentValue)) {redisTemplate.delete(key);return true;}return false;}
}