Redis结构:String
缓存更新策略
内存淘汰、超时剔除、主动更新
缓存与数据库双写一致
1、如果缓存未命中,则查询数据库,将数据库结果写入缓存,并设置超时时间
2、修改信息时,先修改数据库,再删除缓存
一个原因两个优点:
原因:若先删缓存再写数据库,在这两个操作之间可能:又来了一个读请求触发 “缓存未命中(此时缓存被删除) → 查数据库(此时数据库更新操作还未完成) → 写入旧数据到缓存” 的流程,导致脏数据。 优点:而先写数据库后删缓存,即使中间有读请求,也会读取到旧缓存数据,但后续缓存被删除,下次查询会重新获取最新数据; 其次,数据库的操作时间远比缓存要慢,先删掉缓存在去处理数据库的话,这个时间间隔更大,更有可能导致脏数据。
解决缓存穿透
用户访问了一个不存在的资源,且进行大量访问。此时数据库中没有,redis中自然也没有缓存相应的资源,因此大量请求打到数据库中,给数据库带来巨大压力。
// 解决方式一:缓存空对象(即用户访问没有的资源时,将空对象也缓存到redis,下一次请求时就可以直接返回redis中的空结果) public Shop queryWithPassThrough(Long id) { //从redis中查询商铺缓存 String key = "cache:shop:" + id; String s = stringRedisTemplate.opsForValue().get(key); //判断是否存在 //命中,直接返回 if (!StrUtil.isBlank(s)) { Shop shop = JSONUtil.toBean(s, Shop.class); return shop; } //如果redis存入则上面已经返回, //此时只存在两种情况:null(没有查询过,数据库中可能有); ‘’(已经查询过了,数据库中没有) // 如果已经查询过数据库 if (s != null) { return null; } //否则没有查询过数据库, redis中不存在为null, 则根据id查询数据库 Shop shop = getById(id); //数据库不存在,返回错误 if (shop == null) { // 解决缓存穿透 写入空值到redis stringRedisTemplate.opsForValue().set("cache:shop:" + id, "", 5, TimeUnit.MINUTES); return null; } //存在,写入redis stringRedisTemplate.opsForValue().set("cache:shop:" + id, JSONUtil.toJsonStr(shop), 30, TimeUnit.MINUTES); //返回 return shop; } //改进:如进行广泛使用,可对上述方法进行封装 public <R,ID> R queryWithPassThrough(String keyPrefix, ID id, Class<R> type, Function<ID,R> dbFallback, Long time, TimeUnit unit) { // 从redis中取出 String key = keyPrefix + id; String json = stringRedisTemplate.opsForValue().get(key); // 存在则直接返回数据 if (StrUtil.isNotBlank(json)) { return JSONUtil.toBean(json, type); } //如果redis存入则上面已经返回, //此时只存在两种情况:null(没有查询过,数据库中可能有); ‘’(已经查询过了,数据库中没有) // 如果已经查询过数据库 if (json != null) { return null; } //否则没有查询过数据库, redis中不存在为null, 则根据id查询数据库 R r = dbFallback.apply(id); //数据库不存在 if (r == null) { // 将空值写入redis stringRedisTemplate.opsForValue().set(key, "", 10L, TimeUnit.MINUTES); // 返回错误信息 return null; } // 存在,写入redis this.set(key, r, time, unit); return r; } // 将缓存对象到redis public void set(String key, Object value, Long time, TimeUnit unit) { stringRedisTemplate.opsForValue().set(key,JSONUtil.toJsonStr(value)); } // 封装后可直接调用 Shop shop = cacheClient.queryWithPassThrough("cache:shop:", id, Shop.class, this::getById, 10L, TimeUnit.MINUTES); //解决方法二:布隆算法进行过滤
缓存雪崩
同一时间段大量缓存的key同时失效,或者Redis服务宕机,导致大量请求直接到达数据库给数据库带来巨大压力
解决:
1、给不同的Key的TTL添加随机值
2、利用Redis集群提高服务的可用性
3、给缓存业务添加降级限流策略
4、给业务添加多级缓存
缓存击穿
热点key问题,就是一个被高并发访问并且缓存重建业务比较复杂的key突然失效,无数的请求访问会给数据库带来巨大压力
解决方法一:互斥锁(在缓存未命中查询数据库时,只有拿到唯一锁的线程才能查询数据库对缓存进行重建,其它线程休眠等待一段时间后重试查询能否命中缓存)。此方法数据一致性高,性能慢,有死锁风险
解决方法二:逻辑过期(存入Redis缓存时数据过期时间为永不过期,在缓存的数据类型上增加一个逻辑过期时间,每次将数据返回前,需要判断逻辑过期时间是否已到。到了则开一个新线程重新查询数据库进行缓存重建,并将旧数据直接返回)。此方法数据一致性低,性能高,无死锁风险
// 逻辑过期解决缓存击穿 public Shop queryWithLogicalExpire(Long id) { //从redis中查询商铺缓存 String key = "cache:shop:" + id; String s = stringRedisTemplate.opsForValue().get(key); //判断是否存在 Redis缓存中为null,直接返回null,因为热点key是会先做数据预热的,如果redis中没有则表名该key不是热点key if (StrUtil.isBlank(s)) { return null; } // 存在,则判断数据是否过期 // 命中,需要先把json反序列化为对象 RedisData redisData = JSONUtil.toBean(s, RedisData.class); JSONObject data = (JSONObject) redisData.getData(); Shop shop = JSONUtil.toBean(data, Shop.class); LocalDateTime expireTime = redisData.getExpireTime(); if (expireTime.isAfter(LocalDateTime.now())) { //未过期,直接返回店铺信息 return shop; } //已过期则把旧数据直接返回,交给另一个线程完成最新数据的更新 //缓存重建 String lockKey = "lock:shop:" + id; //获取互斥锁 boolean islock = tryLock(lockKey); //是否获取锁成功 //获取锁成功 if (islock) { CACHE_REBUILD_EXECUTOR.submit(() -> { //开启独立线程,实现缓存重建 try { this.saveShopToRedis(id, 20L); } catch (Exception e) { throw new RuntimeException(e); }finally { unLock(lockKey); } }); } //返回过期的商品信息 return shop; } private static final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10); private boolean tryLock(String key) { Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", 10, TimeUnit.SECONDS); return Boolean.TRUE.equals(flag); } private void unLock(String key) { stringRedisTemplate.delete(key); }