高效的并發(fā)管理:房間預(yù)訂 API 的樂觀鎖和消息隊(duì)列
想象一下這樣一個(gè)場(chǎng)景:多名旅行者同時(shí)嘗試預(yù)訂熱門目的地的最后一個(gè)可用房間。如果沒有適當(dāng)?shù)牟l(fā)控制機(jī)制,這種情況很快就會(huì)變成競(jìng)爭(zhēng)狀態(tài),導(dǎo)致房間超額預(yù)訂和客戶沮喪。
我們將深入研究用于應(yīng)對(duì)這些挑戰(zhàn)的兩種關(guān)鍵策略的復(fù)雜性:樂觀鎖定和消息隊(duì)列。
想象一下您正在使用一個(gè)在線酒店預(yù)訂平臺(tái),類似于 Booking.com 或 Expedia 等知名平臺(tái)。以下是同步和異步流程如何發(fā)揮作用:
同步流程:
預(yù)訂房間(同步):
- 您訪問酒店預(yù)訂網(wǎng)站并選擇您的目的地、入住和退房日期以及其他偏好。
- 您點(diǎn)擊“立即預(yù)訂”按鈕即可預(yù)訂房間。
- 該網(wǎng)站使用基于 HTTP 的同步協(xié)議(如 REST 或 SOAP)將您的請(qǐng)求發(fā)送到酒店的預(yù)訂系統(tǒng)。
- 酒店的系統(tǒng)會(huì)立即同步處理您的請(qǐng)求。它檢查房間可用性,為您預(yù)訂房間,并生成預(yù)訂號(hào)碼。
- 預(yù)訂號(hào)碼將發(fā)送回您的瀏覽器,并在幾秒鐘內(nèi)顯示在網(wǎng)站上。
- 您可以立即獲得預(yù)訂號(hào)碼,然后可以放心地繼續(xù)您的旅行計(jì)劃。
創(chuàng)建房間實(shí)體
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
@Entity
public class Room {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String roomType;
private boolean isAvailable;
// getters and setters
}
創(chuàng)建房間存儲(chǔ)庫
import org.springframework.data.jpa.repository.JpaRepository;
public interface RoomRepository extends JpaRepository<Room, Long> {
Room findByRoomType(String roomType);
}
創(chuàng)建客房預(yù)訂請(qǐng)求 DTO
import java.time.LocalDate;
public class RoomBookingRequest {
private String roomType;
private LocalDate checkInDate;
private LocalDate checkOutDate;
// getters and setters
}
創(chuàng)建客房預(yù)訂響應(yīng) DTO
public class RoomBookingResponse {
private String reservationNumber;
// getters and setters
}
創(chuàng)建客房服務(wù)
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.UUID;
@Service
public class RoomService {
@Autowired
private RoomRepository roomRepository;
public RoomBookingResponse bookRoom(RoomBookingRequest bookingRequest) {
String roomType = bookingRequest.getRoomType();
LocalDate checkInDate = bookingRequest.getCheckInDate();
LocalDate checkOutDate = bookingRequest.getCheckOutDate();
Room room = roomRepository.findByRoomType(roomType);
if (room != null && room.isAvailable()) {
// Add validation to check availability based on check-in and check-out dates here.
// For simplicity, we'll assume the room is available.
room.setAvailable(false);
roomRepository.save(room);
// Generate a reservation number (you can implement your logic here).
String reservationNumber = generateReservationNumber();
return new RoomBookingResponse(reservationNumber);
} else {
throw new RoomNotAvailableException();
}
}
private String generateReservationNumber() {
// Generate a unique reservation number (you can implement your logic here).
return UUID.randomUUID().toString();
}
}
創(chuàng)建房間控制器
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/api/rooms")
public class RoomController {
@Autowired
private RoomService roomService;
// Book a room
@PostMapping("/book")
public RoomBookingResponse bookRoom(@RequestBody RoomBookingRequest bookingRequest) {
return roomService.bookRoom(bookingRequest);
}
}
定義自定義異常
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ResponseStatus;
@ResponseStatus(HttpStatus.BAD_REQUEST)
public class RoomNotAvailableException extends RuntimeException {
public RoomNotAvailableException() {
super("The requested room is not available.");
}
}
測(cè)試API
您可以使用 Postman 或 cURL 等工具來測(cè)試您的 API。要預(yù)訂房間,請(qǐng)http://localhost:8080/api/rooms/book使用包含房間類型、入住日期和退房日期的 JSON 正文發(fā)出 POST 請(qǐng)求:
{
"roomType" : "Standard" ,
"checkInDate" : "2023-10-01" ,
"checkOutDate" : "2023-10-05"
}
如果房間可用,API 將返回帶有預(yù)訂編號(hào)的 JSON 響應(yīng)。您可以根據(jù)您的課堂需求自定義預(yù)訂邏輯和預(yù)訂號(hào)碼生成RoomService。
異步流程
當(dāng)多個(gè)用戶同時(shí)調(diào)用Booking API時(shí)
當(dāng)多個(gè)并發(fā)呼叫在系統(tǒng)中搜索同一房間時(shí),可能存在潛在的缺點(diǎn)和挑戰(zhàn):
競(jìng)爭(zhēng)條件:當(dāng)多個(gè)請(qǐng)求嘗試同時(shí)預(yù)訂同一房間時(shí),可能會(huì)出現(xiàn)競(jìng)爭(zhēng)條件。如果處理不當(dāng),這可能會(huì)導(dǎo)致超額預(yù)訂,即系統(tǒng)允許的預(yù)訂數(shù)量超過了可用房間的數(shù)量。
如何解決并發(fā)問題?
樂觀鎖定是一種數(shù)據(jù)庫級(jí)技術(shù),可防止多個(gè)用戶同時(shí)嘗試更新同一資源時(shí)發(fā)生數(shù)據(jù)沖突。
另一方面,消息隊(duì)列是異步通信工具,可確保請(qǐng)求的有序、可靠處理,使其成為分布式系統(tǒng)中處理并發(fā)請(qǐng)求的理想選擇。
方法一:實(shí)現(xiàn)消息隊(duì)列響應(yīng)并發(fā)請(qǐng)求
消息隊(duì)列確保請(qǐng)求按照接收順序進(jìn)行處理,從而防止競(jìng)爭(zhēng)條件和超量預(yù)訂。
- 多個(gè)客戶端向端點(diǎn)發(fā)出 POST 請(qǐng)求/api/rooms/book以同時(shí)預(yù)訂酒店房間。
- 處理RoomController傳入的預(yù)訂請(qǐng)求。
- 該roomService.bookRoom方法接收預(yù)訂請(qǐng)求。
- 它使用該方法將預(yù)訂請(qǐng)求發(fā)送到名為“room-booking”的 RabbitMQ 消息隊(duì)列rabbitTemplate.convertAndSend。
- 它向客戶端返回初步響應(yīng),其中包含一條消息,表明預(yù)訂請(qǐng)求已發(fā)送,客戶端應(yīng)等待確認(rèn)。
- 預(yù)訂請(qǐng)求被放入“房間預(yù)訂”隊(duì)列中。消息隊(duì)列系統(tǒng)(在本例中為 RabbitMQ)確保每個(gè)預(yù)訂請(qǐng)求都按照收到的順序進(jìn)行處理,以防止競(jìng)爭(zhēng)情況。
- 監(jiān)聽RoomBookingMessageConsumer“房間預(yù)訂”隊(duì)列。
- processBookingRequest當(dāng)預(yù)訂請(qǐng)求出隊(duì)時(shí),將調(diào)用消費(fèi)者的方法。在該方法中,您通常會(huì)實(shí)現(xiàn)以下邏輯:
- 根據(jù)請(qǐng)求的房型、入住日期和退房日期檢查客房供應(yīng)情況。
- 如果房間可用,則生成預(yù)訂號(hào)碼。
- 更新數(shù)據(jù)庫中的房間可用性,將其標(biāo)記為不可用,以防止重復(fù)預(yù)訂。
- 通過RabbitMQ向客戶端發(fā)送包含預(yù)約號(hào)的響應(yīng)消息
8. 在 中RoomBookingMessageConsumer,處理預(yù)訂請(qǐng)求并生成預(yù)訂號(hào)碼后,您可以使用傳統(tǒng)的 HTTP 客戶端(例如RestTemplate、HttpClient)將確認(rèn)響應(yīng)直接發(fā)送到客戶端的回調(diào) URL 端點(diǎn)(該端點(diǎn)在請(qǐng)求中發(fā)送)。
執(zhí)行:
創(chuàng)建客房預(yù)訂請(qǐng)求和響應(yīng) DTO
import java.time.LocalDate;
public class RoomBookingRequest {
private String roomType;
private LocalDate checkInDate;
private LocalDate checkOutDate;
private String clientCallbackUrl; // Added to specify the client's callback URL
// getters and setters
}
public class RoomBookingResponse {
private String reservationNumber;
// getters and setters
}
修改控制器
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/api/rooms")
public class RoomController {
@Autowired
private RoomService roomService;
@PostMapping("/book")
public RoomBookingResponse bookRoom(@RequestBody RoomBookingRequest bookingRequest) {
return roomService.bookRoom(bookingRequest);
}
}
創(chuàng)建客房預(yù)訂服務(wù)(生產(chǎn)者)
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.http.ResponseEntity;
import org.springframework.web.client.RestTemplate;
@Service
public class RoomService {
@Autowired
private RoomRepository roomRepository;
@Autowired
private RabbitTemplate rabbitTemplate;
private RestTemplate restTemplate = new RestTemplate();
public RoomBookingResponse bookRoom(RoomBookingRequest bookingRequest) {
String roomType = bookingRequest.getRoomType();
// Send the booking request to the message queue
rabbitTemplate.convertAndSend("room-booking-exchange", "room-booking", bookingRequest);
return new RoomBookingResponse("Booking request sent. Please wait for confirmation.");
}
// This method sends the response to the client's callback URL
public void sendResponseToClient(RoomBookingResponse response, String clientCallbackUrl) {
ResponseEntity<Void> result = restTemplate.postForEntity(clientCallbackUrl, response, Void.class);
if (result.getStatusCode().is2xxSuccessful()) {
// Handle a successful response sent to the client
} else {
// Handle the case when the response to the client failed
}
}
}
創(chuàng)建消息消費(fèi)者
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
@Component
public class RoomBookingMessageConsumer {
@Autowired
private RoomService roomService;
@RabbitListener(queues = "room-booking-queue")
public void processBookingRequest(RoomBookingRequest bookingRequest) {
// Process the booking request
RoomBookingResponse response = processBookingLogic(bookingRequest);
// Send the confirmation response to the client's callback URL
roomService.sendResponseToClient(response, bookingRequest.getClientCallbackUrl());
}
private RoomBookingResponse processBookingLogic(RoomBookingRequest bookingRequest) {
// Implement your booking logic here, e.g., checking room availability and generating a reservation number
// Update room availability in the database
// Send a response message to confirm the booking or indicate unavailability
// For simplicity, we'll assume the room is available and generate a reservation number.
String reservationNumber = generateReservationNumber();
return new RoomBookingResponse(reservationNumber);
}
private String generateReservationNumber() {
// Generate a unique reservation number (you can implement your logic here).
return "RES-" + System.currentTimeMillis();
}
}
方法二:實(shí)現(xiàn)樂觀鎖來處理并發(fā)請(qǐng)求
您可以修改代碼以使用同步方法和 JPA 樂觀鎖定。
步驟1:修改Room實(shí)體:@Version向?qū)嶓w添加一個(gè)字段Room以啟用樂觀鎖定:
import javax.persistence.*;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
@Entity
public class Room {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String roomType;
private boolean isAvailable;
@Version
private Long version;
// getters and setters
}
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
@Service
public class RoomService {
@Autowired
private RoomRepository roomRepository;
private final ConcurrentHashMap<Long, Lock> roomLocks = new ConcurrentHashMap<>();
public RoomBookingResponse bookRoom(RoomBookingRequest bookingRequest) {
String roomType = bookingRequest.getRoomType();
LocalDate checkInDate = bookingRequest.getCheckInDate();
LocalDate checkOutDate = bookingRequest.getCheckOutDate();
Room room = roomRepository.findByRoomType(roomType);
if (room != null) {
Lock roomLock = roomLocks.computeIfAbsent(room.getId(), id -> new ReentrantLock());
roomLock.lock();
try {
if (room.isAvailable()) {
// Add validation to check availability based on check-in and check-out dates here.
// For simplicity, we'll assume the room is available.
room.setAvailable(false);
roomRepository.save(room);
// Generate a reservation number (you can implement your logic here).
String reservationNumber = generateReservationNumber();
return new RoomBookingResponse(reservationNumber);
}
} finally {
roomLock.unlock();
}
}
throw new RoomNotAvailableException();
}
private String generateReservationNumber() {
// Generate a unique reservation number (you can implement your logic here).
return UUID.randomUUID().toString();
}
}
詳細(xì)工作原理:
并發(fā)請(qǐng)求&ConcurrentHashMap:當(dāng)同一房間收到多個(gè)并發(fā)預(yù)訂請(qǐng)求時(shí),它們可能同時(shí)到達(dá)并可能導(dǎo)致競(jìng)爭(zhēng)條件。的引入ConcurrentHashMap確保每個(gè)房間都有自己的鎖。這ConcurrentHashMap是一個(gè)線程安全的映射,可以由多個(gè)線程同時(shí)安全地訪問。
通過鎖定并發(fā)更新房間可用性:如果兩個(gè)線程同時(shí)嘗試預(yù)訂同一個(gè)房間,則只有其中一個(gè)線程會(huì)使用 成功獲取鎖roomLock.lock(),而另一個(gè)線程將暫時(shí)阻塞,直到第一個(gè)線程釋放鎖。
釋放鎖以供其他線程更新:一旦線程獲取了鎖并成功修改了房間的可用性,它就會(huì)使用 釋放鎖roomLock.unlock(),從而允許其他線程繼續(xù)預(yù)訂其他房間。
樂觀鎖防止數(shù)據(jù)庫級(jí)別的競(jìng)爭(zhēng)條件:在代碼中,實(shí)體中的字段啟用數(shù)據(jù)庫級(jí)別的樂觀鎖。更新房間時(shí),JPA 在允許更新之前會(huì)根據(jù)實(shí)體中的版本字段檢查數(shù)據(jù)庫中的版本字段。@VersionRoom
- 如果兩個(gè)事務(wù)同時(shí)嘗試更新同一個(gè)房間,根據(jù)版本號(hào)的比較,只有其中一個(gè)會(huì)成功,從而防止數(shù)據(jù)庫級(jí)別的數(shù)據(jù)沖突。
- 因此 2 個(gè)不同的事務(wù)無法同時(shí)更新數(shù)據(jù)庫中的一個(gè)房間