环境背景预设: 一个典型的方案背景 :日均千万级订单(年36.5亿+),目标支撑5-10年业务增长。严格控制物理库数量(8个),采用ID内嵌分片信息 方案,彻底摒弃哈希取模与跨库路由计算,实现O(1)精准定位。提供MySQL自增号段 与雪花算法 两套独立实现案例,注意语言栈互不耦合,可按需选用。
一、容量规划与架构设计(注意以下仅供标准参考,实际评估可做预留至少20%以上) 补充说明:之所以没有提到Java,非偏见,主要是因为Java生态有成熟的中间件,比如ShardingSphere、MyCat等,而PHP和Go生态相对薄弱,目前没有特别成熟理想的解决方案,也不宜直接套用Java的解决方案 。
1.1 规模评估与分片规划 1 2 3 4 5 6 7 8 9 10 日单量:1000万 5年单量:≈182亿 10年单量:≈365亿 单表安全水位:500万行(MySQL 8.0 InnoDB最佳实践) 分片策略: 物理库:8个(主从架构,读写分离由应用层或代理处理) 单库表数:512张 总分片数:8 × 512 = 4096个分片 总承载量:4096 × 500万 = 204.8亿行(覆盖5-10年)
1.2 整体架构图 graph TB
subgraph "接入层"
NG["Nginx/SLB"]
PHP_PHP["PHP服务 Yii2/Laravel"]
GO_SVC["Go服务 标准库/Gin"]
end
subgraph "路由层"
ROUTER["ID路由解析器 位运算O1"]
CONFIG["分片配置 YAML/Consul"]
end
subgraph "存储层 MySQL 8.0"
DB0["ds_0 512张表"]
DB1["ds_1 512张表"]
DB4["ds_4 512张表"]
DB7["ds_7 512张表"]
end
NG --> PHP_PHP
NG --> GO_SVC
PHP_PHP --> ROUTER
GO_SVC --> ROUTER
ROUTER --> CONFIG
ROUTER --> DB0
ROUTER --> DB1
ROUTER --> DB4
ROUTER --> DB7
style ROUTER fill:#fff4e1
style CONFIG fill:#e1f5ff
style DB0 fill:#f0f9e8
style DB7 fill:#f0f9e8 二、方案一:MySQL自增号段模式(无状态发号器) 2.1 核心思想 摒弃全局单点瓶颈,采用号段模式 。通过一张轻量级中央表利用MySQL AUTO_INCREMENT 批量发放ID号段。号段基数直接编码分片索引 ,应用层拿到号段后本地递增使用,路由时无需查表、无需哈希,纯位运算。
1 2 3 4 5 6 7 8 ID结构(64位无符号): ┌─────────────────────┬──────────┬───────────┬──────┐ │ 48位发号序列 │ 8位库索引 │ 8位表索引 │ 预留 │ │ (MySQL自增生成) │ (0-255) │ (0-255) │ (0) │ └─────────────────────┴──────────┴───────────┴──────┘ 路由计算: 库索引 = (order_id >> 8) & 0xFF 表索引 = (order_id >> 0) & 0xFF (实际低8位为表索引,预留0)
2.2 Go语言实现 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 package idgenimport ( "database/sql" "fmt" "sync" "time" ) type Segment struct { BaseID int64 MaxID int64 CurrentID int64 DBIndex uint8 TableIndex uint8 } type SegmentGenerator struct { db *sql.DB mu sync.Mutex segment *Segment step int64 } func NewSegmentGenerator (db *sql.DB) *SegmentGenerator { return &SegmentGenerator{db: db, step: 1000 } } func (g *SegmentGenerator) NextID() (int64 , error ) { g.mu.Lock() defer g.mu.Unlock() if g.segment == nil || g.segment.CurrentID >= g.segment.MaxID { if err := g.fetchNewSegment(); err != nil { return 0 , err } } id := g.segment.BaseID + (g.segment.CurrentID - g.segment.BaseID) g.segment.CurrentID++ return id, nil } func (g *SegmentGenerator) fetchNewSegment() error { ctx, cancel := context.WithTimeout(context.Background(), 3 *time.Second) defer cancel() _, err := g.db.ExecContext(ctx, "INSERT INTO id_segment (stub) VALUES ('x')" ) if err != nil { return err } var lastID int64 err = g.db.QueryRowContext(ctx, "SELECT LAST_INSERT_ID()" ).Scan(&lastID) if err != nil { return err } dbIdx := uint8 (lastID % 8 ) tableIdx := uint8 ((lastID / 8 ) % 512 ) g.segment = &Segment{ BaseID: lastID << 16 , MaxID: (lastID + g.step) << 16 , CurrentID: lastID << 16 , DBIndex: dbIdx, TableIndex: tableIdx, } return nil } func GetRoute (orderID int64 ) (dbIdx, tableIdx int ) { shard := orderID & 0xFFFF return int ((shard >> 8 ) & 0xFF ), int (shard & 0xFF ) }
2.3 PHP实现(Laravel / Yii2 兼容) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 <?php declare (strict_types=1 );namespace App \Services \ID ;use Illuminate \Support \Facades \DB ; class SegmentGenerator { private ?object $segment = null ; private int $step = 1000 ; private \PDO $pdo ; public function __construct (\PDO $pdo ) { $this ->pdo = $pdo ; } public function nextId ( ): int { if ($this ->segment === null || $this ->segment['current' ] >= $this ->segment['max' ]) { $this ->fetchNewSegment (); } $id = $this ->segment['base' ] + ($this ->segment['current' ] - $this ->segment['base' ]); $this ->segment['current' ]++; return $id ; } private function fetchNewSegment ( ): void { $lastId = DB::insert ("INSERT INTO id_segment (stub) VALUES ('x')" ); $lastId = DB::select ("SELECT LAST_INSERT_ID() as id" )[0 ]->id; $dbIdx = $lastId % 8 ; $tableIdx = intdiv ($lastId , 8 ) % 512 ; $this ->segment = [ 'base' => $lastId << 16 , 'max' => ($lastId + $this ->step) << 16 , 'current' => $lastId << 16 , 'db' => $dbIdx , 'table' => $tableIdx , ]; } public static function parseRoute (int $orderId ): array { $shard = $orderId & 0xFFFF ; return [ 'db' => sprintf ('ds_%02d' , ($shard >> 8 ) & 0xFF ), 'table' => sprintf ('orders_%04d' , $shard & 0xFF ), ]; } }
2.4 号段表结构 1 2 3 4 5 6 7 CREATE DATABASE id_center DEFAULT CHARSET utf8mb4;CREATE TABLE id_segment ( id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, stub CHAR (1 ) NOT NULL DEFAULT 'x' , created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP , PRIMARY KEY (id) ) ENGINE= InnoDB;
三、方案二:雪花算法位运算路由 3.1 核心思想 纯本地计算,无网络依赖。64位ID中直接固化分片路由位 。彻底消除外部依赖,适合高并发场景。
1 2 3 4 5 6 7 8 ID结构(64位): ┌────────────┬──────┬──────┬──────────┬──────────┐ │ 39位时间戳 │ 预留 │ 3位库│ 9位表 │ 13位序列 │ │ (秒级*1000)│ (5) │(0-7) │ (0-511) │ (0-8191) │ └────────────┴──────┴──────┴──────────┴──────────┘ 路由计算: 库索引 = (order_id >> 22) & 0x07 表索引 = (order_id >> 13) & 0x1FF
3.2 Go语言实现 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 package idgenimport ( "sync" "time" "errors" ) const ( Epoch = 1672531200000 SeqBits = 13 TableBits = 9 DbBits = 3 SeqMask = -1 ^ (-1 << SeqBits) TableMask = -1 ^ (-1 << TableBits) DbMask = -1 ^ (-1 << DbBits) ) type Snowflake struct { mu sync.Mutex lastStamp int64 sequence int64 dbIdx int64 tableIdx int64 } func NewSnowflake (db, table int64 ) *Snowflake { return &Snowflake{dbIdx: db, tableIdx: table} } func (s *Snowflake) NextID() (int64 , error ) { s.mu.Lock() defer s.mu.Unlock() now := time.Now().UnixMilli() if now < s.lastStamp { return 0 , errors.New("clock backwards" ) } if now == s.lastStamp { s.sequence = (s.sequence + 1 ) & SeqMask if s.sequence == 0 { for now <= s.lastStamp { now = time.Now().UnixMilli() } } } else { s.sequence = 0 } s.lastStamp = now id := (now - Epoch) << 35 id |= (s.dbIdx & DbMask) << 22 id |= (s.tableIdx & TableMask) << 13 id |= s.sequence & SeqMask return id, nil } func ParseRoute (id int64 ) (int , int ) { db := int ((id >> 22 ) & DbMask) table := int ((id >> 13 ) & TableMask) return db, table }
3.3 PHP实现 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 <?php class SnowflakeGenerator { private const EPOCH = 1672531200000 ; private const SEQ_BITS = 13 ; private const TABLE_BITS = 9 ; private const DB_BITS = 3 ; private int $dbIdx ; private int $tableIdx ; private int $lastTime = -1 ; private int $seq = 0 ; public function __construct (int $db , int $table ) { $this ->dbIdx = $db ; $this ->tableIdx = $table ; } public function nextId ( ): int { $now = (int )(microtime (true ) * 1000 ); if ($now < $this ->lastTime) throw new \RuntimeException ('时钟回拨' ); if ($now === $this ->lastTime) { $this ->seq = ($this ->seq + 1 ) & ((1 << self ::SEQ_BITS ) - 1 ); if ($this ->seq === 0 ) { while (($now = (int )(microtime (true ) * 1000 )) <= $this ->lastTime) {} } } else { $this ->seq = 0 ; } $this ->lastTime = $now ; $id = (($now - self ::EPOCH ) << 35 ) | (($this ->dbIdx & ((1 << self ::DB_BITS ) - 1 )) << 22 ) | (($this ->tableIdx & ((1 << self ::TABLE_BITS ) - 1 )) << 13 ) | ($this ->seq & ((1 << self ::SEQ_BITS ) - 1 )); return $id ; } public static function parseRoute (int $id ): array { return [ 'db' => sprintf ('ds_%02d' , ($id >> 22 ) & 0x07 ), 'table' => sprintf ('orders_%04d' , ($id >> 13 ) & 0x1FF ) ]; } }
四、业务层路由与查询实现 4.1 订单写入(PHP Laravel示例) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 public function createOrder (CreateOrderRequest $req ): Order { $orderId = $this ->idGenerator->nextId (); $route = SegmentGenerator ::parseRoute ($orderId ); $conn = DB::connection ($route ['db' ]); $conn ->table ($route ['table' ])->insert ([ 'order_id' => $orderId , 'user_id' => $req ->user_id, 'amount' => $req ->amount, 'status' => 0 , 'created_at' => now (), ]); return Order ::make (['order_id' => $orderId ]); }
4.2 订单查询(Go标准库示例) 1 2 3 4 5 6 7 8 9 10 11 12 13 func (s *OrderService) GetOrder(ctx context.Context, orderId int64 ) (*Order, error ) { dbIdx, tableIdx := idgen.ParseRoute(orderId) dbName := fmt.Sprintf("ds_%02d" , dbIdx) tableName := fmt.Sprintf("orders_%04d" , tableIdx) query := fmt.Sprintf("SELECT order_id, user_id, amount, status, created_at FROM %s.%s WHERE order_id = ?" , dbName, tableName) var order Order err := s.db.QueryRowContext(ctx, query, orderId).Scan(&order.OrderID, &order.UserID, &order.Amount, &order.Status, &order.CreatedAt) if err != nil { return nil , err } return &order, nil }
五、表结构与MySQL 8.0优化 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 CREATE TABLE `orders` ( `order_id` BIGINT UNSIGNED NOT NULL COMMENT '分布式订单ID(路由键)' , `user_id` BIGINT UNSIGNED NOT NULL COMMENT '用户ID' , `shop_id` BIGINT UNSIGNED NOT NULL DEFAULT '0' , `amount` DECIMAL (10 ,2 ) NOT NULL DEFAULT '0.00' , `status` TINYINT UNSIGNED NOT NULL DEFAULT '0' COMMENT '0待付 1已付 2发货 3完成' , `pay_time` DATETIME NULL , `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP , `updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP , PRIMARY KEY (`order_id`), KEY `idx_user_time` (`user_id`, `created_at`), KEY `idx_status_time` (`status`, `created_at`) ) ENGINE= InnoDB DEFAULT CHARSET= utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT= '订单主表' PARTITION BY RANGE (YEAR (created_at)) ( PARTITION p2024 VALUES LESS THAN (2025 ), PARTITION p2025 VALUES LESS THAN (2026 ), PARTITION p2026 VALUES LESS THAN (2027 ), PARTITION p2027 VALUES LESS THAN (2028 ), PARTITION pmax VALUES LESS THAN MAXVALUE );
💡 MySQL 8.0特性利用 :
启用 utf8mb4_0900_ai_ci 提升排序性能 表级 PARTITION BY RANGE 仅用于按年归档清理数据,不参与路由 SECONDARY ENGINE = NULL 可配置冷热数据分离六、5-10年平滑扩容策略(零停机) 当单库表数接近上限或容量预警时,采用垂直扩容+配置热更新 :
flowchart LR
A["阶段1: 扩容至16库"] --> B["新增 ds_08 ~ ds_15"]
B --> C["更新路由配置 DB_BITS从3增至4"]
C --> D["新订单路由至新库"]
D --> E["历史数据异步迁移"]
E --> F["旧库只读归档"]
style A fill:#e1f5ff
style C fill:#fff4e1
style F fill:#f0f9e8 实施要点 :
预留位设计 :方案二预留5位,方案一低位可灵活调整,扩容无需改ID结构。双写过渡 :通过Nacos/Consul下发新路由配置,应用层拦截器根据配置决定写入新库或旧库。数据搬运 :使用 gh-ost 或 pt-online-schema-change 配合业务低峰期迁移,校验差异率 < 0.001% 后切流。七、方案对比与生产选型建议 维度 方案一:MySQL自增号段 方案二:雪花算法 依赖 依赖轻量级MySQL表(极低负载) 零外部依赖,纯内存计算 性能 极高(批量获取,本地递增) 极高(本地位运算,<1μs) ID趋势 单调递增,利于B+树插入 趋势递增,带时间属性 时钟风险 无 需防时钟回拨(代码已处理) 运维成本 低(单表维护,自动扩容) 极低(无状态,多机部署) 适用场景 强依赖MySQL生态、已有发号器基建 高并发、微服务独立部署、追求极致性能
最终建议 :
选 方案二(雪花算法) 作为首选。无网络往返、无锁竞争、天然支持多语言栈解耦,PHP/Go可独立部署,符合“避免引入过多复杂度”的核心诉求。 仅在需要绝对单调递增 且已有MySQL中央管控平台时考虑方案一。 八、心得感悟 本方案从最精简的角度出发综合各种最佳实践方案,通过ID内嵌路由位 彻底摒弃了哈希取模与跨库JOIN,将分库分表复杂度降至最低。8个物理库×512张表的架构在MySQL 8.0下可稳定支撑百亿级订单,配合预留位设计与动态路由配置,理论上可平滑演进至10年以上周期。代码层PHP与Go完全解耦,团队可根据技术栈独立演进,无需承担分布式中间件的学习与维护成本。
🛠️ 部署环境配置要点 :