普通查询
- 存在缓存穿透风险
- 存在缓存击穿风险
- controller
java
package com.orchids.redisapply.controller;
import com.orchids.redisapply.domain.po.Shop;
import com.orchids.redisapply.service.IShopService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* <p>
* 前端控制器
* </p>
*
* @author nullpointer
* @since 2024-08-01
*/
@Api(tags = "商店信息")
@RestController
@RequestMapping("/shop")
@RequiredArgsConstructor
public class ShopController {
private final IShopService shopService;
@ApiOperation("根据id查询商店信息")
@GetMapping("/{id}")
public Shop queryShopById(@PathVariable("id") Long id){
return shopService.queryShopById(id);
}
}
- service
java
package com.orchids.redisapply.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.orchids.redisapply.domain.po.Shop;
/**
* <p>
* 服务类
* </p>
*
* @author nullpointer
* @since 2024-08-01
*/
public interface IShopService extends IService<Shop> {
Shop queryShopById(Long id);
}
- serviceImpl
java
package com.orchids.redisapply.service.impl;
import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.text.CharSequenceUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONUtil;
import com.orchids.redisapply.domain.po.Shop;
import com.orchids.redisapply.mapper.ShopMapper;
import com.orchids.redisapply.service.IShopService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import lombok.RequiredArgsConstructor;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import static com.orchids.redisapply.domain.constant.RedisConstants.CACHE_SHOP_KEY;
/**
* <p>
* 服务实现类
* </p>
*
* @author nullpointer
* @since 2024-08-01
*/
@Service
@RequiredArgsConstructor
public class ShopServiceImpl extends ServiceImpl<ShopMapper, Shop> implements IShopService {
private final ShopMapper shopMapper;
private final StringRedisTemplate redisTemplate;
@Override
//根据商铺id查询商铺信息
public Shop queryShopById(Long id) {
//1.判断redis中是否有缓存
String key = CACHE_SHOP_KEY+id;
String shopInfo = redisTemplate.opsForValue().get(key);
//2.缓存中存在
if (StrUtil.isNotBlank(shopInfo)) {
return JSONUtil.toBean(shopInfo, Shop.class);
}
//3.没有缓存查数据库
Shop shop = getById(id);
if (shop == null){
throw new RuntimeException("该商铺不存在");
}
//4.重建缓存
String str = JSONUtil.toJsonStr(shop);
redisTemplate.opsForValue().set(key,str);
return shop;
}
}
- 测试
- 第一次查询数据库
- 之后都查redis
- 响应结果
缓存穿透
- 前端发送请求请求查询数据库中一定不存在的值 解决方式有
- 填充空值
- 布隆过滤器
- 填充null实现 如下
java
@Override //填充空值解决缓存穿透
public Shop queryShopById(Long id) {
//1.判断redis中是否有缓存
String key = CACHE_SHOP_KEY+id;
String shopInfo = redisTemplate.opsForValue().get(key);
//2.缓存中存在
if (StrUtil.isNotBlank(shopInfo)) {
//2.1判断是否为null
if ("null".equals(shopInfo)){
throw new RuntimeException("该商铺不存在");
}
return JSONUtil.toBean(shopInfo, Shop.class);
}
//3.没有缓存查数据库
Shop shop = getById(id);
if (shop == null){
//3.1 redis中填充null值
redisTemplate.opsForValue().set(key,"null",30L, TimeUnit.MINUTES);
throw new RuntimeException("该商铺不存在");
}
//4.重建缓存
String str = JSONUtil.toJsonStr(shop);
redisTemplate.opsForValue().set(key,str);
return shop;
}
测试结果
发送不存在的请求 可以添加其他的返回值也可以直接抛出
缓存击穿
- 当热点key失效大量请求请求到数据库 可能压垮数据库解决方案有
- 添加互斥锁
- 逻辑过期
- 互斥锁解决
java
@Override
public Shop queryShopById(Long id) {
//1.判断redis中是否有缓存
String key = CACHE_SHOP_KEY+id;
String shopInfo = redisTemplate.opsForValue().get(key);
//2.缓存中存在
if (StrUtil.isNotBlank(shopInfo)) {
//2.1判断是否为null
if ("null".equals(shopInfo)){
throw new RuntimeException("该商铺不存在");
}
return JSONUtil.toBean(shopInfo, Shop.class);
}
Shop shop = null;
try {
//3.0获取互斥
boolean lock = tryLock(LOCK_SHOP_KEY+id);
//3.1没有有获取到锁
if (!lock){
//睡个500毫秒再重试
Thread.sleep(500);
return queryShopById(id);
}
//3.2获取到了锁 查数据库重建缓存
shop = getById(id);
//3.3数据库中也没有就有恶意攻击风险
if (shop == null){
//3.4redis中填充null值
redisTemplate.opsForValue().set(key,"null",30L, TimeUnit.MINUTES);
throw new RuntimeException("该商铺不存在");
}
String str = JSONUtil.toJsonStr(shop);
//缓存重建完成
redisTemplate.opsForValue().set(key,str);
} catch (InterruptedException e) {
throw new RuntimeException(e);
} finally {
unLock(LOCK_SHOP_KEY+id);
}
//释放锁
return shop;
}
private boolean tryLock(String key){
//锁也要添加过期时间 避免死锁
Boolean lock = redisTemplate.opsForValue().setIfAbsent(key, "1", 10, TimeUnit.SECONDS);
return BooleanUtil.isTrue(lock);
}
private void unLock(String key){
redisTemplate.delete(key);
}
- 压测结果 只有一个线程去查询 数据库
- 逻辑过期解决
java
//逻辑过期解决缓存击穿
public Shop queryShopByIdLogic(Long id) {
//1.判断redis中是否有缓存
String key = CACHE_SHOP_KEY+id;
String redisData = redisTemplate.opsForValue().get(key);
//2.缓存不存在
if (StrUtil.isBlank(redisData)){
throw new RuntimeException("该商铺不存在");
}
//3.缓存存在判断是否过期
RedisData data = JSONUtil.toBean(redisData, RedisData.class);
LocalDateTime expireTime = data.getExpireTime();
Shop shop = (Shop) data.getData();
//判断过期时间是否过期
if (expireTime.isAfter(LocalDateTime.now())){
return shop;
}
//过期缓存重建 使用线程池
Shop reShop;
try {
//3.0获取互斥
boolean lock = tryLock(LOCK_SHOP_KEY+id);
//3.1没有有获取到锁
if (!lock){
//睡个500毫秒再重试
Thread.sleep(50);
return queryShopById(id);
}
//3.2获取到了锁 查数据库重建缓存
reShop = getById(id);
//模拟重建缓存要花很长时间
Thread.sleep(100);
//3.3数据库中也没有就有恶意攻击风险
if (shop == null){
//3.4redis中填充null值
redisTemplate.opsForValue().set(key,"null",30L, TimeUnit.MINUTES);
throw new RuntimeException("该商铺不存在");
}
String str = JSONUtil.toJsonStr(shop);
//缓存重建完成
redisTemplate.opsForValue().set(key,str);
} catch (InterruptedException e) {
throw new RuntimeException(e);
} finally {
unLock(LOCK_SHOP_KEY+id);
}
//释放锁
return reShop;
}
//缓存预热
public void SaveRedis(Long id,Long expireTime){
Shop shop = getById(id);
RedisData redisData = new RedisData();
redisData.setData(shop);
redisData.setExpireTime(LocalDateTime.now().plusSeconds(expireTime));
redisTemplate.opsForValue().set(CACHE_SHOP_KEY+id,JSONUtil.toJsonStr(redisData));
}
java
package com.orchids.redisapply.domain.cache;
import lombok.Data;
import java.time.LocalDateTime;
/**
* @ Author qwh
* @ Date 2024/8/1 9:08
*/
@Data
public class RedisData {
private LocalDateTime expireTime;
private Object data;
}