- 示例工程
【免费下载链接】awesome-low-level-design
Learn Low Level Design (LLD) and prepare for interviews using free resources.
本篇技术指南以awesome-low-level-design仓库中 Concert Ticket Booking System(Go 实现) 为骨架,完整拆解演唱会票务预订系统的需求分析、类设计与枚举建模,并结合 Go 源码深入讲解Singleton 单例、Mutex 并发控制、座位抢占与失败回滚等核心机制。读完本文,你将掌握如何用 Go 设计一个能应对并发订票请求、避免座位重复售卖的 LLD(Low Level Design)解决方案,并能在面试与实战中复现同样的设计思路。
系统概述与需求分析
演唱会票务预订系统是一个经典的低层设计(LLD)考题,核心难点不在于实体多,而在于并发场景下的座位一致性。仓库内 问题描述 与 Go 实现文档 共同定义了以下八条核心需求:
- 系统允许用户查看可购演唱会及其座位布局;
- 用户可按艺人(artist)、场馆(venue)、日期时间(date & time)等条件搜索演唱会;
- 用户可为指定演唱会选择座位并购买门票;
- 系统必须处理并发订票请求,避免座位重复预订(double-booking);
- 系统应保证所有用户获得公平的订票机会;
- 系统应安全处理支付流程;
- 系统应生成预订确认并通过邮件或短信发送给用户;
- 系统应为售罄演唱会提供等待列表(waiting list)功能。
需要说明的是:在仓库的 Go 实现中,需求 1~5 有完整代码支撑;需求 6 以 mock 形式占位(processPayment为空实现);需求 7 在booking.go中以TODO注释标记待实现;需求 8(等待列表)暂未实现。这是本文档与源码中的真实状态,下文会逐一指出。
核心类设计与枚举建模
原文档定义了 6 个类与 3 个枚举,Go 实现将其分别落到了独立的源文件中。下面逐一对照讲解。
Concert 演唱会实体
Concert代表一场演唱会事件,属性包含 ID、艺人、场馆、日期时间以及座位列表。
在 Go 中对应 concert.go:
type Concert struct { ID string Artist string Venue string DateTime time.Time Seats []*Seat }构造函数NewConcert(id, artist, venue string, dateTime time.Time, seats []*Seat)一次性注入所有字段。注意Seats使用[]*Seat指针切片,目的是让后续Booking与Seat之间共享同一份状态——座位被预订后,演唱会视图与预订记录都能感知到状态变化,这是后续并发一致性设计的基础。
Seat 座位与 SeatType / SeatStatus 枚举
Seat表示演唱会中的一个座位,属性包括 ID、座位号、座位类型、价格和状态,并提供book(预订)与release(释放)方法。
Go 实现见 seat.go:
type Seat struct { ID string SeatNumber string Type SeatType Price float64 status SeatStatus mu sync.Mutex }这里有两个值得注意的设计点:
status字段是小写(未导出),外部只能通过GetStatus()读取,无法直接篡改,体现了 Go 语言的封装(encapsulation)惯例;- 每个座位自带一把
sync.Mutex,这是并发安全的关键——同一座位在任何时刻只会被一个 goroutine 修改状态。
枚举定义集中在 types.go:
type SeatType int type SeatStatus int type BookingStatus int const ( SeatTypeRegular SeatType = iota // 常规座 SeatTypePremium // 优选座 SeatTypeVIP // VIP 座 ) const ( StatusAvailable SeatStatus = iota // 可预订 StatusBooked // 已预订 StatusReserved // 已预留 ) const ( BookingStatusPending BookingStatus = iota // 待处理(支付前) BookingStatusConfirmed // 已确认 BookingStatusCancelled // 已取消 )- SeatType:常规(regular)、优选(premium)、VIP 三种档位;
- SeatStatus:可预订(available)、已预订(booked)、已预留(reserved)。
reserved状态在需求中对应“锁座/排队占位”语义,当前 Go 实现中尚未使用该状态; - BookingStatus:待处理(pending)、已确认(confirmed)、已取消(cancelled),完整支撑订单生命周期。
座位定价规则(utils.go)
座位类型与价格如何在真实场景中落地?utils.go 中的GenerateSeats提供了一个可运行的样例规则——按座位编号分段定价:
func GenerateSeats(numberOfSeats int) []*Seat { seats := make([]*Seat, 0, numberOfSeats) for i := 1; i <= numberOfSeats; i++ { seatNumber := fmt.Sprintf("S%d", i) var seatType SeatType var price float64 switch { case i <= 10: // 前 10 个座位 seatType = SeatTypeVIP price = 100.0 case i <= 30: // 第 11~30 个座位 seatType = SeatTypePremium price = 75.0 default: // 其余座位 seatType = SeatTypeRegular price = 50.0 } seats = append(seats, NewSeat(seatNumber, seatNumber, seatType, price)) } return seats }规则一目了然:前 10 个座位为 VIP(100 元)、第 11~30 个为优选(75 元)、其余为常规(50 元)。这虽然是一个演示级定价策略,但它完整演示了“座位类型 → 价格映射”在生产系统中如何被数据化建模,你完全可以将这段逻辑替换为读数据库票价表。
Booking 预订与 BookingStatus
Booking表示用户对某场演唱会及若干座位的预订,包含 ID、用户、演唱会、座位列表、总价和状态,提供confirm(确认)与cancel(取消)方法。
Go 实现见 booking.go:
type Booking struct { ID string User *User Concert *Concert Seats []*Seat TotalPrice float64 Status BookingStatus } func NewBooking(id string, user *User, concert *Concert, seats []*Seat) *Booking { totalPrice := calculateTotalPrice(seats) return &Booking{ ID: id, User: user, Concert: concert, Seats: seats, TotalPrice: totalPrice, Status: BookingStatusPending, } }calculateTotalPrice对座位价格求和得到TotalPrice,新订单初始状态为BookingStatusPending(待支付)。
状态机流转体现在两个方法上:
func (b *Booking) ConfirmBooking() { if b.Status == BookingStatusPending { b.Status = BookingStatusConfirmed // TODO: Send booking confirmation to user } } func (b *Booking) CancelBooking() { if b.Status == BookingStatusConfirmed { b.Status = BookingStatusCancelled for _, seat := range b.Seats { seat.Release() // 释放所有关联座位 } // TODO: Send cancellation notification to user } }两个关键点:
ConfirmBooking仅在 pending 状态下生效,杜绝了已取消/已确认订单被二次确认;CancelBooking会遍历并释放该订单的所有座位,这是“取消订单 → 座位回到可售池”的闭环实现。取消后的座位可以被下一个用户重新预订(demo 中正是靠这一步来演示座位的复用)。
TODO注释表明:向用户发送确认/取消通知的通道(邮件/SMS)在需求 7 中已定义,但当前实现留待扩展。
User 用户
User表示系统用户,属性为 ID、姓名、邮箱。见 user.go:
type User struct { ID string Name string Email string } func NewUser(id, name, email string) *User { return &User{ID: id, Name: name, Email: email} }简单实体,通过Email字段为需求 7 的邮件通知预留数据基础。
SeatNotAvailableError 自定义错误
SeatNotAvailableException是用于处理“座位不可预订”场景的自定义异常。Go 中通过实现error接口完成,见 errors.go:
type SeatNotAvailableError struct { message string } func NewSeatNotAvailableError(message string) *SeatNotAvailableError { return &SeatNotAvailableError{message: message} } func (e *SeatNotAvailableError) Error() string { return e.message }该错误类型被 seat.go 的Seat.Book()与 concert_booking_system.go 的BookTickets复用,作为“座位已售/已锁”的统一错误信号,便于调用方做类型断言区分业务错误。
ConcertTicketBookingSystem:单例 + 并发控制的系统核心
ConcertTicketBookingSystem是整个系统的中枢组件,遵循Singleton 模式保证全局唯一实例,管理演唱会与预订,并提供添加演唱会、搜索演唱会、订票、取消预订等方法。
Singleton 单例实现(sync.Once)
Go 实现见 concert_booking_system.go:
type ConcertTicketBookingSystem struct { concerts map[string]*Concert bookings map[string]*Booking mu sync.Mutex } var ( instance *ConcertTicketBookingSystem once sync.Once ) func GetBookingSystem() *ConcertTicketBookingSystem { once.Do(func() { instance = &ConcertTicketBookingSystem{ concerts: make(map[string]*Concert), bookings: make(map[string]*Booking), } }) return instance }这是 Go 中线程安全单例的标准写法:sync.Once保证初始化函数只执行一次,无论多少 goroutine 并发调用GetBookingSystem(),都会拿到同一个实例。相比加锁判空的懒加载写法,sync.Once更简洁且天然并发安全。同时,系统内部的concerts与bookings使用map +sync.Mutex保护,所有读写方法都先加锁再操作,保证并发安全。
添加与搜索演唱会
func (bs *ConcertTicketBookingSystem) AddConcert(concert *Concert) { bs.mu.Lock() defer bs.mu.Unlock() bs.concerts[concert.ID] = concert } func (bs *ConcertTicketBookingSystem) GetConcert(concertID string) *Concert { bs.mu.Lock() defer bs.mu.Unlock() return bs.concerts[concertID] } func (bs *ConcertTicketBookingSystem) SearchConcerts(artist, venue string, dateTime time.Time) []*Concert { bs.mu.Lock() defer bs.mu.Unlock() var results []*Concert for _, concert := range bs.concerts { if concert.Artist == artist && concert.Venue == venue && concert.DateTime.Equal(dateTime) { results = append(results, concert) } } return results }AddConcert以演唱会 ID 为 key 存入 map;GetConcert按 ID 精确取回;SearchConcerts实现需求 2 的多条件组合搜索:艺人、场馆、日期时间三个条件同时命中才返回。注意time.Time的比较使用了Equal方法(Go 中==对time.Time不可靠)。
BookTickets:防超卖的核心流程
BookTickets是系统的重头戏,完整实现了需求 3 与需求 4(选择座位、购买门票、并发防重):
func (bs *ConcertTicketBookingSystem) BookTickets(user *User, concert *Concert, seats []*Seat) (*Booking, error) { bs.mu.Lock() defer bs.mu.Unlock() // 1. 校验所有座位当前是否可售 for _, seat := range seats { if seat.GetStatus() != StatusAvailable { return nil, NewSeatNotAvailableError(fmt.Sprintf("Seat %s is not available", seat.SeatNumber)) } } // 2. 逐个预订座位,任一失败则回滚已预订的座位 for _, seat := range seats { if err := seat.Book(); err != nil { // 回滚:释放本次已成功预订的前序座位 for _, s := range seats { if s == seat { break } s.Release() } return nil, err } } // 3. 生成预订 ID(基于纳秒时间戳,避免冲突) bookingID := fmt.Sprintf("BKG-%d", time.Now().UnixNano()) booking := NewBooking(bookingID, user, concert, seats) // 4. 支付(当前为 mock 实现) bs.processPayment(booking) // 5. 确认预订并入库 booking.ConfirmBooking() bs.bookings[bookingID] = booking fmt.Printf("Booking %s - %d seats booked\n", booking.ID, len(booking.Seats)) return booking, nil }这个方法体现了“先校验 → 再抢占 → 失败回滚 → 确认入库”的经典事务式流程,层层分析:
- 全量预校验:先遍历所有目标座位确认
StatusAvailable,任一座位不可用立即返回SeatNotAvailableError,不做任何写入; - 逐个抢占 + 原子性回滚:真正写状态时逐个调用
seat.Book();若中途某个座位预订失败,则释放本次已成功预订的前序座位,保证一批座位要么全部预订成功、要么全部释放,避免出现“订了 3 个座位只成功 2 个”的脏状态——这正是需求 4“避免 double-booking”在单请求粒度上的体现; - 预订 ID 生成:使用纳秒时间戳
time.Now().UnixNano()保证同一批次内 ID 唯一; - 支付占位:
processPayment当前为空实现(mock),对应需求 6 的支付处理留待接入真实支付网关; - 确认并入库:状态置为
BookingStatusConfirmed后写入bookingsmap。
并发防超卖的锁粒度设计
需求 4 的“并发请求避免重复预订”在本实现中是双层锁协作完成的:
- 系统级锁(
bs.mu):BookTickets全程持有ConcertTicketBookingSystem.mu,保证同一时刻只有一个订票请求能进入座位抢占流程——这是粗粒度的互斥屏障,从源头杜绝两个用户同时订同一批座位的竞态窗口; - 座位级锁(
seat.mu):seat.go 的Book/Release/GetStatus各自持有Seat.mu,保证座位状态读写的原子性,即使未来去掉系统级锁、改为细粒度锁座(预留场景),单个座位的状态切换依然是安全的。
Seat.Book()的实现是防重的最小闭环:
func (s *Seat) Book() error { s.mu.Lock() defer s.mu.Unlock() if s.status != StatusAvailable { return NewSeatNotAvailableError("Seat is already booked or reserved") } s.status = StatusBooked return nil }先锁 → 再判断状态 → 只有available才能翻转为booked,否则返回业务错误。这个“加锁-校验-更新”的三角关系,正是面试中回答“如何防止座位超卖”时最核心的代码级论据。
CancelBooking:取消与座位回收
func (bs *ConcertTicketBookingSystem) CancelBooking(bookingID string) { bs.mu.Lock() defer bs.mu.Unlock() if booking, exists := bs.bookings[bookingID]; exists { booking.CancelBooking() delete(bs.bookings, bookingID) fmt.Printf("Booking %s cancelled\n", bookingID) } }取消流程同样加锁保护:从bookings取出订单 → 调用booking.CancelBooking()(内部释放全部座位)→ 从 map 删除。释放后的座位回到StatusAvailable,可被后续用户重新预订。
端到端流程走查:从建演唱会到取消重购
demo 文件 中的Run()函数把上述所有组件串成了一条完整的用户旅程,非常适合面试表述与本地验证:
func Run() { bookingSystem := GetBookingSystem() // 创建两场演唱会:C001 有 100 个座位,C002 有 50 个座位 concert1Seats := GenerateSeats(100) concert1 := NewConcert("C001", "Artist 1", "Venue 1", time.Now().Add(30*24*time.Hour), concert1Seats) bookingSystem.AddConcert(concert1) concert2Seats := GenerateSeats(50) concert2 := NewConcert("C002", "Artist 2", "Venue 2", time.Now().Add(60*24*time.Hour), concert2Seats) bookingSystem.AddConcert(concert2) // 创建用户 user1 := NewUser("U001", "John Doe", "john@example.com") user2 := NewUser("U002", "Jane Smith", "jane@example.com") // 按艺人+场馆+时间搜索 searchResults := bookingSystem.SearchConcerts("Artist 1", "Venue 1", time.Now().Add(30*24*time.Hour)) fmt.Println("Search Results:") for _, concert := range searchResults { fmt.Printf("Concert: %s at %s\n", concert.Artist, concert.Venue) } // user1 预订 C001 前 3 个座位 selectedSeats1 := concert1.Seats[:3] booking1, err := bookingSystem.BookTickets(user1, concert1, selectedSeats1) if err != nil { fmt.Printf("Booking error: %v\n", err) } // user2 预订 C002 前 2 个座位 selectedSeats2 := concert2.Seats[:2] booking2, err := bookingSystem.BookTickets(user2, concert2, selectedSeats2) if err != nil { fmt.Printf("Booking error: %v\n", err) } if booking2 != nil { fmt.Printf("Booking Successful\n") } // user1 取消 C001 的预订 → 座位释放 if booking1 != nil { bookingSystem.CancelBooking(booking1.ID) } // user2 随后预订 C001 第 4、5 个座位(注意:前 3 个座位已被 user1 预订过又释放,此处选的是后续座位) selectedSeats3 := concert1.Seats[3:5] booking3, err := bookingSystem.BookTickets(user2, concert1, selectedSeats3) if err != nil { fmt.Printf("Booking error: %v\n", err) } if booking3 != nil { fmt.Printf("Booking Successful\n") } }整个流程覆盖了需求的完整链路:创建演唱会 → 生成座位与定价 → 创建用户 → 条件搜索 → 多座位批量预订 → 取消预订 → 再次预订。demo 刻意设计了两处有代表性的操作:
booking1成功后立即被取消,随后booking3预订了concert1.Seats[3:5](第 4、5 个座位),避开已被释放的前 3 个座位,演示了座位区间的可操作性;- 多次
BookTickets与CancelBooking交错出现,可直观验证“预订-释放-再预订”的状态流转与错误处理分支。
运行方式与依赖环境
该实现位于 Go 模块 solutions/golang(模块名github.com/ashishps1/awesome-low-level-design/solutions/golang,Go 版本1.23.2)。运行方式如下:
- 在仓库根目录进入 Go 解决方案目录并运行整个模块的入口 main.go:入口中已预置
concertbookingsystem.Run()调用(默认被注释,取消注释即可),然后执行:
cd solutions/golang go run main.go- 若希望独立运行本系统的 demo,也可以将
concertticketbookingsystem目录下的源码复制为独立main包后直接执行:
go run .需要注意:当前 demo 的Run()定义于包concertbookingsystem内,直接以包方式调用(concertbookingsystem.Run())比独立可执行更贴合仓库现有的多项目组织结构。该实现无第三方依赖,仅依赖 Go 标准库sync、time、fmt,go.mod已锁定 Go 1.23.2 以上版本即可编译运行。
设计亮点总结与待扩展点
值得在面试中强调的设计亮点
- 双层锁并发模型:系统级
sync.Mutex保证订票流程整体串行,座位级sync.Mutex保证单座位状态切换原子性,二者结合从架构层面回答“如何避免 double-booking”; - 批量操作的原子性回滚:
BookTickets中任一座位失败即释放前序座位,杜绝半成功状态,是“预订事务”的代码级落地; - 状态机约束:
Booking的Confirm/Cancel均校验当前状态,防止非法状态迁移;座位状态由available → booked单向流转并由错误类型兜底; - Singleton 与并发安全:
sync.Once单例 + map 全量加锁,全局状态一致且线程安全; - 封装的 Go 惯例:座位
status字段私有化,仅暴露GetStatus(),避免外部绕过锁直接改状态。
原需求中尚未实现、可继续扩展的点
对照原文档需求列表,以下几点在当前 Go 实现中仍是开放扩展项(代码中均有明确标注),可作后续迭代方向:
- 需求 6 安全支付:processPayment 目前为空 mock,可接入真实支付网关,并在支付失败时回滚座位;
- 需求 7 邮件/SMS 确认:booking.go 中
ConfirmBooking与CancelBooking的发送通知逻辑均为TODO; - 需求 8 等待列表:
SeatStatus中已预留StatusReserved枚举值,但等待列表队列逻辑尚未实现,可在此基础上扩展“锁座 + 排队补位”; - 需求 5 公平性:当前实现通过“先到先得 + 全量加锁”保证公平,若引入预留(reserved)状态则需进一步设计锁座超时释放策略。
与其他实现横向对照
本题在仓库中有多语言实现可供对照阅读:Java 实现位于 solutions/java/src/concertticketbookingsystem/、Python 实现位于 solutions/python/concertticketbookingsystem/、C++ 实现位于 solutions/cpp/concertticketbookingsystem/、C# 实现位于 solutions/csharp/concertticketbookingsystem/。对照阅读时,重点观察各语言如何表达“并发锁”与“状态枚举”,可以加深对 LLD 通用建模思路的理解。
结语
演唱会票务预订系统是低层设计面试的高频题,其精髓在于在简单实体模型之上,用并发控制解决真实的业务一致性难题。本文基于awesome-low-level-design仓库的 Go 实现,从需求、类图、枚举、单例、锁与回滚、端到端 demo 六个层面完成了完整拆解。你可以直接运行 demo 验证整套流程,也可以在TODO标注处继续扩展支付、通知与等待列表,把这道题打磨成面试中最有把握的代表作。
- 示例工程
【免费下载链接】awesome-low-level-design
Learn Low Level Design (LLD) and prepare for interviews using free resources.
相关推荐
演唱会票务预订系统(Concert Ticket Booking System)C++ 实现:从需求分析到源码级设计详解
演唱会票务预订系统(Concert Ticket Booking System)C++ 实现:从需求分析到源码级设计详解 本篇技术指南基于 awesome lo
示例工程电影票预订系统(Movie Ticket Booking System)C++ 低层设计实战:类建模、座位锁定与并发安全
电影票预订系统(Movie Ticket Booking System)C++ 低层设计实战:类建模、座位锁定与并发安全 本文基于 awesome low le
示例工程Movie Ticket Booking System(BookMyShow 式)低层设计全解析:需求拆解、类图建模与并发座位预订实战
Movie Ticket Booking System(BookMyShow 式)低层设计全解析:需求拆解、类图建模与并发座位预订实战 本文以开源仓库 awes
示例工程
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考