在线股票交易系统(Online Stock Brokerage System)低层设计实战:Go 实现剖析

原创2026-10-01 16:54:401,351 阅读
文章标签:示例工程教程后端

在线股票交易系统(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):

  1. 买入 10 股 AAPL @ $150:扣款 $1,500,组合增加 10 股 AAPL;
  2. 买入 5 股 GOOGL @ $2,000:扣款 $10,000 → 余额不足被拒(REJECTED),余额仍为 $8,500;
  3. 卖出 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 模拟,可结合观察者模式向客户端推送报价。

仓库中的更多资源

掌握这套 Go 实现,你就同时拿到了需求拆解、类建模、模式落地与并发安全四层 LLD 技能,可以直接迁移到停车管理系统、ATM 等同样强调"资金/资源校验 + 异步处理"的面试题目中。

登录后查看全文
awesome-low-level-design