2023.12.7
redis实现商户查询缓存
在企业开发中,用户的访问量动辄成百上千万,如果没有缓存机制,数据库将承受很大的压力。本章我们使用redis来实现商户查询缓存。
原来的操作是根据商铺id直接从数据库查询商铺信息,为了防止频繁地对数据库访问,我们使用redis进行缓存,大致流程图如下:
需要改变的地方就两个:①之前是直接从数据库中查,现在是先尝试从redis中查,没查到再去查数据库。②如果查数据库查到了的话,需要将查到的商铺数据先存到redis中,再将数据返回。 代码如下:
@Service
public class ShopServiceImpl extends ServiceImpl<ShopMapper, Shop> implements IShopService {@Resourceprivate StringRedisTemplate stringRedisTemplate;@Overridepublic Result queryById(Long id) {String key = CACHE_SHOP_KEY + id;//1、从redis查询商铺缓存String shopJson = stringRedisTemplate.opsForValue().get(key);//2、判断是否存在if(StrUtil.isNotBlank(shopJson)){//3、存在,直接返回Shop shop = JSONUtil.toBean(shopJson, Shop.class);return Result.ok(shop);}//4、不存在,根据id查询数据库Shop shop = getById(id);//5、数据库没查到数据,返回错误信息if (shop == null){return Result.fail("商铺不存在!");}//6、数据库查到信息了,写入redis并返回商铺信息stringRedisTemplate.opsForValue().set(key,JSONUtil.toJsonStr(shop));return Result.ok(shop);}
}
redis实现商户类型数据缓存
解决商户数据缓存之后,我们趁热打铁也完成一下商户类型数据缓存,即下面这张图中数据的缓存:
而且这个页面数据也不会经常变动,很适合做缓存,需要变更的代码如下:
首先修改 ShopTypeController.java文件,原来是直接从数据库中查数据,这里我们在Controller中自定义一个方法,在service实现类中去编写具体业务代码:
@RestController
@RequestMapping("/shop-type")
public class ShopTypeController {@Resourceprivate IShopTypeService typeService;@GetMapping("list")public Result queryTypeList() {
// List<ShopType> typeList = typeService
// .query().orderByAsc("sort").list();
// return Result.ok(typeList);return typeService.queryList();}
}
对应的接口需要增加该方法:
public interface IShopTypeService extends IService<ShopType> {Result queryList();
}
在对应的实现类ShopTypeServiceImpl.java中编写具体业务代码:
@Service
public class ShopTypeServiceImpl extends ServiceImpl<ShopTypeMapper, ShopType> implements IShopTypeService {@Resourceprivate StringRedisTemplate stringRedisTemplate;@Overridepublic Result queryList() {//1.尝试从redis中查询商户类型数据List<String> shopTypes = stringRedisTemplate.opsForList().range(CACHE_SHOP_TYPE_KEY, 0, -1);//2.在redis中查到数据了,返回ShopType类型数据if(!shopTypes.isEmpty()){List<ShopType> list = new ArrayList<>();for(String shopType : shopTypes){ShopType bean = JSONUtil.toBean(shopType, ShopType.class);list.add(bean);}return Result.ok(list);}//3.在redis中没查到数据,那就去数据库查List<ShopType> list = query().orderByAsc("sort").list(); //从数据库中按照sort字段升序查询//3.1 数据库也没查到,返回错误信息if(list == null){return Result.fail("店铺类型不存在!");}//3.2 数据库查到数据了,存入redis中并返回给用户for (ShopType shopType : list){String jsonStr = JSONUtil.toJsonStr(shopType);shopTypes.add(jsonStr);}stringRedisTemplate.opsForList().leftPushAll(CACHE_SHOP_TYPE_KEY,shopTypes);return Result.ok(list);}
}
本人新手用的笨方法for-each循环逐个转换,高手可以用stream流来简化代码。
缓存更新策略
由于内存资源比较宝贵,向其插入过多数据的话可能导致内存空间爆满,所以需要某种机制对内存的部分数据进行更新或者移除。下面介绍三种缓存更新数据:
内存淘汰
:Redis自动进行,当Redis内存大到某个阈值时,会自动触发淘汰机制,淘汰掉一些不重要的数据(这个机制可以自定义)超时剔除
:当我们给Redis设置了过期时间TTL之后,Redis会将超时的数据进行删除。主动更新
:我们可以手动调用方法把缓存删除掉,通常用于解决缓存和数据库不一致问题,该方法一致性较好,但是维护成本高。
业务场景:
- 在低一致性场景下:使用内存淘汰机制,因为该场景下的数据很长一段时间都不需要更新。
- 在高一致性场景下:使用主动更新策略,即自己编写代码实现高一致性,但也不能100%的保证一致性,所以还需要使用超时剔除策略兜底。
数据库与缓存不一致的解决方案
由于我们的缓存数据来自数据库,而数据库的数据是会发生变化的,因此,如果当数据库中数据发生变化,而缓存却没有同步更新,此时存在数据的一致性问题。
有三种解决方案:
- Cache Aside Pattern 人工编码方式:缓存调用者在更新完数据库之后再去更新缓存,也称之为双写方案
- Read/Write Through Pattern:缓存与数据库整合为一个服务,由服务来维护一致性。调用者调用该服务,无需关心缓存一致性问题。但是维护这样一个服务很复杂,市面上也不容易找到这样的一个现成的服务,开发成本高
- Write Behind Caching Pattern:调用者只操作缓存,其他线程去异步处理数据库,最终实现一致性。但是维护这样的一个异步的任务很复杂,需要实时监控缓存中的数据更新,其他线程去异步更新数据库也可能不太及时,而且缓存服务器如果宕机,那么缓存的数据也就丢失了
实际开发中,一般还是使用方案一,但是如果我们每次操作完数据库之后,都去更新一下缓存,而此期间并没有人查询数据,那么这个更新动作意义就不大了,所以我们可以把缓存直接删除,等到有人再次查询时,再更新缓存。
还有个问题,我们应该先删缓存还是先更新数据库呢?理论上是都可以,如果先删缓存再更新数据库的话,由于删缓存的速度比更新数据库的速度快很多,所以两个操作之间有一段较长的空档期,此期间如果有其他线程进来查询数据库的话查的就是脏数据了。先更新数据库再删缓存当然也存在安全问题,但是几率会比上述小很多,这里不再细说,结论就是采用先更新数据库再删缓存的策略。
实现商铺缓存与数据库的双写一致
主要需要修改两处地方:
- 根据id查询商铺时,将数据库结果写入缓存时,需要设置超时时间。(超时剔除策略)
- 根据id修改店铺时,先修改数据库,再删除缓存。
在ShopServiceImpl.java代码中设置超时时间:
stringRedisTemplate.opsForValue().set(key,JSONUtil.toJsonStr(shop),CACHE_SHOP_TTL, TimeUnit.MINUTES);
修改店铺操作时,先修改数据库,再删除缓存:
@Override@Transactionalpublic 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();}