Issue
I am working on a hotel and a booking microservices where a user can book room/rooms for a specific hotel for a specific check-in and checkout date. A hotel can contain many rooms of the same type. (I am using spring-boot, spring-data-jpa and oracle database)
I want to handle concurrency issues when multiple users are booking the same hotel rooms (considering the rooms are limited and not all users will be successful in booking it).
I am not storing any information about how many rooms are available at a specific check-in and checkout date for that room type because doing that would make checking the availability of the rooms complicated. Therefore, I do not have any entity that I can add @Version annotation and work with optimistic lock. So the approach I am taking is that before the booking gets persisted in database, I am checking in my service method if that room is available for that check-in and checkout date.
The way I have implemented that service method (shown below) is to fetch all the upcoming bookings for that room that conflicts with the check-in and checkout date given by the new user (who wants to book that room). If the room is available, then the booking will persist, or else will throw the exception for the user. Below is the service code (the variable names should be self-explanatory):
@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;
}
What I want is if two users are trying to book an only available room at the same time, only one user should be able to succeed. I do not want to lock this service, but let both the users try booking the room. But, one will fail because of the check in this service method I have shown. As I have explained, I cannot do optimistic locking because I do not have an entity that contains the information of available rooms for specific check-in and checkout date that I will update after booking. The only way I am checking the availability is by fetching the total rooms that are there for a specific room type, and comparing it by the total rooms already booked for that check-in and checkout date.
All the solutions that I have seen so far talks about handling using optimistic approach where the version will change for the row I am updating (but I am not updating any row here). Another approach that I have seen is pessimistic locking the row that I am updating (but as I said, I am not updating any row).
Is there a way to solve this concurrency issue where I let both the users try booking the room (kind of optimistic approach), but the availability check fails for one user in the service method and throw an exception for them.
If any information I have missed, feel free to let me know, and I appreciate the help.
Solution
Since you don't have an object/row to create a lock on you need to create one. For example you could add a lock
or booking-in-progress
column on the Room
table.
The full booking than works like this:
- set
booking-in-progress
totrue
if it is currentlyfalse
, fail the booking otherwise. - commit.
- perform the booking.
- set
booking-in-progress
tofalse
. - commit.
You probably want a job that cleans up locks that are hanging around for too long. This could happen if your application crashes after creating the lock.
Of course you can create more fine grained locks by having a table with room
and day
as the primary key. In such a case you don't update anything, but just insert a row. The primary key will prevent concurrent writes.
Answered By - Jens Schauder