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

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

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

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

这里其实就是去判断,我是去更新缓存还是去删除缓存,要是去更新缓存的话,那我是不是需要一直去更新,我更新了10100次,但是10099次都没有用是不是?只有最后一次有用,那我话不如当去更新的时候直接将缓存清空就行
解决redis缓存穿透问题
首先大家需要先知道什么是缓存穿透,缓存穿透就是我的用户在进行查询的时候,我先去查询redis要是redis里面没有缓存的话,我就要去数据库里面去找,但是肯定不止我这一个用户吧?那我要是很多很多用户同时去数据库里面查找的话,数据库会炸掉的,所以在这里咱们是要去解决这个问题的,
我可以在用户进行查询的时候我去设置一下,要是在数据库里面也是没有存储的话,我就需要将null值设置到redis里面,防止数据库过载
@Overridepublic 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();}
}
使用布隆过滤器来解决缓存穿透问题
//初始化布隆过滤器private static final BitSetBloomFilter bloomFilter = BloomFilterUtil.createBitSet(RedisConstants.EXCEPTION_INSERT_NUM, RedisConstants.CACHE_SHOP_SIZE, RedisConstants.CACHE_SHOP_SPLIT);@PostConstructprivate 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进程缓存未命中,则查询数据库)
缓存击穿问题
使用互斥锁来解决缓存击穿问题
@Overridepublic 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);//在这里使用工具类来判断一下是不是存在,防止最后返回的是nullreturn 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当中是永久存在的
//重建缓存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缓存
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);//在这里使用工具类来判断一下是不是存在,防止最后返回的是nullreturn 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);//写入redisthis.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;}}