好运转系统开发拼团高并发技术Redis缓存解决案例
在开发运转拼团系统中,大量会员设置自动开团,短时间会有大量的用户请求拼团下单峰值,我们要求服务器对用户请求的响应要求在100ms里返回结果,结局方案通过通过高效缓存服务来加快数据交互的的访问效率,缓存通过Redis来实现高并发的解决方案,Redis优化高并发开发交流:李铁牛15889726201,欢迎大家交流
以下是为好运转系统开发拼团并发技术解决方案Redis优化高并发场景解决方案
使用Redis优化高并发场景下的接口性能
数据库乐观锁
前提
活动规则
产品数量有限,比如100个
不限制参与用户数
每个用户只能参与1次秒杀
活动要求
不能多发,也不能少发
100个用户最多抢1个奖品
遵循先到先得原则,先来的用户有奖品
数据库实现
悲观锁性能太差,本文不予讨论,讨论一下使用乐观锁解决高并发问题的优缺点
数据库结构
未中奖时UserId为0,RewardAt为NULL
中奖时UserId为中奖用户ID,RewardAt为中奖时间
乐观锁实现
乐观锁实际上并不存在真正的锁,乐观锁是利用数据的某个字段来做的,比如本文的例子就是以UserId来实现的。
实现流程如下:
1.查询UserId为0的奖品,如果未找到则提示无奖品
1 | SELECT * FROM envelope WHERE user_id=0 LIMIT 1 |
2.更新奖品的用户ID和中奖时间(假设奖品ID为1,中奖用户ID为100,当前时间为2019-10-29 12:00:00),这里的user_id=0就是我们的乐观锁了。
1 | UPDATE envelope SET user_id=100, reward_at= '2019-10-29 12:00:00' WHERE user_id=0 AND id=1 |
3.检测UPDATE语句的执行返回值,如果返回1证明中奖成功,否则证明该奖品被其他人抢了
为什么要添加乐观锁
正常情况下获取奖品、然后把奖品更新给指定用户是没问题的。如果不添加user_id=0时,高并发场景下会出现下面的问题:
两个用户同时查询到了1个未中奖的奖品(发生并发问题)
将奖品的中奖用户更新为用户1,更新条件只有ID=奖品ID
上述SQL执行是成功的,影响行数也是1,此时接口会返回用户1中奖
接下来将中奖用户更新为用户2,更新条件也只有ID=奖品ID
由于是同一个奖品,已经发给用户1的奖品会重新发放给用户2,此时影响行数为1,接口返回用户2也中奖
所以该奖品的最终结果是发放给用户2
用户1就会过来投诉活动方了,因为抽奖接口返回用户1中奖,但他的奖品被抢了,此时活动方只能赔钱了
添加乐观锁之后的抽奖流程
更新用户1时的条件为id=红包ID AND user_id=0 ,由于此时红包未分配给任何人,用户1更新成功,接口返回用户1中奖
当更新用户2时更新条件为id=红包ID AND user_id=0,由于此时该红包已经分配给用户1了,所以该条件不会更新任何记录,接口返回用户2中奖
乐观锁优缺点
优点
性能尚可,因为无锁
不会超发
缺点
通常不满足“先到先得”的活动规则,一旦发生并发,就会发生未中奖的情况,此时奖品库还有奖品
压测
在MacBook Pro 2018上的压测表现如下(Golang实现的HTTP服务器,MySQL连接池大小100,Jmeter压测):
500并发 500总请求数 平均响应时间331ms 发放成功数为31 吞吐量458.7/s
Redis实现
可以看到乐观锁的实现下争抢比太高,不是推荐的实现方法,下面通过Redis来优化这个秒杀业务。
Redis高性能的原因
单线程 省去了线程切换开销
基于内存的操作 虽然持久化操作涉及到硬盘访问,但是那是异步的,不会影响Redis的业务
使用了IO多路复用
实现流程
1.活动开始前将数据库中奖品的code写入Redis队列中
2.活动进行时使用lpop弹出队列中的元素
3.如果获取成功,则使用UPDATE语法发放奖品
1 | UPDATE reward SET user_id=用户ID,reward_at=当前时间 WHERE code= '奖品码' |
4.如果获取失败,则当前无可用奖品,提示未中奖即可
使用Redis的情况下并发访问是通过Redis的lpop()来保证的,该方法是原子方法,可以保证并发情况下也是一个个弹出的。
压测
在MacBook Pro 2018上的压测表现如下(Golang实现的HTTP服务器,MySQL连接池大小100,Redis连接池代销100,Jmeter压测):
500并发 500总请求数 平均响应时间48ms 发放成功数100 吞吐量497.0/s
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 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 | // main.go //程序开发 李铁牛 15889726201 package main import ( "fmt" "github.com/go-redis/redis" _ "github.com/go-sql-driver/mysql" "github.com/jinzhu/gorm" "log" "net/http" "strconv" "time" ) type Envelope struct { Id int `gorm: "primary_key" ` Code string UserId int CreatedAt time.Time RewardAt *time.Time } func (Envelope) TableName() string { return "envelope" } func (p *Envelope) BeforeCreate() error { p.CreatedAt = time.Now() return nil } const ( QueueEnvelope = "envelope" QueueUser = "user" ) var ( db *gorm.DB redisClient *redis.Client ) func init() { var err error db, err = gorm.Open( "mysql" , "root:root@tcp(localhost:3306)/test?charset=utf8&parseTime=True&loc=Local" ) if err != nil { log.Fatal(err) } if err = db.DB().Ping(); err != nil { log.Fatal(err) } db.DB().SetMaxOpenConns(100) fmt.Println( "database connected. pool size 10" ) } func init() { redisClient = redis.NewClient(&redis.Options{ Addr: "localhost:6379" , DB: 0, PoolSize: 100, }) if _, err := redisClient.Ping().Result(); err != nil { log.Fatal(err) } fmt.Println( "redis connected. pool size 100" ) } // 读取Code写入Queue func init() { envelopes := make([]Envelope, 0, 100) if err := db.Debug().Where( "user_id=0" ).Limit(100).Find(&envelopes).Error; err != nil { log.Fatal(err) } if len(envelopes) != 100 { log.Fatal( "不足100个奖品" ) } for i := range envelopes { if err := redisClient.LPush(QueueEnvelope, envelopes[i].Code).Err(); err != nil { log.Fatal(err) } } fmt.Println( "load 100 envelopes" ) } func main() { http.HandleFunc( "/envelope" , func (w http.ResponseWriter, r *http.Request) { uid := r.Header.Get( "x-user-id" ) if uid == "" { w.WriteHeader(401) _, _ = fmt.Fprint(w, "UnAuthorized" ) return } uidValue, err := strconv.Atoi(uid) if err != nil { w.WriteHeader(400) _, _ = fmt.Fprint(w, "Bad Request" ) return } // 检测用户是否抢过了 if result, err := redisClient.HIncrBy(QueueUser, uid, 1).Result(); err != nil || result != 1 { w.WriteHeader(429) _, _ = fmt.Fprint(w, "Too Many Request" ) return } // 检测是否在队列中 code, err := redisClient.LPop(QueueEnvelope).Result() if err != nil { w.WriteHeader(200) _, _ = fmt.Fprint(w, "No Envelope" ) return } // 发放红包 envelope := &Envelope{} err = db.Where( "code=?" , code).Take(&envelope).Error if err == gorm.ErrRecordNotFound { w.WriteHeader(200) _, _ = fmt.Fprint(w, "No Envelope" ) return } if err != nil { w.WriteHeader(500) _, _ = fmt.Fprint(w, err) return } now := time.Now() envelope.UserId = uidValue envelope.RewardAt = &now rowsAffected := db.Where( "user_id=0" ).Save(&envelope).RowsAffected // 添加user_id=0来验证Redis是否真的解决争抢问题 if rowsAffected == 0 { fmt.Printf( "发生争抢. id=%d\n" , envelope.Id) w.WriteHeader(500) _, _ = fmt.Fprintf(w, "发生争抢. id=%d\n" , envelope.Id) return } _, _ = fmt.Fprint(w, envelope.Code) }) fmt.Println( "listen on 8080" ) fmt.Println(http.ListenAndServe( ":8080" , nil)) } |
结论
可以看到Redis的表现是稳定的,不会出现超发,且访问延迟少了8倍左右,吞吐量还没达到瓶颈,可以看出Redis对于高并发系统的性能提升是非常大的!接入成本也不算高,值得学习!
以上是为好运转系统开发拼团过程中的并发技术解决方案Redis优化高并发场景解决方案,Redis优化高并发开发交流:李铁牛 15889726201,欢迎大家交流
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· SQL Server 2025 AI相关能力初探
· Linux系列:如何用 C#调用 C方法造成内存泄露
· AI与.NET技术实操系列(二):开始使用ML.NET
· 记一次.NET内存居高不下排查解决与启示
· 探究高空视频全景AR技术的实现原理
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南