什么是缓存

缓存就是数据交换的缓冲区(称作Cache [ kæʃ ] ),是存贮数据的临时地方,一般读写性能较高。
image.png

image.png

添加Redis缓存

image.png

  1. @RestController
  2. @RequestMapping("/shop")
  3. public class ShopController {
  4. @Resource
  5. public IShopService shopService;
  6. /**
  7. * 根据id查询商铺信息
  8. * @param id 商铺id
  9. * @return 商铺详情数据
  10. */
  11. @GetMapping("/{id}")
  12. public Result queryShopById(@PathVariable("id") Long id) {
  13. return Result.ok(shopService.selectById(id));
  14. }
  15. }
@Service
public class ShopServiceImpl extends ServiceImpl<ShopMapper, Shop> implements IShopService {

    @Autowired
    private StringRedisTemplate redisTemplate;
    @Autowired
    private ObjectMapper objectMapper;

    @Override
    public Result selectById(Long id) {
        try {
            String s = redisTemplate.opsForValue().get(CACHE_SHOP_KEY + id);

            Shop shop;

            if (Strings.isBlank(s)) {
                shop = this.getById(id);
                if (shop == null) {
                    return Result.fail("商品不存在");
                }
                redisTemplate.opsForValue().set(CACHE_SHOP_KEY + id, objectMapper.writeValueAsString(shop));
            } else {
                shop = objectMapper.readValue(s, Shop.class);
            }
            return Result.ok(shop);
        } catch (JsonProcessingException e) {
            e.printStackTrace();
            return Result.fail(e.getMessage());
        }
    }
}

缓存更新策略

image.png

  • 低一致性需求:使用内存淘汰机制。例如店铺类型的查询缓存
  • 高一致性需求:主动更新,并以超时剔除作为兜底方案。例如店铺详情查询的缓存

主动更新策略
image.png
操作缓存和数据库时有三个问题需要考虑:

  1. 删除缓存还是更新缓存?
    • 更新缓存:每次更新数据库都更新缓存,无效写操作较多
    • 删除缓存:更新数据库时让缓存失效,查询时再更新缓存
  2. 如何保证缓存与数据库的操作的同时成功或失败?
    • 单体系统,将缓存与数据库操作放在一个事务
    • 分布式系统,利用TCC等分布式事务方案
  3. 先操作缓存还是先操作数据库?
    • 先删除缓存,再操作数据库
    • 先操作数据库,再删除缓存(理由:写入缓存操作效率远高于更新数据库,缓存大概率是先于数据库完成,所以可能出现缓存成功数据库没成功的问题)

image.png

给查询商铺的缓存添加超时剔除和主动更新的策略

  1. 根据id查询店铺时,缓存未命中,则查询数据库,将数据库结果存入缓存,设置超时时间

    @Override
    public Result selectById(Long id) {
     try {
         String s = redisTemplate.opsForValue().get(CACHE_SHOP_KEY + id);
    
         Shop shop;
    
         if (Strings.isBlank(s)) {
             shop = this.getById(id);
             if (shop == null) {
                 return Result.fail("店铺不存在");
             }
             redisTemplate.opsForValue().set(CACHE_SHOP_KEY + id, objectMapper.writeValueAsString(shop),  CACHE_SHOP_TTL, TimeUnit.MINUTES);
         } else {
             shop = objectMapper.readValue(s, Shop.class);
         }
         return Result.ok(shop);
     } catch (JsonProcessingException e) {
         e.printStackTrace();
         return Result.fail(e.getMessage());
     }
    }
    
  2. 根据id修改店铺时,先修改数据库,再删除缓存

    @Override
    @Transactional
    public Result update(Shop shop) {
     Long id = shop.getId();
     if (id == null) {
         return Result.fail("店铺id不能为空");
     }
     // 1.更新数据库
     updateById(shop);
     // 2.删除缓存
     stringRedisTemplate.delete(CACHE_SHOP_KEY + id);
     return Result.ok();
    }
    

缓存穿透

缓存穿透是指客户端请求的数据在缓存中和数据库中都不存在,这样缓存永远不会生效,这些请求都会打到数据库。

两种解决方案:

  1. 缓存空对象
    • 优点:实现简单、维护方便
    • 缺点:
      • 额外内存消耗
      • 可能短期不一致
  2. 布隆过滤
    • 内存占用少,无多余key
    • 缺点:
      • 实现复杂
      • 存在误判可能

image.png

编码解决缓存穿透
image.png

public <R,ID> R queryWithPassThrough(String keyPrefix, ID id, Class<R> type, Function<ID, R> dbFallback, Long time, TimeUnit unit){
    String key = keyPrefix + id;
    // 1.从redis查询商铺缓存
    String json = stringRedisTemplate.opsForValue().get(key);
    // 2.判断是否存在
    if (StrUtil.isNotBlank(json)) {
        // 3.存在,直接返回
        return JSONUtil.toBean(json, type);
    }

    // 4.不存在,根据id查询数据库
    R r = dbFallback.apply(id);
    // 5.不存在,返回错误
    if (r == null) {
        // 将空值写入redis
        stringRedisTemplate.opsForValue().set(key, "", CACHE_NULL_TTL, TimeUnit.MINUTES);
        // 返回错误信息
        return null;
    }
    // 6.存在,写入redis
    this.set(key, r, time, unit);
    return r;
}

缓存雪崩

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

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

缓存击穿

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

  • 互斥锁
  • 逻辑过期

image.png

解决方案 说明 优点 缺点
互斥锁 一个线程重建缓存时,其他线程只能等待
- 没有额外的内存消耗
- 保证一致性
- 实现简单

- 线程需要等待,性能受影响
- 可能有死锁风险
逻辑过期 通过逻辑字段标识缓存过期,一个线程重建缓存时,其他线程返回过期数据 线程无需等待,性能较好
- 不保证一致性
- 有额外内存消耗
- 实现复杂

基于互斥锁方式解决缓存击穿问题

  1. 需求:修改根据id查询商铺的业务,基于互斥锁方式来解决缓存击穿问题

image.png

public <R, ID> R queryWithMutex(String keyPrefix, ID id, Class<R> type, Function<ID, R> dbFallback, Long time, TimeUnit unit) {
    String key = keyPrefix + id;
    // 1.从redis查询商铺缓存
    String shopJson = stringRedisTemplate.opsForValue().get(key);
    // 2.判断是否存在
    if (StrUtil.isNotBlank(shopJson)) {
        // 3.存在,直接返回
        return JSONUtil.toBean(shopJson, type);
    }

    // 4.实现缓存重建
    // 4.1.获取互斥锁
    String lockKey = LOCK_SHOP_KEY + id;
    R r = null;
    try {
        boolean isLock = tryLock(lockKey);
        // 4.2.判断是否获取成功
        if (!isLock) {
            // 4.3.获取锁失败,休眠并重试
            Thread.sleep(50);
            return queryWithMutex(keyPrefix, id, type, dbFallback, time, unit);
        }
        // 4.4.获取锁成功,根据id查询数据库
        r = dbFallback.apply(id);
        // 5.不存在,返回错误
        if (r == null) {
            // 将空值写入redis
            stringRedisTemplate.opsForValue().set(key, "", CACHE_NULL_TTL, TimeUnit.MINUTES);
            // 返回错误信息
            return null;
        }
        // 6.存在,写入redis
        this.set(key, r, time, unit);
    } catch (InterruptedException e) {
        throw new RuntimeException(e);
    } finally {
        // 7.释放锁
        unlock(lockKey);
    }
    // 8.返回
    return r;
}

private boolean tryLock(String key) {
    Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", 10, TimeUnit.SECONDS);
    return BooleanUtil.isTrue(flag);
}

private void unlock(String key) {
    stringRedisTemplate.delete(key);
}
  1. 需求:修改根据id查询商铺的业务,基于逻辑过期方式来解决缓存击穿问题

image.png

public <R, ID> R queryWithMutex(String keyPrefix, ID id, Class<R> type, Function<ID, R> dbFallback, Long time, TimeUnit unit) {
    String key = keyPrefix + id;
    // 1.从redis查询商铺缓存
    String shopJson = stringRedisTemplate.opsForValue().get(key);
    // 2.判断是否存在
    if (StrUtil.isNotBlank(shopJson)) {
        // 3.存在,直接返回
        return JSONUtil.toBean(shopJson, type);
    }

    // 4.实现缓存重建
    // 4.1.获取互斥锁
    String lockKey = LOCK_SHOP_KEY + id;
    R r = null;
    try {
        boolean isLock = tryLock(lockKey);
        // 4.2.判断是否获取成功
        if (!isLock) {
            // 4.3.获取锁失败,休眠并重试
            Thread.sleep(50);
            return queryWithMutex(keyPrefix, id, type, dbFallback, time, unit);
        }
        // 4.4.获取锁成功,根据id查询数据库
        r = dbFallback.apply(id);
        // 5.不存在,返回错误
        if (r == null) {
            // 将空值写入redis
            stringRedisTemplate.opsForValue().set(key, "", CACHE_NULL_TTL, TimeUnit.MINUTES);
            // 返回错误信息
            return null;
        }
        // 6.存在,写入redis
        this.set(key, r, time, unit);
    } catch (InterruptedException e) {
        throw new RuntimeException(e);
    } finally {
        // 7.释放锁
        unlock(lockKey);
    }
    // 8.返回
    return r;
}

private boolean tryLock(String key) {
    Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", 10, TimeUnit.SECONDS);
    return BooleanUtil.isTrue(flag);
}

private void unlock(String key) {
    stringRedisTemplate.delete(key);
}