如何在没有乐观锁或悲观锁的情况下处理服务方法的并发问题?

How to handle concurrency issue on a service method without an optimistic or pessimistic lock?

我正在开发酒店和预订微服务,用户可以在其中预订 room/rooms 特定酒店的特定入住和退房日期。一家酒店可以包含许多相同类型的房间。 (我正在使用 spring-boot,spring-data-jpa 和 oracle 数据库)

我想处理多个用户预订同一酒店房间时的并发问题(考虑到房间有限,并非所有用户都能预订成功)。

我不会存储任何有关在该房型的特定入住和退房日期有多少房间可用的信息,因为这样做会使检查房间可用性变得复杂。因此,我没有任何可以添加@Version 注释并使用乐观锁的实体。所以我采取的方法是,在预订被保存到数据库之前,我正在检查我的服务方法,如果那个房间在那个入住和退房日期有空。

我实现该服务方法(如下所示)的方式是获取与新用户(想要预订该房间的人)提供的入住和退房日期冲突的该房间的所有即将到来的预订.如果房间可用,则预订将持续存在,否则将为用户抛出异常。下面是服务代码(变量名应该是不言自明的):

@Transactional
public Booking findRoomsAvailibilityAndSave(Booking bookingInfoFromUser) {
        Booking persistedBooking = null;
        boolean isAllRoomsAvailable = true;
        int hotelId = bookingInfoFromUser.getHotelId();
        LocalDate checkInDate = bookingInfoFromUser.getCheckInDate();
        LocalDate checkoutDate = bookingInfoFromUser.getCheckoutDate();
        Set<RoomBookingDetails> newBookingRoomsInfo = bookingInfoFromUser.getRoomBookingDetails();
        Map<Integer, Integer> roomTotalRooms = new HashMap<>();
        Map<Integer, Integer> roomTotalRoomsBooked = new HashMap<>();

        List<BookingSummary> existingBookings = bookingRepository
                .findByHotelIdAndCheckInDateBeforeAndCheckoutDateAfter(hotelId, checkInDate, checkoutDate);

        RestTemplate template = new RestTemplate();
        ResponseEntity<Object> hotelEntity = template.getForEntity("http://localhost:8383/hotel/" + hotelId,
                Object.class);
        Object hotelObj = hotelEntity.getBody();
        ObjectMapper mapper = new ObjectMapper();
        JsonNode hotelNode = mapper.convertValue(hotelObj, JsonNode.class);

        hotelNode.withArray("hotelRooms")
                .forEach(roomNode -> roomTotalRooms.put(roomNode.get("id").asInt(), roomNode.get("noRooms").asInt()));
        
        existingBookings.forEach(existingBooking -> {
            existingBooking.getRoomBookingDetails().forEach(bookedRoom -> {
                if (roomTotalRoomsBooked.containsKey(bookedRoom.getHotelRoomId())) {
                    Integer existingKey = bookedRoom.getHotelRoomId();
                    Integer updateValue = roomTotalRoomsBooked.get(existingKey) + bookedRoom.getNoRooms();
                    roomTotalRoomsBooked.put(existingKey, updateValue);
                } else {
                    roomTotalRoomsBooked.put(bookedRoom.getHotelRoomId(), bookedRoom.getNoRooms());
                }
            });
        });

        for (RoomBookingDetails newRoom : newBookingRoomsInfo) {
            if (!((roomTotalRooms.get(newRoom.getHotelRoomId())
                    - roomTotalRoomsBooked.get(newRoom.getHotelRoomId())) >= newRoom.getNoRooms())) {
                isAllRoomsAvailable = false;
            }
        }

        if (isAllRoomsAvailable) {
            persistedBooking = bookingService.save(persistedBooking);
        } else {
            throw new OptimisticLockException();
        }

        return persistedBooking;
    }

我想要的是,如果两个用户试图同时预订一个唯一可用的房间,那么应该只有一个用户能够成功。我不想锁定此服务,而是让两个用户尝试预订房间。但是,由于在我展示的这种服务方法中的检查,一个将失败。正如我所解释的,我无法进行乐观锁定,因为我没有包含特定入住和退房日期的可用房间信息的实体,我将在预订后更新这些信息。我检查可用性的唯一方法是获取特定房间类型的房间总数,并将其与该入住和退房日期已预订的房间总数进行比较。

到目前为止,我看到的所有解决方案都讨论了使用乐观方法进行处理,其中版本将针对我正在更新的行进行更改(但我不会在此处更新任何行)。我见过的另一种方法是悲观锁定我正在更新的行(但正如我所说,我没有更新任何行)。

有没有办法解决这个并发问题,我让两个用户都尝试预订房间(一种乐观的方法),但是服务方法中一个用户的可用性检查失败并为他们抛出异常。

如果我遗漏了任何信息,请随时告诉我,非常感谢您的帮助。

因为您没有 object/row 来创建锁,所以您需要创建一个。例如,您可以在 Room table.

上添加 lockbooking-in-progress

完整的预订比这样:

  • 如果当前为false,则将booking-in-progress设置为true,否则预订失败。
  • 提交。
  • 执行预订。
  • booking-in-progress 设置为 false
  • 提交。

您可能想要一份清理闲置时间过长的锁的工作。如果您的应用程序在创建锁后崩溃,则可能会发生这种情况。

当然,您可以通过将 table 与 roomday 作为主键来创建更细粒度的锁。在这种情况下,您不会更新任何内容,而只是插入一行。主键将防止并发写入。