redis实战篇--商品缓存模块

缓存

首先大家需要先去知道什么是缓存,以及为什么我们需要去使用缓存.
缓存 ,顾名思义,缓存就是在磁盘中存储数据。
为什么要使用缓存?

言简意赅:速度快,好用

缓存数据存储于代码中,而代码运行在内存中,内存的读写性能远高于磁盘,缓存可以大大降低用户访问并发量带来的服务器读写压力

实际开发中,企业的数据量,少则几十万,多则几千万,这么大的数据量,如果没有缓存来作为避震器系统是几乎撑不住的,所以企业会大量运用缓存技术

但是缓存也会增加代码复杂度和运营成本

商户缓存模块

根据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;


    }


}
相关推荐
AiXed3 小时前
PC微信协议之AES-192-GCM算法
前端·数据库·python
武子康5 小时前
Java-171 Neo4j 备份与恢复 + 预热与执行计划实战
java·开发语言·数据库·性能优化·系统架构·nosql·neo4j
无敌最俊朗@5 小时前
02-SQLite 为了防止多人同时乱写,把整个数据库文件“当一本账本加锁”
jvm·数据库·oracle
小坏讲微服务5 小时前
MaxWell中基本使用原理 完整使用 (第一章)
大数据·数据库·hadoop·sqoop·1024程序员节·maxwell
赵渝强老师5 小时前
【赵渝强老师】MySQL集群解决方案
数据库·mysql
angushine5 小时前
SpringCloud Gateway缓存body参数引发的问题
spring cloud·缓存·gateway
jason.zeng@15022076 小时前
my.cnf详解
运维·数据库·adb
百***62856 小时前
MySQL 常用 SQL 语句大全
数据库·sql·mysql
2501_915918416 小时前
移动端 HTTPS 抓包实战,多工具组合分析与高效排查指南
数据库·网络协议·ios·小程序·https·uni-app·iphone