通过缓存优化系统性能

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第3天,点击查看活动详情

概述

经过SQL优化和添加索引后,几乎都会提升数据库的查询速度,SQL的一次查询应该稳定在300ms以内。随着系统在线上运行的时间越长,数据量越来越大,当出现业务高峰的时候,以前毫秒级的SQL偶尔出现秒级的响应时间,出现这样的情况是因为高峰期的时候这台服务器的资源占用非常高,瞬时大量的请求密集的打到MySQL数据库上,导致数据库服务器的CPU和内存占用率迅速飙升,导致数据库查询非常慢。这种由于资源导致的性能问题,可以通过添加机器配置来获取更多的资源,但是硬件资源是有上限的,不可能通过无限制的提高配置来解决此类问题。这个时候就可以考虑添加缓存的方案来提高系统的整体运行性能。

添加缓存主要是尽量避免大量的读请求密集的打到MySQL服务上,通过缓存承载大量的读请求,数据库承载商量的去请求和全部的写请求,实现了MySQL的流量削峰问题。

image.png

提高缓存命中率

缓存能大大提高系统的读性能,但是系统引入缓存中间件后增加了系统的复杂性。引入缓存后实际使用的过程中需要关注缓存命中率,缓存命中率是衡量缓存有效性的重要指标,命中率越高缓存的使用率也就越高。

所以业务端需要考虑如何提高缓存的命中率。

  • 选择合适的业务场景。缓存适合读多写少的业务场景,高频的查询场景。比如查询订单列表、订单详情。
  • 合理设置缓存容量大小。缓存容量设置较小,会导致缓存频繁的触发内存淘汰机制,导致刚缓存的数据在很短的时间内就被删除掉了,下次查询的时候依然不能通过缓存获取。缓存的容量尽量保证能缓存15%-30%的业务数据。
  • 控制缓存的粒度。单个key的数据单位越小,缓存就不会容易被修改。粒度越小,缓存命中率越高。
  • 合理设置缓存过期时间。避免引起缓存击穿问题,一般会设置缓存的过期时间为1个小时,如果没有命中缓存直接查询数据库的操作一定要加分布式锁,采用双重检查锁机制,同一时间只有一个请求到数据库,查询到数据后返回响应结果,并重新添加缓存,后面的请求再从缓存中查询。
  • 避免缓存穿透。如果redis中没有数据,就会查询MySQL数据库,如果MySQL中也没有数据,如果相同的查询大量的进行访问,就会一直请求MySQL,这就会把数据库直接打宕机。如果查询结果为空也可以添加缓存,为查询的请求设置一个空对象缓存。
  • 做好缓存预热。系统上线的时候提前将部分数据刷入到缓存中,让第一次查询直接走缓存。

缓存实战

因为历史订单几乎不会存在修改的场景,不会被修改。所以查询历史订单的业务场景适合做缓存处理。下面就通过查询历史订单的代码演示如何添加缓存。

  • controller
@GetMapping("getOrderDetail")
public DataResponse getOrderDetail(@RequestParam("orderNo") String orderNo) {
  try {
    long startTime = System.currentTimeMillis();
    OrderDetailVO orderDetailVOList = userOrderInfoService.getOrderDetail(orderNo);
    long endTime = System.currentTimeMillis();
    log.info("查询用户订单明细耗时:[{}]" , (endTime - startTime));
    return DataResponse.success(orderDetailVOList);
  } catch (BaseException e) {
    return DataResponse.error(e.getMessage());
  } catch (Exception e) {
    log.error("getOrderDetail error: [{}]", e.getMessage(), e);
    return DataResponse.error(OrderCode.QUERY_ORDER_ERROR.getDesc());
  }

}
复制代码
  • service
public OrderDetailVO getOrderDetail(String orderNo) {
  // 查询缓存
  OrderDetailVO orderDetail = (OrderDetailVO) redisUtils.get(OrderConstant.OrderRedisKey.PREFIX_KEY + orderNo);
  if (Objects.isNull(orderDetail)) {
    // 缓存不存在 查询数据库
    orderDetail = userOrderRepository.getOrderDetail(orderNo);
    // 添加缓存 即使返回null也缓存 防止缓存穿透
    redisUtils.set(OrderConstant.OrderRedisKey.PREFIX_KEY + orderDetail.getOrderNo(), orderDetail.getOrderNo(), 30L, TimeUnit.SECONDS);
  }
  return orderDetail;
}
复制代码
  • repository
public OrderDetailVO getOrderDetail(String orderNo) {
  //1.查询订单基础信息
  OrderDetailVO orderDetailVO = userOrderInfoMapper.getOrderInfoByNo(orderNo);
  //2.判断订单是否为空
  if (Objects.isNull(orderDetailVO)) {
    return null;
  }
  // 3.根据订单号查询出所有的订单详细信息
  List<OrderItemDetailDto> orderItemDetailList = userOrderItemDetailMapper.getOrderItemDetailList(orderNo);
  //4.返回结果集设置订单详情信息
  orderDetailVO.setOrderItemDetails(orderItemDetailList);
  return orderDetailVO;
}
复制代码

mapper、xml省略

image-20220728224314473.png

总结

以上分析了系统在运行在什么时候什么场景下适合引入缓存,引入缓存后可能带来的问题以及相应的解决方案描述,最后给出了一个实际使用的代码示例。

简单描述一下项目的分层,很多的项目采用controller/service/mapper三层架构进行开发。随着项目的发展引入的中间件越来越多,比如引入缓存、搜索、消息等,如果还是采用三层架构,根据开发习惯controller只进行简单的请求接受和结果响应,有的项目也可能在controller中增加一层校验的功能,mapper主要是用于数据库的操作接口可操作性不是很大,索引大量的业务代码就自然而然的涌入到了service中,项目变得复杂后,service中就包含业务逻辑处理、mapper查询的数据处理、缓存处理、搜索处理、消息处理、openFeign调用等。这样service就会变得相对臃肿复杂,扩展和维护将会是一个问题。

在原来的基础上增加repository层,repository中通过mapper查询数据并进行简单的对象转换处理;增加client层处理第三方接口调用并进行简单的数据处理为service提供数据;缓存和搜索服务通过封装相关的API服务进行单独维护。

image.png

猜你喜欢

转载自juejin.im/post/7125628170209394724