使用Redis实现业务信息缓存(缓存详解,缓存更新策略,缓存三大问题)-更新中
一、什么是缓存?
缓存是一种高效的数据存储方式,它通过将数据保存在内存中来提供快速的读写访问。这种机制特别适用于需要高速数据访问的应用场景,如网站、应用程序和服务。在处理大量数据和高并发请求时, 缓存能显著提高性能和用户体验。
Redis就是一款常用的缓存中间件。
二、如何在业务中结合Redis进行缓存(代码模版)?
1.基本步骤
在业务中结合Redis进行缓存主要有以下步骤:
(1)根据key到redis中查找对应值
(2)若查找到,则直接返回。若redis中没有,则在数据库中进行查找
(3)数据库查找完毕后,先存储查找到的数据到redis中,便于下次查找同样数据的时候,可以直接从redis中获取,不用走数据库,减轻数据库的压力
(4)最后将数据进行返回即可
2.代码模版
这里以查找商铺信息为例,给出结合redis进行商铺信息缓存的代码:
(1)Controller层:
/*** 根据id查询商铺信息* @param id 商铺id* @return 商铺详情数据*/@GetMapping("/{id}")public Result queryShopById(@PathVariable("id") Long id) {return shopService.queryById(id);}
(2)Service层:
@AutowiredStringRedisTemplate stringRedisTemplate;/*** 根据id查询商铺信息* @param id* @return*/@Overridepublic Result queryById(Long id) {//1.从redis查询商铺缓存String key = RedisConstants.CACHE_SHOP_KEY + id;String shopFromRedis = stringRedisTemplate.opsForValue().get(key);//2.判断是否存在if(!StringUtils.isEmpty(shopFromRedis)){//3.redis存在此商铺,返回结果Shop shop = JSONUtil.toBean(shopFromRedis, Shop.class);return Result.ok(shop);}//4.不存在,到数据库中去查询Shop shop = getById(id);//5.数据库不存在,返回错误if(shop == null){return Result.fail("店铺不存在");}//6.数据库存在,写入redisstringRedisTemplate.opsForValue().set(key,JSONUtil.toJsonStr(shop),RedisConstants.CACHE_SHOP_TTL, TimeUnit.MINUTES);//7.返回结果return Result.ok(shop);}
三、缓存更新策略
1.三种缓存更新策略
缓存更新策略主要有:内存淘汰,超时剔除和主动更新这三种。
1.内存淘汰:
简单来说就是Redis根据其内存大小,其淘汰策略(如:键值对的访问频率)等,对键值对进行淘汰。 可以对redis进行配置(如:设置redis内存大小),来控制其内存淘汰。这种方法一般由redis自主控制,因此其一致性也比较差。
2.超时剔除:
简单来说,就是在向redis存入键值对的时候,为这个键值对赋予一个TTL(超时时间),如果超过这个时间,redis就会将这个键值对进行删除。其一致性相较于内存淘汰策略会好一些。
3.主动更新:
也就是在修改数据库的时候对缓存也进行修改。这种方法一致性相较于前两种会好很多,但是需要由程序员自主编码进行控制,会复杂一些。
一般使用主动更新策略为主,结合超时剔除作为兜底,来实现缓存的更新。
2.主动更新详解
2.1.三种主动更新策略
(1)Cache Aside Pattern:
由缓存的调用者,在更新数据库的同时更新缓存
(2)Read/Write Through Pattern:
缓存与数据库整合为一个服务,由服务维护一致性,调用者调用该服务,无需关心缓存一致性问题。但这种方式较为复杂,市面上提供这种服务的框架较少。
(3)Write Behind Caching Pattern:
调用者只操作缓存,由其他线程异步的将缓存的数据持久化到数据库,保证最终一致。这种方式的所有更新操作都是在缓存中进行的,其他线程会按照某种规则(定时/缓存占用达到一定比例)把数据持久化到数据库中,虽然可以减少对数据库的更新操作,但是这种方法也存在一定的弊端:
最新的数据都存储在内存中,一旦redis出现故障,数据会全部丢失,易失性高。
加剧服务器的压力,因为每一个请求都要监视是否需要将缓存持久化到数据库。
总结:
在开发中,一般使用Cache Aside Patter策略,由缓存的调用者,在更新数据库的同时更新缓存。
2.2 Cache Aside Pattern
2.2.1 操作缓存和数据库时三个需要考虑的问题
1.删除缓存还是更新缓存?
- 更新缓存:每次更新数据库都更新缓存,无效写操作较多 ❌
如果更新数据库的次数多,而读取的次数较少,则每次更新时,都会增加无效的更新缓存操作
- 删除缓存:更新数据库时让缓存失效,查询时再更新缓存 ✔️
2.如何保证缓存与数据库的操作的同时成功或失败?
- 单体系统:将缓存与数据库操作放在一个事务
- 分布式系统:利用TCC等分布式事务方案
3.先操作缓存还是先操作数据库?
对缓存的操作比较快,而对数据库的更新操作速度通常会比较慢。
因此,先写数据库在删除缓存,也即先执行慢的操作B,再执行的操作A。这样引发线程安全的线程必须在操作A执行的那段时间达到,相较于先A后B的方式,引发线程安全的概率会小很多。
3.缓存更新策略最终方案
3.1 方案
- 低一致性需求:使用Redis自带的内存淘汰机制
- 高一致性需求:主动更新,并以超时剔除作为兜底方案
读操作:
- 缓存命中则直接返回
- 缓存未命中则查询数据库,并写入缓存,设定超时时间
写操作:
- 先写数据库,然后再删除缓存
- 要确保数据库与缓存操作的原子性
3.2 实现代码:
这里以更新商铺信息功能为例:
/*** 更新店铺信息* @param shop* @return*/@Override@Transactional//保证更新数据库和删除缓存同时成功public Result updateShop(Shop shop) {Long id = shop.getId();if(id==null){return Result.fail("店铺id不能为空");}//1.写入数据库updateById(shop);//2.删除缓存String key = RedisConstants.CACHE_SHOP_KEY + id;stringRedisTemplate.delete(key);return Result.ok();}
四、缓存三大问题(穿透,雪崩、击穿)
1、缓存穿透
1.1
1.2 参考代码
@AutowiredStringRedisTemplate stringRedisTemplate;/*** 根据id查询商铺信息* @param id* @return*/@Overridepublic Result queryById(Long id) {//1.从redis查询商铺缓存String key = RedisConstants.CACHE_SHOP_KEY + id;String shopFromRedis = stringRedisTemplate.opsForValue().get(key);//2.判断是否存在if(!StringUtils.isEmpty(shopFromRedis)){//shopFromRedis不为 null && 不为""的时候为真//3.redis存在此商铺,返回结果Shop shop = JSONUtil.toBean(shopFromRedis, Shop.class);return Result.ok(shop);}if(shopFromRedis != null){//shopFromRedis 为 “”//这个数据在缓存和数据库中不存在,由于之前存储过 “”,直接返回,避免缓存穿透return Result.fail("店铺不存在");}//4.不存在,到数据库中去查询Shop shop = getById(id);//5.数据库不存在,返回错误if(shop == null){//避免缓存穿透,将空值写入redisstringRedisTemplate.opsForValue().set(key,"",RedisConstants.CACHE_NULL_TTL, TimeUnit.MINUTES);return Result.fail("店铺不存在");}//6.数据库存在,写入redisstringRedisTemplate.opsForValue().set(key,JSONUtil.toJsonStr(shop),RedisConstants.CACHE_SHOP_TTL, TimeUnit.MINUTES);//7.返回结果return Result.ok(shop);}