缓存
首先大家需要先去知道什么是缓存,以及为什么我们需要去使用缓存.
缓存 ,顾名思义,缓存就是在磁盘中存储数据。
为什么要使用缓存?
言简意赅:速度快,好用
缓存数据存储于代码中,而代码运行在内存中,内存的读写性能远高于磁盘,缓存可以大大降低用户访问并发量带来的服务器读写压力
实际开发中,企业的数据量,少则几十万,多则几千万,这么大的数据量,如果没有缓存来作为避震器系统是几乎撑不住的,所以企业会大量运用缓存技术
但是缓存也会增加代码复杂度和运营成本
商户缓存模块
根据id来查询店铺缓存

先看controller层,这里需要将自己定义的result去掉,然后去自己定义

这里其实就是去redis里面去查询,要是查到了就返回给前端,要是没查到的话,就去数据库里面查找,要是数据库里面也是没有查找到的话,就给前端返回一个null的提示
list商品类型缓存

这里需要的也是去redis1里面查找,不过需要注意的是,在进行插入的时候是去遍历集合,从第一个开始,最左插入法则,这里主包试了一下最右插入,前端也是正常显示,所以感觉在前端已经做了排序
商品店铺和在进行修改店铺的时候进行的缓存

这里其实就是去判断,我是去更新缓存还是去删除缓存,要是去更新缓存的话,那我是不是需要一直去更新,我更新了10100次,但是10099次都没有用是不是?只有最后一次有用,那我话不如当去更新的时候直接将缓存清空就行
解决redis缓存穿透问题
首先大家需要先知道什么是缓存穿透,缓存穿透就是我的用户在进行查询的时候,我先去查询redis要是redis里面没有缓存的话,我就要去数据库里面去找,但是肯定不止我这一个用户吧?那我要是很多很多用户同时去数据库里面查找的话,数据库会炸掉的,所以在这里咱们是要去解决这个问题的,
我可以在用户进行查询的时候我去设置一下,要是在数据库里面也是没有存储的话,我就需要将null值设置到redis里面,防止数据库过载
java
@Override
public Result update(Shop shop) {
Long id = shop.getId();
if (id == null){
redisTemplate.opsForValue().set(RedisConstants.CACHE_SHOP_KEY + id,"",RedisConstants.CACHE_NULL_TTL, TimeUnit.MINUTES);
return Result.fail("店铺id不能为空");
}
//更新数据库
updateById(shop);
//删除缓存
redisTemplate.delete(RedisConstants.CACHE_SHOP_KEY + id);
return Result.ok();
}
}
使用布隆过滤器来解决缓存穿透问题
java
//初始化布隆过滤器
private static final BitSetBloomFilter bloomFilter = BloomFilterUtil.createBitSet(RedisConstants.EXCEPTION_INSERT_NUM
, RedisConstants.CACHE_SHOP_SIZE
, RedisConstants.CACHE_SHOP_SPLIT);
@PostConstruct
private void insertBloomFilter(){
//将id插入到布隆过滤器当中
query().list().forEach(shop -> {
bloomFilter.add(String.valueOf(shop.getId()));
});
}
首先带大家先简略了解一下布隆过滤器,这个主要就是去根据hash值去存储给的value,所以一定会有误判,但是只要是布隆过滤器说没有值的时候,那肯定是没有值,但是要是说有值的话,也不一定有值,所以大家可以试试。首先肯定是需要创建一个过滤器的,这里主包使用的是工具类来进行创建的,里面需要去指定预期存储的值,误判大小以及线程个数,这里面的误判率也不是越小越好,因为计算的时间会很长,然后就去初始化布隆过滤器,就是将店铺的id转换成字符串然后存进去就行
雪崩
关于雪崩问题就是,我的redis服务器直接宕机
缓存雪崩是指在同一时间段,大量缓存的key同时失效,或者Redis服务宕机,导致大量请求到达数据库,带来巨大压力
解决方案
给不同的Key的TTL添加随机值,让其在不同时间段分批失效
利用Redis集群提高服务的可用性(使用一个或者多个哨兵(Sentinel)实例组成的系统,对redis节点进行监控,在主节点出现故障的情况下,能将从节点中的一个升级为主节点,进行故障转义,保证系统的可用性。 )
给缓存业务添加降级限流策略
给业务添加多级缓存(浏览器访问静态资源时,优先读取浏览器本地缓存;访问非静态资源(ajax查询数据)时,访问服务端;请求到达Nginx后,优先读取Nginx本地缓存;如果Nginx本地缓存未命中,则去直接查询Redis(不经过Tomcat);如果Redis查询未命中,则查询Tomcat;请求进入Tomcat后,优先查询JVM进程缓存;如果JVM进程缓存未命中,则查询数据库)
缓存击穿问题
使用互斥锁来解决缓存击穿问题
java
@Override
public Result querybyid(Long id) {
if (!bloomFilter.contains(String.valueOf(id))){
return Result.fail(SHOP_NOTEXISTS);
}
//解决redis缓存穿透
// Shop shop = queryWithPassThrough(id);
//1.使用互斥锁解决redis缓存击穿
Shop shop = queryWithMutex(id);
if (BeanUtil.isEmpty(shop)){
return Result.ok("店铺不存在");
}
return Result.ok(shop);
}
//互斥锁
private Boolean getLock(Long id) {
String value = RandomUtil.randomString(8);
Boolean flag = redisTemplate
.opsForValue()
.setIfAbsent(RedisConstants.LOCK_SHOP_KEY + id, value, 10, TimeUnit.MINUTES);
//在这里使用工具类来判断一下是不是存在,防止最后返回的是null
return BooleanUtil.isTrue(flag);
}
//释放锁
private void unLock(Long id) {
Boolean unflag = redisTemplate.delete(RedisConstants.LOCK_SHOP_KEY + id);
}
/**
* 缓存穿透解决方法
* @param id
* @return
*/
private Shop queryWithMutex(Long id) {
//1.首先先去查找redis当中是不是存在商品缓存
String shopjson = (String) redisTemplate.opsForValue().get(RedisConstants.CACHE_SHOP_KEY + id);
//存在,直接返回给前端
if (StrUtil.isNotBlank(shopjson)){
Shop shop = JSONUtil.toBean(shopjson, Shop.class);
return shop;
}
Shop shop1 = new Shop();
try{
//2.不存在则去数据库中查询
Boolean lock = getLock(id);
if(!lock){
Thread.sleep(50);
return queryWithMutex(id);
}
//获取锁成功之后还需要再次检测redis当中还是否存在redis缓存
String shopjson1 = (String) redisTemplate.opsForValue().get(RedisConstants.CACHE_SHOP_KEY + id);
if (StrUtil.isNotBlank(shopjson)){
Shop shop = JSONUtil.toBean(shopjson, Shop.class);
return shop;
}
shop1 = getById(id);
if (shop1 == null){
redisTemplate.opsForValue().set(RedisConstants.CACHE_SHOP_KEY + id,"",RedisConstants.CACHE_NULL_TTL, TimeUnit.MINUTES);
return null;
}
//3.将查询到的返回给前端,并且将查询到的数据缓存到redis中
redisTemplate.opsForValue().set(RedisConstants.CACHE_SHOP_KEY + id,JSONUtil.toJsonStr(shop1));
} catch (Exception e) {
System.out.println(e);
} finally {
unLock(id);
}
return shop1;
}
里面的内容其实差不多和缓存穿透一样,第一步也是需要先进行查找redis里面是不是存在商品缓存,存在的话直接返回给前端,要是不存在的话就需要去数据库里查找。在进行查找的时候还是需要先获取锁,要是获取到了去数据库中查找,要是没有获取到的话就是睡眠,然后递归获取,直到获取到锁,获取到锁之后化石需要再次检测redis当中是不是还是存在redis缓存,要是存在的话直接进行返回就行,要是不存在的话需要去数据库里面查找,找到的话存到redis中然后返回
然后使用apifox来进行性能测试


使用逻辑过期来解决缓存击穿问题
所谓逻辑过期就是去在后端来判断是不是真的超过时间了,在redis当中是永久存在的
java
//重建缓存
public void setShopExpireTime(Long id, Long Time){
Shop shop = getById(id);
LocalDateTime localDateTime = LocalDateTime.now().plusSeconds(Time);
RedisData redisData = new RedisData(localDateTime,shop);
String jsonStr = JSONUtil.toJsonStr(redisData);
redisTemplate.opsForValue().set(RedisConstants.CACHE_SHOP_KEY+id,jsonStr);
}
private Shop queryLogicalExpire(Long id) {
//1.首先先去查找redis当中是不是存在商品缓存
String shopjson = (String) redisTemplate.opsForValue().get(RedisConstants.CACHE_SHOP_KEY + id);
//存在,直接返回给前端
if (StrUtil.isBlank(shopjson)){
// Shop shop = JSONUtil.toBean(shopjson, Shop.class);
// return shop;
return null;
}
RedisData redisData = JSONUtil.toBean(shopjson, RedisData.class);
JSONObject data = (JSONObject) redisData.getData();
Shop shop = JSONUtil.toBean((JSONObject) data, Shop.class);
LocalDateTime expireTime = redisData.getExpireTime();
//判断是不是过期了
if (expireTime.isAfter(LocalDateTime.now())){
// 存在,直接返回
return shop;
}
//不存在
// 获取锁
Boolean lock = getLock(id);
if (lock){
try {
executorService.submit(() -> {
setShopExpireTime(id,RedisConstants.CACHE_SHOP_TTL);
});
}catch (Exception e){
System.out.println(e);
}
finally {
unLock( id);
}
return shop ;
}
return shop ;
}
思路和互斥锁差不多,不同的是我再判断要是空的话返回给前端,要是非空的话我就需要去做数据转换,获取时间,判断时间是不是过期,要是过期的话需要缓存重建,要是没有过期的话就直接返回给前端即可

这里使用独立线程去重建缓存,重建缓存之后释放互斥锁
封装redis缓存
java
package com.hmdp.utils;
import cn.hutool.core.util.BooleanUtil;
import cn.hutool.core.util.RandomUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import com.hmdp.entity.Shop;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.RedisTemplate;
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 cn.hutool.core.lang.RegexPool.UUID;
@Component
@Slf4j
public class CachUtils {
private StringRedisTemplate redisTemplate;
public CachUtils(StringRedisTemplate redisTemplate) {
this.redisTemplate =redisTemplate ;
}
private static final ExecutorService executor = Executors.newFixedThreadPool(10);
//设置过期时间
public void setExpireTime(String key,Object value, Long Time, TimeUnit unit) {
redisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(value),Time,unit);
}
//缓存穿透
public <R,ID> R queryWithPassThrough(String keyPrefix, ID id
, Class<R> type
, Function<ID,R> dbFallback
,Long Time, TimeUnit unit) {
String key = keyPrefix + id;
String json = (String) redisTemplate.opsForValue().get(key);
if (StrUtil.isNotBlank(json)) {
return JSONUtil.toBean(json, type);
}
if (json != null) {
return null;
}
R r = dbFallback.apply(id);
if (r == null) {
redisTemplate.opsForValue().set(key, "", Time, unit);
return null;
}
this.setExpireTime(key,r,Time,unit);
return r;
}
//互斥锁解决缓存击穿
public <R,ID> R queryWithMutex(String keyPrefix, ID id
, Class<R> type
, Function<ID,R> dbFallback
,Long Time, TimeUnit unit){
String key = keyPrefix + id;
String json = (String) redisTemplate.opsForValue().get(key);
if (StrUtil.isNotBlank(json)) {
return JSONUtil.toBean(json, type);
}
try{
String keys = keyPrefix + id+ "mutex";
//2.不存在则去数据库中查询
Boolean lock = getLock(id,keys);
if(!lock){
Thread.sleep(50);
return queryWithMutex(keyPrefix, id, type, dbFallback, Time, unit);
}
//获取锁成功之后还需要再次检测redis当中还是否存在redis缓存
String shopjson1 = (String) redisTemplate.opsForValue().get(key);
if (StrUtil.isNotBlank(shopjson1)){
R t = JSONUtil.toBean(shopjson1, type);
return t;
}
R apply = dbFallback.apply(id);
if (apply == null){
redisTemplate.opsForValue().set(key,"",RedisConstants.CACHE_NULL_TTL, TimeUnit.MINUTES);
return null;
}
//3.将查询到的返回给前端,并且将查询到的数据缓存到redis中
redisTemplate.opsForValue().set(key,JSONUtil.toJsonStr(apply));
} catch (Exception e) {
System.out.println(e);
} finally {
unLock(id, key);
}
return dbFallback.apply(id);
}
private<T> Boolean getLock(T id,String key) {
String value = RandomUtil.randomString(8);
Boolean flag = redisTemplate
.opsForValue()
.setIfAbsent(key, value, RedisConstants.LOCK_SHOP_TTL, TimeUnit.SECONDS);
//在这里使用工具类来判断一下是不是存在,防止最后返回的是null
return BooleanUtil.isTrue(flag);
}
//释放锁
private <T>void unLock(T id,String key) {
Boolean unflag = redisTemplate.delete(key + id);
}
//逻辑过期解决缓存击穿
public <R,ID> R queryWithLogicalExpire(String keyPrefix, ID id
, Class<R> type
, Function<ID,R> dbFallback
,Long Time, TimeUnit unit){
String key = keyPrefix + id;
String json = (String) redisTemplate.opsForValue().get(key);
if (StrUtil.isBlank(json)) {
return null;
}
//1.判断是不是过期
RedisData redisData = JSONUtil.toBean(json, RedisData.class);
JSONObject data = (JSONObject) redisData.getData();
R r = JSONUtil.toBean(data, type);
LocalDateTime expireTime = redisData.getExpireTime();
if (expireTime.isAfter(LocalDateTime.now())) {
//没有过期
return r;
}
//过期了,重建缓存
try {
Boolean lock = getLock(id, key);
if (lock) {
try {
executor.submit(() -> {
//查询数据库
R apply = dbFallback.apply(id);
//写入redis
this.setExpireTime(key,apply,Time,unit);
});
} catch (Exception e) {
throw new RuntimeException(e);
}
return r;
}
} catch (Exception e) {
throw new RuntimeException(e);
} finally {
unLock(id, key);
}
return r;
}
}