Redis(一):缓存穿透及其解决方法(SpringBoot+mybatis-plus)
·
一、概述
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;
/**
* 1.缓存穿透(Cache Penetration)
* 缓存穿透(Cache Penetration)是指客户端请求的数据在缓存中和数据库中都不存在,这样缓存永远不会生效,这些请求都会打到数据库,导致数据库压力骤增甚至崩溃。
* 触发原因:恶意攻击、参数伪造、业务逻辑漏洞。
* 核心问题:大量请求访问数据库中不存在的数据,缓存无法拦截。
* 攻击方式:
* 攻击者使用不存在的用户id频繁请求;
* 这些请求都会直接访问数据库,导致数据库压力过大;
* 解决缓存穿透的常见方案:
* (1)缓存空值:当查询数据库发现数据不存在时,将空结果(如null)写入缓存,并设置较短的过期时间;
* (2)布隆过滤器(Bloom Filter):再缓存层前加布隆过滤器,预先存储所有合法Key的哈希值,查询时先检查布隆过滤器,若返回”不存在“,直接拦截请求,若返回”可能存在“,继续查询缓存/数据库;
* (3)互斥锁(Mutex Lock):缓存未命中时,通过互斥锁(如 Redis 的 SETNX)保证只有一个线程查询数据库,其他线程等待回填缓存。
* (4)接口层校验:在 API 入口处校验参数合法性,拦截明显无效的请求(如非法 ID 格式、负数等)
* (5)热点数据永不过期:对高频访问的热点数据设置永不过期,通过后台线程主动更新缓存。
* (6)缓存预热:在系统启动或低峰期,预先加载热点数据到缓存中。
* (7)实时监控与限流:监控异常流量(如大量 null 响应),触发限流策略(如令牌桶、漏桶算法),保护数据库。
* <p>
* 2.缓存击穿(Cache Breakdown)
* 缓存击穿的定义:缓存击穿(Cache Breakdown)是指某个热点key(如爆款商品信息)在缓存中过期后,大量并发请求同时访问数据库(请求数据存在),导致数据库压力骤增。
* 触发原因:缓存过期时间到期,且高并发场景下请求集中失效。
* 核心问题:单个热点key失效后,大量请求同时访问数据库。
* 3.缓存雪崩(Cache Avalanche)
* 问题描述:
* 大量缓存key在同一时间过期;
* 大量请求直接访问数据库,导致数据库崩溃;
*/
@RestController
@RequestMapping("/userRedis")
public class CacheTestController {
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Autowired
private UserService userService;
// 缓存key前缀
private static final String KEY_HOT = "hot:user:";
// 热点ID列表——实际可放到配置中心或DB
private static final List<String> HOT_ID_LIST = Arrays.asList("1001", "1002", "1003");
// 缓存前缀
private static final String KEY_HOT_PRE = "user:cache:hot:";
/* ========== 1. 令牌桶:10 QPS,突发 20 ========== */
private final RateLimiter rateLimiter = RateLimiter.create(10.0);
/* ========== 2. 异常计数 key 前缀 ========== */
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; // 1 分钟内最多 30 次 null
private static final int BLOCK_SECONDS = 60; // 触发后封 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);
}
/**
* 解决方案一:缓存空值
* 原理:当查询数据库发现数据不存在时,将空结果(如null)写入缓存,并设置较短的过期时间;
* 优点:简单易实现,直接拦截后续相同请求;
* 缺点:1)内存浪费(存储大量无效null值)
* 2)可能出现短时不一致,如:数据已补录,但缓存未及时失效(如需强一致性,可以在更新数据时,删除/覆盖缓存)
*
* @param user
* @return
*/
@PostMapping("/selectByIdNull")
public R selectByIdNull(@RequestBody User user) {
// 1.从redis查询用户信息缓存;
String key = String.valueOf(user.getId());
String userJson = stringRedisTemplate.opsForValue().get(key);
// 2.判断是否存在;
if (StrUtil.isNotBlank(userJson)) {
//3.存在,直接返回;
User user1 = JSONUtil.toBean(userJson, User.class);
return R.ok(user1);
}
// 判断命中的是否是空值
if (userJson != null) {
// 返回错误信息,解决缓存穿透问题
return R.fail("用户信息不存在");
}
// 4.不存在,根据id查询数据库;
User userInfo = userService.getById(user.getId());
if (userInfo == null) {
//5.如果数据库也不存在,将空字符串写入redis,设置过期事件,解决缓存穿透问题;
stringRedisTemplate.opsForValue().set(key, "", 1000, TimeUnit.MINUTES);
// 返回错误信息,解决缓存穿透问题
return R.fail("用户信息不存在");
}
// 6.存在,写入redis
stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(userInfo), 1000, TimeUnit.MINUTES);
return R.ok(userInfo);
}
/**
* 解决方案二:布隆过滤器(Bloom Filter)
* 原理:在缓存层前加布隆过滤器,预先存储所有合法Key的哈希值,查询时先检查布隆过滤器,若返回”不存在“,直接拦截请求,若返回”可能存在“,继续查询缓存/数据库;
* 优点:内存占用低(没有多余的Key),适合海量数据,查询时间复杂度O(1);
* 缺点:1)存在错误的可能(可能将不存在判断未存在);
* 2)实现复杂,删除元素困难(需重建过滤器);
* 适用场景:数据量大且允许误判(如黑名单校验);
*
* @param user
* @return
*/
@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);
}
}
/**
* 直接查询用户信息——与使用布隆过滤器做对比
*
* @param user
* @return
*/
@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);
}
}
/**
* 解决方案三:互斥锁
* 原理:缓存未命中时,通过互斥锁(如 Redis 的 SETNX)保证只有一个线程查询数据库,其他线程等待回填缓存。
* 优点:避免大量请求同时穿透到数据库。
* 缺点:1)分布式环境下需使用分布式锁(如 Redis RedLock)。
* 2)锁竞争可能成为性能瓶颈。
*
* @param user
* @return
*/
@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.selectByIdWithLock(user); //使用Redisson分布式锁
// User userInfo = userService.selectByIdWithSpringLock(user); //使用Spring的Redis分布式锁
User userInfo = userService.selectByIdWithSimpleLock(user); //使用RedisTemplate实现简单分布式锁
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);
}
}
/**
* 解决方案四;接口层校验
* 原理:在 API 入口处校验参数合法性,拦截明显无效的请求(如非法 ID 格式、负数等)。
* 优点:低成本防御恶意攻击(如扫描全表 ID)。
* 缺点:无法拦截合法参数但实际不存在的数据请求。
*
* @param user
* @return
*/
@PostMapping("/selectByIdWithInterfacter")
public R selectByIdWithInterfacter(@RequestBody User user) {
// 1.接口层校验:必须是纯数字且正整数
Long id = validId(String.valueOf(user.getId()));
if (id == null) {
return R.fail("非法id格式");
}
// 2.业务查询
User userInfo = userService.getById(user.getId());
return user == null ? R.fail("用户不存在") : R.ok(userInfo);
}
/**
* 校验 ID 合法性:纯数字、正整数、非空
* 合法返回 Long,非法返回 null
* 作用:用于接口层校验
*/
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;
}
}
/**
* 解决方法五:热点数据永不过期
* 原理:对高频访问的热点数据设置永不过期,通过后台线程主动更新缓存。
* 优点:彻底避免缓存失效导致的穿透。
* 缺点:数据一致性依赖更新机制,需处理脏数据问题。
* 实现:结合定时任务或事件驱动更新缓存。
*
* @param user
* @return
*/
@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; // 最多自旋 3 次
while (retry-- > 0) {
/* 1. 缓存命中立即返回 */
String json = stringRedisTemplate.opsForValue().get(key);
if (StrUtil.isNotBlank(json)) {
return R.ok(JSON.parseObject(json, User.class));
}
/* 2. 抢锁(5 s 自动过期) */
Boolean locked = stringRedisTemplate.opsForValue()
.setIfAbsent(lockKey, lockVal, 5, TimeUnit.SECONDS);
if (Boolean.TRUE.equals(locked)) {
try {
/* 3. double check */
json = stringRedisTemplate.opsForValue().get(key);
if (StrUtil.isNotBlank(json)) {
return R.ok(JSON.parseObject(json, User.class));
}
/* 4. 回源 DB */
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 {
/* 5. 原子释放锁 */
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);
}
}
/* 6. 没抢到锁,自旋 100 ms 继续重试 */
ThreadUtil.sleep(100);
}
/* 7. 重试耗尽 */
return R.fail("系统繁忙,请稍后再试");
}
/* ===================== 后台定时更新 ===================== */
/**
* 每 30 秒刷新一次热点缓存,保证“永不过期”且数据最终一致
*/
@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 {
// DB 已删除,同步删缓存
stringRedisTemplate.delete(key);
}
}
}
/**
* 解决方案六:缓存预热
* 原理:在系统启动或低峰期,预先加载热点数据到缓存中。
* 优点:减少冷启动时的缓存穿透风险。
* 缺点:需提前知道热点数据(可通过历史日志分析)。
*
* @param user
* @return
*/
@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("用户信息不存在");
}
/**
* 解决方案七:实时监控与限流
* 原理:监控异常流量(如大量 null 响应),触发限流策略(如令牌桶、漏桶算法),保护数据库。
* 优点:兜底防御,避免突发攻击。
* 缺点:需配套监控和告警系统。
*
* @param user
* @return
*/
@PostMapping("/selectById4")
public R selectById4(@RequestBody User user) {
/* 3. 先拿令牌,拿不到直接限流 */
if (!rateLimiter.tryAcquire()) {
return R.fail("系统繁忙,请稍后再试");
}
/* 4. 是否处于封禁期 */
String blockKey = KEY_BLOCK_FLAG + user.getId();
if (Boolean.TRUE.equals(stringRedisTemplate.hasKey(blockKey))) {
return R.fail("请求过于频繁,请稍后再试");
}
/* 5. 查库 */
User userInfo = userService.getById(user.getId());
/* 6. 异常计数(null 视为异常) */
if (userInfo == null) {
String countKey = KEY_NULL_COUNT + user.getId();
long count = stringRedisTemplate.opsForValue().increment(countKey);
stringRedisTemplate.expire(countKey, 60, TimeUnit.SECONDS); // 1 分钟窗口
if (count >= MAX_NULL_THRESHOLD) {
stringRedisTemplate.opsForValue().set(blockKey, "1", BLOCK_SECONDS, TimeUnit.SECONDS);
return R.fail("触发保护策略,稍后再试");
}
return R.fail("用户信息不存在");
}
/* 7. 正常返回 */
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> {
/**
* 使用布隆过滤器查询用户信息
*
* @param user
* @return
*/
User selectByIdBloomFilter(User user);
/**
* 直接查询用户信息——不使用布隆过滤器
*
* @param user
* @return
*/
User selectByIdDirect(User user);
/**
* 根据id查询用户 —— 使用Redisson分布式锁(推荐!!!)
*
* @param user
* @return
*/
public User selectByIdWithLock(User user);
/**
* 根据id查询用户 —— 使用Spring的Redis分布式锁
*
* @param user
* @return
*/
public User selectByIdWithSpringLock(User user);
/**
* 根据id查询用户 —— 使用RedisTemplate实现简单分布式锁
*
* @param user
* @return
*/
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 {
@Autowired
private BloomFilter bloomFilter;
@Autowired
private userMapper userMapper;
@Autowired
private RedissonClient redissonClient;
@Autowired
private RedisTemplate<String, Object> redisTemplate;
@Autowired
private RedisLockUtil redisLockUtil;
private static final String CACHE_PREFIX = "user:";
private static final String LOCK_PREFIX = "lock:user";
private static final long CACHE_EXPIRE = 300; //5分钟
/**
* 使用布隆过滤器查询用户信息
*
* @param user
* @return
*/
@Override
public User selectByIdBloomFilter(User user) {
// 1.先检查布隆过滤器;
if (!bloomFilter.contains("user:" + user.getId())) {
return null;
}
// 2.布隆过滤器通过,查询数据库;
User userInfo = userMapper.selectById(user.getId());
return userInfo;
}
/**
* 直接查询用户信息
*
* @param user
* @return
*/
@Override
public User selectByIdDirect(User user) {
User userInfo = userMapper.selectById(user.getId());
return userInfo;
}
/**
* 根据id查询用户 - 使用Redisson分布式锁(推荐!!!)
*
* @param user
* @return
*/
@Override
public User selectByIdWithLock(User user) {
String cacheKey = CACHE_PREFIX + user.getId();
String lockKey = LOCK_PREFIX + user.getId();
// 1.先查缓存
User userInfo = (User) redisTemplate.opsForValue().get(cacheKey);
if (userInfo != null) {
return "NULL".equals(userInfo) ? null : userInfo;
}
// 2.获取分布式锁
RLock lock = redissonClient.getLock(lockKey);
try {
// 尝试获取锁:等待时间5秒,锁过期时间是30秒
boolean locked = lock.tryLock(5, 30, TimeUnit.SECONDS);
if (locked) {
// 3.双重检查缓存
userInfo = (User) redisTemplate.opsForValue().get(cacheKey);
if (userInfo != null) {
return "NULL".equals(userInfo) ? null : userInfo;
}
// 4.查询数据库
userInfo = userMapper.selectById(user.getId());
// 5.回填缓存
if (userInfo != null) {
redisTemplate.opsForValue().set(cacheKey, userInfo, CACHE_EXPIRE, TimeUnit.SECONDS);
} else {
redisTemplate.opsForValue().set(cacheKey, "NULL", 60, TimeUnit.SECONDS); //短时间缓存空值
}
return userInfo;
} else {
// 6.获取锁失败,等待并重试
Thread.sleep(100); //短暂等待
return selectByIdWithLock(user); //递归重试
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new RuntimeException("获取锁被中断", e);
} finally {
// 7.释放锁
if (lock.isHeldByCurrentThread()) {
lock.unlock();
}
}
}
/**
* 根据id查询用户 —— 使用Spring的Redis分布式锁
*
* @param user
* @return
*/
@Override
public User selectByIdWithSpringLock(User user) {
String cacheKey = CACHE_PREFIX + user.getId();
String lockKey = LOCK_PREFIX + user.getId();
// 1.先查缓存
User userInfo = (User) redisTemplate.opsForValue().get(cacheKey);
if (userInfo != null) {
return "NULL".equals(userInfo) ? null : userInfo;
}
// 2.使用Spring的分布式锁
RedisLockRegistry lockRegistry = new RedisLockRegistry(redisTemplate.getConnectionFactory(), "user-lock");
Lock lock = lockRegistry.obtain(lockKey);
try {
// 尝试获取锁
if (lock.tryLock(5, TimeUnit.SECONDS)) {
try {
// 3.双重检查缓存
userInfo = (User) redisTemplate.opsForValue().get(cacheKey);
if (userInfo != null) {
return "NULL".equals(userInfo) ? null : userInfo;
}
// 4.查询数据库
userInfo = userMapper.selectById(user.getId());
// 5.回填缓存
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);
}
}
/**
* 根据id查询用户 —— 使用RedisTemplate实现简单分布式锁
*
* @param user
* @return
*/
@Override
public User selectByIdWithSimpleLock(User user) {
String cacheKey = "user:" + user.getId();
String lockKey = "lock:user:" + user.getId();
String lockValue = UUID.randomUUID().toString();
// 1.先查缓存
User userInfo = (User) redisTemplate.opsForValue().get(cacheKey);
if (userInfo != null) {
return "NULL".equals(userInfo) ? null : userInfo;
}
boolean locked = false;
try {
// 2.尝试获取锁
for (int i = 0; i < 3; i++) {
locked = redisLockUtil.tryLock(lockKey, lockValue, 30, TimeUnit.SECONDS);
if (locked) break;
Thread.sleep(100);
}
if (locked) {
// 3.双重检查缓存
userInfo = (User) redisTemplate.opsForValue().get(cacheKey);
if (userInfo != null) {
return "NULL".equals(userInfo) ? null : userInfo;
}
// 4.查询数据库
userInfo = userMapper.selectById(user.getId());
// 5.回填缓存
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);
}
}
}
/**
* 初始化布隆过滤器
*/
@PostConstruct
public void initBloomFilter() {
// 1.查询所有用户信息
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>
<!-- 集成redis依赖 -->
<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>
<!--mybatis-plus依赖-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.5</version>
</dependency>
<!-- mysql依赖-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.28</version>
</dependency>
<!-- hutool依赖-->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.7.16</version>
</dependency>
<!-- 分布式锁 + Redis 客户端一站式 -->
<!-- 分布式锁 starter:redisson-spring-boot-starter -->
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson-spring-boot-starter</artifactId>
<version>3.24.3</version>
</dependency>
<!-- 或者使用Spring Boot Starter -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-integration</artifactId>
</dependency>
<!--Spring分布式锁-->
<dependency>
<groupId>org.springframework.integration</groupId>
<artifactId>spring-integration-redis</artifactId>
</dependency>
<!--用于json类型转换-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.83</version>
</dependency>
<!-- 若用 Guava 令牌桶 -->
<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 {
/**
* 创建布隆过滤器
* 参数说明:预计插入数量,误判率
*/
@Bean
public BloomFilter bloomFilter() {
// 预计插入1000个元素,误判率0.01
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:"; // 与 Controller 保持一致
private static final List<Long> HOT_ID_LIST = Arrays.asList(1001L, 1002L, 1003L);
@Override
public 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 {
@Bean
public 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;
}
// redisson锁
@Bean
public 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 {
@Autowired
private 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;
}
}
更多推荐
所有评论(0)