缓存定义
缓存(Cache)是数据交换的缓冲区,是存储数据的临时地方,读写性能较高。缓存的作用:降低后端负载并且提高读写时间,降低响应时间。缓存的成本:数据一致性成本,代码维护成本和运维成本。
Redis缓存实战
此代码模拟商户查询,查询商户时首先从redis缓存中查询是否有符合自己id的商户,如果有直接返回,如果没有然后从数据库中查询,如果数据库中查询到对应的数据,然后将该数据添加到redis缓存中,并且返回该数据,如果还是没有查询到就会返回异常信息。代码如下
@GetMapping("/select") public Result select(Long id){ TbShop tbShop = (TbShop) redisTemplate.opsForValue().get("shopcache" + id); if(tbShop!=null){ return Result.success(tbShop); } TbShop tbShop1 = tbShopMapper.selectById(id); if(tbShop1==null){ return Result.error("404","未发现该商铺"); } redisTemplate.opsForValue().set("shopcache"+id,tbShop1,30,TimeUnit.MINUTES); return Result.success(tbShop1); }
对于更新数据库信息,缓存更新策略如下图
这里采用主动更新策略,在更新数据库的时候同时更新缓存。操作缓存和数据库有三个问题需要考虑如下。
1删除缓存还是更新缓存?
更新缓存:每次更新数据库都更新缓存,无效操作比较多
删除缓存:更新数据库让缓存失效,查询时更新缓存,效果比较好
2如何保证缓存与数据库的操作同时成功或者失败?
单体系统:将缓存和数据库放在一个事务中。
分布式系统:利用TCC等分布式事务方案。
3先操作数据库还是缓存?
先删除数据库再操作数据库会遇到如下问题
经过如图所示过程就会出现缓存保存旧的数据的情况。并且这种情况的发生概念是非常高的。
先操作数据库,再删除缓存会出现如下情况
这种情况下也会发生缓存中存入旧数据的情况,但是由于缓存的执行时间远远小于数据库操作数据,所有这种情况的发生概念会比较小,因此可以选择先更新数据库,然后再删除缓存。
代码如下
@Transactional @PostMapping("/update") public Result update(@RequestBody TbShop tbShop){ int update = tbShopMapper.updateById(tbShop); redisTemplate.delete("shopcache"+tbShop.getId()); return Result.success(); }
缓存问题
缓存穿透
缓存穿透是指客户端请求的数据在缓存和数据库中都不存在,这样缓存永远都不会生效,这些请求都会打到数据中。
常见的解决方案:缓存空对象或者布隆过滤。
缓存空对象优点:实现简单,维护方便,缺点:额外的内存消耗,可能造成短期的不一致。
布隆过滤:优点:内存占用少,没有多余的key。缺点:实现复杂和存在误判可能。
缓存雪崩
缓存雪崩是指在同一时段大量的缓存key同时失效或者Redis服务宕机,导致大量请求到达数据库,带来巨大压力。
解决方案
给不同的Key的TTL添加随机值
利用Redis集群提高服务的可用性
给缓存业务添加降级限流策略
给业务添加多级缓存
缓存击穿
缓存击穿问题也叫热点Key问题,就是一个被高并发访问并且缓存重建业务较复杂的key突然失效了,无数的请求访问会在瞬间给数据库带来巨大的冲击。
解决方案:可以通过互斥锁和逻辑过期的方式进行解决。
这里利用互斥锁的方式来进行模拟
互斥锁流程如下图
代码如下
@GetMapping("/select1") public Result select1(Long id){ TbShop tbShop = (TbShop) redisTemplate.opsForValue().get("shopcache" + id); if(tbShop!=null){ return Result.success(tbShop); } TbShop tbShop1 = null; String lockkey = "lockshop"+id; try { boolean trylock = trylock(lockkey); if(!trylock){ Thread.sleep(50); return select(id); } tbShop1 = tbShopMapper.selectById(id); System.out.println("数据库查询"); Thread.sleep(200); if(tbShop1==null){ return Result.error("404","未发现该商铺"); } redisTemplate.opsForValue().set("shopcache"+id,tbShop1,30,TimeUnit.MINUTES); } catch (InterruptedException e) { e.printStackTrace(); } finally { unlock(lockkey); } return Result.success(tbShop1); } private boolean trylock(String key){ Boolean aBoolean = redisTemplate.opsForValue().setIfAbsent(key, "1", 10, TimeUnit.SECONDS); return BooleanUtil.isTrue(aBoolean); } private void unlock(String key){ redisTemplate.delete(key); }
通过jmeter压测工具使用一千多个线程进行测试,后端仅仅只是执行了一次调用数据库操作,实验结果因此得到验证。