酒店预订系统:酒店预订系统流程图
在当今瞬息万变的软件开发领域,构建一个既可靠又高效的酒店预订系统无疑是一项充满挑战的任务,特别是在处理并发请求和数据一致性方面。
在当今瞬息万变的软件开发领域,构建一个既可靠又高效的酒店预订系统无疑是一项充满挑战的任务,特别是在处理并发请求和数据一致性方面本文将深入剖析如何运用 Go 语言,结合悲观锁、乐观锁以及两阶段提交(2PC)协议,依托 PostgreSQL 数据库,来实现一个功能完备的酒店预订系统。
一、系统架构与数据模型概述我们的酒店预订系统核心由两个数据表构成:“room_type_inventory”(房间类型库存)和“reservation”(预订)“room_type_inventory”表的设计独具匠心,旨在优化预订流程。
用户在预订时首先选择房间类型(如标准间、大床房或两居室),而具体房间号则在入住时才分配为便于管理和查询特定日期范围内的预订信息,我们为每一天的数据单独保留一行记录
该表采用(“hotel_id”,“room_type_id”,“date”)作为复合主键,确保数据的唯一性和完整性示例记录如下:hotel_idroom_type_iddatetotal_inventory。
total_reserved112024-01-01103112024-01-02105而“reservation”表则专注于存储用户的预订信息,字段清晰明了。
其中,“Status”字段表示预订状态,包括“pending”(待处理)、“cancelled”(已取消)、“paid”(已支付)、“refunded”(已退款)以及“rejected”(已拒绝)等二、单体架构下的预订功能实现
(一)事务处理机制在单体架构中,由于系统仅依赖一个数据库,我们采用事务机制来确保“room_type_inventory”和“reservation”表的数据完整性和一致性以下是一个事务处理的示例代码:。
// Create 方法用于创建一个新的预订 func(repo *defaultRepository)Create(ctx context.Context, reservation *Reservation)
error { // 开始一个数据库事务 tx, err := repo.db.DB().BeginTx(ctx, nil) if err != nil {
return fmt.Errorf("事务初始化错误: %w", err) } defer tx.Rollback() // 如果函数返回前没有提交事务,则回滚 // 检查库存
inventory, err := checkInventory(tx, reservation) if err != nil { // 如果检查库存失败,返回错误
return fmt.Errorf("检查库存错误: %w", err) } // 如果已预订数量加1超过了总库存数量,则返回没有足够容量的错误 if inventory.TotalReserved+
1 > inventory.TotalInventory { return ErrNoEnoughCapacity } // 在数据库中创建预订 if
err = createReservation(tx, reservation); err != nil { // 如果插入预订失败,返回错误 return fmt.Errorf(
"插入预订错误: %w", err) } // 更新库存 if err = updateInventory(tx, reservation); err != nil {
// 如果更新库存失败,返回错误 return fmt.Errorf("更新库存错误: %w", err) } // 提交事务 if err = tx.Commit(); err !=
nil { // 如果提交事务失败,返回错误 return fmt.Errorf("提交事务错误: %w", err) } // 如果所有操作都成功,返回nil表示没有错误
returnnil } (二)幂等性问题及其解决方案当用户多次点击“创建预订”按钮时,可能会引发重复预订问题为解决此问题,我们将“reservation_id”设为主键这样,当尝试插入具有相同预订 ID 的多条记录时,数据库将抛出唯一性约束错误,从而有效防止重复预订。
(三)并发冲突与锁定策略在多用户并发场景中,若多个用户同时尝试预订同一房间,且数据库隔离级别非可序列化,则可能出现竞争条件为解决此问题,我们引入悲观锁和乐观锁1. 悲观锁策略悲观锁策略采取谨慎态度,预先锁定资源。
在 PostgreSQL 中,可使用“FOR UPDATE”语句实现例如:checkInventoryQuery := ` SELECTdate, total_inventory, total_reserved
FROM room_type_inventory WHERE hotel_id = $1AND room_type_id = $2ANDdateBETWEEN $3AND $4FORUPDATE
` 当一个事务执行此查询时,会锁定符合条件的行,其他事务若尝试修改这些行,将被阻塞,直至锁释放但悲观锁存在死锁和性能下降的风险,需精心设计无死锁代码,并设置合理的锁时间或实现死锁检测机制2. 乐观锁策略。
乐观锁策略基于并发冲突概率较低的假设,不进行预先锁定在准备持久化更改时,验证数据是否发生变化若已变化,则中止或重试操作在我们的系统中,通过在“room_type_inventory”表中添加“version”列实现乐观锁。
具体操作如下:首先,使用 SELECT 查询获取最新版本值:checkInventoryQuery := ` SELECTdate, total_inventory, total_reserved,
versionFROM room_type_inventory WHERE hotel_id = $1AND room_type_id = $2ANDdateBETWEEN $3AND $4`
然后,在 UPDATE 查询的 WHERE 子句中使用该版本值:updateInventoryQuery := ` UPDATE room_type_inventory SET total_reserved = total_reserved +
1, version = version + 1WHERE hotel_id = $1AND room_type_id = $2ANDdateBETWEEN $3AND $4ANDversion = $
5` 乐观锁在并发可能性较低的环境中性能卓越,具有良好的可扩展性但在高并发环境下,由于冲突检测和重试机制,可能导致性能损失三、微服务架构下的预订功能实现优化在我们的系统中,存在两个关键的数据库:库存数据库与预订数据库。
它们各自包含一个核心表——“room_type_inventory”和“reservation”,这些表承载着系统运行所必需的数据字段随着系统架构逐步向微服务转型,数据一致性的维护难度显著增加为了应对微服务环境中的这一挑战,我们选择了2PC(两阶段提交)协议作为解决方案,并重点探讨了如何在PostgreSQL中利用其预备事务功能来实现2PC。
(一)2PC原理简述两阶段提交协议的核心目标是确保在多个节点上执行的事务具有原子性,即要么所有节点的事务都成功提交,要么全部中止,以此来维护数据的一致性(二)PostgreSQL中的2PC实现策略在深入探讨实现细节之前,让我们先了解一下如何在PostgreSQL中运用预备事务功能。
首先,需要将max_prepared_transactions配置参数传递给PostgreSQL服务器,以支持预备事务随后,可以按以下方式使用:BEGIN; UPDATE room_type_inventory
SET total_reserved = total_reserved + 1WHERE hotel_id = 100AND room_type_id = 1ANDdateBETWEEN2023-10-12
AND2023-10-13; PREPARETRANSACTIONtransaction-id; -- 提交或回滚事务 COMMIT PREPARED transaction-id;
-- 提交更新事务 ROLLBACK PREPARED transaction-id; -- 回滚更新事务对于预订记录的插入操作,同样可以采用预备事务的方式:BEGIN; INSERTINTO reservation (reservation_id, hotel_id, room_type_id, start_date, end_date,
status) VALUES (65ac63c6-0f65-42c2-a05c-78a1ac9a384a, 100, 1, 2023-10-12, 2023-10-13, pending_pay);
PREPARETRANSACTIONtransaction-create-id; -- 提交或回滚事务 COMMIT PREPARED transaction-create-id; -- 提交插入事务
ROLLBACK PREPARED transaction-create-id; -- 注意:正确的回滚命令为ROLLBACK PREPARED,而非ROLLBACK PREPARE TRANSACTION
此外,您还可以在“pg_prepared_xacts”系统表中查询当前活动的预备事务:SELECT * FROM pg_prepared_xacts; 以下是一个使用Go语言编写的示例代码,展示了如何在PostgreSQL中实现基于预备事务的库存更新和预订插入操作:
func(d *defaultService)CreateReservation(ctx context.Context, reservation *Reservation)error { inventoryTxID := uuid.NewString() reservationTxID := uuid.NewString()
// 执行库存数据库操作 _, err := d.inventoryService.UpdatePrepared(ctx, reservation, inventoryTxID)
if err != nil { d.inventoryService.RollbackPrepared(ctx, inventoryTxID) return err }
// 执行预订数据库操作 if err = d.reservationService.CreatePrepared(ctx, reservation, reservationTxID); err !=
nil { d.inventoryService.RollbackPrepared(ctx, inventoryTxID) return err }
// 提交事务 d.inventoryService.CommitPrepared(ctx, inventoryTxID) d.reservationService.CommitPrepared(ctx, reservationTxID)
returnnil } 在上述代码中,我们为每个事务生成了唯一的事务ID,并分别在库存服务和预订服务中执行预备事务若任一操作失败,则立即回滚对应的事务;若全部成功,则提交两个事务,确保预订流程的原子性和数据一致性。
同时,通过查询“pg_prepared_xacts”表,我们可以有效地监控和管理系统中的预备事务四、总结结合Go语言中的锁机制与2PC协议,以及PostgreSQL的强大功能,我们能够构建一个既高效又可靠的酒店预订系统,该系统能够出色地处理并发和数据一致性问题。
当然,在实际应用中,我们还需要根据系统的具体需求、性能要求以及业务场景等因素,灵活选择并优化这些技术手段,以实现最佳的系统性能和用户体验如果您想了解更多的代码,可以私信我。
专栏golang实用技巧作者:SuperOps11.11币50人已购查看
免责声明:本站所有信息均搜集自互联网,并不代表本站观点,本站不对其真实合法性负责。如有信息侵犯了您的权益,请告知,本站将立刻处理。联系QQ:1640731186