写点什么

一文讲清如何设计一个秒杀系统(Sentinel 熔断限流+令牌桶削峰)

作者:王中阳Go
  • 2025-11-24
    北京
  • 本文字数:19866 字

    阅读完需:约 65 分钟

一文讲清如何设计一个秒杀系统(Sentinel熔断限流+令牌桶削峰)

这篇文章的内容都是基于我们GoFrame微服务电商项目的实践,感兴趣的朋友可以点击查看

1. 系统架构设计

1.1 整体架构图

┌─────────────┐     ┌──────────────┐     ┌────────────────┐│  客户端请求  │ ──> │  Gateway-H5  │ ──> │  Sentinel网关限流  │└─────────────┘     └──────────────┘     └────────────────┘┌─────────────┐     ┌──────────────┐     ┌────────────────┐     ┌──────────────┐│ 数据库存储   │ <── │  秒杀服务     │ <── │  令牌桶削峰组件  │ <── │  RabbitMQ    │└─────────────┘     └──────┬───────┘     └────────────────┘     └──────────────┘                           │                                        ▲                           ▼                                        │                     ┌──────────────┐                      ┌─────────────────┐                     │  Redis缓存    │ <─────────────────── │  库存服务        │                     └──────────────┘                      └─────────────────┘
复制代码

1.2 核心组件说明

  1. Gateway-H5:前端入口,接收秒杀请求

  2. Sentinel 网关限流:流量控制和熔断降级,保护后端服务

  3. 令牌桶削峰组件:平滑突发流量,避免系统过载

  4. RabbitMQ:消息队列,用于异步处理秒杀请求和结果通知

  5. 秒杀服务:专门处理秒杀业务逻辑

  6. Redis 缓存:存储秒杀商品信息、库存和分布式锁

  7. 库存服务:处理库存扣减,基于现有库存管理实现扩展

  8. 数据库存储:持久化秒杀相关数据

1.3 架构设计原则

  1. 高可用:通过熔断、降级、限流等机制确保系统在高并发下的可用性

  2. 可扩展性:模块化设计,方便横向扩展

  3. 高性能:使用缓存、异步处理等方式提高系统性能

  4. 安全性:防刷、防超卖等安全机制

  5. 兼容性:与现有系统无缝集成,不影响现有功能

2. 秒杀系统核心流程

2.1 秒杀流程详细设计

2.1.1 预热流程

  1. 定时任务触发:系统启动时或秒杀活动开始前通过定时任务触发预热

  2. 商品数据加载:从数据库加载即将开始的秒杀商品信息

  3. 库存初始化:在 Redis 中初始化秒杀商品库存

  4. 热点数据缓存:缓存秒杀商品详情、活动规则等热点数据

  5. 预热结果记录:记录预热结果,用于监控和告警

2.1.2 请求处理流程

  1. 请求接收:Gateway-H5 接收用户秒杀请求

  2. 参数校验:验证请求参数的合法性

  3. Sentinel 限流:经过 Sentinel 网关限流处理

  4. 令牌桶削峰:通过令牌桶算法平滑流量

  5. 用户验证:验证用户身份、登录状态、购买权限

  6. 资格检查:检查用户是否已购买过该秒杀商品(防重复购买)

  7. 库存检查:检查商品库存是否充足

  8. 库存扣减:使用 Redis Lua 脚本原子性扣减库存

  9. 消息入队:将秒杀成功的请求信息发送到 RabbitMQ 队列

  10. 结果返回:返回初步的秒杀结果(异步处理需要后续查询最终结果)

2.1.3 异步处理流程

  1. 消息消费:秒杀服务消费者从队列获取秒杀请求

  2. 订单创建:创建秒杀订单记录

  3. 库存确认:再次确认并持久化库存扣减

  4. 事务处理:使用数据库事务确保数据一致性

  5. 结果记录:记录秒杀结果到 Redis 和数据库

  6. 消息通知:发送订单创建成功通知

2.1.4 结果查询流程

  1. 查询请求:用户查询秒杀结果

  2. 缓存查询:首先从 Redis 查询秒杀结果

  3. 数据库查询:Redis 未命中时从数据库查询

  4. 结果返回:返回秒杀结果给用户

2.2 接口详细设计

2.2.1 秒杀商品接口

// 秒杀商品信息请求结构type FlashSaleGoodsListReq struct {    ActivityId   uint32 `json:"activity_id" v:"required"` // 活动ID    PageNum      int    `json:"page_num" v:"min:1"`      // 页码    PageSize     int    `json:"page_size" v:"min:1,max:100"` // 每页数量    StartTime    int64  `json:"start_time"`                // 开始时间过滤    EndTime      int64  `json:"end_time"`                  // 结束时间过滤}
// 秒杀商品列表响应结构type FlashSaleGoodsListRes struct { Total int64 `json:"total"` // 总数量 List []*FlashSaleGoodsInfo `json:"list"` // 商品列表}
// 秒杀商品详情请求/响应结构type FlashSaleGoodsDetailReq struct { GoodsId uint32 `json:"goods_id" v:"required"` // 商品ID ActivityId uint32 `json:"activity_id" v:"required"` // 活动ID}
type FlashSaleGoodsDetailRes struct { GoodsInfo *FlashSaleGoodsInfo `json:"goods_info"` // 商品信息 StockInfo *StockInfo `json:"stock_info"` // 库存信息 ActivityInfo *ActivityInfo `json:"activity_info"` // 活动信息 RemainSeconds int64 `json:"remain_seconds"` // 距离开始/结束的秒数}
// 秒杀商品信息服务接口type FlashSaleGoodsService interface { GetFlashSaleGoodsList(ctx context.Context, req *FlashSaleGoodsListReq) (*FlashSaleGoodsListRes, error) GetFlashSaleGoodsDetail(ctx context.Context, req *FlashSaleGoodsDetailReq) (*FlashSaleGoodsDetailRes, error) PreheatFlashSaleGoods(ctx context.Context, activityId uint32) error}
复制代码

2.2.2 秒杀操作接口

// 创建秒杀订单请求/响应结构type CreateFlashSaleOrderReq struct {    GoodsId    uint32 `json:"goods_id" v:"required"`    // 商品ID    ActivityId uint32 `json:"activity_id" v:"required"` // 活动ID    UserId     uint32 `json:"user_id" v:"required"`     // 用户ID    Count      int    `json:"count" v:"min:1,max:10"`   // 购买数量}
type CreateFlashSaleOrderRes struct { Success bool `json:"success"` // 是否成功 OrderNo string `json:"order_no"` // 订单号(如果成功) Message string `json:"message"` // 提示信息 ResultId string `json:"result_id"` // 结果查询ID Status int `json:"status"` // 状态码:0-处理中,1-成功,2-失败}
// 查询秒杀结果请求/响应结构type GetFlashSaleResultReq struct { ResultId string `json:"result_id" v:"required"` // 结果查询ID UserId uint32 `json:"user_id" v:"required"` // 用户ID}
type GetFlashSaleResultRes struct { Status int `json:"status"` // 状态码:0-处理中,1-成功,2-失败 Message string `json:"message"` // 提示信息 OrderNo string `json:"order_no"` // 订单号(如果成功) GoodsId uint32 `json:"goods_id"` // 商品ID PayAmount int64 `json:"pay_amount"` // 支付金额}
// 秒杀操作服务接口type FlashSaleService interface { CreateFlashSaleOrder(ctx context.Context, req *CreateFlashSaleOrderReq) (*CreateFlashSaleOrderRes, error) GetFlashSaleResult(ctx context.Context, req *GetFlashSaleResultReq) (*GetFlashSaleResultRes, error) ProcessFlashSaleOrder(ctx context.Context, orderInfo *FlashSaleOrderInfo) error}
复制代码

2.2.3 库存操作接口扩展

// 库存信息结构type StockInfo struct {    GoodsId       uint32 `json:"goods_id"`       // 商品ID    TotalStock    int    `json:"total_stock"`    // 总库存    AvailableStock int   `json:"available_stock"` // 可用库存    LockedStock   int    `json:"locked_stock"`   // 锁定库存}
// 库存服务秒杀扩展接口type FlashSaleStockService interface { ReduceFlashSaleStock(ctx context.Context, goodsId uint32, userId uint32, count int) (bool, error) InitFlashSaleStock(ctx context.Context, goodsId uint32, count int) (bool, error) GetFlashSaleStock(ctx context.Context, goodsId uint32) (*StockInfo, error) LockFlashSaleStock(ctx context.Context, goodsId uint32, orderNo string, count int) (bool, error) ConfirmFlashSaleStock(ctx context.Context, goodsId uint32, orderNo string) (bool, error) UnlockFlashSaleStock(ctx context.Context, goodsId uint32, orderNo string) (bool, error)}
// 集成到现有库存管理func (s *StockManagerImpl) ReduceFlashSaleStock(ctx context.Context, goodsId uint32, userId uint32, count int) (bool, error) { // 复用现有库存扣减逻辑,使用专门的秒杀库存键 // 结合Redis Lua脚本确保原子性操作 return s.luaStockManager.ReduceFlashSaleStock(ctx, goodsId, userId, count)}
复制代码

2.2.4 消息队列接口

// 秒杀消息结构type FlashSaleMessage struct {    OrderNo    string `json:"order_no"`    // 订单号    GoodsId    uint32 `json:"goods_id"`    // 商品ID    ActivityId uint32 `json:"activity_id"` // 活动ID    UserId     uint32 `json:"user_id"`     // 用户ID    Count      int    `json:"count"`       // 购买数量    Amount     int64  `json:"amount"`      // 金额    CreateTime int64  `json:"create_time"` // 创建时间}
// 消息队列服务接口type MessageQueueService interface { PublishFlashSaleMessage(ctx context.Context, msg *FlashSaleMessage) error ConsumeFlashSaleMessage(ctx context.Context, handler func(ctx context.Context, msg *FlashSaleMessage) error) error}
复制代码

2.3 服务间调用关系

  1. Gateway-H5 → Sentinel 限流 → 令牌桶削峰 → FlashSaleService.CreateFlashSaleOrder

  2. FlashSaleService.CreateFlashSaleOrder → FlashSaleStockService.ReduceFlashSaleStock

  3. FlashSaleService.CreateFlashSaleOrder → MessageQueueService.PublishFlashSaleMessage

  4. MessageQueueService.ConsumeFlashSaleMessage → FlashSaleService.ProcessFlashSaleOrder

  5. FlashSaleService.ProcessFlashSaleOrder → FlashSaleStockService.ConfirmFlashSaleStock

  6. FlashSaleService.ProcessFlashSaleOrder → OrderService.CreateOrder

  7. FlashSaleService.GetFlashSaleResult → Redis/Database 查询

3. Sentinel 熔断限流设计

3.1 Sentinel 集成方案

3.1.1 Gateway 层集成

// app/gateway-h5/internal/middleware/sentinel.gopackage middleware
import ( "context" "github.com/alibaba/sentinel-golang/api" "github.com/alibaba/sentinel-golang/core/base" "github.com/gogf/gf/v2/net/ghttp" "net/http" "time")
// SentinelMiddleware Sentinel中间件func SentinelMiddleware() ghttp.HandlerFunc { return func(r *ghttp.Request) { resourceName := r.RequestURI // 请求路径作为资源名 ctx := context.Background() // 执行限流控制 entry, blockErr := api.Entry( resourceName, api.WithResourceType(base.ResTypeWeb), api.WithTrafficType(base.Inbound), ) if blockErr != nil { // 被限流或熔断 r.Response.WriteJsonExit(map[string]interface{}{ "code": 429, "message": "当前请求人数过多,请稍后重试", "data": nil, }) return } defer entry.Exit() // 继续处理请求 r.Middleware.Next() }}
复制代码

3.1.2 服务层集成

// app/flash-sale/internal/service/sentinel.gopackage service
import ( "context" "github.com/alibaba/sentinel-golang/api" "github.com/alibaba/sentinel-golang/core/base" "github.com/gogf/gf/v2/errors/gerror")
// SentinelService Sentinel服务封装type SentinelService struct{}
func NewSentinelService() *SentinelService { return &SentinelService{}}
// DoWithSentinel 使用Sentinel包装业务逻辑func (s *SentinelService) DoWithSentinel(ctx context.Context, resourceName string, fn func(ctx context.Context) error) error { entry, blockErr := api.Entry( resourceName, api.WithResourceType(base.ResTypeCommon), api.WithTrafficType(base.Inbound), ) if blockErr != nil { return gerror.New("当前请求人数过多,请稍后重试") } defer entry.Exit() return fn(ctx)}
复制代码

3.1.3 Sentinel 初始化配置

// app/gateway-h5/internal/init/sentinel.gopackage init
import ( "github.com/alibaba/sentinel-golang/core/flow" "github.com/alibaba/sentinel-golang/core/rule" "github.com/alibaba/sentinel-golang/core/circuitbreaker" "github.com/alibaba/sentinel-golang/core/hotspot" "github.com/alibaba/sentinel-golang/ext/datasource" "time")
// InitSentinel 初始化Sentinelfunc InitSentinel() error { // 初始化Sentinel if err := api.InitDefault(); err != nil { return err } // 加载限流、熔断、热点参数规则 loadFlowRules() loadCircuitBreakerRules() loadHotspotRules() return nil}
复制代码

3.2 限流规则详细设计

3.2.1 全局限流规则

// 全局限流规则func loadFlowRules() {    rules := []*flow.Rule{        // 全局QPS限流        {            Resource:               "global",            TokenCalculateStrategy: flow.Direct,            ControlBehavior:        flow.Reject,            Threshold:              10000, // 全局QPS上限            StatIntervalInMs:       1000,        },        // 秒杀接口限流        {            Resource:               "/api/v1/flash-sale/create",            TokenCalculateStrategy: flow.Direct,            ControlBehavior:        flow.Reject,            Threshold:              5000, // 秒杀接口QPS上限            StatIntervalInMs:       1000,        },        // 秒杀商品详情接口限流(预热模式)        {            Resource:               "/api/v1/flash-sale/goods/detail",            TokenCalculateStrategy: flow.Direct,            ControlBehavior:        flow.WarmUp,            Threshold:              20000, // 详情接口QPS上限            WarmUpPeriodSec:        10,    // 10秒预热            StatIntervalInMs:       1000,        },    }        _, err := flow.LoadRules(rules)    if err != nil {        // 记录错误日志    }}
复制代码

3.2.2 用户级别限流

// 用户级别限流 - 自定义资源名func createUserResourceName(userId uint32, baseResource string) string {    return fmt.Sprintf("%s:user:%d", baseResource, userId)}
// 可在运行时动态为活跃用户添加限流规则,或结合热点参数限流实现
复制代码

3.2.3 热点参数限流

// 热点参数限流规则func loadHotspotRules() {    rules := []*hotspot.Rule{        // 对秒杀商品ID进行热点参数限流        {            Resource:       "/api/v1/flash-sale/create",            ParamIdx:       0, // 商品ID为第一个参数            MetricType:     hotspot.QPS,            ControlBehavior: flow.Reject,            BurstCount:     10,            ParamFlowItems: []*hotspot.ParamFlowItem{                {                    Object:    "1001", // 热门商品ID                    ClassType: "string",                    Count:     100, // 该商品QPS限制                },            },        },        // 对用户ID限流(防刷单)        {            Resource:       "/api/v1/flash-sale/create",            ParamIdx:       1, // 用户ID为第二个参数            MetricType:     hotspot.QPS,            ControlBehavior: flow.Reject,            Count:          5, // 单个用户每秒最多5个请求        },    }        _, err := hotspot.LoadRules(rules)    if err != nil {        // 记录错误日志    }}
复制代码

3.3 熔断降级策略详细设计

3.3.1 错误率熔断规则

// 错误率熔断规则func loadCircuitBreakerRules() {    rules := []*circuitbreaker.Rule{        // 基于错误率的熔断        {            Resource:               "flash_sale_create_order",            Strategy:               circuitbreaker.ErrorRatio,            MinRequestAmount:       100,          // 最小请求数            Threshold:              0.5,          // 错误率阈值50%            RecoveryTimeoutSec:     5,            // 熔断恢复时间5秒            StatIntervalInMs:       1000,         // 统计窗口            StatSlidingWindowBucketCount: 10,     // 滑动窗口桶数量        },        // 基于响应时间的熔断        {            Resource:               "flash_sale_create_order",            Strategy:               circuitbreaker.SlowRequestRatio,            MinRequestAmount:       100,            Threshold:              0.5,          // 慢调用比例阈值50%            SlowRatioThreshold:     0.5,            MaxAllowedRtMs:         500,          // 最大允许响应时间500ms            RecoveryTimeoutSec:     5,            StatIntervalInMs:       1000,            StatSlidingWindowBucketCount: 10,        },    }        _, err := circuitbreaker.LoadRules(rules)    if err != nil {        // 记录错误日志    }}
复制代码

3.3.2 熔断降级处理

// 在秒杀服务中处理熔断降级func (s *FlashSaleServiceImpl) CreateFlashSaleOrder(ctx context.Context, req *CreateFlashSaleOrderReq) (*CreateFlashSaleOrderRes, error) {    var result *CreateFlashSaleOrderRes    var err error        // 使用Sentinel包装业务逻辑    sentinelErr := s.sentinelService.DoWithSentinel(ctx, "flash_sale_create_order", func(ctx context.Context) error {        result, err = s.doCreateFlashSaleOrder(ctx, req)        return err    })        if sentinelErr != nil {        return &CreateFlashSaleOrderRes{            Success: false,            Message: "当前请求人数过多,请稍后重试",            Status:  2,        }, nil    }        return result, err}
复制代码

3.4 动态配置和监控

3.4.1 动态数据源配置(简化)

// 配置动态数据源(以etcd为例)func setupDynamicDataSource() error {    client, err := clientv3.New(clientv3.Config{        Endpoints:   []string{"localhost:2379"},        DialTimeout: 5 * time.Second,    })    if err != nil {        return err    }        // 流控规则数据源    flowDataSource := datasource.NewEtcdDataSource(        client, "sentinel/rules/flow", datasource.FlowRuleJsonParser,    )    flow.RegisterFlowDataSource(flowDataSource)        // 熔断规则数据源    cbDataSource := datasource.NewEtcdDataSource(        client, "sentinel/rules/circuitbreaker", datasource.CircuitbreakerRuleJsonParser,    )    circuitbreaker.RegisterCircuitbreakerDataSource(cbDataSource)        return nil}
复制代码

3.4.2 监控和告警配置

// 配置监控和告警func setupMonitoring() error {    config := &dashboard.Config{        CollectorAddr: "localhost:8719", // Sentinel Dashboard地址        HeartbeatIntervalMs: 10000,      // 心跳间隔    }        return dashboard.Start(config)}
复制代码

3.5 Sentinel 规则调优建议

  1. 预热期设置:秒杀开始前设置预热期,让系统逐步提升处理能力

  2. 排队等待策略:秒杀请求采用排队等待,而非直接拒绝

  3. 热点参数优先级:热门商品设置更低 QPS 限制,防止过度请求

  4. 动态调整:根据实际流量动态调整限流参数

  5. 监控告警阈值:设置合理阈值,及时发现问题

3.6 与令牌桶削峰的协同

  1. Sentinel 提供粗粒度限流和熔断保护

  2. 令牌桶提供细粒度流量平滑控制

  3. 两者协同确保秒杀场景下的系统稳定性

4. 令牌桶削峰设计

4.1 令牌桶实现原理

4.1.1 基本原理

  1. 令牌生成:系统以固定速率向桶中添加令牌

  2. 令牌存储:桶有最大容量,多余令牌丢弃

  3. 请求处理:请求需获取令牌才能被处理

  4. 流量控制:通过控制生成速率和桶容量实现流量平滑

4.1.2 数学模型

  • 令牌生成速率:r(个/秒)

  • 桶最大容量:b(个令牌)

  • 最后更新时间:t_last

  • 当前令牌数:tokens

  • 令牌补充公式:tokens = min(b, tokens + r * (t_now - t_last))

4.2 分布式令牌桶实现

4.2.1 Redis 实现架构

// app/flash-sale/internal/service/token_bucket.gopackage service
import ( "context" "fmt" "time" "github.com/gogf/gf/v2/errors/gerror" "github.com/gogf/gf/v2/util/gconv")
// TokenBucketService 令牌桶服务type TokenBucketService struct { redisClient *redis.Client}
func NewTokenBucketService(redisClient *redis.Client) *TokenBucketService { return &TokenBucketService{redisClient: redisClient}}
// 尝试获取令牌func (s *TokenBucketService) TryAcquire(ctx context.Context, key string, rate float64, burst int, tokens int) (bool, error) { result, err := s.redisClient.Eval(ctx, tokenBucketLuaScript, []string{key}, rate, burst, tokens, time.Now().UnixNano()/1000000, ).Result() if err != nil { return false, gerror.Wrap(err, "尝试获取令牌失败") } return gconv.Bool(result), nil}
// 令牌桶键定义const ( TokenBucketKeyPrefix = "flash_sale:token_bucket:" GlobalTokenBucketKey = "flash_sale:token_bucket:global" GoodsTokenBucketKey = "flash_sale:token_bucket:goods:%d" UserTokenBucketKey = "flash_sale:token_bucket:user:%d" ActivityTokenBucketKey = "flash_sale:token_bucket:activity:%d")
// 生成各类令牌桶键func GetGoodsTokenBucketKey(goodsId uint32) string { return fmt.Sprintf(GoodsTokenBucketKey, goodsId)}// 类似实现GetUserTokenBucketKey、GetActivityTokenBucketKey
复制代码

4.2.2 Lua 脚本实现

// 令牌桶Lua脚本(原子性操作)const tokenBucketLuaScript = `local key = KEYS[1]local rate = tonumber(ARGV[1])       -- 令牌生成速率(个/毫秒)local burst = tonumber(ARGV[2])      -- 桶容量local tokens = tonumber(ARGV[3])     -- 请求令牌数local now = tonumber(ARGV[4])        -- 当前时间戳(毫秒)
-- 获取当前桶状态local bucket = redis.call('hmget', key, 'last_refill_time', 'available_tokens')local lastRefillTime = tonumber(bucket[1] or now)local availableTokens = tonumber(bucket[2] or burst)
-- 计算补充的令牌数local timeElapsed = now - lastRefillTimelocal newTokens = timeElapsed * rateavailableTokens = math.min(burst, availableTokens + newTokens)
-- 检查并扣除令牌if availableTokens >= tokens then availableTokens = availableTokens - tokens redis.call('hmset', key, 'last_refill_time', now, 'available_tokens', availableTokens) redis.call('expire', key, 86400) return 1else return 0end`
复制代码

4.2.3 多层令牌桶策略

// MultiLevelTokenBucketService 多层令牌桶服务type MultiLevelTokenBucketService struct {    tokenBucketService *TokenBucketService}
func NewMultiLevelTokenBucketService(tokenBucketService *TokenBucketService) *MultiLevelTokenBucketService { return &MultiLevelTokenBucketService{tokenBucketService: tokenBucketService}}
// 尝试获取秒杀令牌(多层检查)func (s *MultiLevelTokenBucketService) TryAcquireFlashSaleToken(ctx context.Context, goodsId, userId, activityId uint32) (bool, error) { // 1. 全局令牌桶检查 if ok, _ := s.tokenBucketService.TryAcquire(ctx, GlobalTokenBucketKey, 1000.0/1000, 1000, 1); !ok { return false, nil } // 2. 活动令牌桶检查 activityKey := GetActivityTokenBucketKey(activityId) if ok, _ := s.tokenBucketService.TryAcquire(ctx, activityKey, 500.0/1000, 500, 1); !ok { return false, nil } // 3. 商品令牌桶检查 goodsKey := GetGoodsTokenBucketKey(goodsId) if ok, _ := s.tokenBucketService.TryAcquire(ctx, goodsKey, 100.0/1000, 100, 1); !ok { return false, nil } // 4. 用户令牌桶检查(防刷单) userKey := GetUserTokenBucketKey(userId) if ok, _ := s.tokenBucketService.TryAcquire(ctx, userKey, 5.0/1000, 10, 1); !ok { return false, nil } return true, nil}
复制代码

4.3 动态调整令牌生成速率

4.3.1 自适应令牌桶管理器

// AdaptiveTokenBucketManager 自适应令牌桶管理器type AdaptiveTokenBucketManager struct {    redisClient *redis.Client    mu          sync.RWMutex    configs     map[string]*TokenBucketConfig    monitor     *SystemMonitor}
// TokenBucketConfig 令牌桶配置type TokenBucketConfig struct { Key string `json:"key"` BaseRate float64 `json:"base_rate"` // 基础速率 MaxRate float64 `json:"max_rate"` // 最大速率 MinRate float64 `json:"min_rate"` // 最小速率 Burst int `json:"burst"` // 桶容量 AdjustStep float64 `json:"adjust_step"` // 调整步长}
// 启动自适应调整func (m *AdaptiveTokenBucketManager) StartAdaptiveAdjustment(ctx context.Context, interval time.Duration) { ticker := time.NewTicker(interval) defer ticker.Stop() for { select { case <-ctx.Done(): return case <-ticker.C: m.adjustRates(ctx) } }}
// 调整令牌生成速率func (m *AdaptiveTokenBucketManager) adjustRates(ctx context.Context) { load, err := m.monitor.GetSystemLoad(ctx) if err != nil { // 记录错误日志 return } m.mu.RLock() defer m.mu.RUnlock() for key, config := range m.configs { var newRate float64 if load.CPUUsage > 0.8 { // 高负载,降低速率 newRate = math.Max(config.MinRate, config.BaseRate * 0.9) } else if load.CPUUsage < 0.5 { // 低负载,提高速率 newRate = math.Min(config.MaxRate, config.BaseRate * 1.1) } else { newRate = config.BaseRate } config.BaseRate = newRate // 保存配置到Redis if err := m.saveConfigToRedis(ctx, key, config); err != nil { // 记录错误日志 } }}
复制代码

4.4 与秒杀业务集成

4.4.1 秒杀服务中的令牌桶集成

// 在秒杀服务中集成令牌桶func (s *FlashSaleServiceImpl) CreateFlashSaleOrder(ctx context.Context, req *CreateFlashSaleOrderReq) (*CreateFlashSaleOrderRes, error) {    // 1. 令牌桶削峰检查    success, err := s.tokenBucketService.TryAcquireFlashSaleToken(        ctx, req.GoodsId, req.UserId, req.ActivityId,    )    if err != nil {        return nil, gerror.Wrap(err, "令牌桶检查失败")    }        if !success {        // 未获取令牌,返回排队信息        return &CreateFlashSaleOrderRes{            Success:  false,            Message:  "当前请求人数过多,请排队等待",            ResultId: generateResultId(req.UserId, req.GoodsId),            Status:   0, // 处理中        }, nil    }        // 2. 后续业务逻辑(库存检查、订单创建等)    // ...        return result, nil}
复制代码

4.4.2 预热期令牌桶配置

// WarmupTokenBucketService 预热期令牌桶服务type WarmupTokenBucketService struct {    tokenBucketService   *TokenBucketService    adaptiveTokenManager *AdaptiveTokenBucketManager}
// 开始预热func (s *WarmupTokenBucketService) StartWarmup(ctx context.Context, activityId uint32, startTime time.Time, duration time.Duration) { now := time.Now() if now.After(startTime) { return // 活动已开始,无需预热 } // 计算预热间隔和步骤 warmupDuration := startTime.Sub(now) if warmupDuration < time.Minute { warmupDuration = time.Minute // 最小预热1分钟 } steps := int(warmupDuration.Seconds() / 10) // 每10秒调整一次 if steps < 1 { steps = 1 } // 初始速率与目标速率 initialRate := 10.0 / 1000 // 10个/秒 targetRate := 100.0 / 1000 // 100个/秒 rateIncrement := (targetRate - initialRate) / float64(steps) // 启动预热协程 go func() { ticker := time.NewTicker(10 * time.Second) defer ticker.Stop() currentRate := initialRate for i := 0; i < steps; i++ { select { case <-ctx.Done(): return case <-ticker.C: activityKey := GetActivityTokenBucketKey(activityId) // 更新令牌桶配置 s.adaptiveTokenManager.RegisterTokenBucket(&TokenBucketConfig{ Key: activityKey, BaseRate: currentRate, MaxRate: targetRate, MinRate: initialRate, Burst: int(currentRate * 2000), // 2秒突发容量 }) currentRate += rateIncrement // 提升速率 } } }()}
复制代码

5. Redis 缓存设计

5.1 缓存数据结构设计

秒杀场景下 Redis 缓存需满足高频读写、原子操作、防超卖等需求,核心数据结构按业务场景分类设计,避免冗余存储。

5.1.1 核心缓存键设计

// 秒杀核心缓存键定义(按业务维度分类)const (    // 商品维度:Hash存储商品基本信息,String存储库存/已售量    FlashSaleGoodsInfoKey  = "flash_sale:goods:info:%d"   // %d=商品ID,存储商品名称、价格、活动规则    FlashSaleGoodsStockKey = "flash_sale:goods:stock:%d"  // %d=商品ID,存储实时可用库存    FlashSaleGoodsSoldKey  = "flash_sale:goods:sold:%d"   // %d=商品ID,存储已售数量(用于最终核对)        // 用户维度:Set防重复购买,String存储秒杀结果    FlashSaleUserBuyKey    = "flash_sale:user:buy:%d:%d"  // %d=用户ID:%d=商品ID,存储购买记录(防重复)    FlashSaleResultKey     = "flash_sale:result:%s"       // %s=结果ID,存储秒杀状态(0-处理中/1-成功/2-失败)        // 活动维度:Set存储活动商品列表,ZSet排序待预热商品    FlashSaleActivityGoods = "flash_sale:activity:goods:%d" // %d=活动ID,存储参与活动的商品ID集合    FlashSalePreheatQueue  = "flash_sale:preheat:queue"     // 待预热商品队列(按活动开始时间排序))
// 商品信息缓存结构体(与数据库字段对齐,精简冗余字段)type FlashSaleGoodsCache struct { GoodsId uint32 `json:"goods_id"` // 商品ID ActivityId uint32 `json:"activity_id"` // 活动ID Price int64 `json:"price"` // 秒杀价(分) MaxBuy int `json:"max_buy"` // 单用户限购数 StartTime int64 `json:"start_time"` // 活动开始时间戳 EndTime int64 `json:"end_time"` // 活动结束时间戳}
复制代码

5.1.2 分布式锁实现(防超卖核心)

基于 Redis 实现分布式锁,确保库存操作的原子性,避免并发场景下的超卖问题,同时增加锁自动释放机制防止死锁。


// RedisLock 分布式锁核心实现type RedisLock struct {    redisClient *redis.Client    key         string        // 锁键    value       string        // 唯一标识(防止误释放)    expiry      time.Duration // 过期时间}
// NewRedisLock 创建锁实例(秒杀场景建议过期时间3-5秒)func NewRedisLock(redisClient *redis.Client, goodsId uint32) *RedisLock { lockKey := fmt.Sprintf("flash_sale:lock:stock:%d", goodsId) return &RedisLock{ redisClient: redisClient, key: lockKey, value: util.RandomString(16), // 生成16位随机唯一值 expiry: 3 * time.Second, }}
// Lock 尝试获取锁(支持重试,秒杀场景重试次数建议≤3)func (l *RedisLock) Lock(ctx context.Context, retry int) (bool, error) { for i := 0; i < retry; i++ { // SET NX EX 原子操作:不存在则设置,同时指定过期时间 success, err := l.redisClient.SetNX(ctx, l.key, l.value, l.expiry).Result() if err != nil { return false, err } if success { return true, nil } time.Sleep(100 * time.Millisecond) // 重试间隔100ms,减轻Redis压力 } return false, nil}
// Unlock 安全释放锁(Lua脚本确保原子性)func (l *RedisLock) Unlock(ctx context.Context) error { unlockScript := ` if redis.call('GET', KEYS[1]) == ARGV[1] then return redis.call('DEL', KEYS[1]) end return 0 ` _, err := l.redisClient.Eval(ctx, unlockScript, []string{l.key}, l.value).Result() return err}
复制代码

5.2 缓存预热与一致性保障

5.2.1 缓存预热策略

秒杀开始前 30 分钟触发预热,将活动商品数据加载至 Redis,避免活动初期缓存穿透。通过分布式锁防止重复预热,支持按活动批量处理。


// PreheatService 缓存预热服务type PreheatService struct {    goodsRepo    *repository.FlashSaleGoodsRepo    redisClient  *redis.Client    logger       *glog.Logger}
// PreheatByActivity 按活动ID批量预热商品func (s *PreheatService) PreheatByActivity(ctx context.Context, activityId uint32) error { // 1. 获取预热锁,防止多实例重复执行 lock := NewRedisLock(s.redisClient, activityId) lockSuccess, err := lock.Lock(ctx, 2) if err != nil { s.logger.Error(ctx, "预热锁获取失败", g.Map{"activity_id": activityId, "err": err}) return err } if !lockSuccess { s.logger.Info(ctx, "活动已在预热中", g.Map{"activity_id": activityId}) return nil } defer lock.Unlock(ctx)
// 2. 查询活动下所有商品(分页查询避免数据量过大) goodsList, err := s.goodsRepo.GetByActivityId(ctx, activityId, 1, 1000) if err != nil { return err }
// 3. 批量缓存商品信息与库存 pipe := s.redisClient.Pipeline() for _, goods := range goodsList { // 缓存商品基本信息 infoKey := fmt.Sprintf(FlashSaleGoodsInfoKey, goods.GoodsId) pipe.HSet(ctx, infoKey, gconv.Map(&FlashSaleGoodsCache{ GoodsId: goods.GoodsId, ActivityId: goods.ActivityId, Price: goods.FlashPrice, MaxBuy: goods.MaxBuy, StartTime: goods.StartTime, EndTime: goods.EndTime, })) // 设置过期时间(活动结束后24小时) expiry := time.Duration(goods.EndTime - time.Now().Unix() + 86400) * time.Second pipe.Expire(ctx, infoKey, expiry)
// 初始化库存(从数据库同步) stockKey := fmt.Sprintf(FlashSaleGoodsStockKey, goods.GoodsId) pipe.Set(ctx, stockKey, goods.Stock, expiry)
// 加入活动商品列表 activityGoodsKey := fmt.Sprintf(FlashSaleActivityGoods, activityId) pipe.SAdd(ctx, activityGoodsKey, goods.GoodsId) pipe.Expire(ctx, activityGoodsKey, expiry) } _, err = pipe.Exec(ctx) return err}
复制代码

5.2.2 缓存与数据库一致性

采用“先删缓存再更新数据库”+“延迟双删”策略,解决秒杀场景下的缓存一致性问题,结合消息队列确保最终一致。


// 库存更新后的缓存一致性处理func (s *StockServiceImpl) UpdateStockAfterSale(ctx context.Context, goodsId uint32, newStock int) error {    // 1. 先删除Redis缓存(避免脏读)    stockKey := fmt.Sprintf(FlashSaleGoodsStockKey, goodsId)    _, err := s.redisClient.Del(ctx, stockKey).Result()    if err != nil {        s.logger.Warn(ctx, "删除库存缓存失败", g.Map{"goods_id": goodsId, "err": err})    }
// 2. 更新数据库库存(开启事务) tx := s.db.Begin() if err := tx.Model(&model.FlashSaleStock{}). Where("goods_id = ?", goodsId). Update("available_stock", newStock).Error; err != nil { tx.Rollback() return err } tx.Commit()
// 3. 延迟1秒再次删除缓存(解决并发更新问题) go func() { time.Sleep(1 * time.Second) _, _ = s.redisClient.Del(context.Background(), stockKey).Result() }()
// 4. 发送库存更新消息,用于后续监控与补偿 _ = s.msgService.PublishStockUpdateMsg(ctx, &message.StockUpdateMsg{ GoodsId: goodsId, NewStock: newStock, UpdateTime: time.Now().Unix(), }) return nil}
复制代码

6. 数据库设计与优化

6.1 核心表结构设计

秒杀系统数据库需精简字段,聚焦核心业务,同时通过分库分表应对高并发写入,主要包含活动表、商品表、订单表、库存表。


6.2 分库分表策略

秒杀订单表采用“用户 ID 哈希分表”,将订单数据分散至 8 个分表,降低单表并发压力,分表规则通过 Sharding-JDBC 实现。


// Sharding-JDBC分表配置(秒杀订单表)spring:  shardingsphere:    rules:      sharding:        tables:          flash_sale_order:            actual-data-nodes: ds0.flash_sale_order_${0..7}  # 8个分表            database-strategy:              none:  # 单库分表            table-strategy:              standard:                sharding-column: user_id  # 分表字段                sharding-algorithm-name: flash_sale_order_inline        sharding-algorithms:          flash_sale_order_inline:            type: INLINE            props:              algorithm-expression: flash_sale_order_${user_id % 8}  # 哈希取模分表    props:      sql-show: false  # 生产环境关闭SQL日志
复制代码

6.3 数据库性能优化

  1. 索引优化:秒杀订单表建立联合索引(user_id, goods_id),避免重复购买查询全表;库存表建立主键索引(goods_id),提升库存扣减效率。

  2. 事务优化:库存更新与订单创建采用“短事务”,减少锁持有时间;非核心操作(如日志记录)移出事务。

  3. 读写分离:活动商品查询、订单结果查询路由至从库,写入操作路由至主库,通过中间件自动切换。

7. 防超卖与防刷设计

7.1 防超卖核心实现

结合 Redis 原子操作与数据库乐观锁,实现双重防超卖,确保库存数据准确。Redis 层先进行预扣减,数据库层最终校验。


// 基于Redis Lua脚本的原子库存扣减(防超卖第一步)const stockReduceLua = `local stockKey = KEYS[1]local soldKey = KEYS[2]local reduceCount = tonumber(ARGV[1])
-- 1. 检查库存是否充足local stock = tonumber(redis.call('GET', stockKey))if not stock or stock < reduceCount then return 0 -- 库存不足end
-- 2. 原子扣减库存与累加已售量redis.call('DECRBY', stockKey, reduceCount)redis.call('INCRBY', soldKey, reduceCount)return 1 -- 扣减成功`
// ReduceStock 库存扣减(Redis预扣减+数据库最终确认)func (s *StockServiceImpl) ReduceStock(ctx context.Context, goodsId uint32, userId uint32, count int) (bool, error) { stockKey := fmt.Sprintf(FlashSaleGoodsStockKey, goodsId) soldKey := fmt.Sprintf(FlashSaleGoodsSoldKey, goodsId)
// 1. Redis原子扣减 result, err := s.redisClient.Eval(ctx, stockReduceLua, []string{stockKey, soldKey}, count).Result() if err != nil || result == 0 { return false, err }
// 2. 数据库乐观锁最终确认(防止Redis与DB数据不一致) rowsAffected, err := s.db.Model(&model.FlashSaleStock{}). Where("goods_id = ? AND available_stock >= ?", goodsId, count). Update("available_stock", gorm.Expr("available_stock - ?", count)). RowsAffected if err != nil { // 数据库扣减失败,回滚Redis库存 s.redisClient.IncrBy(ctx, stockKey, int64(count)) s.redisClient.DecrBy(ctx, soldKey, int64(count)) return false, err }
return rowsAffected > 0, nil}
复制代码

7.2 防刷机制设计

从用户身份校验、行为频率限制、设备指纹三个维度实现防刷,避免恶意用户占用秒杀资源。


// AntiBrushService 防刷服务type AntiBrushService struct {    redisClient *redis.Client    userService *service.UserService}
// CheckUserValid 校验用户秒杀资格(防刷核心逻辑)func (s *AntiBrushService) CheckUserValid(ctx context.Context, req *dto.CreateFlashSaleOrderReq) (bool, string) { userId := req.UserId goodsId := req.GoodsId
// 1. 校验用户状态(是否为黑名单用户) if s.userService.IsBlackList(ctx, userId) { return false, "您的账号存在异常,暂无法参与秒杀" }
// 2. 限制单用户单商品请求频率(10秒内最多3次) freqKey := fmt.Sprintf("flash_sale:freq:user:%d:goods:%d", userId, goodsId) reqCount, _ := s.redisClient.Incr(ctx, freqKey).Result() if reqCount == 1 { s.redisClient.Expire(ctx, freqKey, 10*time.Second) } if reqCount > 3 { return false, "请求过于频繁,请稍后再试" }
// 3. 校验用户购买记录(是否已购买该商品) buyKey := fmt.Sprintf(FlashSaleUserBuyKey, userId, goodsId) if s.redisClient.SIsMember(ctx, buyKey, goodsId).Val() { return false, "您已购买过该秒杀商品,请勿重复提交" }
// 4. 设备指纹校验(非核心逻辑,可集成第三方SDK) // deviceValid := s.checkDeviceFingerprint(ctx, req.DeviceId) // if !deviceValid { // return false, "设备异常,暂无法参与秒杀" // }
return true, ""}
复制代码

8. 系统监控与告警

8.1 核心监控指标

基于 Prometheus+Grafana 构建监控体系,重点监控流量、库存、订单、服务健康四类指标,确保问题早发现。


  • 流量指标:网关 QPS、Sentinel 限流次数、令牌桶拒绝次数,监控阈值:QPS 突增 50%触发告警。

  • 库存指标:Redis 与 DB 库存差异、库存扣减成功率,监控阈值:差异>10 触发告警。

  • 订单指标:订单创建成功率、订单超时率,监控阈值:成功率<80%触发告警。

  • 服务指标:服务响应时间(P99)、接口错误率,监控阈值:P99>500ms 或错误率>5%触发告警。

8.2 告警机制实现

// 基于Prometheus AlertManager的告警规则配置groups:- name: flash_sale_alerts  rules:  # 1. 网关QPS突增告警  - alert: GatewayQpsSurge    expr: sum(rate(gateway_requests_total[5m])) / sum(rate(gateway_requests_total[15m])) > 1.5    for: 1m    labels:      severity: critical    annotations:      summary: "秒杀网关QPS突增"      description: "网关QPS在5分钟内较15分钟前增长超过50%,当前QPS: {{ $value }}"
# 2. 库存差异告警 - alert: StockInconsistency expr: abs(redis_stock_total - db_stock_total) > 10 for: 2m labels: severity: warning annotations: summary: "秒杀库存数据不一致" description: "Redis与DB库存差异超过10,Redis库存: {{ $labels.redis_stock_total }}, DB库存: {{ $labels.db_stock_total }}"
# 3. 服务响应时间告警 - alert: ServiceResponseSlow expr: histogram_quantile(0.99, sum(rate(service_request_duration_seconds_bucket[5m])) by (le, service)) > 0.5 for: 1m labels: severity: critical annotations: summary: "秒杀服务响应缓慢" description: "{{ $labels.service }}服务P99响应时间超过500ms,当前值: {{ $value }}s"
复制代码

9. 高可用部署方案

9.1 集群部署架构

采用“多活部署”架构,核心服务(秒杀服务、库存服务)部署至少 3 个节点,Redis 采用主从+哨兵模式,RabbitMQ 集群部署确保消息不丢失。


// Redis主从哨兵配置(简化)sentinel monitor mymaster 192.168.1.100 6379 2  # 主节点地址,2个哨兵确认主节点故障sentinel down-after-milliseconds mymaster 3000  # 3秒无响应标记为故障sentinel failover-timeout mymaster 10000       # 故障转移超时时间10秒sentinel parallel-syncs mymaster 1             # 故障转移后同步从节点数量
// RabbitMQ集群配置(镜像队列)rabbitmqctl set_policy ha-all "^flash_sale_" '{"ha-mode":"all","ha-sync-mode":"automatic"}'# 对flash_sale_前缀的队列启用镜像队列,所有节点同步消息
复制代码

9.2 容灾与降级策略

  1. 服务降级:当系统负载超过阈值(CPU>85%),自动降级非核心接口(如商品详情页评论、分享功能),优先保障秒杀核心流程。

  2. 故障转移:Redis 主节点故障时,哨兵自动切换至从节点;服务节点故障时,注册中心自动剔除,请求路由至健康节点。

  3. 流量熔断:当某一依赖服务(如库存服务)错误率超过 50%,Sentinel 自动熔断,采用本地缓存临时返回“系统繁忙”,避免雪崩。

10. 总结与优化方向

本秒杀系统通过“限流-削峰-缓存-异步”核心架构,解决了高并发场景下的性能与数据一致性问题。后续优化可聚焦三个方向:


  • 性能优化:引入 Redis Cluster 提升缓存容量与并发能力;采用协程池优化异步任务处理效率。

  • 安全增强:集成验证码、短信验证等多因素认证,进一步提升防刷能力;对敏感接口进行加密传输。

  • 可观测性:引入分布式追踪(如 Jaeger),实现全链路调用追踪,快速定位跨服务问题。


如果你对这种技术问题有疑问,或者对这个微服务项目感兴趣,都可以直接私信我:wangzhongyang1993。

发布于: 刚刚阅读数: 6
用户头像

王中阳Go

关注

靠敲代码在北京买房的程序员 2022-10-09 加入

【微信】wangzhongyang1993【公众号】程序员升职加薪之旅【成就】InfoQ专家博主👍掘金签约作者👍B站&掘金&CSDN&思否等全平台账号:王中阳Go

评论

发布
暂无评论
一文讲清如何设计一个秒杀系统(Sentinel熔断限流+令牌桶削峰)_微服务_王中阳Go_InfoQ写作社区