在线股票交易系统(Online Stock Brokerage System)低层设计实战:Go 实现剖析
在线股票交易系统(Online Stock Brokerage System)低层设计实战:Go 实现剖析
在线股票交易系统是低层设计(LLD)面试中的经典题目,覆盖账户管理、买卖委托、订单执行、组合持仓与并发一致性等核心领域。本指南以仓库中 Go 实现 的 README 设计文档为主体骨架,逐一解析需求、核心类设计与关键源码实现,帮助你掌握订单状态机、Singleton 经纪商、组合校验与并发安全等实战技能,并能独立复现一套可运行的 Go 版本交易系统。
需求梳理:从业务功能到系统约束
关联文档 README.md 开篇列出了 8 条需求,可归纳为三大维度:
| 维度 | 需求条目 | 落地要点 |
|---|---|---|
| 核心业务 | 创建并管理交易账户;买卖股票;查看持仓与交易历史 | 用户、账户、股票、委托、组合等核心实体 |
| 交易链路 | 实时行情与市场数据;订单的下单、执行与结算;业务规则校验 | 订单队列、execute() 执行、余额/持仓校验 |
| 系统品质 | 并发请求下的数据一致性;可扩展性;安全性 | 锁、原子计数、缓冲 channel、最小权限模型 |
其中,第 5 条"业务规则校验"(检查账户余额与股票可用数量)和第 6 条"并发一致性"(处理并发请求并保证数据完整)是设计中的两大硬核考点,后续源码分析将重点展开。
系统骨架:用户、账户与股票
User:交易者主体
user.go 用最简结构体承载用户三要素,通过构造函数 NewUser(userID, name, email) 统一创建:
type User struct {
UserID string
Name string
Email string
}
Account:资金账户与组合的宿主
account.go 展示了"账户持有组合"的组合关系(Composition):Account 内嵌 Portfolio,并在构造函数中自动初始化:
type Account struct {
AccountID string
User *User
balance float64
Portfolio *Portfolio
mu sync.RWMutex
}
func NewAccount(accountID string, user *User, initialBalance float64) *Account {
account := &Account{AccountID: accountID, User: user, balance: initialBalance}
account.Portfolio = NewPortfolio(account)
return account
}
func (a *Account) Deposit(amount float64) { /* balance += amount */ }
func (a *Account) Withdraw(amount float64) error {
a.mu.Lock()
defer a.mu.Unlock()
if a.balance < amount {
return NewInsufficientFundsError("insufficient funds in account")
}
a.balance -= amount
return nil
}
Withdraw 是资金校验的入口:余额不足时返回 InsufficientFundsError,而不是直接修改余额,这正是需求第 5 条"检查账户余额"的具体实现。balance 字段刻意保持小写(包内私有),对外仅暴露 GetBalance() 读取接口,体现封装性。
Stock:可交易证券与价格更新
stock.go 的 Stock 对应需求第 3 条的"实时报价/市场数据",提供 UpdatePrice 与 GetPrice,并同样用 sync.RWMutex 保护价格:
type Stock struct {
Symbol string
Name string
price float64
mu sync.RWMutex
}
func (s *Stock) UpdatePrice(newPrice float64) { s.price = newPrice }
func (s *Stock) GetPrice() float64 { return s.price }
订单模型:接口抽象 + 状态机 + 模板方法
关联文档明确点出:Order 是抽象基类,BuyOrder / SellOrder 是具体实现,各自实现 execute()。这是典型的**模板方法模式(Template Method)**在订单执行中的应用。
Order 接口与 BaseOrder 公共属性
order.go 定义了行为契约与共享字段:
type Order interface {
Execute() error
GetStatus() OrderStatus
SetStatus(status OrderStatus)
}
type BaseOrder struct {
OrderID string
Account *Account
Stock *Stock
Quantity int
Price float64
Status OrderStatus
}
OrderStatus 枚举
types.go 用 Go 的常量 + iota 实现了文档所述的 PENDING / EXECUTED / REJECTED 三态:
type OrderStatus int
const (
OrderStatusPending OrderStatus = iota
OrderStatusExecuted
OrderStatusRejected
)
BuyOrder:先扣款、再入仓
BuyOrder.Execute() 体现了买入的完整校验链:先计算总成本,调用 Account.Withdraw 扣款(失败即置为 REJECTED),成功后再把股票加入组合并置为 EXECUTED:
func (o *BuyOrder) Execute() error {
totalCost := float64(o.Quantity) * o.Price
if err := o.Account.Withdraw(totalCost); err != nil {
o.Status = OrderStatusRejected
return err
}
o.Account.Portfolio.AddStock(o.Stock, o.Quantity)
o.Status = OrderStatusExecuted
return nil
}
SellOrder:先验持仓、再入账
SellOrder.Execute() 则先调用 Portfolio.RemoveStock 校验并扣减持仓(不足即 REJECTED),成功后把卖出所得 Quantity × Price 通过 Deposit 入账:
func (o *SellOrder) Execute() error {
if err := o.Account.Portfolio.RemoveStock(o.Stock, o.Quantity); err != nil {
o.Status = OrderStatusRejected
return err
}
totalProceeds := float64(o.Quantity) * o.Price
o.Account.Deposit(totalProceeds)
o.Status = OrderStatusExecuted
return nil
}
买入/卖出共享同一执行框架,差异只在于"扣款-加仓"与"验仓-入账"的顺序——这正是模板方法模式的直观体现。
组合(Portfolio):持仓校验的核心
portfolio.go 实现需求第 2 条的"查看持仓",核心是 holdings map[string]int(股票代码 → 持有数量):
func (p *Portfolio) RemoveStock(stock *Stock, quantity int) error {
p.mu.Lock()
defer p.mu.Unlock()
currentQuantity, exists := p.holdings[stock.Symbol]
if !exists {
return NewInsufficientStockError("stock not found in portfolio")
}
if currentQuantity < quantity {
return NewInsufficientStockError("insufficient stock quantity in portfolio")
}
p.holdings[stock.Symbol] -= quantity
if p.holdings[stock.Symbol] == 0 {
delete(p.holdings, stock.Symbol)
}
return nil
}
两点设计细节值得注意:
- 双重校验:先检查持仓是否存在,再检查数量是否充足,分别返回
InsufficientStockError的不同错误信息; - 零持仓清理:数量减到 0 时用
delete删除键,避免组合中出现空壳条目;GetHoldings返回副本 map,防止外部修改破坏内部状态。
自定义错误:InsufficientFunds / InsufficientStock
exceptions.go 实现了文档中提到的两类自定义异常。Go 没有 Java 式异常,这里以实现了 error 接口的自定义类型替代,并提供了带消息的构造函数:
type InsufficientFundsError struct{ message string }
func NewInsufficientFundsError(message string) *InsufficientFundsError { ... }
func (e *InsufficientFundsError) Error() string { return e.message }
type InsufficientStockError struct{ message string }
func NewInsufficientStockError(message string) *InsufficientStockError { ... }
func (e *InsufficientStockError) Error() string { return e.message }
InsufficientFundsError 由 Account.Withdraw 抛出(资金侧),InsufficientStockError 由 Portfolio.RemoveStock 抛出(持仓侧),二者共同支撑需求第 5 条的全部校验路径。
StockBroker:Singleton 调度中枢与并发处理
stock_broker.go 是整个系统的心脏,文档明确要求其遵循 Singleton 模式。Go 实现采用 sync.Once 惰性单例:
var (
instance *StockBroker
once sync.Once
)
func GetStockBroker() *StockBroker {
once.Do(func() {
instance = &StockBroker{
accounts: make(map[string]*Account),
stocks: make(map[string]*Stock),
orderQueue: make(chan Order, 100), // Buffered channel for orders
}
go instance.processOrders() // Start order processing goroutine
})
return instance
}
once.Do 保证无论多少个 goroutine 同时调用 GetStockBroker,实例只被初始化一次且线程安全。StockBroker 内部结构与关键机制如下:
| 成员 | 类型 | 作用 |
|---|---|---|
accounts |
map[string]*Account |
账户注册表,按 accountID 索引 |
stocks |
map[string]*Stock |
股票注册表,按 Symbol 索引 |
orderQueue |
chan Order(缓冲 100) |
待执行订单队列 |
accountIDCount |
int64 |
原子账户号计数器 |
mu |
sync.RWMutex |
账户/股票表并发读写锁 |
异步订单处理是该实现的一大亮点:PlaceOrder 仅将订单写入缓冲 channel 即返回(生产端),后台 goroutine processOrders 循环消费并执行(消费端):
func (sb *StockBroker) PlaceOrder(order Order) { sb.orderQueue <- order }
func (sb *StockBroker) processOrders() {
for order := range sb.orderQueue {
if err := order.Execute(); err != nil {
fmt.Printf("Order failed: %v\n", err)
}
}
}
这是经典的生产者-消费者模型:下单入口与订单执行解耦,天然支持批量排队、削峰与异步结算,呼应需求第 4 条"订单放置、执行与结算"和第 7 条"可扩展性"。账户 ID 使用 atomic.AddInt64 生成(A001 格式),在并发生成账号时不会产生重复 ID。
运行演示与验证
stock_broker_demo.go 是入口演示程序 Run(),完整演示了"开户 → 添加股票 → 下单 → 查持仓"全流程:
func Run() {
broker := GetStockBroker()
// Create user and account
user := NewUser("U001", "John Doe", "john@example.com")
broker.CreateAccount(user, 10000.0)
account := broker.GetAccount("A001")
// Add stocks
apple := NewStock("AAPL", "Apple Inc.", 150.0)
google := NewStock("GOOGL", "Alphabet Inc.", 2000.0)
broker.AddStock(apple)
broker.AddStock(google)
// Place buy orders
buyOrder1 := NewBuyOrder("O001", account, apple, 10, 150.0)
buyOrder2 := NewBuyOrder("O002", account, google, 5, 2000.0)
broker.PlaceOrder(buyOrder1)
broker.PlaceOrder(buyOrder2)
// Place sell order
sellOrder := NewSellOrder("O003", account, apple, 5, 160.0)
broker.PlaceOrder(sellOrder)
// Print account details
fmt.Printf("Account Balance: $%.2f\n", account.GetBalance())
fmt.Println("Portfolio Holdings:")
for symbol, quantity := range account.Portfolio.GetHoldings() {
fmt.Printf("%s: %d shares\n", symbol, quantity)
}
}
手动推演验证(初始余额 $10,000):
- 买入 10 股 AAPL @ $150:扣款 $1,500,组合增加 10 股 AAPL;
- 买入 5 股 GOOGL @ $2,000:扣款 $10,000 → 余额不足被拒(REJECTED),余额仍为 $8,500;
- 卖出 5 股 AAPL @ $160:组合减少 5 股,入账 $800,余额变为 $9,300。
最终组合为 AAPL 5 股、GOOGL 0 股(该买单被拒),余额 $9,300。注意演示代码在真实系统中应使用 WaitGroup 或 channel 关闭等同步机制等待订单完成(源码注释也提示了这一点);此处先下单后直接读取余额,依赖处理时序,仅为教学演示。
设计要点总结与面试延伸
本实现的设计模式清单
| 模式 | 应用位置 | 效果 |
|---|---|---|
| Singleton | GetStockBroker() + sync.Once |
全局唯一经纪商实例,天然线程安全 |
| Template Method | Order.Execute() 接口 + BaseOrder |
统一订单执行框架,买卖类型各自实现细节 |
| Composition | Account 内嵌 Portfolio |
账户生命周期与组合绑定,构造即初始化 |
| 生产者-消费者 | orderQueue channel + processOrders goroutine |
下单与执行解耦,异步削峰 |
并发与一致性策略
- 读写分离锁:
Account、Stock、Portfolio内部均持有sync.RWMutex,读多写少的场景下并发读不互斥; - 原子操作:账户号生成用
atomic.AddInt64,避免锁内计数竞争; - 单例级串行化:所有账户/股票注册表的读写经
StockBroker.mu保护,配合缓冲 channel 串行执行订单,从整体上保证"同一订单只被处理一次、资金与持仓变更串行一致"。
面试加分项与可扩展方向(仓库证据之外)
关联文档与源码覆盖的是单机版核心交易链路。面试中可进一步讨论:
- 成交价格取值:当前实现以用户下单价格成交,真实系统常用订单簿撮合(限价单/市价单/盘口队列),可扩展
OrderType枚举; - 事务性:买入的"扣款 + 加仓"两步目前非原子,崩溃可能造成资金与持仓不一致,可用分布式事务或本地消息表改进;
- 持久化:源码中账户、持仓均为内存 map,重启即失;生产系统需引入数据库并支持交易历史查询(需求第 2 条);
- 行情推送:需求第 3 条"实时行情"在单机版中由
Stock.UpdatePrice模拟,可结合观察者模式向客户端推送报价。
仓库中的更多资源
- 问题陈述与类图:在线股票交易系统题目(含 UML 类图:onlinestockbrokeragesystem-class-diagram.png)
- 同一问题的其他语言实现:Java、C++、C#、Python
- 设计模式专题:Java 版设计模式 与 Go 版设计模式(Singleton、Template Method、Observer 等均有可运行示例)
掌握这套 Go 实现,你就同时拿到了需求拆解、类建模、模式落地与并发安全四层 LLD 技能,可以直接迁移到停车管理系统、ATM 等同样强调"资金/资源校验 + 异步处理"的面试题目中。