redis缓存基础

什么是缓存

缓存就是数据交换的缓冲区(称作cache [ kze]]),是存贮数据的临时地方,一般读写性能较高。

缓存的作用:

  • 降低后端负载
  • 提高读写效率,降低响应时间

缓存的成本:

  • 数据一致性成本
  • 代码维护成本
  • 运维成本 

缓存更新策略

解决缓存与数据库不一致

  • 低一致性需求:使用内存淘汰机制。
  • 高一致性需求:主动更新,并以超时剔除作为兜底方案。

操作缓存和数据库时有三个问题需要考虑:

1.删除缓存还是更新缓存?

  • 更新缓存:每次更新数据库都更新缓存,无效写操作较多
  • 删除缓存:更新数据库时让缓存失效,查询时再更新缓存

2.如何保证缓存与数据库的操作的同时成功或失败?

  • 单体系统,将缓存与数据库操作放在一个事务
  • 分布式系统,利用TCC等分布式事务方案

3.先操作缓存还是先操作数据库?

  • 先删除缓存,再操作数据库
  • 先操作数据库,再删除缓存 

缓存雪崩

缓存雪崩是指在同一时段大量的缓存key同时失效或者Redis服务宕机,导致大量请求到达数据库,带来巨大压力。

解决方案:

  • 给不同的Key的TTL添加随机值
  • 利用Redis集群提高服务的可用性
  • 给缓存业务添加降级限流策略
  • 给业务添加多级缓存

缓存穿透

缓存穿透是指客户端请求的数据在缓存中和数据库中都不存在,这样缓存永远不会生效,这些请求都会打到数据库。如果不断发起这样的请求,便会给数据库带来巨大压力。

常见的解决方案:

  • 缓存空对象
  • 布隆过滤
  • 做好数据的基础格式校验
  • 加强用户权限校验
  • 做好热点参数的限流

缓存空对象

缓存空数据,查询返回的数据为空,仍把这个空结果进行缓存

  • 优点:简单
  • 缺点:消耗内存,可能会发生不一致的问题

  

代码示例:

public Shop queryWithPassThrough(Long id) {
        //获取缓存
        String shopjson = redisTemplate.opsForValue().get("cache:shop:" + id);
        //判断店铺是否存在  isNotBlank的判断情况:1.null=false 2.""=false 3."\n\t"=false 4."abc"=ture
        if (StrUtil.isNotBlank(shopjson)) {
            return JSONUtil.toBean(shopjson,Shop.class);
        }
        //判断是否是空值 不等于空说明是值为空字符串的垃圾缓存(用于解决缓存穿透),如果等于空说明还没有此id的缓存需要查询数据库
        if (shopjson != null) {
            return null;
        }
        //没有缓存查询数据库
        Shop shopone = getById(id);
        //不存在,添加缓存并返回空值
        if (shopone == null) {
            //防止缓存击穿,存入短效空值
            redisTemplate.opsForValue().set("cache:shop:" + id, "",3l,TimeUnit.MINUTES);
            return null;
        }
        //设置随机值 防止缓存雪崩
        Long randomNumbers = Long.valueOf(RandomUtil.randomNumbers(2));
        //将数据添加到缓存
        redisTemplate.opsForValue().set("cache:shop:" + id, JSONUtil.toJsonStr(shopone),30 + randomNumbers, TimeUnit.MINUTES);
        return shopone;
    }

布隆过滤

  • 优点:内存占用较少,没有多余key
  • 缺点:实现复杂,存在误判

布隆过滤器原理

存储数据时将数据id通过多个hash函数获取值,并将其对应的数组位置改为1。当下次查询数据时首先进过布隆过滤器。将数据id使用相同的hash函数计算出对应的位置是否都为1(都为1:说明数据存在。反之只要有一个不为1则数据不存在)

bitmap(位图)︰相当于是一个以(bit)位为单位的数组,数组中每个单元只能存储二进制数0或1

布隆过滤器作用:布隆过滤器可以用于检索一个元素是否在一个集合中。

但布隆过滤器有可能存在误判,当其它数据进过hash计算将其改为1后,此时恰好查询一个不存在的数据经过hash计算的值都被其它数据改为1,这时就会出现误判情况。

缓存击穿

缓存击穿问题也叫热点Key问题,就是一个被高并发访问并且缓存重建业务较复杂的key突然失效了,无数的请求访问会在瞬间给数据库带来巨大的冲击。

常见的两种解决方案:

互斥锁

为了避免无数的线程都来重建缓存,可以给它加锁,当线程进来的时候首先要获取锁,只有获取锁成功的人才可以进行缓存重建,这时其它线程进来只有等第一个线程释放锁后才能重建。

思维导图

代码示例:

public boolean tryLock(String key) {
        Boolean aBoolean = redisTemplate.opsForValue().setIfAbsent(key, "1", 10, TimeUnit.SECONDS);
        return BooleanUtil.isTrue(aBoolean);
    }
    public boolean unlock(String key) {
        Boolean aBoolean = redisTemplate.delete(key);
        return BooleanUtil.isTrue(aBoolean);
    }
    /**
     * 缓存击穿lock
     */
    public Shop queryWithMutex(Long id) {
        //获取商铺缓存
        String shopjson = redisTemplate.opsForValue().get("cache:shop:" + id);
        if (StrUtil.isNotBlank(shopjson)) {
            return JSONUtil.toBean(shopjson,Shop.class);
        }
        //判断是否是空值
        if (shopjson != null) {
            return null;
        }
        //实现缓存重建
        //获取互斥锁
        String lock = "lock:shop:" + id;
        boolean tryLock = tryLock(lock);
        Shop shopone = null;
        try {
            //判断是否获取锁
            if (!tryLock) {
                //失败,回调
                return queryWithMutex(id);
            }
            //没有缓存查询数据库
            shopone = getById(id);
            if (shopone == null) {
                //防止缓存穿透,存入短效空值
                redisTemplate.opsForValue().set("cache:shop:" + id, "",3,TimeUnit.MINUTES);
                return null;
            }
            //设置随机值
            Long randomNumbers = Long.valueOf(RandomUtil.randomNumbers(2));
            //将数据添加到缓存
            redisTemplate.opsForValue().set("cache:shop:" + id, JSONUtil.toJsonStr(shopone),30 + randomNumbers, TimeUnit.MINUTES);
        } finally {
            //释放锁
            unlock(lock);
        }
        return shopone;
    }

逻辑过期

用户查询某个热门产品信息,如果缓存未命中(即信息为空),则直接返回空,不去查询数据库。如果缓存信息命中,则判断是否逻辑过期,未过期返回缓存信息,过期则重建缓存,尝试获得互斥锁,获取失败则直接返回已过期缓存数据,获取成功则开启独立线程去重构缓存然后直接返回旧的缓存信息,重构完成之后就释放互斥锁。

思维导图:

代码示例:

逻辑过期实体类:

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

 逻辑过期业务处理类:

    public boolean tryLock(String key) {
        Boolean aBoolean = redisTemplate.opsForValue().setIfAbsent(key, "1", 10, TimeUnit.SECONDS);
        return BooleanUtil.isTrue(aBoolean);
    }
    public boolean unlock(String key) {
        Boolean aBoolean = redisTemplate.delete(key);
        return BooleanUtil.isTrue(aBoolean);
    }
    //线程池
    private static final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10);
    /**
     * 缓存击穿 逻辑过期
     */
    public Shop queryWithLogicalExpire(Long id) {
        //获取商铺缓存
        String shopjson = redisTemplate.opsForValue().get(CACHE_SHOP_KEY + id);
        if (StrUtil.isBlank(shopjson)) {
            return null;
        }
        //命中,把json反序列化为对象
        RedisData redisData = JSON.parseObject(shopjson, RedisData.class);
        String datajson = redisData.getData().toString();
        Shop shop = JSON.parseObject(datajson, Shop.class);
        LocalDateTime expireTime = redisData.getExpireTime();
        //判断是否逻辑过期
        if (expireTime.isAfter(LocalDateTime.now())) {
            //未过期
            return shop;
        }
        //已过期,尝试获取锁
        String lock = "lock:shop:" + id;
        boolean tryLock = tryLock(lock);
        //判断是否获取锁成功
        if (tryLock) {
            //成功,开启独立线程,实现缓存重建
            CACHE_REBUILD_EXECUTOR.submit(() -> {
                try {
                    //重建缓存
                    //查询店铺
                    Shop shopById = getById(id);
                    //封装为逻辑过期
                    RedisData redisData1 = new RedisData();
                    redisData1.setData(shopById);
                    redisData1.setExpireTime(LocalDateTime.now().plusSeconds(30l));
                    //添加到缓存
                    String jsonStr = JSONUtil.toJsonStr(redisData1);
                    redisTemplate.opsForValue().set(CACHE_SHOP_KEY + id,jsonStr);
                } catch (Exception e) {
                    throw new RuntimeException(e);
                } finally {
                    //释放锁
                    unlock(lock);
                }
            });
        }
        return shop;
    }

缓存工具封装

@Component
public class CacheClient {
    @Autowired
    private StringRedisTemplate redisTemplate;

    //将任意Java对象序列化为json并存储在string类型的key中,并且可以设置TTL过期时间
    public void set(String key, Object value, Long ttl, TimeUnit timeUnit) {
        String jsonStr = JSONUtil.toJsonStr(value);
        redisTemplate.opsForValue().set(key,jsonStr,ttl,timeUnit);
    }

    //将任意Java对象序列化为json并存储在string类型的key中,并且可以设置逻辑过期时间
    public void setWithLogicalExpire(String key, Object value, Long ttl, TimeUnit timeUnit) {
        RedisData redisData = new RedisData();
        redisData.setData(value);
        //将时间添加到ExpireTime,timeUnit.toSeconds()将数据转为秒
        redisData.setExpireTime(LocalDateTime.now().plusSeconds(timeUnit.toSeconds(ttl)));
        redisTemplate.opsForValue().set(key,JSONUtil.toJsonStr(redisData));
    }

    //根据指定的key查询缓存,并反序列化为指定类型,利用缓存空值的方式解决缓存穿透问题
    public <Result, ID> Result queryWithPassThrough(String key, ID id, Class<Result> type, Function<ID,Result> dbFallback, Long ttl, TimeUnit timeUnit) {
        //获取商铺缓存
        String json = redisTemplate.opsForValue().get(key);
        if (StrUtil.isNotBlank(json)) {
            return JSONUtil.toBean(json, type);
        }
        //判断是否是空值
        if (json != null) {
            return null;
        }
        //没有缓存查询数据库
        Result result = dbFallback.apply(id);
        //不存在返回错误
        if (result == null) {
            //将空值写入redis,预防缓存穿透
            redisTemplate.opsForValue().set(key,"",2l,TimeUnit.MINUTES);
            //返回错误信息
            return null;
        }
        String jsonStr = JSONUtil.toJsonStr(result);
        redisTemplate.opsForValue().set(key,jsonStr,ttl,timeUnit);
        return result;
    }

    //根据指定的key查询缓存,并反序列化为指定类型,需要利用逻辑过期解决缓存击穿问题
    private static final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10);
    public <Result, ID> Result queryWithLogicalExpire(String keyprefix,ID id, Class<Result> type,Function<ID,Result> dbFallback, Long ttl, TimeUnit timeUnit) {
        //获取商铺缓存
        String json = redisTemplate.opsForValue().get(keyprefix + id);
        if (StrUtil.isBlank(json)) {
            return null;
        }
        //命中,把json反序列化为对象
        RedisData redisData = JSON.parseObject(json, RedisData.class);
        String datajson = redisData.getData().toString();
        Result result = JSON.parseObject(datajson, type);
        LocalDateTime expireTime = redisData.getExpireTime();
        //判断是否逻辑过期
        if (expireTime.isAfter(LocalDateTime.now())) {
            //未过期
            return result;
        }
        //已过期,尝试获取锁
        String lock = keyprefix + id;
        boolean tryLock = tryLock(lock);
        //判断是否获取锁成功
        if (tryLock) {
            //成功,开启独立线程,实现缓存重建
            CACHE_REBUILD_EXECUTOR.submit(() -> {
                try {
                    //重建缓存,查数据库
                    Result apply = dbFallback.apply(id);
                    this.set(keyprefix + id,apply,ttl,timeUnit);
                } catch (Exception e) {
                    throw new RuntimeException(e);
                } finally {
                    //释放锁
                    unlock(lock);
                }
            });
        }
        return result;
    }


    public boolean tryLock(String key) {
        Boolean aBoolean = redisTemplate.opsForValue().setIfAbsent(key, "1", 10, TimeUnit.SECONDS);
        return BooleanUtil.isTrue(aBoolean);
    }
    public boolean unlock(String key) {
        Boolean aBoolean = redisTemplate.delete(key);
        return BooleanUtil.isTrue(aBoolean);
    }
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值