マイクロサービスフレームワークSpring Cloud紹介Part 4:Eureka,Ribbon,Feignを用いてRESTサービスクライアントを実現


原文住所:http://skaka.me/blog/2016/08/25/springcloud4/
前の文章では、ユーザー登録サービスを開発しました.この文章では、mysteam注文サービスの注文機能を開発する方法を紹介します.注文機能はサービス間の相互作用とイベントの処理に関連し、開発中に使用したフレームワークとクラスライブラリを簡単に説明します.コードを書く前に、注文の処理プロセスを見てみましょう. 
ここで、1,2,3,4,11ステップの黒い矢印は同期操作を表し、5,6,7,8,9,10ステップは非同期操作である.注文インタフェースは注文する製品ID、数量と使用するクーポンIDを受信し、製品サービスのインタフェースを呼び出して製品情報を照会し、クーポンインタフェースを呼び出してクーポンが有効であるかどうかを検証し、口座インタフェースを呼び出して口座金額が十分であるかどうかを判断する(mysteamは仮想アイテムモールで、先にチャージして購入する形式を採用しています).これらのチェックが成功すると、注文サービスは口座引き落としイベントとクーポン使用イベントをMQに送信し、口座サービスとクーポンサービスはMQからイベントを読み取る処理を行い、処理が成功すると注文サービスは結果を受け取ることができ、注文状態を に設定し、処理が失敗またはタイムアウトすると注文状態を に設定する.
1.Modelの実装
プロセスは分かりました.コードを見てみましょう.注文クラスは$YOUR_PATH/mysteam/order/core/src/main/java/com/akkafun/order/domain/Order.javaで、注文クラスは前のユーザークラスと似ています.このうち2つのフィールドに注意する必要があります.
1
2
3
4
5
6
@Column
@Enumerated(value = EnumType.STRING)
private OrderStatus status;

@OneToMany(fetch = FetchType.LAZY, mappedBy = "order")
private List<OrderItem> orderItemList = new ArrayList<>(0);

OrderStatusは、受注ステータスを表す列挙です.OrderItemは受注アイテムです.
2.DAOの実現
DAO層には実際のコードがほとんどないので貼らない.
3.サービスの実装
注文したビジネスロジックはサービス内にあり、$YOUR_PATH/mysteam/order/core/src/main/java/com/akkafun/order/service/OrderService.javaを開き、placeOrderの方法を見つけます.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
/**
 *    
 *
 * @param placeOrderDto
 * @return
 */
@Transactional
public Order placeOrder(PlaceOrderDto placeOrderDto) {

    //...

    //#1
    //      
    List<Long> productIds = placeOrderDto.getPlaceOrderItemList().stream()
            .map(PlaceOrderItemDto::getProductId)
            .collect(Collectors.toList());

    List<ProductDto> productDtoList = productGateway.findProducts(productIds);

    //...

    //#2
    //       
    List<OrderCoupon> orderCouponList = new ArrayList<>();
    Set<Long> couponIdSet = new HashSet<>(placeOrderDto.getCouponIdList());
    if (!couponIdSet.isEmpty()) {

        //#2
        List<CouponDto> couponDtoList = couponGateway.findCoupons(new ArrayList<>(couponIdSet));

        orderCouponList = couponDtoList.stream().map(couponDto -> {
            OrderCoupon orderCoupon = new OrderCoupon();
            orderCoupon.setCouponAmount(couponDto.getAmount());
            orderCoupon.setCouponCode(couponDto.getCode());
            orderCoupon.setCouponId(couponDto.getId());
            return orderCoupon;
        }).collect(Collectors.toList());

    }

    //#3
    //      
    long couponAmount = orderCouponList.stream().mapToLong(OrderCoupon::getCouponAmount).sum();
    order.setPayAmount(order.calcPayAmount(order.getTotalAmount(), couponAmount));

    //          
    if (order.getPayAmount() > 0L) {

        boolean balanceEnough = accountGateway.isBalanceEnough(placeOrderDto.getUserId(), order.getPayAmount());
        if(!balanceEnough) {
            throw new AppBusinessException(CommonErrorCode.BAD_REQUEST, "    ,       ");
        }
    }

    //...

    //#4
    eventBus.ask(
            AskParameterBuilder.askOptional(askReduceBalance, askUseCoupon)
                    .callbackClass(OrderCreateCallback.class)
                    .addParam("orderId", String.valueOf(order.getId()))
                    .build()
    );

    return order;
}

コードは少し長くて、私はその中の一部のコードを省略しました.  placeOrderメソッドで主に行うことは、1.注文した製品IDに基づいて、製品サービスに製品情報を問い合わせ、注文金額を計算すること.2.クーポンを使用している場合、クーポンサービスにクーポンが有効かどうかを問い合わせる(RESTインタフェースを要求する).3.注文金額に基づいて、ユーザー残高が十分かどうかを口座サービスに問い合わせる(RESTインタフェースを要求する)4.上記の手順が正常に完了すると、口座引き落としイベントおよびクーポン使用イベント(クーポンがある場合)を送信し、コールバックメソッドを登録してイベント結果を待つ.イベント処理に成功するとmarkCreateSuccessメソッドが呼び出され、処理に失敗するとmarkCreateFailが呼び出され、  markCreateSuccessメソッドのコードは次のとおりです.
1
2
3
4
5
6
7
@Transactional
public void markCreateSuccess(Long orderId) {
    Order order = checkOrderBeforeMarkSuccessOrFail(orderId);
    order.setStatus(OrderStatus.CREATED);

    orderRepository.save(order);
}

この方法は注文の状態を にするだけで、プロセスは完了する.  markCreateFailの処理過程は同様であるが、注文状態を に変更したにすぎない.
ここを見て、まずサービス呼び出しの実現の詳細を管理しないで、細心の注意を払うあなたはいくつか疑問を生むかもしれません:1.第4歩はどうして事件の形式を使って引き落としてクーポンを処理して、前の照会操作と同じようにRESTインタフェースを使って処理することができませんか?2.どうして先にユーザーの残高が十分かどうかを調べてから、引き落とし事件を送って、直接引き落とし事件を送るのはよくありませんか?残高照会に成功した後、他の業務で残高が修正されましたが、引き落とし事件を処理する際に残高が不足したらどうしますか?3.第4歩で引き落とし事件とクーポン使用事件を同時に送信しましたが、引き落としに成功した場合、クーポン使用に失敗したらどうしますか?
実はこれらの問題はすべて同じ問題ドメインを指しています:分布式の事務.分布式の事務はマイクロサービスを開発してまず解決しなければならない問題です.分布式の事務は1つのとても大きい話題で、ここで私はただ簡単にeBayのDan Pritchardが提出したBASEの原則を紹介します:基本的に利用可能な(Basically Available)ソフト状態(Soft state)最終的に一致します(Eventually consistent)
BASEは実は従来のデータベースのACIDと2つの异なる思想で、私达の上の注文システムを例にします.注文サービスは口座サービスに控除事件を送って、口座サービスは事件を受け取ってそして処理に成功して、しかしまだ処理结果を注文サービスに送っていないで、この时システムのデータは短い不一致の状态にあります:ユーザーの口座の残高はすでに差し引かれて减らされて、しかし受注ステータスか か.しばらくすると,受注サービスは引き落としイベントの処理結果を取得し,受注ステータスを にする.この時点でシステムは最終的に一致する状態に達する.このトランザクション方法はすべての業務に適用されるものではなく,一致性を強くする必要がある場合は,2 PCまたは3 PCで完了しなければならない.
mysteamの取引の原则を理解して、私达は振り返ってさっき提出した3つの问题を见ます:1.mysteamは事件の方式を使って取引を行うので、RESTインタフェースは普通はただ照会あるいはその他の取引を必要としない操作を実现するために用います.だからデータの修正に関连して、普通はすべて事件を通じて(通って)完成します.イベント操作は、イベント処理に失敗するとイベント取り消しに関わるため、比較的時間のかかる操作であるため、残高照会を先に行い、残高不足の直接プロセスは中止された.我々の経験によると、一般的に残高照会に成功した後の控除に失敗する確率は比較的小さいため、収益は支払いより大きい.3.これはイベントの取り消し処理にかかわる.mysteamの受注サービスでは、引き落とし成功とクーポン使用失敗の2つのイベントの結果を受け取ると、注文サービスはイベント取り消しプロセスを開始し、口座サービスに引き落とし取り消しイベントを送信し、注文ステータスを注文失敗にする.
以上、mysteamのトランザクション処理はBASEに従い、実装方法はイベントを使用しています.トランザクションのその他の詳細とイベントの実装方法については、後で個別の文章で紹介します.ここでは、本編のテーマに戻り、RESTインタフェースを呼び出す方法について説明します.ここでは、Eureka、Ribbon、Feignの3つのコンポーネントについて簡単に紹介します.Eureka:サービス登録センター.私たちのRESTサービス起動時に自分のアドレスをEurekaに登録し、他のサービスを必要とするアプリケーションはEurekaにサービスアドレッシングを要求し、ターゲットサービスのipアドレスを取得すると、そのアドレスを使用してターゲットサービスに直結する.Ribbon:クライアント負荷等化クラスライブラリ.クライアントが要求するターゲットサービスに複数のインスタンスがある場合、Ribbonはリクエストを各インスタンスに分散する.一般的にEurと結合するekaと一緒に使用する.Feign:HTTPクライアントクラスライブラリ.Feignが提供する注釈を用いてHTTPインタフェースのクライアントコードを記述するのは非常に簡単で,Javaインタフェースを宣言して少量の注釈を加えるだけで完了する.
次に、コードの例を見てみましょう.アカウントサービスのインタフェースを例にとると、placeOrderメソッドでアカウント残高を照会したコードは以下の通りです.
1
boolean balanceEnough = accountGateway.isBalanceEnough(placeOrderDto.getUserId(), order.getPayAmount());
$YOUR_PATH/mysteam/order/core/src/main/java/com/akkafun/order/service/gateway/AccountGateway.javaを開きます.コードは次のとおりです.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Service
public class AccountGateway {

    protected Logger logger = LoggerFactory.getLogger(AccountGateway.class);

    @Autowired
    AccountClient accountClient;

    @HystrixCommand(ignoreExceptions = RemoteCallException.class)
    public boolean isBalanceEnough(Long userId, Long amount) {
        return accountClient.checkEnoughBalance(userId, amount).isSuccess();
    }

}

AccountClientはFeign注記を追加したインタフェースです.
1
2
3
4
5
6
7
@FeignClient(AccountUrl.SERVICE_HOSTNAME)
public interface AccountClient {

    @RequestMapping(method = RequestMethod.GET, value = AccountUrl.CHECK_ENOUGH_BALANCE)
    BooleanWrapper checkEnoughBalance(@PathVariable("userId") Long userId, @RequestParam("balance") Long balance);

}
@FeignClient注記:サービスidを宣言する必要があります.このサービスidは、YAMLプロファイルに割り当てられているspring.application.nameの値です.たとえば、account.ymlspring.application.nameの値はaccountです.要求されたRESTインタフェースにはurlパスパラメータuserIdとクエリーパラメータbalanceが必要です.コードではRibbonのコードを直接呼び出す必要はありません.Feignは処理を支援します.すべて.我々のAccountClientインタフェースの宣言によると、FeignはSpringコンテナが起動した後、生成したエージェントクラスをAccountGatewayに注入するので、HTTP呼び出しの実装コードを書く必要がなく、RESTインタフェースの呼び出しを完了することができる.
ここまで注文するロジックは完成しました.分散環境の下で、サービス間の依存は脆弱で不安定であることを知っています.1つのサービスインスタンスの遅延やダウンタイムによってすべてのサービスが利用できない可能性が高いことを知っています.mysteamにはhystrixが導入されています.注意深い同学はAccountGateway@HystrixCommand注釈を発見した可能性があります.次の文章ではhystrixの基本的な使い方、hystrix boardとturbineをどのように使用してhystrixサービスを監視するか.