1. 概览
数据库 Join 真的太香了,但由于各种原因,在实际项目中越来越受局限,只能由开发人员在应用层完成。这种繁琐、无意义的“体力劳动”让我们离“快乐生活”越来越远。
1.1. 背景
不知道什么时候,数据库 join 成为了公认的“性能杀手”,对此,很多公司严厉禁止其使用。上有政策下有对策,你的应对之道是什么?
数据库 Join 退出历史舞台,主要由以下几大推动力:
-
微服务。微服务要求“数据资产私有化”,也就是说每个服务的数据库是私有资产,不允许其他服务的直接访问;如果需要访问,只能通过服务所提供的接口完成;
-
分库分表的限制。当数据量超过 MySQL 单实例承载能力时,通常会通过“分库分表”这一技术手段来解决,分库分表后,数据被分散到多个分区中,导致 join 语句失效;
-
性能瓶颈。在高并发情况下,join 存在一定的性能问题,高并发、高性能端场景不适合使用;
不管原因几何,目前,很多大厂已经将 “禁止join” 列入编码规范,我们该如何面对?
只定规范,不给工具,是一种极度不负责任的表现。
1.1.1. 线上问题跟踪
线上 order/list 接口 tp99 超过 2s,严重影响用户体验,同时还有愈演愈烈之势。通过 Trace 系统,发现一个请求居然存在几百甚至上千次 DB 调用!
第一反应,肯定是在 for 循环中调用了 DB,翻看代码果然如此,代码示例如下:
@Override
public List<? extends OrderDetailVO> getByUserId(Long userId) {
List<Order> 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;
}
代码非常简单,只做了几件事:
-
获取用户的 order 信息;
-
遍历每一个 order,为其装配关联数据;
-
返回最终结果;
逻辑非常清晰,单请求数据库访问总次数 = 1(获取用户订单)+ N(订单数量) * 3(需要抓取的关联数据)
可见,N(订单数量) * 3(关联数据数量) 是性能的最大杀手,存在严重的读放大效应。不同的用户,订单数量相差巨大,导致该接口性能差距巨大。
1.1.2. 繁琐、无意义的代码
如何应对?第一反应就是 批量获取,然后在内存中完成 Join。这是一个好的方案,但引入了大量繁琐、无意义的代码。
该问题常规解决方案如下:
@Override
public List<? extends OrderDetailVO> getByUserId(Long userId) {
List<Order> orders = this.orderRepository.getByUserId(userId);
List<OrderDetailVOV2> orderDetailVOS = orders.stream()
.map(order -> new OrderDetailVOV2(OrderVO.apply(order)))
.collect(toList());
List<Long> userIds = orders.stream()
.map(Order::getUserId)
.collect(toList());