假设你正在构建一个电子商务平台,使用微服务来管理库存、支付和运输。客户下单后,你面临一个老生常谈的问题——如何确保所有这些服务能够和谐地协作,而不让你头疼不已?

传统的ACID事务在单体应用中表现出色,但在微服务世界中却显得力不从心。就像用大锤砸核桃——过于强硬,可能会引发更多问题。这时,LRA(长时间运行的活动)就派上用场了。

为什么ACID不够用:

  • 服务之间的紧密耦合(在微服务中是大忌)
  • 由于锁定导致的性能瓶颈
  • 随着系统增长而出现的可扩展性问题

LRA采用了不同的方法。它不强求跨服务的原子性事务,而是采用最终一致性模型。就像协调一场舞蹈,每个舞者(服务)都知道自己的角色,并知道如果有人踩到他们的脚该如何恢复。

MicroProfile LRA

那么,MicroProfile LRA究竟是什么?可以把它想象成你微服务芭蕾舞的编舞者。它提供了一种标准化的方法来管理跨多个服务的长时间运行的分布式操作。

关键概念:

  • 长时间运行的操作:可能需要几秒、几分钟甚至几小时才能完成的操作
  • 补偿:如果事情出错,能够撤销操作
  • 最终一致性:接受跨服务的一致性需要时间

LRA不试图强求即时一致性。相反,它为你提供了管理这些长时间运行操作生命周期的工具,确保最终你的系统达到一致状态。

设置LRA:逐步指南

准备好为你的项目添加一些LRA的魔力了吗?让我们一步步来设置:

1. 添加MicroProfile LRA依赖

首先,你需要将MicroProfile LRA依赖添加到你的项目中。如果你使用Maven,将以下内容添加到你的pom.xml中:


<dependency>
    <groupId>org.eclipse.microprofile.lra</groupId>
    <artifactId>microprofile-lra-api</artifactId>
    <version>1.0</version>
</dependency>

2. 配置你的LRA协调器

你需要一个LRA协调器来管理你的LRA。这可以是一个独立的服务或是你现有基础设施的一部分。以下是使用Quarkus的简单配置:


quarkus.lra.coordinator.url=http://localhost:8080/lra-coordinator

3. 注解你的方法

现在是有趣的部分——为你的方法添加注解以参与LRA。以下是一个简单的例子:


@Path("/order")
public class OrderService {

    @POST
    @Path("/create")
    @LRA(LRA.Type.REQUIRED)
    public Response createOrder(@HeaderParam(LRA.LRA_HTTP_CONTEXT_HEADER) String lraId) {
        // 你的订单创建逻辑
        return Response.ok().build();
    }

    @PUT
    @Path("/complete")
    @Complete
    public Response completeOrder(@HeaderParam(LRA.LRA_HTTP_CONTEXT_HEADER) String lraId) {
        // 完成订单的逻辑
        return Response.ok().build();
    }

    @PUT
    @Path("/compensate")
    @Compensate
    public Response compensateOrder(@HeaderParam(LRA.LRA_HTTP_CONTEXT_HEADER) String lraId) {
        // 补偿(取消)订单的逻辑
        return Response.ok().build();
    }
}

在这个例子中,createOrder启动或加入一个LRA,completeOrder在LRA成功完成时调用,而compensateOrder在需要回滚LRA时调用。

LRA注解:你的新好朋友

LRA带有一组注解,使管理长时间运行的操作变得轻而易举。让我们来看看最重要的几个:

@LRA

这是主角。用它来定义你的LRA范围:


@LRA(LRA.Type.REQUIRED)
public Response doSomething() {
    // 这个方法将始终在LRA中运行
}

Type参数可以是:

  • REQUIRED:加入现有LRA或创建一个新的
  • REQUIRES_NEW:总是创建一个新的LRA
  • MANDATORY:必须在现有LRA中调用
  • SUPPORTS:如果存在LRA则使用,否则不使用
  • NOT_SUPPORTED:即使存在LRA也不使用

@Compensate

这是你的安全网。用它来定义如果事情出错时该怎么办:


@Compensate
public Response undoStuff(URI lraId) {
    // 清理逻辑
    return Response.ok().build();
}

@Complete

这是成功的路径。当你的LRA成功完成时调用这个方法:


@Complete
public Response finalizeStuff(URI lraId) {
    // 完成逻辑
    return Response.ok().build();
}

协调操作:LRA舞蹈

现在我们已经有了注解,让我们看看LRA如何协调跨服务的操作。想象一下,我们正在构建一个在线书店,拥有独立的库存、支付和运输服务。

订单流程:


@Path("/order")
public class OrderService {

    @Inject
    InventoryService inventoryService;
    
    @Inject
    PaymentService paymentService;
    
    @Inject
    ShippingService shippingService;

    @POST
    @Path("/place")
    @LRA(LRA.Type.REQUIRED)
    public Response placeOrder(Order order) {
        // 第一步:预留库存
        inventoryService.reserveBooks(order.getBooks());
        
        // 第二步:处理支付
        paymentService.processPayment(order.getTotal());
        
        // 第三步:安排运输
        shippingService.arrangeShipment(order.getShippingDetails());
        
        return Response.ok().build();
    }

    @Compensate
    public Response compensateOrder(URI lraId) {
        // 如果出现问题,将调用此方法撤销所有操作
        inventoryService.releaseReservation(lraId);
        paymentService.refundPayment(lraId);
        shippingService.cancelShipment(lraId);
        return Response.ok().build();
    }

    @Complete
    public Response completeOrder(URI lraId) {
        // 当一切成功时调用此方法
        inventoryService.confirmReservation(lraId);
        paymentService.confirmPayment(lraId);
        shippingService.confirmShipment(lraId);
        return Response.ok().build();
    }
}

在这个例子中,如果任何步骤失败(例如支付未通过),将调用@Compensate方法,撤销所有先前的步骤。如果一切成功,@Complete方法将最终确定订单。

超时和取消策略:保持你的LRA受控

LRA本质上是长时间运行的,但有时“长时间运行”会变成“永无止境”。为了防止你的LRA失控,MicroProfile LRA提供了超时和取消机制。

设置超时

你可以使用@LRA注解的timeLimit参数为你的LRA设置超时:


@LRA(value = LRA.Type.REQUIRED, timeLimit = 10, timeUnit = ChronoUnit.MINUTES)
public Response longRunningOperation() {
    // 这个LRA将在10分钟后自动超时
    // ...
}

如果LRA在指定时间内未完成,它将被自动取消,并调用@Compensate方法。

手动取消

有时,你可能需要手动取消LRA。你可以通过注入LRAClient并调用其cancel方法来实现:


@Inject
LRAClient lraClient;

public void cancelOperation(URI lraId) {
    lraClient.cancel(lraId);
}

LRA与Sagas:选择你的武器

此时,你可能会想,“等等,这听起来很像Saga模式!”你没错。LRA和Sagas在分布式事务家族中就像是表亲。让我们来看看它们的相似之处和不同之处:

相似之处:

  • 都处理长时间运行的分布式事务
  • 都使用补偿来撤销部分工作
  • 都旨在实现最终一致性

不同之处:

  • LRA是一个标准化的规范,而Saga是一个模式
  • LRA通过注解提供内置支持,使其更易于实现
  • Sagas通常使用事件进行协调,而LRA使用HTTP头

那么,什么时候应该使用LRA而不是Sagas呢?

  • 如果你使用的是MicroProfile兼容框架(如Quarkus或Open Liberty)
  • 当你想要一种标准化的方法,减少样板代码
  • 如果你更喜欢使用注解的声明式风格

另一方面,如果:

  • 你需要对补偿过程进行更细粒度的控制
  • 你的系统是高度事件驱动的
  • 你没有使用MicroProfile兼容框架

监控和管理LRA生命周期

现在我们已经启动并运行了LRA,如何监控它们呢?监控LRA对于了解分布式事务的健康状况和性能至关重要。

需要关注的指标

MicroProfile LRA提供了几个开箱即用的指标:

  • lra_started:启动的LRA数量
  • lra_completed:成功完成的LRA数量
  • lra_cancelled:取消的LRA数量
  • lra_duration:LRA的持续时间

你可以使用MicroProfile Metrics来公开这些指标。以下是在Quarkus中设置的方法:


quarkus.smallrye-metrics.extensions.lra.enabled=true

与监控工具的集成

一旦你公开了你的指标,你可以将它们与流行的监控工具集成。以下是如何设置Prometheus抓取配置的快速示例:


scrape_configs:
  - job_name: 'lra-metrics'
    metrics_path: '/metrics'
    static_configs:
      - targets: ['localhost:8080']

以及一个简单的Grafana仪表板查询,用于可视化LRA持续时间:


rate(lra_duration_seconds_sum[5m]) / rate(lra_duration_seconds_count[5m])

日志记录和警报

不要忘记为你的LRA设置适当的日志记录。以下是使用SLF4J的简单示例:


@Inject
Logger logger;

@LRA
public void someOperation() {
    logger.info("Starting LRA: {}", Context.getObjectId());
    // ...
}

@Complete
public void completeOperation() {
    logger.info("Completing LRA: {}", Context.getObjectId());
    // ...
}

@Compensate
public void compensateOperation() {
    logger.warn("Compensating LRA: {}", Context.getObjectId());
    // ...
}

为取消的LRA或超过特定持续时间的LRA设置警报,以便及早发现潜在问题。

真实世界的例子:LRA的实际应用

让我们看看几个MicroProfile LRA大放异彩的真实场景:

示例1:旅行预订系统

想象一个旅行预订系统,用户可以在一次交易中预订航班、酒店和租车。以下是使用LRA可能的结构:


@Path("/booking")
public class BookingService {

    @Inject
    FlightService flightService;
    
    @Inject
    HotelService hotelService;
    
    @Inject
    CarRentalService carRentalService;

    @POST
    @Path("/create")
    @LRA(LRA.Type.REQUIRES_NEW)
    public Response createBooking(BookingRequest request) {
        flightService.bookFlight(request.getFlightDetails());
        hotelService.reserveRoom(request.getHotelDetails());
        carRentalService.rentCar(request.getCarDetails());
        return Response.ok().build();
    }

    @Compensate
    public Response compensateBooking(URI lraId) {
        flightService.cancelFlight(lraId);
        hotelService.cancelReservation(lraId);
        carRentalService.cancelRental(lraId);
        return Response.ok().build();
    }

    @Complete
    public Response completeBooking(URI lraId) {
        flightService.confirmFlight(lraId);
        hotelService.confirmReservation(lraId);
        carRentalService.confirmRental(lraId);
        return Response.ok().build();
    }
}

在这个例子中,如果预订的任何部分失败(例如酒店已满),整个交易将被回滚,确保用户不会得到部分预订。

示例2:电子商务订单处理

以下是电子商务订单处理系统如何使用LRA来处理订单履行的复杂性:


@Path("/order")
public class OrderProcessingService {

    @Inject
    InventoryService inventoryService;
    
    @Inject
    PaymentService paymentService;
    
    @Inject
    ShippingService shippingService;
    
    @Inject
    NotificationService notificationService;

    @POST
    @Path("/process")
    @LRA(LRA.Type.REQUIRES_NEW, timeLimit = 30, timeUnit = ChronoUnit.MINUTES)
    public Response processOrder(Order order) {
        String orderId = order.getId();
        inventoryService.reserveItems(orderId, order.getItems());
        paymentService.processPayment(orderId, order.getTotalAmount());
        String trackingNumber = shippingService.createShipment(orderId, order.getShippingAddress());
        notificationService.sendOrderConfirmation(orderId, trackingNumber);
        return Response.ok().build();
    }

    @Compensate
    public Response compensateOrder(URI lraId) {
        String orderId = extractOrderId(lraId);
        inventoryService.releaseReservedItems(orderId);
        paymentService.refundPayment(orderId);
        shippingService.cancelShipment(orderId);
        notificationService.sendOrderCancellationNotice(orderId);
        return Response.ok().build();
    }

    @Complete
    public Response completeOrder(URI lraId) {
        String orderId = extractOrderId(lraId);
        inventoryService.confirmItemsShipped(orderId);
        paymentService.finalizePayment(orderId);
        shippingService.dispatchShipment(orderId);
        notificationService.sendShipmentDispatchedNotification(orderId);
        return Response.ok().build();
    }

    private String extractOrderId(URI lraId) {
        // 从LRA ID中提取订单ID
        // ...
    }
}

这个例子展示了LRA如何管理复杂的订单处理流程,确保所有步骤(库存管理、支付处理、运输和客户通知)都得到协调,并在需要时可以回滚。

结论:拥抱LRA带来的分布式和谐

MicroProfile LRA为分布式事务世界带来了一股清新的空气。它提供了一种标准化的、基于注解的方法来管理跨微服务的长时间运行操作,在一致性和分布式系统的现实之间取得了平衡。

关键要点:

  • LRA接受最终一致性,使其非常适合微服务架构
  • 基于注解的方法减少了样板代码,使事务边界更易于理解
  • 内置的补偿支持允许优雅地处理失败
  • 与MicroProfile的集成使监控和指标变得轻而易举

当你进入分布式事务的世界时,考虑尝试MicroProfile LRA。它可能正是你的微服务一直渴望的秘密武器!

“在分布式系统中,完美是好的敌人。LRA帮助我们在不牺牲可扩展性或性能的情况下实现‘足够好’的一致性。”

记住,虽然LRA很强大,但它不是万能的。选择LRA、Sagas或其他分布式事务模式时,始终考虑你的具体用例和需求。

编码愉快,愿你的分布式事务永远对你有利!