数据库 Join 真的太香了,但由于各种原因,在实际项目中越来越受局限,只能由开发人员在应用层完成。这种繁琐、无意义的“体力劳动”让我们离“快乐生活”越来越远。
不知道什么时候,数据库 join 成为了公认的“性能杀手”,对此,很多公司严厉禁止其使用。上有政策下有对策,你的应对之道是什么?
数据库 Join 退出历史舞台,主要由以下几大推动力:
不管原因几何,目前,很多大厂已经将 “禁止join” 列入编码规范,我们该如何面对?
只定规范,不给工具,是一种极度不负责任的表现。
线上 order/list 接口 tp99 超过 2s,严重影响用户体验,同时还有愈演愈烈之势。通过 Trace 系统,发现一个请求居然存在几百甚至上千次 DB 调用!
第一反应,肯定是在 for 循环中调用了 DB,翻看代码果然如此,代码示例如下:
@Overridepublic List<? extends OrderDetailVO> getByUserId(Long userId) { List orders = this.orderRepository.getByUserId(userId); return orders.stream() .map(order -> convertToOrderDetailVO(order)) .collect(toList());}private OrderDetailVOV1 convertToOrderDetailVO(Order order) { OrderVO orderVO = OrderVO.apply(order); OrderDetailVOV1 orderDetailVO = new OrderDetailVOV1(orderVO); Address address = this.addressRepository.getById(order.getAddressId()); AddressVO addressVO = AddressVO.apply(address); orderDetailVO.setAddress(addressVO); User user = this.userRepository.getById(order.getUserId()); UserVO userVO = UserVO.apply(user); orderDetailVO.setUser(userVO); Product product = this.productRepository.getById(order.getProductId()); ProductVO productVO = ProductVO.apply(product); orderDetailVO.setProduct(productVO); return orderDetailVO;}
代码非常简单,只做了几件事:
逻辑非常清晰,单请求数据库访问总次数 = 1(获取用户订单)+ N(订单数量) * 3(需要抓取的关联数据)
可见,N(订单数量) * 3(关联数据数量) 是性能的最大杀手,存在严重的读放大效应。不同的用户,订单数量相差巨大,导致该接口性能差距巨大。
如何应对?第一反应就是 批量获取,然后在内存中完成 Join。这是一个好的方案,但引入了大量繁琐、无意义的代码。
该问题常规解决方案如下:
@Overridepublic List<? extends OrderDetailVO> getByUserId(Long userId) { List orders = this.orderRepository.getByUserId(userId); List orderDetailVOS = orders.stream() .map(order -> new OrderDetailVOV2(OrderVO.apply(order))) .collect(toList()); List userIds = orders.stream() .map(Order::getUserId) .collect(toList()); List users = this.userRepository.getByIds(userIds); Map userMap = users.stream() .collect(toMap(User::getId, Function.identity(), (a, b) -> a)); for (OrderDetailVOV2 orderDetailVO : orderDetailVOS){ User user = userMap.get(orderDetailVO.getOrder().getUserId()); UserVO userVO = UserVO.apply(user); orderDetailVO.setUser(userVO); } List addressIds = orders.stream() .map(Order::getAddressId) .collect(toList()); List addresses = this.addressRepository.getByIds(addressIds); Map addressMap = addresses.stream() .collect(toMap(Address::getId, Function.identity(), (a, b) -> a)); for (OrderDetailVOV2 orderDetailVO : orderDetailVOS){ Address address = addressMap.get(orderDetailVO.getOrder().getAddressId()); AddressVO addressVO = AddressVO.apply(address); orderDetailVO.setAddress(addressVO); } List productIds = orders.stream() .map(Order::getProductId) .collect(toList()); List products = this.productRepository.getByIds(productIds); Map productMap = products.stream() .collect(toMap(Product::getId, Function.identity(), (a, b) -> a)); for (OrderDetailVOV2 orderDetailVO : orderDetailVOS){ Product product = productMap.get(orderDetailVO.getOrder().getProductId()); ProductVO productVO = ProductVO.apply(product); orderDetailVO.setProduct(productVO); } return orderDetailVOS;}
相对上一版本,代码量和复杂性提升不少,每一处核心代码逻辑基本一致,主要包括:
经过改造,单请求中数据库访问总次数 = 1(获取用户订单)+ 3(关联数据数量)。数据库访问总次数大大降低,性能提升明显。
聪明的伙伴可能马上会提出,上面方案还有优化空间,引入多线程并行执行 内存 join。
非常优秀,多线程引入会再次提升性能,但也提升了系统复杂性(并发安全性、资源配置等)。先准再快,建议有必要时再引入。
代码调整如下:
@Overridepublic List<? extends OrderDetailVO> getByUserId(Long userId) { List orders = this.orderRepository.getByUserId(userId); List orderDetailVOS = orders.stream() .map(order -> new OrderDetailVOV2(OrderVO.apply(order))) .collect(toList()); List> callables = Lists.newArrayListWithCapacity(3); callables.add(() -> { bindUser(orders, orderDetailVOS); return null; }); callables.add(() ->{ bindAddress(orders, orderDetailVOS); return null; }); callables.add(() -> { bindProduct(orders, orderDetailVOS); return null; }); this.executorService.invokeAll(callables); return orderDetailVOS;}private void bindProduct(List orders, List orderDetailVOS) { List productIds = orders.stream() .map(Order::getProductId) .collect(toList()); List products = this.productRepository.getByIds(productIds); Map productMap = products.stream() .collect(toMap(Product::getId, Function.identity(), (a, b) -> a)); for (OrderDetailVOV2 orderDetailVO : orderDetailVOS){ Product product = productMap.get(orderDetailVO.getOrder().getProductId()); ProductVO productVO = ProductVO.apply(product); orderDetailVO.setProduct(productVO); }}private void bindAddress(List orders, List orderDetailVOS) { List addressIds = orders.stream() .map(Order::getAddressId) .collect(toList()); List addresses = this.addressRepository.getByIds(addressIds); Map addressMap = addresses.stream() .collect(toMap(Address::getId, Function.identity(), (a, b) -> a)); for (OrderDetailVOV2 orderDetailVO : orderDetailVOS){ Address address = addressMap.get(orderDetailVO.getOrder().getAddressId()); AddressVO addressVO = AddressVO.apply(address); orderDetailVO.setAddress(addressVO); }}private void bindUser(List orders, List orderDetailVOS) { List userIds = orders.stream() .map(Order::getUserId) .collect(toList()); List users = this.userRepository.getByIds(userIds); Map userMap = users.stream() .collect(toMap(User::getId, Function.identity(), (a, b) -> a)); for (OrderDetailVOV2 orderDetailVO : orderDetailVOS){ User user = userMap.get(orderDetailVO.getOrder().getUserId()); UserVO userVO = UserVO.apply(user); orderDetailVO.setUser(userVO); }}
可见,复杂性又提升不少。
能否做的更好?我们先列下小目标:
在项目中引入 joininmemory-starter,具体如下:
com.geekhalo.lego lego-starter-joininmemory 0.0.1-SNAPSHOT
在结果 Bean 的属性上添加 @JoinInMemory 注解,具体如下:
@Datapublic class OrderDetailVOV4 extends OrderDetailVO { private final OrderVO order; @JoinInMemory(keyFromSourceData = "#{order.userId}", keyFromJoinData = "#{id}", loader = "#{@userRepository.getByIds(#root)}", dataConverter = "#{T(com.geekhalo.lego.joininmemory.web.UserVO).apply(#root)}" ) private UserVO user; @JoinInMemory(keyFromSourceData = "#{order.addressId}", keyFromJoinData = "#{id}", loader = "#{@addressRepository.getByIds(#root)}", dataConverter = "#{T(com.geekhalo.lego.joininmemory.web.AddressVO).apply(#root)}" ) private AddressVO address; @JoinInMemory(keyFromSourceData = "#{order.productId}", keyFromJoinData = "#{id}", loader = "#{@productRepository.getByIds(#root)}", dataConverter = "#{T(com.geekhalo.lego.joininmemory.web.ProductVO).apply(#root)}" ) private ProductVO product;}
JoinInMemory 注解定义如下:
@Target({ElementType.FIELD, ElementType.TYPE})@Retention(RetentionPolicy.RUNTIME)public @interface JoinInMemory { /** * 从 sourceData 中提取 key * @return */ String keyFromSourceData(); /** * 从 joinData 中提取 key * @return */ String keyFromJoinData(); /** * 批量数据抓取 * @return */ String loader(); /** * 结果转换器 * @return */ String joinDataConverter() default ""; /** * 运行级别,同一级别的 join 可 并行执行 * @return */ int runLevel() default 10;}
JoinInMemory 注解属性有些多,以 UserVO 为例,解释如下:
@JoinInMemory(keyFromSourceData = "#{order.userId}", keyFromJoinData = "#{id}", loader = "#{@userRepository.getByIds(#root)}", joinDataConverter = "#{T(com.geekhalo.lego.joininmemory.web.UserVO).apply(#root)}" )private UserVO user;
属性 | 含义 |
keyFromSourceData = "#{order.userId}" | 以 order 中的 userId 作为 JoinKey |
keyFromJoinData = "#{id}" | 以 user 的 id 作为 JoinKey |
loader = "#{@userRepository.getByIds(#root)}" | 将 userRepository bean 的 getByIds 方法作为加载器,其中 #root 为 joinKey 集合(user id 集合) |
joinDataConverter = "#{T(com.geekhalo.lego.joininmemory.web.UserVO).apply(#root)}" | 将 com.geekhalo.lego.joininmemory.web.UserVO 静态方法 apply 作为转换器,#root 指的是 User 对象 |
配置中用到大量的 SpEL 表达式,不熟悉的同学可以自行 Google;
@JoinInMemory 注解赋予 OrderDetailVOV4 自动 Join 的能力,具体使用如下:
@Overridepublic List<? extends OrderDetailVO> getByUserId(Long userId) { List orders = this.orderRepository.getByUserId(userId); List orderDetailVOS = orders.stream() .map(order -> new OrderDetailVOV4(OrderVO.apply(order))) .collect(toList()); // 执行关联数据抓取 this.joinService.joinInMemory(OrderDetailVOV4.class, orderDetailVOS); return orderDetailVOS;}
其中,this.joinService.joinInMemory(OrderDetailVOV4.class, orderDetailVOS); 完成对 orderDetailVOS 关联数据的组装。
@JoinInMemory 注解属性过多,使用起来过于繁琐,同时有很多属性是通用的,分散到各处不利于维护,此时,建议使用 Spring AliasFor 对其进行简化。
首先,新建自定义注解
@Target(ElementType.FIELD)@Retention(RetentionPolicy.RUNTIME)@JoinInMemory(keyFromSourceData = "", keyFromJoinData = "#{id}", loader = "#{@userRepository.getByIds(#root)}", joinDataConverter = "#{T(com.geekhalo.lego.joininmemory.web.UserVO).apply(#root)}")public @interface JoinUserVOOnId { @AliasFor( annotation = JoinInMemory.class ) String keyFromSourceData();}
在新注解上,添加 @JoinInMemory 完成对通用属性的配置;
新增属性,使用 @AliasFor 为 @JoinInMemory 进行个性化配置;
使用自定义注解的新 OrderDetailVO 如下:
@Datapublic class OrderDetailVOV5 extends OrderDetailVO { private final OrderVO order; @JoinUserVOOnId(keyFromSourceData = "#{order.userId}") private UserVO user; @JoinAddressVOOnId(keyFromSourceData = "#{order.addressId}") private AddressVO address; @JoinProductVOOnId(keyFromSourceData = "#{order.productId}") private ProductVO product;}
其他使用方式不变,相对于底层的 @JoinInMemory,配置简化不少;
如果需要使用并行处理方案进一步提升性能,也非常简单,只需在 OrderDetailVO 上新增一个注解即可,具体如下:
@Data@JoinInMemoryConfig(executorType = JoinInMemeoryExecutorType.PARALLEL)public class OrderDetailVOV6 extends OrderDetailVO { private final OrderVO order; @JoinUserVOOnId(keyFromSourceData = "#{order.userId}") private UserVO user; @JoinAddressVOOnId(keyFromSourceData = "#{order.addressId}") private AddressVO address; @JoinProductVOOnId(keyFromSourceData = "#{order.productId}") private ProductVO product;}
其他部分不变,其中 @JoinInMemoryConfig 有如下几个属性:
属性 | 含义 |
executorType | PARALLEL 并行执行;SERIAL 串行执行 |
executorName | 执行器名称,并行执行所使用的线程池名称,默认为 defaultExecutor |
测试环境简单如下:
简单对比性能如下:
方案 | 耗时 |
for + 单条抓取 | 1130ms |
批量 + 内存join (手工) | 42ms |
批量 + 内存join (手工) + 并行 | 16ms |
@JoinInMemory | 50ms |
@自定义注解 | 48ms |
@自定义注解 + 并行 | 24ms |
附上项目地址:https://gitee.com/litao851025/lego
留言与评论(共有 0 条评论) “” |