日韩成人免费在线_国产成人一二_精品国产免费人成电影在线观..._日本一区二区三区久久久久久久久不

當(dāng)前位置:首頁 > 科技  > 軟件

DDD死黨:內(nèi)存Join——將復(fù)用和擴(kuò)展用到極致

來源: 責(zé)編: 時間:2023-12-14 16:38:07 252觀看
導(dǎo)讀1. 為什么"內(nèi)存Join"是個無法繞過的話題首先,我們先簡單解釋下,什么是“內(nèi)存Join”。相信大家對關(guān)系數(shù)據(jù)庫的 join 語句肯定不陌生,其作用就是通過關(guān)聯(lián)關(guān)系從多個表中查詢數(shù)據(jù),關(guān)聯(lián)條件和數(shù)據(jù)聚合全部由 數(shù)據(jù)庫服務(wù)完成。

1. 為什么"內(nèi)存Join"是個無法繞過的話題

首先,我們先簡單解釋下,什么是“內(nèi)存Join”。a7g28資訊網(wǎng)——每日最新資訊28at.com

相信大家對關(guān)系數(shù)據(jù)庫的 join 語句肯定不陌生,其作用就是通過關(guān)聯(lián)關(guān)系從多個表中查詢數(shù)據(jù),關(guān)聯(lián)條件和數(shù)據(jù)聚合全部由 數(shù)據(jù)庫服務(wù)完成。a7g28資訊網(wǎng)——每日最新資訊28at.com

a7g28資訊網(wǎng)——每日最新資訊28at.com

而 內(nèi)存 Join,簡單來說就是把原本數(shù)據(jù)庫幫我們完成的數(shù)據(jù)聚合操作遷移到應(yīng)用服務(wù),在應(yīng)用服務(wù)的內(nèi)存中完成。a7g28資訊網(wǎng)——每日最新資訊28at.com

a7g28資訊網(wǎng)——每日最新資訊28at.com

數(shù)據(jù)庫join非常簡單,但隨著系統(tǒng)的發(fā)展,內(nèi)存join變得越來越重要,其核心驅(qū)動力有:a7g28資訊網(wǎng)——每日最新資訊28at.com

  1. 微服務(wù)。微服務(wù)要求“數(shù)據(jù)資產(chǎn)私有化”,也就是說每個服務(wù)的數(shù)據(jù)庫是私有資產(chǎn),不允許其他服務(wù)的直接訪問。如果需要訪問,只能通過服務(wù)所提供的接口完成
  2. 分庫分表的限制。當(dāng)數(shù)據(jù)量超過 MySQL 單實例承載能力時,通常會通過“分庫分表”這一技術(shù)手段來解決,分庫分表后,數(shù)據(jù)被分散到多個分區(qū)中,導(dǎo)致 join 語句失效
  3. 性能瓶頸。在高并發(fā)情況下,join 存在一定的性能問題,高并發(fā)、高性能端場景不適合使用。很多公司規(guī)范中對 join 的使用做出了明確的限制

2. 課程先導(dǎo)

發(fā)現(xiàn)變化,封裝變化,管理變化,是開發(fā)人員的必備技能。a7g28資訊網(wǎng)——每日最新資訊28at.com

本篇文章從查詢訂單這個業(yè)務(wù)場景為入口,針對數(shù)據(jù)的內(nèi)存join進(jìn)行多次抽象和封裝,最終實現(xiàn)“內(nèi)存Join聲明化”。a7g28資訊網(wǎng)——每日最新資訊28at.com

首先,先看下最終的效果,從直觀上感受下“抽象”帶來的效率提升。a7g28資訊網(wǎng)——每日最新資訊28at.com

a7g28資訊網(wǎng)——每日最新資訊28at.com

通過抽象,可以達(dá)到如下效果:a7g28資訊網(wǎng)——每日最新資訊28at.com

  1. 左邊一坨“模板代碼” 等價于右邊一個注解
  2. 模型需要綁定 UserVO 數(shù)據(jù),只需使用 @JoinUserVOOnId 注解進(jìn)行聲明配置即可
  3. @JoinInMemoryConfig 注解的 PARALLEL 配置將開啟多線程并行處理,以提供性能

神秘背后的本質(zhì)便是“抽象”。讓我們以訂單查詢?yōu)榫€索,層層遞進(jìn),最終實現(xiàn)“能力聲明化”。a7g28資訊網(wǎng)——每日最新資訊28at.com

能力聲明化,是抽象的一種高級表現(xiàn),無需編寫代碼,通過配置的方式為特定組件進(jìn)行能力加強(qiáng)。a7g28資訊網(wǎng)——每日最新資訊28at.com

在正式開始之前,可以先了解下整體的推演流程:a7g28資訊網(wǎng)——每日最新資訊28at.com

a7g28資訊網(wǎng)——每日最新資訊28at.com

3.【案例分析】訂單查詢

假設(shè),我們是訂單中心的一位研發(fā)伙伴,需要開發(fā) “我的訂單” 模塊,其核心接口包括:a7g28資訊網(wǎng)——每日最新資訊28at.com

  1. 我的訂單,查詢用戶的全部訂單,包括 訂單信息、用戶信息、郵寄地址信息、商品信息等;
  2. 訂單詳情,查詢某個訂單的詳細(xì)信息,包括 訂單信息、用戶信息、郵寄地址信息、商品信息、支付信息等;

根據(jù)需求定義 OrderService 接口如下:a7g28資訊網(wǎng)——每日最新資訊28at.com

public interface OrderService {      // 我的訂單      List<OrderListVO> getByUserId(Long userId);      // 訂單詳情      OrderDetailVO getDetailByOrderId(Long orderId);  }    // 為配合多種實現(xiàn)策略,使用抽象類進(jìn)行統(tǒng)一  public abstract class OrderListVO {      public abstract OrderVO getOrder();        public abstract UserVO getUser();        public abstract AddressVO getAddress();        public abstract ProductVO getProduct();  }    // 為配合多種實現(xiàn)策略,使用抽象類進(jìn)行統(tǒng)一  public abstract class OrderDetailVO {      public abstract OrderVO getOrder();        public abstract UserVO getUser();        public abstract AddressVO getAddress();        public abstract ProductVO getProduct();        public abstract List<PayInfoVO> getPayInfo();  }

3.1. Foreach + 單條抓取方案

這么簡單的需求,那不是信手拈來,很快就提供了一版a7g28資訊網(wǎng)——每日最新資訊28at.com

a7g28資訊網(wǎng)——每日最新資訊28at.com

代碼具體如下:a7g28資訊網(wǎng)——每日最新資訊28at.com

@Service  public class OrderServiceCodingV1 implements OrderService {      @Autowired      private OrderRepository orderRepository;      @Autowired      private AddressRepository addressRepository;      @Autowired      private ProductRepository productRepository;      @Autowired      private UserRepository userRepository;      @Autowired      private PayInfoRepository payInfoRepository;        @Override      public List<OrderListVO> getByUserId(Long userId) {          // 獲取用戶訂單          List<Order> orders = this.orderRepository.getByUserId(userId);          // 依次進(jìn)行數(shù)據(jù)綁定          return orders.stream()                  .map(order -> convertToOrderListVO(order))                  .collect(toList());      }        private OrderListVOCodingV1 convertToOrderListVO(Order order) {          OrderVO orderVO = OrderVO.apply(order);            OrderListVOCodingV1 orderDetailVO = new OrderListVOCodingV1(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;      }        @Override      public OrderDetailVO getDetailByOrderId(Long orderId) {          // 暫時忽略          Order order = this.orderRepository.getById(orderId);          return convertToOrderDetailVO(order);      }        private OrderDetailVO convertToOrderDetailVO(Order order) {          OrderDetailVOCodingV1 orderDetail = new OrderDetailVOCodingV1(OrderVO.apply(order));          // 獲取地址并進(jìn)行綁定          Address address = this.addressRepository.getById(order.getAddressId());          AddressVO addressVO = AddressVO.apply(address);          orderDetail.setAddress(addressVO);          // 獲取用戶并進(jìn)行綁定          User user = this.userRepository.getById(order.getUserId());          UserVO userVO = UserVO.apply(user);          orderDetail.setUser(userVO);          // 獲取商品并進(jìn)行綁定          Product product = this.productRepository.getById(order.getProductId());          ProductVO productVO = ProductVO.apply(product);          orderDetail.setProduct(productVO);          // 獲取支付信息并進(jìn)行綁定          List<PayInfo> payInfos = this.payInfoRepository.getByOrderId(order.getId());          List<PayInfoVO> payInfoVOList = payInfos.stream()                  .map(PayInfoVO::apply)                  .collect(toList());          orderDetail.setPayInfo(payInfoVOList);          return orderDetail;      }    }

如果真的這樣實現(xiàn),那你離“被跑路”不遠(yuǎn)了。a7g28資訊網(wǎng)——每日最新資訊28at.com

為什么會這么說呢?因為 ==“我的訂單”這個接口存在嚴(yán)重的性能問題!==a7g28資訊網(wǎng)——每日最新資訊28at.com

“我的訂單”接口具體實現(xiàn)如下:a7g28資訊網(wǎng)——每日最新資訊28at.com

  1. 查詢 order 信息
  2. 依次對其進(jìn)行數(shù)據(jù)抓取
  3. 完成數(shù)據(jù)綁定并返回結(jié)果

單個用戶請求,數(shù)據(jù)庫訪問總次數(shù) = 1(獲取用戶訂單)+ N(訂單數(shù)量) * 3(需要抓取的關(guān)聯(lián)數(shù)據(jù))a7g28資訊網(wǎng)——每日最新資訊28at.com

其中,N(訂單數(shù)量) * 3(關(guān)聯(lián)數(shù)據(jù)數(shù)量) 存在性能隱患,存在嚴(yán)重的==讀放大效應(yīng)==。一旦遇到忠實用戶,存在成百上千訂單,除了超時別無辦法。a7g28資訊網(wǎng)——每日最新資訊28at.com

“訂單詳情”接口實現(xiàn),目前問題不大,最大的問題為:“訂單詳情”與“我的訂單”兩個接口存在大量的重復(fù)邏輯!a7g28資訊網(wǎng)——每日最新資訊28at.com

3.2. 批量查詢 + 內(nèi)存Join

首先,我們先來解決 “我的訂單”接口的性能問題。從之前的分析可知,性能低下的根本原因在于 “讀放大效應(yīng)”,數(shù)據(jù)庫請求次數(shù)與用戶訂單數(shù)成正比,為了更好的保障性能,最好將數(shù)據(jù)庫操作控制在一個常量。a7g28資訊網(wǎng)——每日最新資訊28at.com

整體思路為:先批量獲取要綁定的數(shù)據(jù),然后遍歷每一個訂單,在內(nèi)存中完成數(shù)據(jù)綁定。a7g28資訊網(wǎng)——每日最新資訊28at.com

a7g28資訊網(wǎng)——每日最新資訊28at.com

實現(xiàn)代碼如下:a7g28資訊網(wǎng)——每日最新資訊28at.com

@Service  public class OrderServiceCodingV2 implements OrderService {      @Autowired      private OrderRepository orderRepository;      @Autowired      private AddressRepository addressRepository;      @Autowired      private ProductRepository productRepository;      @Autowired      private UserRepository userRepository;      @Autowired      private PayInfoRepository payInfoRepository;        @Override      public List<OrderListVO> getByUserId(Long userId) {          List<Order> orders = this.orderRepository.getByUserId(userId);            List<OrderListVOCodingV2> orderDetailVOS = orders.stream()                  .map(order -> new OrderListVOCodingV2(OrderVO.apply(order)))                  .collect(toList());          // 批量獲取用戶,并依次進(jìn)行綁定          List<Long> userIds = orders.stream()                  .map(Order::getUserId)                  .collect(toList());          List<User> users = this.userRepository.getByIds(userIds);          Map<Long, User> userMap = users.stream()                  .collect(toMap(User::getId, Function.identity(), (a, b) -> a));          for (OrderListVOCodingV2 orderDetailVO : orderDetailVOS){              User user = userMap.get(orderDetailVO.getOrder().getUserId());              UserVO userVO = UserVO.apply(user);              orderDetailVO.setUser(userVO);          }          // 批量獲取地址,并依次進(jìn)行綁定          List<Long> addressIds = orders.stream()                  .map(Order::getAddressId)                  .collect(toList());          List<Address> addresses = this.addressRepository.getByIds(addressIds);          Map<Long, Address> addressMap = addresses.stream()                  .collect(toMap(Address::getId, Function.identity(), (a, b) -> a));          for (OrderListVOCodingV2 orderDetailVO : orderDetailVOS){              Address address = addressMap.get(orderDetailVO.getOrder().getAddressId());              AddressVO addressVO = AddressVO.apply(address);              orderDetailVO.setAddress(addressVO);          }          // 批量獲取商品,并依次進(jìn)行綁定          List<Long> productIds = orders.stream()                  .map(Order::getProductId)                  .collect(toList());          List<Product> products = this.productRepository.getByIds(productIds);          Map<Long, Product> productMap = products.stream()                  .collect(toMap(Product::getId, Function.identity(), (a, b) -> a));          for (OrderListVOCodingV2 orderDetailVO : orderDetailVOS){              Product product = productMap.get(orderDetailVO.getOrder().getProductId());              ProductVO productVO = ProductVO.apply(product);              orderDetailVO.setProduct(productVO);          }            return orderDetailVOS.stream()                  .collect(toList());      }        @Override      public OrderDetailVO getDetailByOrderId(Long orderId) {          // 暫時忽略          Order order = this.orderRepository.getById(orderId);          return convertToOrderDetailVO(order);      }        private OrderDetailVO convertToOrderDetailVO(Order order) {          // 暫時忽略            return orderDetail;      }  }

調(diào)整之后,對于“我的訂單”接口,單個用戶請求==數(shù)據(jù)庫的訪問次數(shù)變成了常量(4)==。a7g28資訊網(wǎng)——每日最新資訊28at.com

如果你是這么實現(xiàn)的,那恭喜你,你已步入合格程序員行列a7g28資訊網(wǎng)——每日最新資訊28at.com

3.3. 并行批量查詢 + 內(nèi)存Join

批量查詢+內(nèi)存Join 方案能滿足大部分場景,如果要抓取的數(shù)據(jù)太多,也就是數(shù)據(jù)庫訪問這個==常量變大==時,性能也會越來越差。a7g28資訊網(wǎng)——每日最新資訊28at.com

原因很簡單,由于串行執(zhí)行,整體耗時 = 獲取訂單耗時 + sum(抓取數(shù)據(jù)耗時)a7g28資訊網(wǎng)——每日最新資訊28at.com

聰明的同學(xué)早就躍躍欲試,這個我會:多線程并行執(zhí)行唄。a7g28資訊網(wǎng)——每日最新資訊28at.com

是的,基于 Future 的實現(xiàn)如下(還有很多版本,比如 CountDownLatch)a7g28資訊網(wǎng)——每日最新資訊28at.com

整體設(shè)計如下:a7g28資訊網(wǎng)——每日最新資訊28at.com

a7g28資訊網(wǎng)——每日最新資訊28at.com

示例代碼如下:a7g28資訊網(wǎng)——每日最新資訊28at.com

@Service  public class OrderServiceCodingV3 implements OrderService {      private ExecutorService executorService;        @Autowired      private OrderRepository orderRepository;      @Autowired      private AddressRepository addressRepository;      @Autowired      private ProductRepository productRepository;      @Autowired      private UserRepository userRepository;      @Autowired      private PayInfoRepository payInfoRepository;        @PostConstruct      public void init(){          // 初始化線程池(不要使用Executors,這里只是演示,需要對資源進(jìn)行評估)          this.executorService = Executors.newFixedThreadPool(20);      }        @SneakyThrows      @Override      public List<OrderListVO> getByUserId(Long userId) {          List<Order> orders = this.orderRepository.getByUserId(userId);            List<OrderListVOCodingV2> orderDetailVOS = orders.stream()                  .map(order -> new OrderListVOCodingV2(OrderVO.apply(order)))                  .collect(toList());            List<Callable<Void>> callables = Lists.newArrayListWithCapacity(3);          // 創(chuàng)建異步任務(wù)          callables.add(() -> {              // 批量獲取用戶,并依次進(jìn)行綁定              List<Long> userIds = orders.stream()                      .map(Order::getUserId)                      .collect(toList());              List<User> users = this.userRepository.getByIds(userIds);              Map<Long, User> userMap = users.stream()                      .collect(toMap(User::getId, Function.identity(), (a, b) -> a));              for (OrderListVOCodingV2 orderDetailVO : orderDetailVOS){                  User user = userMap.get(orderDetailVO.getOrder().getUserId());                  UserVO userVO = UserVO.apply(user);                  orderDetailVO.setUser(userVO);              }              return null;          });          // 創(chuàng)建異步任務(wù)          callables.add(() ->{              // 批量獲取地址,并依次進(jìn)行綁定              List<Long> addressIds = orders.stream()                      .map(Order::getAddressId)                      .collect(toList());              List<Address> addresses = this.addressRepository.getByIds(addressIds);              Map<Long, Address> addressMap = addresses.stream()                      .collect(toMap(Address::getId, Function.identity(), (a, b) -> a));              for (OrderListVOCodingV2 orderDetailVO : orderDetailVOS){                  Address address = addressMap.get(orderDetailVO.getOrder().getAddressId());                  AddressVO addressVO = AddressVO.apply(address);                  orderDetailVO.setAddress(addressVO);              }              return null;          });          // 創(chuàng)建異步任務(wù)          callables.add(() -> {              // 批量獲取商品,并依次進(jìn)行綁定              List<Long> productIds = orders.stream()                      .map(Order::getProductId)                      .collect(toList());              List<Product> products = this.productRepository.getByIds(productIds);              Map<Long, Product> productMap = products.stream()                      .collect(toMap(Product::getId, Function.identity(), (a, b) -> a));              for (OrderListVOCodingV2 orderDetailVO : orderDetailVOS){                  Product product = productMap.get(orderDetailVO.getOrder().getProductId());                  ProductVO productVO = ProductVO.apply(product);                  orderDetailVO.setProduct(productVO);              }              return null;          });            // 執(zhí)行異步任務(wù)          this.executorService.invokeAll(callables);            return orderDetailVOS.stream()                  .collect(toList());      }        @Override      public OrderDetailVO getDetailByOrderId(Long orderId) {          // 暫時忽略          Order order = this.orderRepository.getById(orderId);          return convertToOrderDetailVO(order);      }        private OrderDetailVO convertToOrderDetailVO(Order order) {        // 暫時忽略      }  }

多線程并發(fā)執(zhí)行,整體耗時 = 獲取訂單耗時 + max(抓取數(shù)據(jù)耗時)a7g28資訊網(wǎng)——每日最新資訊28at.com

如果你能夠這樣實現(xiàn)的,那恭喜你,你已步入高級程序員行列a7g28資訊網(wǎng)——每日最新資訊28at.com

然后呢,到此為止了?NO,接下來才是高潮!!!a7g28資訊網(wǎng)——每日最新資訊28at.com

讓我們打開認(rèn)知,開啟“抽象+封裝”之旅。a7g28資訊網(wǎng)——每日最新資訊28at.com

4. Fetcher封裝

仔細(xì)研究上述代碼,尋找里面的==“變與不變”==,你會發(fā)現(xiàn):a7g28資訊網(wǎng)——每日最新資訊28at.com

  1. 由于“我的訂單” 和 “訂單詳情” 返回的是不同的 VO,導(dǎo)致在實現(xiàn)綁定操作時寫了兩套基本一樣的邏輯;
  2. Address、User、Product 的綁定邏輯骨架是一樣的,一些細(xì)節(jié)操作存在差異;

找到邏輯中的變化點(diǎn),接下來便是有針對性的進(jìn)行封裝。a7g28資訊網(wǎng)——每日最新資訊28at.com

4.1. 消除方法中的重復(fù)代碼

對于 “我的訂單” 和 “訂單詳情” 返回==不同的 VO==,該怎么處理呢?a7g28資訊網(wǎng)——每日最新資訊28at.com

非常簡單,思路如下:a7g28資訊網(wǎng)——每日最新資訊28at.com

  1. 【不變】抽象出“行為接口” Fetcher,統(tǒng)一操作行為
  2. 【變化】基于多態(tài),不同的 VO 派生自相同的接口,但可以自己定義實現(xiàn),從而實現(xiàn)個性化變化

整體設(shè)計如下:a7g28資訊網(wǎng)——每日最新資訊28at.com

a7g28資訊網(wǎng)——每日最新資訊28at.com

簡單示例如下:a7g28資訊網(wǎng)——每日最新資訊28at.com

// 以 UserVO 為例,ProductVO、AddressVO,PayInfoVO 基本一致,不在贅述  public interface UserVOFetcherV1 {      Long getUserId();        void setUser(UserVO user);  }  // OrderDetailVO 實現(xiàn)對應(yīng)的接口,為了突出重點(diǎn)暫時忽略具體實現(xiàn)  public class OrderDetailVOFetcherV1 extends OrderDetailVO      implements AddressVOFetcherV1,          ProductVOFetcherV1,          UserVOFetcherV1,          PayInfoVOFetcherV1{  }  // OrderListVO 實現(xiàn)對應(yīng)接口,為了突出重點(diǎn)暫時忽略具體實現(xiàn)  public class OrderListVOFetcherV1 extends OrderListVO      implements AddressVOFetcherV1,          ProductVOFetcherV1,          UserVOFetcherV1 {  }

有了統(tǒng)一的操作接口,接下來便是抽取具體的綁定邏輯,以 UserVOFetcherExecutor 為例:a7g28資訊網(wǎng)——每日最新資訊28at.com

@Component  public class UserVOFetcherExecutorV1 {      @Autowired      private UserRepository userRepository;        public void fetch(List<? extends UserVOFetcherV1> fetchers){          List<Long> ids = fetchers.stream()                  .map(UserVOFetcherV1::getUserId)                  .distinct()                  .collect(Collectors.toList());            List<User> users = userRepository.getByIds(ids);            Map<Long, User> userMap = users.stream()                  .collect(toMap(user -> user.getId(), Function.identity()));            fetchers.forEach(fetcher -> {              Long userId = fetcher.getUserId();              User user = userMap.get(userId);              if (user != null){                  UserVO userVO = UserVO.apply(user);                  fetcher.setUser(userVO);              }          });      }  }

實現(xiàn)邏輯沒有變化,最重要的變化在于“入?yún)㈩愋汀保辉谑蔷唧w的 VO,而是抽象的 UserVOFetcher 接口。a7g28資訊網(wǎng)——每日最新資訊28at.com

AddressVOFetcherExecutorProductVOFetcherExecutorPayInfoVOFetcherExecutor  UserVOFetcherExecutorV1 邏輯基本一致,篇幅問題不在贅述。a7g28資訊網(wǎng)——每日最新資訊28at.com

這樣一個小小的調(diào)整,會給使用方帶來什么便利?一起看下使用方的變化:a7g28資訊網(wǎng)——每日最新資訊28at.com

@Service  public class OrderServiceFetcherV1 implements OrderService {      @Autowired      private OrderRepository orderRepository;      @Autowired      private AddressVOFetcherExecutorV1 addressVOFetcherExecutorV1;      @Autowired      private ProductVOFetcherExecutorV1 productVOFetcherExecutorV1;      @Autowired      private UserVOFetcherExecutorV1 userVOFetcherExecutorV1;      @Autowired      private PayInfoVOFetcherExecutorV1 payInfoVOFetcherExecutorV1;        @Override      public List<OrderListVO> getByUserId(Long userId) {          List<Order> orders = this.orderRepository.getByUserId(userId);            List<OrderListVOFetcherV1> orderDetailVOS = orders.stream()                  .map(order -> new OrderListVOFetcherV1(OrderVO.apply(order)))                  .collect(toList());          // 直接使用 FetcherExecutor 完成數(shù)據(jù)綁定          this.addressVOFetcherExecutorV1.fetch(orderDetailVOS);          this.productVOFetcherExecutorV1.fetch(orderDetailVOS);          this.userVOFetcherExecutorV1.fetch(orderDetailVOS);            return orderDetailVOS.stream()                  .collect(toList());      }        @Override      public OrderDetailVO getDetailByOrderId(Long orderId) {          Order order = this.orderRepository.getById(orderId);          OrderDetailVOFetcherV1 orderDetail = new OrderDetailVOFetcherV1(OrderVO.apply(order));          List<OrderDetailVOFetcherV1> orderDetailVOS = Arrays.asList(orderDetail);          // 直接使用 FetcherExecutor 完成數(shù)據(jù)綁定          this.addressVOFetcherExecutorV1.fetch(orderDetailVOS);          this.productVOFetcherExecutorV1.fetch(orderDetailVOS);          this.userVOFetcherExecutorV1.fetch(orderDetailVOS);          this.payInfoVOFetcherExecutorV1.fetch(orderDetailVOS);          return orderDetail;      }  }

兩個方法直接使用 FetcherExecutor 完成數(shù)據(jù)抓取和綁定,實現(xiàn)了==綁定邏輯的復(fù)用==。a7g28資訊網(wǎng)——每日最新資訊28at.com

如果再有 VO 需要進(jìn)行數(shù)據(jù)綁定,只需:a7g28資訊網(wǎng)——每日最新資訊28at.com

  1. VO 實現(xiàn) XXXFetcher 接口,實現(xiàn)對應(yīng)方法,提供關(guān)聯(lián)數(shù)據(jù)并完成數(shù)據(jù)綁定
  2. 使用 XXXFetcherExecutor 完成數(shù)據(jù)綁定

至此,面對新業(yè)務(wù)基本上與“綁定邏輯”說再見了。a7g28資訊網(wǎng)——每日最新資訊28at.com

4.2. 重構(gòu)綁定邏輯

接下來讓我們一起聚焦于綁定邏輯,先對比下上述的UserVOFetcherExecutor 與下面的 AddressVOFetcherExecutor, 找到里面的變化與不變:a7g28資訊網(wǎng)——每日最新資訊28at.com

@Component  public class AddressVOFetcherExecutorV1 {      @Autowired      private AddressRepository addressRepository;        public void fetch(List<? extends AddressVOFetcherV1> fetchers){          // 獲取關(guān)聯(lián)信息          List<Long> ids = fetchers.stream()                  .map(AddressVOFetcherV1::getAddressId)                  .distinct()                  .collect(Collectors.toList());          // 查詢關(guān)聯(lián)數(shù)據(jù)          List<Address> addresses = addressRepository.getByIds(ids);            // 轉(zhuǎn)為為 Map          Map<Long, Address> addressMap = addresses.stream()                  .collect(toMap(address -> address.getId(), Function.identity()));            // 依次進(jìn)行數(shù)據(jù)綁定          fetchers.forEach(fetcher -> {              Long addressId = fetcher.getAddressId();              Address address = addressMap.get(addressId);              if (address != null){                  // 轉(zhuǎn)換為 VO                  AddressVO addressVO = AddressVO.apply(address);                  // 將數(shù)據(jù)寫回到結(jié)果                  fetcher.setAddress(addressVO);              }          });      }  }

仔細(xì)觀察,會發(fā)現(xiàn):a7g28資訊網(wǎng)——每日最新資訊28at.com

【不變】邏輯骨架基本一致,基本是由:a7g28資訊網(wǎng)——每日最新資訊28at.com

  1. 獲取關(guān)聯(lián)信息
  2. 查詢關(guān)聯(lián)數(shù)據(jù)
  3. 將其轉(zhuǎn)換為 Map
  4. 講數(shù)據(jù)轉(zhuǎn)化為 VO
  5. 將 VO 綁定到結(jié)果對象

【變化】實現(xiàn)細(xì)節(jié)存在差異a7g28資訊網(wǎng)——每日最新資訊28at.com

  1. 從什么接口中獲取關(guān)聯(lián)信息
  2. 如何查詢關(guān)聯(lián)數(shù)據(jù)
  3. 轉(zhuǎn)換為 Map 的鍵是什么
  4. 如何將數(shù)據(jù)轉(zhuǎn)換為 VO
  5. 如何完成數(shù)據(jù)的綁定

熟悉設(shè)計模式的伙伴是否眼前一亮?停頓一下好好回想一下,哪種模式就是用來處理這種問題的?a7g28資訊網(wǎng)——每日最新資訊28at.com

答案便是:模板方法模式a7g28資訊網(wǎng)——每日最新資訊28at.com

整體思想為:a7g28資訊網(wǎng)——每日最新資訊28at.com

  1. 將不變的邏輯骨架封裝在父類方法
  2. 將變化的實現(xiàn)細(xì)節(jié)放在子類中進(jìn)行擴(kuò)展

整體設(shè)計如下:a7g28資訊網(wǎng)——每日最新資訊28at.com

a7g28資訊網(wǎng)——每日最新資訊28at.com

抽取公共父類如下:a7g28資訊網(wǎng)——每日最新資訊28at.com

abstract class BaseItemFetcherExecutor<FETCHER extends ItemFetcher, DATA, RESULT>          implements ItemFetcherExecutor<FETCHER>{        @Override      public void fetch(List<FETCHER> fetchers) {          // 獲取關(guān)聯(lián)信息          List<Long> ids = fetchers.stream()                  .map(this::getFetchId)                  .distinct()                  .collect(Collectors.toList());          // 查詢關(guān)聯(lián)數(shù)據(jù)          List<DATA> datas = loadData(ids);          // 轉(zhuǎn)為為 Map          Map<Long, List<DATA>> dataMap = datas.stream()                  .collect(groupingBy(this::getDataId));          // 依次進(jìn)行數(shù)據(jù)綁定          fetchers.forEach(fetcher -> {              Long id = getFetchId(fetcher);              List<DATA> ds = dataMap.get(id);              if (ds != null){                  // 轉(zhuǎn)換為 VO                  List<RESULT> result = ds.stream()                          .map( data -> convertToVo(data))                                  .collect(Collectors.toList());                  // 將數(shù)據(jù)寫回到結(jié)果                  setResult(fetcher, result);              }          });      }        protected abstract Long getFetchId(FETCHER fetcher);        protected abstract List<DATA> loadData(List<Long> ids);        protected abstract Long getDataId(DATA data);        protected abstract RESULT convertToVo(DATA data);        protected abstract void setResult(FETCHER fetcher, List<RESULT> result);  }

基于 BaseItemFetcherExecutor  UserFetcherExecutor 如下:a7g28資訊網(wǎng)——每日最新資訊28at.com

@Component  public class UserVOFetcherExecutorV2      extends BaseItemFetcherExecutor<UserVOFetcherV2, User, UserVO>{      @Autowired      private UserRepository userRepository;        @Override      protected Long getFetchId(UserVOFetcherV2 fetcher) {          return fetcher.getUserId();      }        @Override      protected List<User> loadData(List<Long> ids) {          return this.userRepository.getByIds(ids);      }        @Override      protected Long getDataId(User user) {          return user.getId();      }        @Override      protected UserVO convertToVo(User user) {          return UserVO.apply(user);      }        @Override      protected void setResult(UserVOFetcherV2 fetcher, List<UserVO> userVO) {          if (CollectionUtils.isNotEmpty(userVO)) {              fetcher.setUser(userVO.get(0));          }      }        @Override      public boolean support(Class<UserVOFetcherV2> cls) {          // 暫時忽略,稍后會細(xì)講          return UserVOFetcherV2.class.isAssignableFrom(cls);      }  }

UserVOFetcherExecutor究竟發(fā)生什么變化呢?好像變得更復(fù)雜了:a7g28資訊網(wǎng)——每日最新資訊28at.com

  1. 從代碼量角度(行數(shù))變得更多了,因為類函數(shù)明顯變大
  2. 從復(fù)雜度角度(邏輯)變得更加簡單,每個方法基本都是一兩句語句

那我們究竟得到了什么好處?可以花幾分鐘好好思考一下!!!a7g28資訊網(wǎng)——每日最新資訊28at.com

在說結(jié)果之前,讓我們看下另一個變化點(diǎn)。回想下 FetcherExecutor 的執(zhí)行點(diǎn),如下:a7g28資訊網(wǎng)——每日最新資訊28at.com

@Override  public List<OrderListVO> getByUserId(Long userId) {      List<Order> orders = this.orderRepository.getByUserId(userId);      List<OrderListVOFetcherV1> orderDetailVOS = orders.stream()              .map(order -> new OrderListVOFetcherV1(OrderVO.apply(order)))              .collect(toList());      // 手工調(diào)用,OrderListVO 實現(xiàn)新接口,需要增加新的依賴和調(diào)用      this.addressVOFetcherExecutorV1.fetch(orderDetailVOS);      this.productVOFetcherExecutorV1.fetch(orderDetailVOS);      this.userVOFetcherExecutorV1.fetch(orderDetailVOS);      return orderDetailVOS.stream()              .collect(toList());  }  @Override  public OrderDetailVO getDetailByOrderId(Long orderId) {      Order order = this.orderRepository.getById(orderId);      OrderDetailVOFetcherV1 orderDetail = new OrderDetailVOFetcherV1(OrderVO.apply(order));      List<OrderDetailVOFetcherV1> orderDetailVOS = Arrays.asList(orderDetail);      // 手工調(diào)用,OrderDetailVO 實現(xiàn)新接口,需要增加新的依賴和調(diào)用      this.addressVOFetcherExecutorV1.fetch(orderDetailVOS);      this.productVOFetcherExecutorV1.fetch(orderDetailVOS);      this.userVOFetcherExecutorV1.fetch(orderDetailVOS);      this.payInfoVOFetcherExecutorV1.fetch(orderDetailVOS);      return orderDetail;  }

其實,需要調(diào)用哪些 FetcherExecutor 完全可以由 VO 實現(xiàn)的接口來確定。也就是說,需要綁定新數(shù)據(jù),只需 VO 繼承并實現(xiàn)新的 Fetcher 接口即可。a7g28資訊網(wǎng)——每日最新資訊28at.com

對此,我們需要:a7g28資訊網(wǎng)——每日最新資訊28at.com

  1. 一個統(tǒng)一的訪問入口,對外提供訪問
  2. 每個 FetcherExecutor 能夠識別 VO 并執(zhí)行綁定邏輯

哪個設(shè)計模式是用來解決這個問題?花幾分鐘好好思考一下!a7g28資訊網(wǎng)——每日最新資訊28at.com

答案是:責(zé)任鏈模型a7g28資訊網(wǎng)——每日最新資訊28at.com

標(biāo)準(zhǔn)的責(zé)任鏈模式用起來比較繁瑣,在 Spring 實現(xiàn)中大量使用他的一種變現(xiàn),及提供一個驗證接口,由組件自身完成判斷,用于決定是否執(zhí)行自身邏輯。a7g28資訊網(wǎng)——每日最新資訊28at.com

整體設(shè)計如下:a7g28資訊網(wǎng)——每日最新資訊28at.com

a7g28資訊網(wǎng)——每日最新資訊28at.com

首先,為了統(tǒng)一 FetcherExecutor 的行為,抽取通用接口:a7g28資訊網(wǎng)——每日最新資訊28at.com

public interface ItemFetcherExecutor<F extends ItemFetcher> {      /**       * 該組件是否能處理 cls 類型       * @param cls       * @return       */      boolean support(Class<F> cls);        /**       *  執(zhí)行真正的數(shù)據(jù)綁定       * @param fetchers       */      void fetch(List<F> fetchers);  }

具體的實現(xiàn),可以見 UserVOFetcherExecutorV2 的 support 方法:a7g28資訊網(wǎng)——每日最新資訊28at.com

@Override  public boolean support(Class<UserVOFetcherV2> cls) {      return UserVOFetcherV2.class.isAssignableFrom(cls);  }

實現(xiàn)邏輯非常簡單,只是判斷 cls 是否實現(xiàn)了 UserVOFetcherV2 接口。a7g28資訊網(wǎng)——每日最新資訊28at.com

有了 FetcherExecutor 組件后,接下來就是為其提供統(tǒng)一的訪問入口:a7g28資訊網(wǎng)——每日最新資訊28at.com

@Service  public class FetcherService {      @Autowired      private List<ItemFetcherExecutor> itemFetcherExecutors;        public <F extends ItemFetcher> void fetch(Class<F> cls, List<F> fetchers){          if (CollectionUtils.isNotEmpty(fetchers)){              this.itemFetcherExecutors.stream()                      // 是否能處理該類型                      .filter(itemFetcherExecutor -> itemFetcherExecutor.support(cls))                      // 執(zhí)行真正的綁定                      .forEach(itemFetcherExecutor -> itemFetcherExecutor.fetch(fetchers));          }      }  }

邏輯即為簡單,依次遍歷 FetcherExecutor,根據(jù) support 執(zhí)行結(jié)果,執(zhí)行 fetch 邏輯。a7g28資訊網(wǎng)——每日最新資訊28at.com

【小常識】Spring 可以將容器中的全部實現(xiàn)直接注入到 List<Bean>。在上述代碼中,將會把所有的 ItemFetcherExecutor 實現(xiàn)注入到 itemFetcherExecutors 屬性。因此,在新增 FetcherExecutor 時,只需將其聲明為 Spring Bean,無需調(diào)整代碼邏輯。a7g28資訊網(wǎng)——每日最新資訊28at.com

OK,我們有了 FetcherService 提供統(tǒng)一的數(shù)據(jù)綁定能力,原來 OrderServiceFetcher 中 fetch 操作的變化點(diǎn)轉(zhuǎn)移到 FetcherService,自身變得非常穩(wěn)定。具體如下:a7g28資訊網(wǎng)——每日最新資訊28at.com

@Service  public class OrderServiceFetcherV2 implements OrderService {      @Autowired      private OrderRepository orderRepository;      @Autowired      private FetcherService fetcherService;        @Override      public List<OrderListVO> getByUserId(Long userId) {          List<Order> orders = this.orderRepository.getByUserId(userId);            List<OrderListVOFetcherV2> orderDetailVOS = orders.stream()                  .map(order -> new OrderListVOFetcherV2(OrderVO.apply(order)))                  .collect(toList());          // VO 數(shù)據(jù)綁定發(fā)生變化,只需調(diào)整 VO 實現(xiàn)接口,此處無需變化          fetcherService.fetch(OrderListVOFetcherV2.class, orderDetailVOS);            return orderDetailVOS.stream()                  .collect(toList());      }        @Override      public OrderDetailVO getDetailByOrderId(Long orderId) {          Order order = this.orderRepository.getById(orderId);          OrderDetailVOFetcherV2 orderDetail = new OrderDetailVOFetcherV2(OrderVO.apply(order));          // VO 數(shù)據(jù)綁定發(fā)生變化,只需調(diào)整 VO 實現(xiàn)接口,此處無需變化          fetcherService.fetch(OrderDetailVOFetcherV2.class, Arrays.asList(orderDetail));          return orderDetail;      }  }

終于,我們將變化收斂到 VO 內(nèi),VO 需要綁定新的數(shù)據(jù),只需實現(xiàn)對應(yīng)接口即可。a7g28資訊網(wǎng)——每日最新資訊28at.com

4.3. 并發(fā)綁定

經(jīng)過重構(gòu),代碼結(jié)構(gòu)變得非常清晰,如果想通過多線程并發(fā)方式提供性能,需要調(diào)整哪些組件呢?好好想想!!!a7g28資訊網(wǎng)——每日最新資訊28at.com

只需對FetcherService進(jìn)行調(diào)整,讓我們來一個并發(fā)版本,具體如下:a7g28資訊網(wǎng)——每日最新資訊28at.com

@Service  public class ConcurrentFetcherService {      private ExecutorService executorService;      @Autowired      private List<ItemFetcherExecutor> itemFetcherExecutors;        @PostConstruct      public void init(){          this.executorService = Executors.newFixedThreadPool(20);      }        @SneakyThrows      public <F extends ItemFetcher> void fetch(Class<F> cls, List<F> fetchers){          if (CollectionUtils.isNotEmpty(fetchers)){              // 創(chuàng)建異步執(zhí)行任務(wù)              List<Callable<Void>> callables = this.itemFetcherExecutors.stream()                      .filter(itemFetcherExecutor -> itemFetcherExecutor.support(cls))                      .map(itemFetcherExecutor -> (Callable<Void>) () -> {                          itemFetcherExecutor.fetch(fetchers);                          return null;                      }).collect(Collectors.toList());              // 線程池中并行執(zhí)行              this.executorService.invokeAll(callables);          }      }  }

OrderServiceFetcherV3 只需使用 ConcurrentFetcherService 替代 原來的 FetcherService 并擁有了并發(fā)能力。a7g28資訊網(wǎng)——每日最新資訊28at.com

5. 注解方案

5.1. 復(fù)雜配置 @JoinInMemory 來幫忙

縱觀整個 Fetcher 封裝,雖然結(jié)構(gòu)清晰,但細(xì)節(jié)過于繁瑣,特別是:a7g28資訊網(wǎng)——每日最新資訊28at.com

  1. 待抓取數(shù)據(jù)需要抽取 Fetcher 接口
  2. 需要提供自己的 FetcherExecutor 實現(xiàn)
  3. VO 需要實現(xiàn)多個 Fetcher 接口

這些不便將成為落地最大的阻礙,那有沒有辦法進(jìn)行進(jìn)一步簡化?a7g28資訊網(wǎng)——每日最新資訊28at.com

這需要思考下這些設(shè)計背后的深層需求:a7g28資訊網(wǎng)——每日最新資訊28at.com

Fetcher接口目的包括a7g28資訊網(wǎng)——每日最新資訊28at.com

  1. 提供綁定信息
  2. 設(shè)置綁定結(jié)果
  3. 被 FetcherExecutor 識別并進(jìn)行處理

FetcherExecutor設(shè)計的目標(biāo)包括:a7g28資訊網(wǎng)——每日最新資訊28at.com

  1. 識別待處理的 Fetcher
  2. 定制個性化流程

所有這些需求是否可用 ==注解== 的方式實現(xiàn)?a7g28資訊網(wǎng)——每日最新資訊28at.com

  1. 在 VO 屬性上增加注解,說明綁定結(jié)果寫回到該屬性上
  2. 注解配置來源屬性,提供綁定信息
  3. 注解配置流程屬性,完成 FetcherExecutor 的個性化定制
  4. 每個注解背后是一個 FetcherExecutor 實現(xiàn),完成 FetcherExecutor 與 “Fetcher” 綁定

根據(jù)上述分析,注解可完成全部任務(wù),新建注解如下:a7g28資訊網(wǎng)——每日最新資訊28at.com

@Target({ElementType.FIELD, ElementType.TYPE})  @Retention(RetentionPolicy.RUNTIME)  public @interface JoinInMemory {      /**       * 從 sourceData 中提取 key       * @return       */      String keyFromSourceData();        /**       * 從 joinData 中提取 key       * @return       */      String keyFromJoinData();        /**       * 批量數(shù)據(jù)抓取       * @return       */      String loader();        /**       * 結(jié)果轉(zhuǎn)換器       * @return       */      String joinDataConverter() default "";        /**       * 運(yùn)行級別,同一級別的 join 可 并行執(zhí)行       * @return       */      int runLevel() default 10;  }

乍一看,需要配置的信息真多,其實大多數(shù)配置全部與 FetcherExecutor 實現(xiàn)相關(guān)。a7g28資訊網(wǎng)——每日最新資訊28at.com

abstract class AbstractJoinItemExecutor<SOURCE_DATA, JOIN_KEY, JOIN_DATA, JOIN_RESULT> implements JoinItemExecutor<SOURCE_DATA> {        /**       * 從原始數(shù)據(jù)中生成 JoinKey       * @param data       * @return       */      protected abstract JOIN_KEY createJoinKeyFromSourceData(SOURCE_DATA data);        /**       * 根據(jù) JoinKey 批量獲取 JoinData       * @param joinKeys       * @return       */      protected abstract List<JOIN_DATA> getJoinDataByJoinKeys(List<JOIN_KEY> joinKeys);        /**       * 從 JoinData 中獲取 JoinKey       * @param joinData       * @return       */      protected abstract JOIN_KEY createJoinKeyFromJoinData(JOIN_DATA joinData);        /**       * 將 JoinData 轉(zhuǎn)換為 JoinResult       * @param joinData       * @return       */      protected abstract JOIN_RESULT convertToResult(JOIN_DATA joinData);        /**       * 將 JoinResult 寫回至 SourceData       * @param data       * @param JoinResults       */      protected abstract void onFound(SOURCE_DATA data, List<JOIN_RESULT> JoinResults);        /**       * 未找到對應(yīng)的 JoinData       * @param data       * @param joinKey       */      protected abstract void onNotFound(SOURCE_DATA data, JOIN_KEY joinKey);        @Override      public void execute(List<SOURCE_DATA> sourceDatas) {          // 從源數(shù)據(jù)中提取 JoinKey          List<JOIN_KEY> joinKeys = sourceDatas.stream()                  .filter(Objects::nonNull)                  .map(this::createJoinKeyFromSourceData)                  .filter(Objects::nonNull)                  .distinct()                  .collect(toList());          log.debug("get join key {} from source data {}", joinKeys, sourceDatas);            // 根據(jù) JoinKey 獲取 JoinData          List<JOIN_DATA> allJoinDatas = getJoinDataByJoinKeys(joinKeys);          log.debug("get join data {} by join key {}", allJoinDatas, joinKeys);            // 將 JoinData 以 Map 形式進(jìn)行組織          Map<JOIN_KEY, List<JOIN_DATA>> joinDataMap = allJoinDatas.stream()                  .filter(Objects::nonNull)                  .collect(groupingBy(this::createJoinKeyFromJoinData));          log.debug("group by join key, result is {}", joinDataMap);            // 處理每一條 SourceData          for (SOURCE_DATA data : sourceDatas){              // 從 SourceData 中 獲取 JoinKey              JOIN_KEY joinKey = createJoinKeyFromSourceData(data);              if (joinKey == null){                  log.warn("join key from join data {} is null", data);                  continue;              }              // 根據(jù) JoinKey 獲取 JoinData              List<JOIN_DATA> joinDatasByKey = joinDataMap.get(joinKey);              if (CollectionUtils.isNotEmpty(joinDatasByKey)){                  // 獲取到 JoinData, 轉(zhuǎn)換為 JoinResult,進(jìn)行數(shù)據(jù)寫回                  List<JOIN_RESULT> joinResults = joinDatasByKey.stream()                          .filter(Objects::nonNull)                          .map(joinData -> convertToResult(joinData))                          .collect(toList());                    log.debug("success to convert join data {} to join result {}", joinDatasByKey, joinResults);                  onFound(data, joinResults);                  log.debug("success to write join result {} to source data {}", joinResults, data);              }else {                  log.warn("join data lost by join key {} for source data {}", joinKey, data);                  // 為獲取到 JoinData,進(jìn)行 notFound 回調(diào)                  onNotFound(data, joinKey);              }          }      }  }

JoinInMemory 注解屬性和AbstractJoinItemExecutor基本一致,在此就不做贅述,我們先看下具體的使用方式:a7g28資訊網(wǎng)——每日最新資訊28at.com

@Data  public class OrderDetailVOAnnV1 extends OrderDetailVO {      private final OrderVO order;      @JoinInMemory(keyFromSourceData = "#{order.userId}",              keyFromJoinData = "#{id}",              loader = "#{@userRepository.getByIds(#root)}",              joinDataConverter = "#{T(com.geekhalo.lego.joininmemory.order.UserVO).apply(#root)}"          )      private UserVO user;        // 其他暫時忽略    }    @Data  public class OrderListVOAnnV1 extends OrderListVO {      private final OrderVO order;      @JoinInMemory(keyFromSourceData = "#{order.userId}",              keyFromJoinData = "#{id}",              loader = "#{@userRepository.getByIds(#root)}",              joinDataConverter = "#{T(com.geekhalo.lego.joininmemory.order.UserVO).apply(#root)}"          )      private UserVO user;        // 其他暫時忽略  }

我們以 UserVO user 屬性為例a7g28資訊網(wǎng)——每日最新資訊28at.com

a7g28資訊網(wǎng)——每日最新資訊28at.com

@JoinInMemory 注解中大量使用 SpEL,不熟悉的伙伴可以自行網(wǎng)上進(jìn)行檢索。a7g28資訊網(wǎng)——每日最新資訊28at.com

其他部分不變,定義 OrderService 如下:a7g28資訊網(wǎng)——每日最新資訊28at.com

@Servicepublic class OrderServiceAnnV1 implements OrderService {    @Autowired    private OrderRepository orderRepository;    @Autowired    private JoinService joinService;    @Override    public List<OrderListVO> getByUserId(Long userId) {        List<Order> orders = this.orderRepository.getByUserId(userId);        List<OrderListVOAnnV1> orderDetailVOS = orders.stream()                .map(order -> new OrderListVOAnnV1(OrderVO.apply(order)))                .collect(toList());        this.joinService.joinInMemory(OrderListVOAnnV1.class, orderDetailVOS);        return orderDetailVOS.stream()                .collect(toList());    }    @Override    public OrderDetailVO getDetailByOrderId(Long orderId) {        Order order = this.orderRepository.getById(orderId);        OrderDetailVOAnnV1 orderDetail = new OrderDetailVOAnnV1(OrderVO.apply(order));        this.joinService.joinInMemory(OrderDetailVOAnnV1.class, Arrays.asList(orderDetail));        return orderDetail;    }}

相對于 Fetcher 抽象,我們將 Fetcher、FetcherExecutor 全部配置化,并通過 注解的方式進(jìn)行呈現(xiàn),相對于 Coding 方案,注解方案更加靈活,工作量也更小。a7g28資訊網(wǎng)——每日最新資訊28at.com

5.2. 復(fù)雜配置 @Alias 來幫忙

相對于 Fetcher 封裝,一個 @JoinInMemory 成功干掉了兩個組件,但觀其自身配置起來還是非常繁瑣。比如,在訂單查詢這個場景,在 OrderListVO  OrderDetailVO 中都需要對 UserVO 進(jìn)行數(shù)據(jù)綁定,觀察兩個注解,我們會發(fā)現(xiàn)很多重復(fù)配置:a7g28資訊網(wǎng)——每日最新資訊28at.com

//OrderListVO@JoinInMemory(keyFromSourceData = "#{order.userId}",            keyFromJoinData = "#{id}",            loader = "#{@userRepository.getByIds(#root)}",            joinDataConverter = "#{T(com.geekhalo.lego.joininmemory.order.UserVO).apply(#root)}"        )private UserVO user;// OrderDetailVO@JoinInMemory(keyFromSourceData = "#{order.userId}",            keyFromJoinData = "#{id}",            loader = "#{@userRepository.getByIds(#root)}",            joinDataConverter = "#{T(com.geekhalo.lego.joininmemory.order.UserVO).apply(#root)}"        )private UserVO user;

兩個配置完全一樣,細(xì)品之后會發(fā)現(xiàn):a7g28資訊網(wǎng)——每日最新資訊28at.com

【變化】入?yún)⒆兓x取的屬性不同,只是本次恰巧相同而已a7g28資訊網(wǎng)——每日最新資訊28at.com

  • OrderListVO 指的是 OrderListVO 屬性 order 的id值
  • OrderDetailVO 指的是 OrderDetailVO 屬性 order 的值

【不變】處理邏輯不變a7g28資訊網(wǎng)——每日最新資訊28at.com

  • keyFromJoinData 指的是 user對象的 id
  • loader 指的是通過 userRepository 的 getByIds 加載數(shù)據(jù)
  • joinDataConverter 指的是將 user 轉(zhuǎn)換為 UserVO

【不變】a7g28資訊網(wǎng)——每日最新資訊28at.com

  • 將綁定結(jié)果 UserVO 綁定到屬性上(屬性名不同沒有影響)

對于不變部分如何進(jìn)行統(tǒng)一管理?a7g28資訊網(wǎng)——每日最新資訊28at.com

自定義注解 結(jié)合 Spring @AliasFor 便可以解決這個問題,以 UserVO 為例:a7g28資訊網(wǎng)——每日最新資訊28at.com

@Target(ElementType.FIELD)@Retention(RetentionPolicy.RUNTIME)// 管理通用屬性@JoinInMemory(keyFromSourceData = "",        keyFromJoinData = "#{id}",        loader = "#{@userRepository.getByIds(#root)}",        joinDataConverter = "#{T(com.geekhalo.lego.joininmemory.order.UserVO).apply(#root)}")public @interface JoinUserVOOnId {    // 使用別名將 keyFromSourceData 的配置暴露出來    @AliasFor(            annotation = JoinInMemory.class    )    String keyFromSourceData();}

新注解有如下幾個特點(diǎn):a7g28資訊網(wǎng)——每日最新資訊28at.com

  • 在注解上使用 @JoinInMemory 注解完成對通用屬性的配置
  • 在自定義注解 JoinUserVOOnId  keyFromSourceData 屬性上,添加 @AliasFor 注解,將配置暴露給使用方

有了自定義注解,使用變的非常方便:a7g28資訊網(wǎng)——每日最新資訊28at.com

@Data  public class OrderListVOAnnV2 extends OrderListVO {      private final OrderVO order;      // 只需配置參數(shù)即可,其他配置由 JoinUserVOOnId 進(jìn)行管理      @JoinUserVOOnId(keyFromSourceData = "#{order.userId}")      private UserVO user;  }    @Data  public class OrderDetailVOAnnV2 extends OrderDetailVO {      private final OrderVO order;      // 只需配置參數(shù)即可,其他配置由 JoinUserVOOnId 進(jìn)行管理      @JoinUserVOOnId(keyFromSourceData = "#{order.userId}")      private UserVO user;  }

其他使用方式不變,但實現(xiàn)了邏輯簡化:a7g28資訊網(wǎng)——每日最新資訊28at.com

  1. 新增綁定數(shù)據(jù),只需自定義綁定注解
  2. VO 需新的綁定數(shù)據(jù),只需在屬性上添加綁定注解

5.3. 開啟并發(fā) @JoinInMemoryConfig 來幫忙

如果擔(dān)心性能,可以一鍵開啟并發(fā)綁定,示例如下:a7g28資訊網(wǎng)——每日最新資訊28at.com

@Data  @JoinInMemoryConfig(executorType = JoinInMemeoryExecutorType.PARALLEL)  public class OrderListVOAnnV3 extends OrderListVO {      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 配置如下:a7g28資訊網(wǎng)——每日最新資訊28at.com

a7g28資訊網(wǎng)——每日最新資訊28at.com

6. 最佳實踐

6.1.將定義注解視為最佳實踐

@JoinInMemory 注解上配置的信息太多,如果直接在業(yè)務(wù)代碼中使用,非常難以維護(hù),當(dāng)每個配置發(fā)生變化后,很難一次性修改到位。所以,建議只將他作為“原注解”使用。a7g28資訊網(wǎng)——每日最新資訊28at.com

整體思路詳見:a7g28資訊網(wǎng)——每日最新資訊28at.com

a7g28資訊網(wǎng)——每日最新資訊28at.com

6.2. 注意線程池隔離

對于不同的數(shù)據(jù)綁定需求,建議使用不同的線程池,從資源層面對不同功能進(jìn)行隔離,從而將由于依賴接口發(fā)生阻塞導(dǎo)致線程耗盡所造成的影響控制在最小范圍。a7g28資訊網(wǎng)——每日最新資訊28at.com

@JoinInMemoryConfig  executorName 屬性配置的便是執(zhí)行器名稱,不配置直接使用 “defaultExecutor”,具體代碼如下:a7g28資訊網(wǎng)——每日最新資訊28at.com

@Bean  public ExecutorService defaultExecutor(){      BasicThreadFactory basicThreadFactory = new BasicThreadFactory.Builder()              .namingPattern("JoinInMemory-Thread-%d")              .daemon(true)              .build();      int maxSize = Runtime.getRuntime().availableProcessors() * 3;      return new ThreadPoolExecutor(0, maxSize,              60L, TimeUnit.SECONDS,              new SynchronousQueue<>(),              basicThreadFactory,              new ThreadPoolExecutor.CallerRunsPolicy());  }

如需使用自定義線程池需:a7g28資訊網(wǎng)——每日最新資訊28at.com

  1. 自定義線程池,并將其注冊到Spring 容器
  2. @JoinInMemoryConfig executorName 設(shè)置為線程池的 bean name

7. 小結(jié)

推導(dǎo)邏輯有點(diǎn)長不知道你get到多少,先簡單回顧一下:a7g28資訊網(wǎng)——每日最新資訊28at.com

  1. 今天面對的問題是:如何在應(yīng)用成進(jìn)行數(shù)據(jù) Join 操作;
  2. 我們以我的訂單和訂單詳情兩個接口為業(yè)務(wù)切入點(diǎn),層層進(jìn)行抽象,發(fā)現(xiàn)變化、封裝變化、管理變化
  3. 首先是手寫代碼,包括 foreach+單條抓取,批量查詢+內(nèi)存Join,并行查詢 + 內(nèi)存Join。在這個層次基本沒有抽象可言,存在大量重復(fù)代碼,系統(tǒng)擴(kuò)展性低
  4. 其次是 Fetcher方案,為了分離“變化”與“不變”抽取出 Fetcher 和 FetcherExecutor 兩個接口,并使用模板方法和責(zé)任鏈模式對其進(jìn)行抽象,提升系統(tǒng)的擴(kuò)展性,但實現(xiàn)過于繁瑣不便于推廣
  5. 最后是注解方案,使用 @JoinInMemory 注解完成繁瑣的配置工作,將通用配置保留在自定義注解進(jìn)行統(tǒng)一管理,基于 @AliasFor 完成入?yún)⒌呐渲茫€可以使用 @JoinInMemoryConfig 開啟并發(fā)處理

本文鏈接:http://www.www897cc.com/showinfo-26-45496-0.htmlDDD死黨:內(nèi)存Join——將復(fù)用和擴(kuò)展用到極致

聲明:本網(wǎng)頁內(nèi)容旨在傳播知識,若有侵權(quán)等問題請及時與本網(wǎng)聯(lián)系,我們將在第一時間刪除處理。郵件:2376512515@qq.com

上一篇: Python 也能干大事,訂閱與發(fā)布

下一篇: Node在項目中應(yīng)用案例之給幾百個下拉框統(tǒng)一加Filterable實現(xiàn)可搜索

標(biāo)簽:
  • 熱門焦點(diǎn)
  • 直屏旗艦來了 iQOO 12和K70 Pro同臺競技

    旗艦機(jī)基本上使用的都是雙曲面屏幕,這就讓很多喜歡直屏的愛好者在苦等一款直屏旗艦,這次,你們等到了。據(jù)博主數(shù)碼閑聊站帶來的最新爆料稱,Redmi下代旗艦K70 Pro和iQOO 12兩款手
  • Flowable工作流引擎的科普與實踐

    一.引言當(dāng)我們在日常工作和業(yè)務(wù)中需要進(jìn)行各種審批流程時,可能會面臨一系列技術(shù)和業(yè)務(wù)上的挑戰(zhàn)。手動處理這些審批流程可能會導(dǎo)致開發(fā)成本的增加以及業(yè)務(wù)復(fù)雜度的上升。在這
  • 如何通過Python線程池實現(xiàn)異步編程?

    線程池的概念和基本原理線程池是一種并發(fā)處理機(jī)制,它可以在程序啟動時創(chuàng)建一組線程,并將它們置于等待任務(wù)的狀態(tài)。當(dāng)任務(wù)到達(dá)時,線程池中的某個線程會被喚醒并執(zhí)行任務(wù),執(zhí)行完任
  • 一文搞定Java NIO,以及各種奇葩流

    大家好,我是哪吒。很多朋友問我,如何才能學(xué)好IO流,對各種流的概念,云里霧里的,不求甚解。用到的時候,現(xiàn)百度,功能雖然實現(xiàn)了,但是為什么用這個?不知道。更別說效率問題了~下次再遇到,
  • 小紅書1周漲粉49W+,我總結(jié)了小白可以用的N條漲粉筆記

    作者:黃河懂運(yùn)營一條性教育視頻,被54萬人&ldquo;珍藏&rdquo;是什么體驗?最近,情感博主@公主是用鮮花做的,火了!僅僅憑借一條視頻,光小紅書就有超過128萬人,為她瘋狂點(diǎn)贊!更瘋狂的是,這
  • 東方甄選單飛:有些鳥注定是關(guān)不住的

    文/彭寬鴻編輯/羅卿東方甄選創(chuàng)始人俞敏洪帶隊的&ldquo;7天甘肅行&rdquo;直播活動已在近日順利收官。成立后一年多時間里,東方甄選要脫離抖音自立門戶的傳聞不絕于耳,&ldquo;7
  • 造車兩年股價跌六成,小米的估值邏輯變了嗎?

    如果從小米官宣造車后的首個交易日起持有小米集團(tuán)的股票,那么截至2023年上半年最后一個交易日,投資者將浮虧59.16%,同區(qū)間的恒生科技指數(shù)跌幅為52.78%
  • 華為發(fā)布HarmonyOS 4:更好玩、更流暢、更安全

    在8月4日的華為開發(fā)者大會2023(HDC.Together)大會上,HarmonyOS 4正式發(fā)布。自2019年發(fā)布以來,HarmonyOS一直以用戶為中心,經(jīng)歷四年多的發(fā)展HarmonyOS已
  • iQOO 11S新品發(fā)布會

    iQOO將在7月4日19:00舉行新品發(fā)布會,推出杭州亞運(yùn)會電競賽事官方用機(jī)iQOO 11S。
Top 主站蜘蛛池模板: 介休市| 三江| 洮南市| 沂南县| 丁青县| 黎城县| 怀柔区| 和政县| 江孜县| 黎平县| 鄂州市| 库伦旗| 阿城市| 浦县| 五大连池市| 洛宁县| 延庆县| 东乌珠穆沁旗| 夏津县| 广饶县| 萨迦县| 定边县| 宣武区| 江源县| 平泉县| 绥化市| 龙岩市| 来凤县| 阜阳市| 巢湖市| 建瓯市| 武清区| 普定县| 平南县| 镇宁| 平昌县| 会昌县| 若尔盖县| 澄江县| 缙云县| 天峻县|