Redis缓存穿透、缓存击穿解决方案及工具类

编程入门 行业动态 更新时间:2024-10-27 14:22:50

Redis<a href=https://www.elefans.com/category/jswz/34/1771061.html style=缓存穿透、缓存击穿解决方案及工具类"/>

Redis缓存穿透、缓存击穿解决方案及工具类

一、缓存穿透

缓存穿透,指的就是缓存中以及数据库中都不存在的数据,这个时候有可能是攻击者在尝试攻击网站,导致数据库压力大大增加。

解决方案:缓存空值key-null。

    /*** 缓存穿透** @param id* @return*/private Shop queryWithPassThough(Long id) {//1.对传入的店铺id进行数据校验if (id == null) {return null;}//2.先从redis中查询String jsonShop = stringRedisTemplate.opsForValue().get(CACHE_SHOP_KEY + id);if (StrUtil.isNotBlank(jsonShop)) {Shop shop = JSONUtil.toBean(jsonShop, Shop.class);return shop;}//2.2命中是否是空值if ("".equals(jsonShop)) {return null;}//3.如果缓存没有数据,就从数据库中查询并且缓存到redis中Shop shop = shopMapper.selectById(id);if (shop == null) {stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY + id, "", CACHE_NULL_TTL, TimeUnit.MINUTES);return null;}//3.2缓存到Redis中stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY + id, JSONUtil.toJsonStr(shop), CACHE_SHOP_TTL, TimeUnit.MINUTES);//4.返回商户信息return shop;}

二、缓存击穿

缓存击穿,指的是某个具体的key可能承担着非常大的并发量,假如刚好到了该key的过期时间,持续续的大并发就穿破缓存,直接请求到数据库,引起数据库压力瞬间增大,造成过大压力。

解决方案:

(1.)使用互斥锁

1.重建缓存利用独立线程异步执行

(性能低,会有死锁风险,但是一致性好)

    private Shop queryWithMutex(Long id) {//1.对传入的店铺id进行数据校验if (id == null) {return null;}//2.先从redis中查询String jsonShop = stringRedisTemplate.opsForValue().get(CACHE_SHOP_KEY + id);//2.1存在就直接返回if (StrUtil.isNotBlank(jsonShop)) {Shop shop = JSONUtil.toBean(jsonShop, Shop.class);return shop;}//2.2命中是否是空值if ("".equals(jsonShop)) {return null;}//3.实现缓存重建//3.1获取互斥锁String lockKey = LOCK_SHOP_KEY + id;Shop shop = null;try {Boolean result = tryLock(lockKey);//3.2判断是否获取成功//3.3失败,就休眠,然后重试if (!result) {Thread.sleep(50);return queryWithMutex(id);}//3.4成功,查询数据库shop = shopMapper.selectById(id);//4.数据库不存就返回错误if (shop == null) {stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY + id, "", CACHE_NULL_TTL, TimeUnit.MINUTES);return null;}//4.2如果存在,缓存到Redis中stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY + id, JSONUtil.toJsonStr(shop), CACHE_SHOP_TTL, TimeUnit.MINUTES);} catch (InterruptedException e) {throw new RuntimeException();} finally {//5.释放互斥锁unLock(lockKey);}//6.返回商户信息return shop;}
/*** 获取互斥锁*/private Boolean tryLock(String key) {Boolean result = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", 10, TimeUnit.SECONDS);return BooleanUtil.isTrue(result);}/*** 释放互斥锁** @param key*/private void unLock(String key) {stringRedisTemplate.delete(key);}

(2.)设置逻辑过期

1.热点key缓存永不过期,而是设置一个逻辑过期时间,查询到数据时通过对逻辑过期时间判断,来决定是否需要重建缓存

2.重建缓存也通过互斥锁保证单线程执行

3.重建缓存利用独立线程异步执行

4.重建缓存利用独立线程异步执行

(性能高,但是不保证一致性)

 public Shop queryWithLogicalExpire(Long id){//1.从redis获取缓存String jsonShop = stringRedisTemplate.opsForValue().get(CACHE_SHOP_KEY + id);//2.判断是否存在if (StrUtil.isBlank(jsonShop)) {return null;}//3.命中,json数据序列化城对象RedisData redisData = JSONUtil.toBean(jsonShop, RedisData.class);JSONObject data = (JSONObject) redisData.getData();Shop shop = JSONUtil.toBean(data, Shop.class);LocalDateTime expireTime = redisData.getExpireTime();//4.判断是否过期if (expireTime.isAfter(LocalDateTime.now())){//4.1未过期,直接返回return shop;}//4.2过期,重建返回//5.缓存重建//5.1获取互斥锁String lockKey =LOCK_SHOP_KEY + id;Boolean result = tryLock(lockKey);//5.2判断是否成功if (result){//成功就开启独立线程,实现缓存重建CACHE_REBUILD_EXECUTOR.submit(() ->{//重建缓存try {this.saveShop2Redis(id,20L);} catch (Exception e) {throw  new RuntimeException();}finally {//释放锁unLock(lockKey);}});}//6.返回信息return shop;}
 public void saveShop2Redis(Long id, Long expireSecond){//1.查询是否有数据Shop shop = getById(id);//2.封装成逻辑过期时间RedisData redisData = new RedisData();redisData.setData(shop);redisData.setExpireTime(LocalDateTime.now().plusSeconds(expireSecond));//3.写入redisstringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY+id, JSONUtil.toJsonStr(redisData));}

@Data
public class RedisData {private LocalDateTime expireTime;private Object data;
}

修改成工具类,实现传入任何一个java对象都可以序列化成json然后存储到string类型的key中用来处理缓存穿透/击穿;

package com.hmdp.utils;import cn.hutool.core.util.BooleanUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;import java.time.LocalDateTime;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.function.Function;import static com.hmdp.utils.RedisConstants.*;@Slf4j
@Component
public class CacheClient {private final StringRedisTemplate stringRedisTemplate;public CacheClient(StringRedisTemplate stringRedisTemplate) {this.stringRedisTemplate = stringRedisTemplate;}/*** 设置Redis和过期时间* @param key* @param value* @param time* @param timeUnit*/public void set(String key, Object value, Long time, TimeUnit timeUnit) {stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(value), time, timeUnit);}/*** 设置逻辑过期,用于缓存击穿** @param key* @param value* @param time* @param timeUnit*/public void setWithLogicalExpire(String key, Object value, Long time, TimeUnit timeUnit) {//设置逻辑过期RedisData redisData = new RedisData();redisData.setData(value);redisData.setExpireTime(LocalDateTime.now().plusSeconds(timeUnit.toSeconds(time)));//写入redisstringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(redisData));}/*** 解决缓存穿透Redis* @param keyPrefix 前缀* @param id 查询id* @param type 数据类型* @param dbFallback 函数* @param time 缓存过期时间* @param timeUnit 时间单位* @param <R> 返回值类型* @param <ID> 查询id类型* @return*/public  <R, ID> R queryWithPassThough(String keyPrefix, ID id, Class<R> type, Function<ID, R> dbFallback, Long time, TimeUnit timeUnit) {String key = keyPrefix + id;//2.先从redis中查询String json = stringRedisTemplate.opsForValue().get(key);if (StrUtil.isNotBlank(json)) {return JSONUtil.toBean(json, type);}//2.2命中是否是空值if ("".equals(json)) {return null;}//3.如果缓存没有数据,就从数据库中查询并且缓存到redis中R r = dbFallback.apply(id);if (r == null) {stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY + id, "", CACHE_NULL_TTL, TimeUnit.MINUTES);return null;}//3.2缓存到Redis中this.set(key, r, time, timeUnit);//4.返回商户信息return r;}private static  final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10);/*** 逻辑过期解决Redis缓存击穿* @param keyPrefix 前缀* @param id 查询id* @param type 数据类型* @param dbFallback 函数* @param time 缓存过期时间* @param timeUnit 时间单位* @param <R> 返回值类型* @param <ID> 查询id类型* @return*/public <R, ID> R queryWithLogicalExpire(String keyPrefix, ID id, Class<R> type, Function<ID, R> dbFallback, Long time, TimeUnit timeUnit){//1.从redis获取缓存String key = keyPrefix+ id;String json = stringRedisTemplate.opsForValue().get(key);//2.判断是否存在if (StrUtil.isBlank(json)) {return null;}//3.命中,json数据序列化城对象RedisData redisData = JSONUtil.toBean(json, RedisData.class);JSONObject data = (JSONObject) redisData.getData();R r = JSONUtil.toBean(data, type);LocalDateTime expireTime = redisData.getExpireTime();//4.判断是否过期if (expireTime.isAfter(LocalDateTime.now())){//4.1未过期,直接返回return r;}//4.2过期,重建返回//5.缓存重建//5.1获取互斥锁String lockKey =LOCK_SHOP_KEY + id;Boolean result = tryLock(lockKey);//5.2判断是否成功if (result){//成功就开启独立线程,实现缓存重建CACHE_REBUILD_EXECUTOR.submit(() ->{//重建缓存try {//查询数据库R r1 = dbFallback.apply(id);//写入redisthis.setWithLogicalExpire(key,r1,time,timeUnit);} catch (Exception e) {throw  new RuntimeException();}finally {//释放锁unLock(lockKey);}});}//6.返回信息return r;}/*** 获取互斥锁*/private Boolean tryLock(String key) {Boolean result = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", 10, TimeUnit.SECONDS);return BooleanUtil.isTrue(result);}/*** 释放互斥锁** @param key*/private void unLock(String key) {stringRedisTemplate.delete(key);}/*** 互斥锁解决Redis缓存击穿* @param keyPrefix 前缀* @param id 查询id* @param type 数据类型* @param dbFallback 函数* @param time 缓存过期时间* @param timeUnit 时间单位* @param <R> 返回值类型* @param <ID> 查询id类型* @return*/public  <R, ID> R queryWithMutex(String keyPrefix, ID id, Class<R> type, Function<ID, R> dbFallback, Long time, TimeUnit timeUnit) {String key = keyPrefix+ id;//2.先从redis中查询String json = stringRedisTemplate.opsForValue().get(key);//2.1存在就直接返回if (StrUtil.isNotBlank(json)) {R r = JSONUtil.toBean(json, type);return r;}//2.2命中是否是空值if ("".equals(json)) {return null;}//3.实现缓存重建//3.1获取互斥锁String lockKey = "lock:shop:" + id;R r = null;try {Boolean result = tryLock(lockKey);//3.2判断是否获取成功//3.3失败,就休眠,然后重试if (!result) {Thread.sleep(50);return queryWithMutex(keyPrefix,id,type,dbFallback,time,timeUnit);}//3.4成功,查询数据库r = dbFallback.apply(id);//4.数据库不存就创建空值 并返回if (r == null) {stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY + id, "", CACHE_NULL_TTL, TimeUnit.MINUTES);return null;}//4.2如果存在,缓存到Redis中this.set(key,r,time,timeUnit);} catch (InterruptedException e) {throw new RuntimeException();} finally {//5.释放互斥锁unLock(lockKey);}//6.返回商户信息return r;}
}

更多推荐

Redis缓存穿透、缓存击穿解决方案及工具类

本文发布于:2024-03-13 15:54:01,感谢您对本站的认可!
本文链接:https://www.elefans.com/category/jswz/34/1734330.html
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,我们将在24小时内删除。
本文标签:缓存   解决方案   工具   Redis

发布评论

评论列表 (有 0 条评论)
草根站长

>www.elefans.com

编程频道|电子爱好者 - 技术资讯及电子产品介绍!