一文讲清如何设计一个秒杀系统(Sentinel 熔断限流+令牌桶削峰)
- 2025-11-24 北京
本文字数:19866 字
阅读完需:约 65 分钟

这篇文章的内容都是基于我们GoFrame微服务电商项目的实践,感兴趣的朋友可以点击查看
1. 系统架构设计
1.1 整体架构图
┌─────────────┐ ┌──────────────┐ ┌────────────────┐│ 客户端请求 │ ──> │ Gateway-H5 │ ──> │ Sentinel网关限流 │└─────────────┘ └──────────────┘ └────────────────┘ │ ▼┌─────────────┐ ┌──────────────┐ ┌────────────────┐ ┌──────────────┐│ 数据库存储 │ <── │ 秒杀服务 │ <── │ 令牌桶削峰组件 │ <── │ RabbitMQ │└─────────────┘ └──────┬───────┘ └────────────────┘ └──────────────┘ │ ▲ ▼ │ ┌──────────────┐ ┌─────────────────┐ │ Redis缓存 │ <─────────────────── │ 库存服务 │ └──────────────┘ └─────────────────┘
1.2 核心组件说明
Gateway-H5:前端入口,接收秒杀请求
Sentinel 网关限流:流量控制和熔断降级,保护后端服务
令牌桶削峰组件:平滑突发流量,避免系统过载
RabbitMQ:消息队列,用于异步处理秒杀请求和结果通知
秒杀服务:专门处理秒杀业务逻辑
Redis 缓存:存储秒杀商品信息、库存和分布式锁
库存服务:处理库存扣减,基于现有库存管理实现扩展
数据库存储:持久化秒杀相关数据
1.3 架构设计原则
高可用:通过熔断、降级、限流等机制确保系统在高并发下的可用性
可扩展性:模块化设计,方便横向扩展
高性能:使用缓存、异步处理等方式提高系统性能
安全性:防刷、防超卖等安全机制
兼容性:与现有系统无缝集成,不影响现有功能
2. 秒杀系统核心流程
2.1 秒杀流程详细设计
2.1.1 预热流程
定时任务触发:系统启动时或秒杀活动开始前通过定时任务触发预热
商品数据加载:从数据库加载即将开始的秒杀商品信息
库存初始化:在 Redis 中初始化秒杀商品库存
热点数据缓存:缓存秒杀商品详情、活动规则等热点数据
预热结果记录:记录预热结果,用于监控和告警
2.1.2 请求处理流程
请求接收:Gateway-H5 接收用户秒杀请求
参数校验:验证请求参数的合法性
Sentinel 限流:经过 Sentinel 网关限流处理
令牌桶削峰:通过令牌桶算法平滑流量
用户验证:验证用户身份、登录状态、购买权限
资格检查:检查用户是否已购买过该秒杀商品(防重复购买)
库存检查:检查商品库存是否充足
库存扣减:使用 Redis Lua 脚本原子性扣减库存
消息入队:将秒杀成功的请求信息发送到 RabbitMQ 队列
结果返回:返回初步的秒杀结果(异步处理需要后续查询最终结果)
2.1.3 异步处理流程
消息消费:秒杀服务消费者从队列获取秒杀请求
订单创建:创建秒杀订单记录
库存确认:再次确认并持久化库存扣减
事务处理:使用数据库事务确保数据一致性
结果记录:记录秒杀结果到 Redis 和数据库
消息通知:发送订单创建成功通知
2.1.4 结果查询流程
查询请求:用户查询秒杀结果
缓存查询:首先从 Redis 查询秒杀结果
数据库查询:Redis 未命中时从数据库查询
结果返回:返回秒杀结果给用户
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 服务间调用关系
Gateway-H5 → Sentinel 限流 → 令牌桶削峰 → FlashSaleService.CreateFlashSaleOrder
FlashSaleService.CreateFlashSaleOrder → FlashSaleStockService.ReduceFlashSaleStock
FlashSaleService.CreateFlashSaleOrder → MessageQueueService.PublishFlashSaleMessage
MessageQueueService.ConsumeFlashSaleMessage → FlashSaleService.ProcessFlashSaleOrder
FlashSaleService.ProcessFlashSaleOrder → FlashSaleStockService.ConfirmFlashSaleStock
FlashSaleService.ProcessFlashSaleOrder → OrderService.CreateOrder
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 规则调优建议
预热期设置:秒杀开始前设置预热期,让系统逐步提升处理能力
排队等待策略:秒杀请求采用排队等待,而非直接拒绝
热点参数优先级:热门商品设置更低 QPS 限制,防止过度请求
动态调整:根据实际流量动态调整限流参数
监控告警阈值:设置合理阈值,及时发现问题
3.6 与令牌桶削峰的协同
Sentinel 提供粗粒度限流和熔断保护
令牌桶提供细粒度流量平滑控制
两者协同确保秒杀场景下的系统稳定性
4. 令牌桶削峰设计
4.1 令牌桶实现原理
4.1.1 基本原理
令牌生成:系统以固定速率向桶中添加令牌
令牌存储:桶有最大容量,多余令牌丢弃
请求处理:请求需获取令牌才能被处理
流量控制:通过控制生成速率和桶容量实现流量平滑
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 数据库性能优化
索引优化:秒杀订单表建立联合索引(user_id, goods_id),避免重复购买查询全表;库存表建立主键索引(goods_id),提升库存扣减效率。
事务优化:库存更新与订单创建采用“短事务”,减少锁持有时间;非核心操作(如日志记录)移出事务。
读写分离:活动商品查询、订单结果查询路由至从库,写入操作路由至主库,通过中间件自动切换。
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 容灾与降级策略
服务降级:当系统负载超过阈值(CPU>85%),自动降级非核心接口(如商品详情页评论、分享功能),优先保障秒杀核心流程。
故障转移:Redis 主节点故障时,哨兵自动切换至从节点;服务节点故障时,注册中心自动剔除,请求路由至健康节点。
流量熔断:当某一依赖服务(如库存服务)错误率超过 50%,Sentinel 自动熔断,采用本地缓存临时返回“系统繁忙”,避免雪崩。
10. 总结与优化方向
本秒杀系统通过“限流-削峰-缓存-异步”核心架构,解决了高并发场景下的性能与数据一致性问题。后续优化可聚焦三个方向:
性能优化:引入 Redis Cluster 提升缓存容量与并发能力;采用协程池优化异步任务处理效率。
安全增强:集成验证码、短信验证等多因素认证,进一步提升防刷能力;对敏感接口进行加密传输。
可观测性:引入分布式追踪(如 Jaeger),实现全链路调用追踪,快速定位跨服务问题。
如果你对这种技术问题有疑问,或者对这个微服务项目感兴趣,都可以直接私信我:wangzhongyang1993。
版权声明: 本文为 InfoQ 作者【王中阳Go】的原创文章。
原文链接:【http://xie.infoq.cn/article/293648e447b6d197608bdcadf】。文章转载请联系作者。
王中阳Go
靠敲代码在北京买房的程序员 2022-10-09 加入
【微信】wangzhongyang1993【公众号】程序员升职加薪之旅【成就】InfoQ专家博主👍掘金签约作者👍B站&掘金&CSDN&思否等全平台账号:王中阳Go







评论