背景
受到微信红包的设计思路的启发,单独来分享一下这个并发抢锁的问题及解决方案
抢微信红包的事务过程
- begin 事务
- select ... for update 锁住主红包记录,其他并发请求等待锁释放
- 创建子红包记录
- 更新库存
- commit 事务
主红包加悲观锁的问题
- 性能低下,无法支撑高QPS的抢红包请求。
- DB压力大,系统整体性能上不去
在这个背景下,微信红包团队使用了串行排队同一个主红包ID的抢请求,FIFO的去处理每个抢红包请求,本质是一种避免抢锁的无锁化设计,其理论基础是: 如果并发抢锁给到DB的压力大,那就避免抢锁,而FIFO的队列是一种比较好的规避手段。 下面用golang来实现这样的一个思想:
- 本代码只模拟抢同一个主红包的。
- 本地队列: 使用slice + 锁 来实现
- http 服务器: 负责创建任务,写任务到本地队列 Enqueue。
- worker协程: 从本地队列Dequeu,处理完成后写任务中的chan(size是1的chan)
代码
package main
import (
"encoding/json"
"fmt"
"log"
"net/http"
"sync"
"time"
)
// 1. 定义任务结构:包含业务参数、结果回传通道、任务ID(用于追踪)
type Task struct {
TaskID string // 任务唯一标识
BusinessParam string // 业务参数(如红包ID、金额等)
ResultChan chan TaskResult // 结果回传通道(每个任务独立通道,避免混淆)
Timeout time.Duration // 任务处理超时时间
}
// 2. 定义任务结果结构
type TaskResult struct {
TaskID string // 关联的任务ID
Success bool // 处理是否成功
Data string // 处理结果数据
ErrMsg string // 错误信息(失败时非空)
}
// 3. 并发安全的FIFO队列:基于切片+互斥锁实现
type SafeFifoQueue struct {
mu sync.Mutex // 互斥锁,保证并发安全
items []*Task // 存储任务的切片
}
// 初始化队列
func NewSafeFifoQueue() *SafeFifoQueue {
return &SafeFifoQueue{
items: make([]*Task, 0),
}
}
// 入队:添加任务到队尾(并发安全)
func (q *SafeFifoQueue) Enqueue(task *Task) {
q.mu.Lock()
defer q.mu.Unlock()
q.items = append(q.items, task)
}
// 出队:从队头取出任务(并发安全)
func (q *SafeFifoQueue) Dequeue() *Task {
q.mu.Lock()
defer q.mu.Unlock()
if len(q.items) == 0 {
return nil
}
// 取出队头任务
task := q.items[0]
// 移除队头(切片切片操作)
q.items = q.items[1:]
return task
}
// 4. 任务处理器:单协程串行处理队列中的任务
func startTaskProcessor(queue *SafeFifoQueue) {
log.Println("任务处理器启动,开始串行处理任务...")
for {
// 从队列取任务(无任务时会取到nil,短暂休眠后重试)
task := queue.Dequeue()
if task == nil {
time.Sleep(10 * time.Millisecond) // 避免空轮询消耗CPU
continue
}
log.Printf("开始处理任务[%s],参数:%s\n", task.TaskID, task.BusinessParam)
// 模拟业务处理(如扣减主红包库存、写入子红包记录)
var result TaskResult
result.TaskID = task.TaskID
// 模拟处理耗时(100~300ms)
processTime := time.Duration(100+int(time.Now().UnixNano()%200)) * time.Millisecond
time.Sleep(processTime)
// 模拟业务逻辑(成功/失败)
if task.BusinessParam == "error" {
result.Success = false
result.ErrMsg = "业务处理失败:参数错误"
} else {
result.Success = true
result.Data = fmt.Sprintf("处理完成(耗时:%v),参数:%s", processTime, task.BusinessParam)
}
// 将结果回传到任务的通道(带超时保护,避免请求协程已退出导致阻塞)
select {
case task.ResultChan <- result:
log.Printf("任务[%s]处理完成,已回传结果\n", task.TaskID)
case <-time.After(task.Timeout):
log.Printf("任务[%s]结果回传超时(请求可能已超时退出)\n", task.TaskID)
}
close(task.ResultChan) // 关闭通道,释放资源
}
}
// 5. HTTP请求处理函数:接收请求→创建任务→入队→等待结果→回包
func handleRequest(w http.ResponseWriter, r *http.Request, queue *SafeFifoQueue) {
// 解析请求参数
r.ParseForm()
param := r.Form.Get("param")
if param == "" {
http.Error(w, "参数param不能为空", http.StatusBadRequest)
return
}
// 创建任务:生成唯一ID,初始化结果通道
task := &Task{
TaskID: fmt.Sprintf("task-%d", time.Now().UnixNano()), // 用时间戳生成唯一ID
BusinessParam: param,
ResultChan: make(chan TaskResult, 1), // 带缓冲通道,避免写入阻塞
Timeout: 5 * time.Second, // 任务结果回传超时时间
}
// 任务入队
queue.Enqueue(task)
log.Printf("请求参数[%s]已入队,任务ID:%s\n", param, task.TaskID)
// 等待任务处理结果(带超时)
var result TaskResult
select {
case result = <-task.ResultChan:
// 正常获取结果
case <-time.After(task.Timeout + 100*time.Millisecond):
// 超时处理(比任务回传超时多留100ms缓冲)
result = TaskResult{
TaskID: task.TaskID,
Success: false,
ErrMsg: "请求超时(超过" + task.Timeout.String() + ")",
}
}
// 回包(JSON格式)
w.Header().Set("Content-Type", "application/json;charset=utf-8")
_ = json.NewEncoder(w).Encode(result)
}
func main() {
// 初始化并发安全队列
queue := NewSafeFifoQueue()
// 启动任务处理器(单协程,保证FIFO串行处理)
go startTaskProcessor(queue)
// 注册HTTP请求处理函数
http.HandleFunc("/process", func(w http.ResponseWriter, r *http.Request) {
handleRequest(w, r, queue)
})
// 启动HTTP服务
log.Println("服务启动,监听端口8080,示例请求:http://localhost:8080/process?param=test123")
log.Fatal(http.ListenAndServe(":8080", nil))
}
压测
压测脚本
curl http://localhost:8080/process?param=test001 &
curl http://localhost:8080/process?param=test002 &
curl http://localhost:8080/process?param=test003 &
curl http://localhost:8080/process?param=test004 &
curl http://localhost:8080/process?param=test005 &
curl http://localhost:8080/process?param=test006 &
curl http://localhost:8080/process?param=test007 &
curl http://localhost:8080/process?param=test008 &
curl http://localhost:8080/process?param=test009 &
curl http://localhost:8080/process?param=test010 &
curl http://localhost:8080/process?param=test011 &
curl http://localhost:8080/process?param=test012 &
curl http://localhost:8080/process?param=test013 &
curl http://localhost:8080/process?param=test014 &
curl http://localhost:8080/process?param=test015 &
curl http://localhost:8080/process?param=test016 &
curl http://localhost:8080/process?param=test017 &
curl http://localhost:8080/process?param=test018 &
curl http://localhost:8080/process?param=test019 &
curl http://localhost:8080/process?param=test020 &
运行截图
客户端回包
服务端日志
为啥主红包不用乐观锁?
- 用户体验差: 并发请求,只能有1个成功,其他用户抢不到红包
- 还是用户体验: 不符合FIFO,先抢的人因为并发失败抢不到,反而后来的人抢到了,体验差。
- 事务回滚: 并发失败的请求会触发DB事务回滚,DB压力大。