精品欧美一区二区三区在线观看 _久久久久国色av免费观看性色_国产精品久久在线观看_亚洲第一综合网站_91精品又粗又猛又爽_小泽玛利亚一区二区免费_91亚洲精品国偷拍自产在线观看 _久久精品视频在线播放_美女精品久久久_欧美日韩国产成人在线

DDD死黨:內存Join--將復用和擴展用到極致

開發 前端
使用 @JoinInMemory 注解完成繁瑣的配置工作,將通用配置保留在自定義注解進行統一管理,基于 @AliasFor 完成入參的配置,還可以使用 @JoinInMemoryConfig 開啟并發處理。

1. 為什么"內存Join"是個無法繞過的話題

首先,我們先簡單解釋下,什么是“內存Join”。

相信大家對關系數據庫的 join 語句肯定不陌生,其作用就是通過關聯關系從多個表中查詢數據,關聯條件和數據聚合全部由 數據庫服務完成。

圖片圖片

而 內存 Join,簡單來說就是把原本數據庫幫我們完成的數據聚合操作遷移到應用服務,在應用服務的內存中完成。

圖片圖片

數據庫join非常簡單,但隨著系統的發展,內存join變得越來越重要,其核心驅動力有:

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

2. 課程先導

發現變化,封裝變化,管理變化,是開發人員的必備技能。

本篇文章從查詢訂單這個業務場景為入口,針對數據的內存join進行多次抽象和封裝,最終實現“內存Join聲明化”。

首先,先看下最終的效果,從直觀上感受下“抽象”帶來的效率提升。

圖片圖片

通過抽象,可以達到如下效果:

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

神秘背后的本質便是“抽象”。讓我們以訂單查詢為線索,層層遞進,最終實現“能力聲明化”。

能力聲明化,是抽象的一種高級表現,無需編寫代碼,通過配置的方式為特定組件進行能力加強。

在正式開始之前,可以先了解下整體的推演流程:

圖片圖片

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

假設,我們是訂單中心的一位研發伙伴,需要開發 “我的訂單” 模塊,其核心接口包括:

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

根據需求定義 OrderService 接口如下:

public interface OrderService {
    // 我的訂單
    List<OrderListVO> getByUserId(Long userId);
    // 訂單詳情
    OrderDetailVO getDetailByOrderId(Long orderId);
}

// 為配合多種實現策略,使用抽象類進行統一
public abstract class OrderListVO {
    public abstract OrderVO getOrder();

    public abstract UserVO getUser();

    public abstract AddressVO getAddress();

    public abstract ProductVO getProduct();
}

// 為配合多種實現策略,使用抽象類進行統一
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 + 單條抓取方案

這么簡單的需求,那不是信手拈來,很快就提供了一版

圖片圖片

代碼具體如下:

@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);
        // 依次進行數據綁定
        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));
        // 獲取地址并進行綁定
        Address address = this.addressRepository.getById(order.getAddressId());
        AddressVO addressVO = AddressVO.apply(address);
        orderDetail.setAddress(addressVO);
        // 獲取用戶并進行綁定
        User user = this.userRepository.getById(order.getUserId());
        UserVO userVO = UserVO.apply(user);
        orderDetail.setUser(userVO);
        // 獲取商品并進行綁定
        Product product = this.productRepository.getById(order.getProductId());
        ProductVO productVO = ProductVO.apply(product);
        orderDetail.setProduct(productVO);
        // 獲取支付信息并進行綁定
        List<PayInfo> payInfos = this.payInfoRepository.getByOrderId(order.getId());
        List<PayInfoVO> payInfoVOList = payInfos.stream()
                .map(PayInfoVO::apply)
                .collect(toList());
        orderDetail.setPayInfo(payInfoVOList);
        return orderDetail;
    }

}

如果真的這樣實現,那你離“被跑路”不遠了。

為什么會這么說呢?因為 ==“我的訂單”這個接口存在嚴重的性能問題!==

“我的訂單”接口具體實現如下:

  1. 查詢 order 信息
  2. 依次對其進行數據抓取
  3. 完成數據綁定并返回結果

單個用戶請求,數據庫訪問總次數 = 1(獲取用戶訂單)+ N(訂單數量) * 3(需要抓取的關聯數據)

其中,N(訂單數量) * 3(關聯數據數量) 存在性能隱患,存在嚴重的==讀放大效應==。一旦遇到忠實用戶,存在成百上千訂單,除了超時別無辦法。

“訂單詳情”接口實現,目前問題不大,最大的問題為:==“訂單詳情”與“我的訂單”兩個接口存在大量的重復邏輯!==

3.2. 批量查詢 + 內存Join

首先,我們先來解決 “我的訂單”接口的性能問題。從之前的分析可知,性能低下的根本原因在于==“讀放大效應”==,數據庫請求次數與用戶訂單數成正比,為了更好的保障性能,最好將數據庫操作控制在一個常量。

整體思路為:先批量獲取要綁定的數據,然后遍歷每一個訂單,在內存中完成數據綁定。

圖片圖片

實現代碼如下:

@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());
        // 批量獲取用戶,并依次進行綁定
        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);
        }
        // 批量獲取地址,并依次進行綁定
        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);
        }
        // 批量獲取商品,并依次進行綁定
        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;
    }
}

調整之后,對于“我的訂單”接口,單個用戶請求==數據庫的訪問次數變成了常量(4)==。

如果你是這么實現的,那恭喜你,你已步入==合格程序員行列==。

3.3. 并行批量查詢 + 內存Join

批量查詢+內存Join 方案能滿足大部分場景,如果要抓取的數據太多,也就是數據庫訪問這個==常量變大==時,性能也會越來越差。

原因很簡單,由于串行執行,==整體耗時 = 獲取訂單耗時 + sum(抓取數據耗時)==

聰明的同學早就躍躍欲試,這個我會:==多線程并行執行唄。==

是的,基于 Future 的實現如下(還有很多版本,比如 CountDownLatch)

整體設計如下:

圖片圖片

示例代碼如下:

@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,這里只是演示,需要對資源進行評估)
        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);
        // 創建異步任務
        callables.add(() -> {
            // 批量獲取用戶,并依次進行綁定
            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;
        });
        // 創建異步任務
        callables.add(() ->{
            // 批量獲取地址,并依次進行綁定
            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;
        });
        // 創建異步任務
        callables.add(() -> {
            // 批量獲取商品,并依次進行綁定
            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;
        });

        // 執行異步任務
        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) {
      // 暫時忽略
    }
}

多線程并發執行,==整體耗時 = 獲取訂單耗時 + max(抓取數據耗時)==

如果你能夠這樣實現的,那恭喜你,你已步入==高級程序員行列==。

然后呢,到此為止了?NO,接下來才是高潮?。?!

讓我們==打開認知==,開啟==“抽象+封裝”==之旅。

4. Fetcher封裝

仔細研究上述代碼,尋找里面的==“變與不變”==,你會發現:

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

找到邏輯中的變化點,接下來便是有針對性的進行封裝。

4.1. 消除方法中的重復代碼

對于 “我的訂單” 和 “訂單詳情” 返回==不同的 VO==,該怎么處理呢?

非常簡單,思路如下:

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

整體設計如下:

圖片圖片

簡單示例如下:

// 以 UserVO 為例,ProductVO、AddressVO,PayInfoVO 基本一致,不在贅述
public interface UserVOFetcherV1 {
    Long getUserId();

    void setUser(UserVO user);
}
// OrderDetailVO 實現對應的接口,為了突出重點暫時忽略具體實現
public class OrderDetailVOFetcherV1 extends OrderDetailVO
    implements AddressVOFetcherV1,
        ProductVOFetcherV1,
        UserVOFetcherV1,
        PayInfoVOFetcherV1{
}
// OrderListVO 實現對應接口,為了突出重點暫時忽略具體實現
public class OrderListVOFetcherV1 extends OrderListVO
    implements AddressVOFetcherV1,
        ProductVOFetcherV1,
        UserVOFetcherV1 {
}

有了統一的操作接口,接下來便是抽取具體的綁定邏輯,以 UserVOFetcherExecutor 為例:

@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);
            }
        });
    }
}

實現邏輯沒有變化,最重要的變化在于“入參類型”,不在是具體的 VO,而是抽象的 UserVOFetcher 接口。

AddressVOFetcherExecutor、ProductVOFetcherExecutor、PayInfoVOFetcherExecutor 與 UserVOFetcherExecutorV1 邏輯基本一致,篇幅問題不在贅述。

這樣一個小小的調整,會給使用方帶來什么便利?一起看下使用方的變化:

@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 完成數據綁定
        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 完成數據綁定
        this.addressVOFetcherExecutorV1.fetch(orderDetailVOS);
        this.productVOFetcherExecutorV1.fetch(orderDetailVOS);
        this.userVOFetcherExecutorV1.fetch(orderDetailVOS);
        this.payInfoVOFetcherExecutorV1.fetch(orderDetailVOS);
        return orderDetail;
    }
}

兩個方法直接使用 FetcherExecutor 完成數據抓取和綁定,實現了==綁定邏輯的復用==。

如果再有 VO 需要進行數據綁定,只需:

  1. VO 實現 XXXFetcher 接口,實現對應方法,提供關聯數據并完成數據綁定
  2. 使用 XXXFetcherExecutor 完成數據綁定

至此,面對新業務基本上與“綁定邏輯”說再見了。

4.2. 重構綁定邏輯

接下來讓我們一起聚焦于綁定邏輯,先對比下上述的UserVOFetcherExecutor 與下面的 AddressVOFetcherExecutor, 找到里面的變化與不變:

@Component
public class AddressVOFetcherExecutorV1 {
    @Autowired
    private AddressRepository addressRepository;

    public void fetch(List<? extends AddressVOFetcherV1> fetchers){
        // 獲取關聯信息
        List<Long> ids = fetchers.stream()
                .map(AddressVOFetcherV1::getAddressId)
                .distinct()
                .collect(Collectors.toList());
        // 查詢關聯數據
        List<Address> addresses = addressRepository.getByIds(ids);

        // 轉為為 Map
        Map<Long, Address> addressMap = addresses.stream()
                .collect(toMap(address -> address.getId(), Function.identity()));

        // 依次進行數據綁定
        fetchers.forEach(fetcher -> {
            Long addressId = fetcher.getAddressId();
            Address address = addressMap.get(addressId);
            if (address != null){
                // 轉換為 VO
                AddressVO addressVO = AddressVO.apply(address);
                // 將數據寫回到結果
                fetcher.setAddress(addressVO);
            }
        });
    }
}

仔細觀察,會發現:

  • ==【不變】邏輯骨架基本一致==,基本是由:

獲取關聯信息

查詢關聯數據

將其轉換為 Map

講數據轉化為 VO

將 VO 綁定到結果對象

  • ==【變化】實現細節存在差異==;
  • 從什么接口中獲取關聯信息
  • 如何查詢關聯數據
  • 轉換為 Map 的鍵是什么
  • 如何將數據轉換為 VO
  • 如何完成數據的綁定

熟悉設計模式的伙伴是否眼前一亮?停頓一下好好回想一下,哪種模式就是用來處理這種問題的?

答案便是:模板方法模式

整體思想為:

  • 將不變的邏輯骨架封裝在父類方法
  • 將變化的實現細節放在子類中進行擴展

整體設計如下:

圖片圖片

抽取公共父類如下:

abstract class BaseItemFetcherExecutor<FETCHER extends ItemFetcher, DATA, RESULT>
        implements ItemFetcherExecutor<FETCHER>{

    @Override
    public void fetch(List<FETCHER> fetchers) {
        // 獲取關聯信息
        List<Long> ids = fetchers.stream()
                .map(this::getFetchId)
                .distinct()
                .collect(Collectors.toList());
        // 查詢關聯數據
        List<DATA> datas = loadData(ids);
        // 轉為為 Map
        Map<Long, List<DATA>> dataMap = datas.stream()
                .collect(groupingBy(this::getDataId));
        // 依次進行數據綁定
        fetchers.forEach(fetcher -> {
            Long id = getFetchId(fetcher);
            List<DATA> ds = dataMap.get(id);
            if (ds != null){
                // 轉換為 VO
                List<RESULT> result = ds.stream()
                        .map( data -> convertToVo(data))
                                .collect(Collectors.toList());
                // 將數據寫回到結果
                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 如下:

@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) {
        // 暫時忽略,稍后會細講
        return UserVOFetcherV2.class.isAssignableFrom(cls);
    }
}

UserVOFetcherExecutor究竟發生什么變化呢?好像變得更復雜了:

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

那我們究竟得到了什么好處?可以花幾分鐘好好思考一下?。?!

在說結果之前,讓我們看下另一個變化點。回想下 FetcherExecutor 的執行點,如下:

@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());
        // 手工調用,OrderListVO 實現新接口,需要增加新的依賴和調用
        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);
        // 手工調用,OrderDetailVO 實現新接口,需要增加新的依賴和調用
        this.addressVOFetcherExecutorV1.fetch(orderDetailVOS);
        this.productVOFetcherExecutorV1.fetch(orderDetailVOS);
        this.userVOFetcherExecutorV1.fetch(orderDetailVOS);
        this.payInfoVOFetcherExecutorV1.fetch(orderDetailVOS);
        return orderDetail;
    }

其實,需要調用哪些 FetcherExecutor 完全可以由 VO 實現的接口來確定。也就是說,==需要綁定新數據,只需 VO 繼承并實現新的 Fetcher 接口即可。==

對此,我們需要:

  • 一個統一的訪問入口,對外提供訪問
  • 每個 FetcherExecutor 能夠識別 VO 并執行綁定邏輯

哪個設計模式是用來解決這個問題?花幾分鐘好好思考一下!

答案是:==責任鏈模型==

標準的責任鏈模式用起來比較繁瑣,在 Spring 實現中大量使用他的一種變現,及提供一個驗證接口,由組件自身完成判斷,用于決定是否執行自身邏輯。

整體設計如下:

圖片圖片

首先,為了統一 FetcherExecutor 的行為,抽取通用接口:

public interface ItemFetcherExecutor<F extends ItemFetcher> {
    /**
     * 該組件是否能處理 cls 類型
     * @param cls
     * @return
     */
    boolean support(Class<F> cls);

    /**
     *  執行真正的數據綁定
     * @param fetchers
     */
    void fetch(List<F> fetchers);
}

具體的實現,可以見 UserVOFetcherExecutorV2 的 support 方法:

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

實現邏輯非常簡單,只是判斷 cls 是否實現了 UserVOFetcherV2 接口。

有了 FetcherExecutor 組件后,接下來就是為其提供統一的訪問入口:

@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))
                    // 執行真正的綁定
                    .forEach(itemFetcherExecutor -> itemFetcherExecutor.fetch(fetchers));
        }
    }
}

邏輯即為簡單,依次遍歷 FetcherExecutor,根據 support 執行結果,執行 fetch 邏輯。

【小常識】Spring 可以將容器中的全部實現直接注入到 List<Bean>。在上述代碼中,將會把所有的 ItemFetcherExecutor 實現注入到 itemFetcherExecutors 屬性。因此,在新增 FetcherExecutor 時,只需將其聲明為 Spring Bean,無需調整代碼邏輯。

OK,我們有了 FetcherService 提供統一的數據綁定能力,原來 OrderServiceFetcher 中 fetch 操作的變化點轉移到 FetcherService,自身變得非常穩定。具體如下:

@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 數據綁定發生變化,只需調整 VO 實現接口,此處無需變化
        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 數據綁定發生變化,只需調整 VO 實現接口,此處無需變化
        fetcherService.fetch(OrderDetailVOFetcherV2.class, Arrays.asList(orderDetail));
        return orderDetail;
    }
}

終于,我們將變化收斂到 VO 內,VO 需要綁定新的數據,只需實現對應接口即可。

4.3. 并發綁定

經過重構,代碼結構變得非常清晰,如果想通過多線程并發方式提供性能,需要調整哪些組件呢?好好想想?。?!

只需對FetcherService進行調整,讓我們來一個并發版本,具體如下:

@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)){
            // 創建異步執行任務
            List<Callable<Void>> callables = this.itemFetcherExecutors.stream()
                    .filter(itemFetcherExecutor -> itemFetcherExecutor.support(cls))
                    .map(itemFetcherExecutor -> (Callable<Void>) () -> {
                        itemFetcherExecutor.fetch(fetchers);
                        return null;
                    }).collect(Collectors.toList());
            // 線程池中并行執行
            this.executorService.invokeAll(callables);
        }
    }
}

OrderServiceFetcherV3 只需使用 ConcurrentFetcherService 替代 原來的 FetcherService 并擁有了并發能力。

5. 注解方案

5.1. 復雜配置 @JoinInMemory 來幫忙

縱觀整個 Fetcher 封裝,雖然結構清晰,但細節過于繁瑣,特別是:

  • 待抓取數據需要抽取 Fetcher 接口
  • 需要提供自己的 FetcherExecutor 實現
  • VO 需要實現多個 Fetcher 接口

這些不便將成為落地最大的阻礙,那有沒有辦法進行進一步簡化?

這需要思考下這些設計背后的深層需求:

  • Fetcher接口目的包括

提供綁定信息

設置綁定結果

被 FetcherExecutor 識別并進行處理

  • FetcherExecutor設計的目標包括:
  • 識別待處理的 Fetcher
  • 定制個性化流程

所有這些需求是否可用 ==注解== 的方式實現?

  • 在 VO 屬性上增加注解,說明綁定結果寫回到該屬性上
  • 注解配置來源屬性,提供綁定信息
  • 注解配置流程屬性,完成 FetcherExecutor 的個性化定制
  • 每個注解背后是一個 FetcherExecutor 實現,完成 FetcherExecutor 與 “Fetcher” 綁定

根據上述分析,注解可完成全部任務,新建注解如下:

@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;
}

乍一看,需要配置的信息真多,其實大多數配置全部與 FetcherExecutor 實現相關。

abstract class AbstractJoinItemExecutor<SOURCE_DATA, JOIN_KEY, JOIN_DATA, JOIN_RESULT> implements JoinItemExecutor<SOURCE_DATA> {

    /**
     * 從原始數據中生成 JoinKey
     * @param data
     * @return
     */
    protected abstract JOIN_KEY createJoinKeyFromSourceData(SOURCE_DATA data);

    /**
     * 根據 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 轉換為 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);

    /**
     * 未找到對應的 JoinData
     * @param data
     * @param joinKey
     */
    protected abstract void onNotFound(SOURCE_DATA data, JOIN_KEY joinKey);

    @Override
    public void execute(List<SOURCE_DATA> sourceDatas) {
        // 從源數據中提取 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);

        // 根據 JoinKey 獲取 JoinData
        List<JOIN_DATA> allJoinDatas = getJoinDataByJoinKeys(joinKeys);
        log.debug("get join data {} by join key {}", allJoinDatas, joinKeys);

        // 將 JoinData 以 Map 形式進行組織
        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;
            }
            // 根據 JoinKey 獲取 JoinData
            List<JOIN_DATA> joinDatasByKey = joinDataMap.get(joinKey);
            if (CollectionUtils.isNotEmpty(joinDatasByKey)){
                // 獲取到 JoinData, 轉換為 JoinResult,進行數據寫回
                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,進行 notFound 回調
                onNotFound(data, joinKey);
            }
        }
    }
}

JoinInMemory 注解屬性和AbstractJoinItemExecutor基本一致,在此就不做贅述,我們先看下具體的使用方式:

@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 屬性為例

屬性

含義

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.order.UserVO).apply(#root)}"

將 com.geekhalo.lego.joininmemory.order.UserVO 靜態方法 apply 作為轉換器,#root 指的是 User 對象

@JoinInMemory 注解中大量使用 SpEL,不熟悉的伙伴可以自行網上進行檢索。

其他部分不變,定義 OrderService 如下:

@Service
public 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 全部配置化,并通過 注解的方式進行呈現,相對于 Coding 方案,注解方案更加靈活,工作量也更小。

5.2. 復雜配置 @Alias 來幫忙

相對于 Fetcher 封裝,一個 @JoinInMemory 成功干掉了兩個組件,但觀其自身配置起來還是非常繁瑣。比如,在訂單查詢這個場景,在 OrderListVO 和 OrderDetailVO 中都需要對 UserVO 進行數據綁定,觀察兩個注解,我們會發現很多重復配置:

//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;

兩個配置完全一樣,細品之后會發現:

  • 【變化】入參變化,讀取的屬性不同,只是本次恰巧相同而已

OrderListVO 指的是 OrderListVO 屬性 order 的id值

OrderDetailVO 指的是 OrderDetailVO 屬性 order 的值

  • 【不變】處理邏輯不變
  • keyFromJoinData 指的是 user對象的 id
  • loader 指的是通過 userRepository 的 getByIds 加載數據
  • joinDataConverter 指的是將 user 轉換為 UserVO
  • 【不變】
  • 將綁定結果 UserVO 綁定到屬性上(屬性名不同沒有影響)

對于不變部分如何進行統一管理?

自定義注解 結合 Spring @AliasFor 便可以解決這個問題,以 UserVO 為例:

@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();
}

新注解有如下幾個特點:

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

有了自定義注解,使用變的非常方便:

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

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

其他使用方式不變,但實現了邏輯簡化:

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

5.3. 開啟并發 @JoinInMemoryConfig 來幫忙

如果擔心性能,可以一鍵開啟并發綁定,示例如下:

@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 配置如下:

屬性

含義

executorType

PARALLEL 并行執行;SERIAL 串行執行

executorName

執行器名稱,并行執行所使用的線程池名稱,默認為 defaultExecutor

6. 最佳實踐

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

@JoinInMemory 注解上配置的信息太多,如果直接在業務代碼中使用,非常難以維護,當每個配置發生變化后,很難一次性修改到位。所以,建議只將他作為“原注解”使用。

整體思路詳見:

圖片圖片

6.2. 注意線程池隔離

對于不同的數據綁定需求,建議使用不同的線程池,從資源層面對不同功能進行隔離,從而將由于依賴接口發生阻塞導致線程耗盡所造成的影響控制在最小范圍。

@JoinInMemoryConfig 的 executorName 屬性配置的便是執行器名稱,不配置直接使用 “defaultExecutor”,具體代碼如下:

@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());
}

如需使用自定義線程池需:

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

7. 小結

推導邏輯有點長不知道你get到多少,先簡單回顧一下:

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

不知道你有什么感觸,接下來,有什么計劃?

  1. 沒有改變,習慣怎么做就怎么做?
  2. 將 JoinInMemory 引入到自己的工程?
  3. 嘗試發現項目中的“變化”和“不變”,嘗試自己封裝,增加抽象能力?
責任編輯:武曉燕 來源: geekhalo
相關推薦

2023-12-14 10:03:52

內存數據

2023-12-18 20:46:49

MySQLB+Tree單引擎查詢

2014-07-09 15:45:50

寬帶

2014-03-18 10:08:35

2022-05-31 10:57:56

數據庫云原生

2021-12-31 07:32:50

AI人工智能主播

2011-11-21 10:58:01

Java遞歸分形幾何

2025-08-27 06:15:00

2011-12-26 15:11:36

JavaScript

2015-04-30 11:18:49

七牛

2024-06-18 08:57:42

Repository模式數據

2013-04-17 14:27:16

物聯網俠諾無線網絡

2011-05-20 15:38:37

SAPDELL云計算

2015-02-25 20:16:06

2023-12-12 16:46:44

AI云團隊云管理

2013-10-16 10:20:20

2023-10-06 23:36:50

2021-03-24 08:03:38

NettyJava NIO網絡技術

2012-06-11 08:58:45

Win 8微軟
點贊
收藏

51CTO技術棧公眾號

干日本少妇视频| 国产精品极品尤物在线观看| 日韩精品在线播放视频| ririsao久久精品一区| 成人aa视频在线观看| 午夜精品久久久久久久99黑人| 中文字幕av网址| 亚洲狼人在线| 亚洲第一主播视频| 亚洲电影免费| 色综合视频在线| 久久成人免费日本黄色| 久久久久久久网站| 日韩福利在线视频| 成人动态视频| 欧美在线观看视频一区二区三区| 欧美日韩中文字幕在线播放| 美州a亚洲一视本频v色道| 日韩av在线播放中文字幕| 欧美激情精品久久久久久蜜臀 | 美女免费视频一区二区| 欧美极品少妇xxxxⅹ免费视频 | 欧美成人免费网| 不卡一区二区在线观看| 成人在线视频区| 色综合久久综合中文综合网| 日韩一二区视频| 国产h在线观看| av男人天堂一区| 999精品视频一区二区三区| 日本免费在线观看视频| 伊人久久久大香线蕉综合直播| 夜夜嗨av一区二区三区免费区| www.黄色网| 亚洲我射av| 在线免费观看视频一区| 大j8黑人w巨大888a片| av免费在线免费| 中文字幕一区二区三区四区不卡| 欧美日韩一区二区三区在线观看免| 精品人妻一区二区三区三区四区 | 一级网站在线观看| 国产精品.xx视频.xxtv| 色综合一区二区三区| 国产免费黄色一级片| 18网站在线观看| 亚洲少妇最新在线视频| 亚洲不卡1区| 香蕉视频911| 国产成人av电影| 91久久久久久久久久久久久| 中文字幕免费高清在线观看| 日韩中文字幕区一区有砖一区 | 日本福利视频网站| 国产在线观看a视频| 国产精品久久久久一区二区三区| 欧美一区二视频在线免费观看| 亚洲aⅴ在线观看| 91香蕉视频mp4| 精品欧美日韩| 亚洲欧美日本在线观看| 91天堂素人约啪| 蜜桃av噜噜一区二区三| 深夜福利在线看| 91香蕉视频污在线| 欧美精品亚洲| 国产69久久| 中文字幕欧美一| 黄色网址在线免费看| 韩国av网站在线| 亚洲一级不卡视频| 久久国产亚洲精品无码| 色吧亚洲日本| 在线观看av不卡| 天堂av在线8| 亚洲国产欧美国产第一区| 精品少妇一区二区三区在线播放 | 成人av网站在线| 精品在线视频一区二区| 韩国福利在线| 亚洲欧洲日产国产综合网| 亚洲色图都市激情| 理论不卡电影大全神| 日本精品一级二级| 天天综合天天添夜夜添狠狠添| 成人av在线播放| 亚洲成人动漫在线播放| 性久久久久久久久久| 成人羞羞在线观看网站| 欧美精品在线观看91| 日韩手机在线观看| 免费av成人在线| 99久久精品久久久久久ai换脸| 无码国产精品高潮久久99| 国产蜜臀av在线一区二区三区| 热这里只有精品| 高清毛片在线观看| 欧美日韩三级视频| 成人在线电影网站| 91欧美在线| 91成人天堂久久成人| 怡春院在线视频| av电影在线观看完整版一区二区| 亚洲国产一区二区三区在线 | 久久a爱视频| 中文日韩在线视频| 韩国av免费观看| 久久成人久久鬼色| 狼狼综合久久久久综合网| 黄av在线播放| 图片区日韩欧美亚洲| 久热精品在线观看视频| 欧美巨大xxxx| 久久99久久亚洲国产| 中文字幕第三页| 99久久国产综合精品麻豆| 三年中国中文在线观看免费播放| 在线天堂资源www在线污| 日韩三级av在线播放| 婷婷色一区二区三区| 亚洲清纯自拍| 亚洲影院高清在线| jizz在线观看| 欧美性生交xxxxx久久久| 激情小说欧美色图| 无码一区二区三区视频| 国产精品久久久久99| 亚洲 欧美 自拍偷拍| 亚洲一区二区不卡免费| 国产农村妇女精品久久| 日韩欧美伦理| 国产成人一区二区在线| 青草久久伊人| 粉嫩老牛aⅴ一区二区三区| 超级砰砰砰97免费观看最新一期 | 福利视频网站一区二区三区| 一区二区视频国产| 亚洲a∨精品一区二区三区导航| 亚洲国产美女精品久久久久∴| 欧美特级一级片| 久久超碰97人人做人人爱| 亚洲欧洲国产精品久久| 国模一区二区| 亚洲人精品午夜在线观看| 五月婷婷色丁香| 波多野结衣中文字幕一区二区三区| 亚洲啊啊啊啊啊| 精品国产一区二区三区2021| 日韩最新免费不卡| 在线观看国产精品入口男同| 久久精品水蜜桃av综合天堂| av免费网站观看| 国产乱码精品一区二区三区四区| 欧洲成人免费视频| 欧美少妇另类| 在线一区二区视频| 一二三四国产精品| 久久成人羞羞网站| 欧美h视频在线观看| 国产一区一区| 欧美激情啊啊啊| 日本成人动漫在线观看| 亚洲成年人影院| 大黑人交xxx极品hd| 男人的天堂亚洲| 日韩中文字幕一区| 四虎影视成人精品国库在线观看| 久久天天躁狠狠躁老女人| 超碰免费在线97| 亚洲国产美女搞黄色| 99久久人妻精品免费二区| 老鸭窝亚洲一区二区三区| 日韩久久久久久久| 亚洲日韩中文字幕一区| 欧美激情久久久| 日av在线播放| 欧美日韩色一区| 欧美在线视频第一页| av毛片久久久久**hd| 国产精品人人妻人人爽人人牛| 欧美a级成人淫片免费看| 成人免费视频网址| а√在线天堂官网| 亚洲午夜久久久久久久| 国产模特av私拍大尺度| 性做久久久久久免费观看| 亚洲精品国产一区黑色丝袜| 国内精品自线一区二区三区视频| 国产成a人亚洲精v品在线观看| 少妇久久久久| 国产深夜精品福利| 国产美女高潮在线观看| 中文字幕日韩精品在线观看| 国产成人精品毛片| 色偷偷一区二区三区| 爱爱视频免费在线观看| 99国产精品久久久久久久久久 | 精品粉嫩超白一线天av| 黄色片视频免费| 亚洲理论在线观看| 麻豆av免费观看| 国产成人在线看| 簧片在线免费看| 亚洲三级色网| 天堂v在线视频| 你懂的一区二区三区| 91精品国产一区二区三区动漫| 小视频免费在线观看| 超碰日本道色综合久久综合| 日本大片在线观看| 日韩免费观看高清完整版在线观看| 精品国产xxx| 亚欧色一区w666天堂| 国产女人18水真多毛片18精品| 久久久不卡影院| 在线观看国产免费视频| 国产精品一区二区视频| 亚洲黄色av网址| 午夜亚洲精品| 国产无限制自拍| 欧美久久影院| 夜夜嗨av一区二区三区免费区| 久久久久久久9| 精品国产乱码久久久| 国产一区二区三区奇米久涩| 精品久久亚洲| 成人a免费视频| 欧亚一区二区| 欧美一区深夜视频| av毛片午夜不卡高**水| 美女精品视频一区| 国产在线看片| www.亚洲免费视频| av影片在线看| 亚洲情综合五月天| 天堂资源中文在线| 亚洲国产成人久久| 亚洲AV无码一区二区三区性| 日韩一区二区三区在线| 国产精品久久久久久久免费看| 欧美特级限制片免费在线观看| 伊人中文字幕在线观看| 五月婷婷色综合| 国产在线视频99| 亚洲一区二区av在线| 国产精品第一页在线观看| 亚洲综合丝袜美腿| 久久网免费视频| 亚洲丰满少妇videoshd| 国产午夜福利精品| 精品国产乱码久久久久久虫虫漫画| 精品在线视频免费| 欧美日韩国产一区在线| 麻豆久久久久久久久久| 欧美性xxxxx极品| 亚洲天堂一区在线| 欧洲精品一区二区三区在线观看| 国产熟妇一区二区三区四区| 色婷婷精品大在线视频| 天堂网一区二区| 欧美视频在线一区二区三区| 最近中文字幕av| 7777精品伊人久久久大香线蕉完整版| 99久久精品无免国产免费| 日韩精品在线一区| 你懂的网站在线| 精品五月天久久| 国产精品麻豆一区二区三区| 日韩在线视频导航| 怡红院红怡院欧美aⅴ怡春院| 久久青草福利网站| 欧美大胆成人| 成人福利在线观看| 999精品视频在这里| 欧美精品123| 欧美oldwomenvideos| 久久福利一区二区| 国产欧美另类| 岛国毛片在线播放| 国产成人免费视频精品含羞草妖精| 污污污www精品国产网站| 久久精品免费在线观看| 国产麻豆a毛片| 亚洲影视在线播放| 久久国产乱子伦精品| 欧美一三区三区四区免费在线看 | h片在线观看下载| 欧美专区第一页| 伊人久久大香| 精品国产aⅴ麻豆| 欧美mv日韩| 少妇高潮喷水在线观看| 奇米影视一区二区三区| 欧美一级大片免费看| 久久精品视频在线免费观看 | 精品magnet| 在线观看av大片| 亚洲国产高潮在线观看| av在线首页| 97精品国产97久久久久久免费| 精品欧美日韩精品| 国产乱人伦精品一区二区| 成人aaaa| 国产精品一区二区免费在线观看| 激情文学综合插| 免费黄色在线视频| 一二三区精品视频| 中文字幕在线观看1| 亚洲第一在线视频| 免费av毛片在线看| 日本高清不卡在线| 国产成人一二| 最近看过的日韩成人| 新67194成人永久网站| 91av免费观看| 国产精品色呦呦| 亚洲天堂日韩av| 日韩精品一区在线| 四虎久久免费| 国产福利精品av综合导导航| av毛片精品| 黄色高清视频网站| 日韩中文字幕一区二区三区| 国产麻豆天美果冻无码视频| 亚洲黄一区二区三区| 国产又粗又大又爽| 亚洲最新在线视频| 刘亦菲一区二区三区免费看| 国产日韩在线一区二区三区| 正在播放日韩欧美一页| 激情视频免费网站| 欧美高清在线精品一区| 久久精品视频1| 亚洲精品福利视频| 999av小视频在线| 福利视频久久| 欧美三区美女| 麻豆免费在线观看视频| 亚洲精品视频观看| a天堂视频在线| 欧美成年人网站| 不卡一区视频| 97超碰免费观看| 韩国三级在线一区| xxxx日本少妇| 欧美一区二区国产| 性欧美猛交videos| 3d蒂法精品啪啪一区二区免费| 香蕉久久网站| 91视频福利网| 一区二区三区在线视频播放| 99在线观看精品视频| 欧美成人免费一级人片100| 国产精品一区免费在线| 影音先锋成人资源网站| 国产福利91精品一区二区三区| 极品盗摄国产盗摄合集| 日韩欧美美女一区二区三区| 女囚岛在线观看| 精品日韩欧美| 久久婷婷av| 国产成人免费观看网站| 欧美精品黑人性xxxx| 菠萝蜜视频国产在线播放| 91青青草免费观看| 在线播放一区| 性久久久久久久久久| 欧美视频在线一区二区三区| 黄色av电影在线播放| 超碰97国产在线| 在线亚洲免费| 性欧美一区二区| 宅男噜噜噜66一区二区66| 亚洲奶水xxxx哺乳期| 精品视频导航| 美女视频第一区二区三区免费观看网站| frxxee中国xxx麻豆hd| 精品999在线播放| 久久精品女人天堂av免费观看| 一区二区三区四区| 成人永久看片免费视频天堂| 黄色污污网站在线观看| 久久影院资源网| 美女一区2区| 亚洲77777| 亚洲国产精品久久艾草纯爱| 你懂的视频在线播放| 成人黄色影片在线| 精品999网站| 无码人中文字幕| 精品裸体舞一区二区三区| 日本综合久久| 男人天堂a在线| 国产精品久线观看视频| 丰满少妇被猛烈进入| 国产精品黄色av| 一本久道久久久| 国产精品嫩草影院俄罗斯 | 国产69久久| 国产伦精品一区二区三区免|