当前位置: 首页 > 科技观察

Redis高并发下秒杀性能优化

时间:2023-03-14 00:53:59 科技观察

本文使用Redis优化高并发场景下的接口性能数据库乐观锁随着双11的临近,各种促销活动火了起来,比较主流的是秒杀和抢单优惠券、团体交易等。同资源高并发竞争场景主要有闪购、抢券等。活动规则的前提是奖品数量有限。比如有100个无限参与用户,每个用户只能参与一次限时抢购。该事件不需要更多或更少的分发。100份奖品必须全部寄出。1个用户最多可领取1个奖品。先到先得的原则,先到先得的用户有奖品。数据库中悲观锁的性能太差了。本文将不讨论它们。我们将讨论使用乐观锁解决高并发问题的优缺点。当数据库结构不中奖时,UserId为0,RewardAt为NULL。中奖时,UserId为中奖用户ID,RewardAt为中奖时间。乐观锁实现了乐观锁。事实上,并没有真正的锁。乐观锁是通过使用数据的某个字段来完成的,比如本文的例子就是用UserId来实现的。实现过程如下:1、查询UserId为0的奖品,如果没有找到,提示没有奖品SELECT*FROMenvelopeWHEREuser_id=0LIMIT12。更新奖品的用户ID和中奖时间(假设奖品ID为1,中奖用户ID为100,当前时间为2019-10-2912:00:00),其中user_id=0是我们看好的锁。UPDATEenvelopeSETuser_id=100,reward_at='2019-10-2912:00:00'WHEREuser_id=0ANDid=13。查看UPDATE语句的返回值,如果返回1,证明中奖成功,否则证明奖品被别人抢走了。为什么要添加一般情况下,乐观锁获取奖品,然后更新奖品给指定用户是没有问题的。如果不加user_id=0,在高并发场景下会出现如下问题:两个用户同时查询一个未中奖的奖品(出现并发问题)将奖品的中奖用户更新为用户1,更新条件只有ID=奖品ID上面SQL执行成功,受影响的行数也为1,此时接口会返回用户1中奖,然后更新中奖用户为用户2,更新条件只有ID=奖品编号。由于是同一个奖品,已经发给用户1的奖品会重新分配给用户2,此时受影响的行数为1,接口返回用户2也中奖,所以奖品的最终结果是分配给用户2,用户1会来向活动方投诉,因为抽奖界面返回用户1中奖了,但是他的奖品被抢了。这时候,活动方只能亏本了。加乐观锁后的抽奖流程更新用户1时的条件是id=红包IDANDuser_id=0,因为红包没有分配给Anyone,用户1更新成功,接口返回用户1中奖.更新用户2时,更新条件为id=redpacketIDANDuser_id=0。由于此时红包已经分配给用户1,该条件不会更新任何记录,接口返回给用户2中奖优劣势。MacBookPro2018压力测试性能如下(Golang实现的HTTP服务器,MySQL连接池大小100,Jmeter压力测试):500个并发500个总请求,平均响应时间331ms,分发成功数31Throughput458.7/sRedis实现可以看到在乐观锁实现下竞争率过高,不是推荐的实现方式。接下来使用Redis优化秒杀业务。Redis之所以高性能,是因为单线程消除了线程切换的开销。基于内存的操作。持久化操作虽然涉及到硬盘访问,但是是异步的,不会影响Redis的业务。使用IO多路复用实现流程1.活动开始前将数据库中奖品的代码写入Redis队列2.活动进行时使用lpop弹出队列中的元素3.如果获取成功,使用UPDATE语法发放奖品UPDATErewardSETuser_id=userID,reward_at=currenttimeWHEREcode='prizeCode'4.如果获取失败,则当前没有奖品可用。如果没有中奖提示使用Redis,则并发访问由Redis的lpop()保证。弹出窗口。在MacBookPro2018压测性能如下(Golang实现的HTTP服务器,MySQL连接池大小100,Redis连接池寄售100,Jmeter压测):500并发500总请求,平均响应时间48ms,发布成功次数100吞吐量为497.0/s结论。可以看出Redis的性能稳定,不会出现oversend,访问时延少了8倍左右,吞吐量还没有达到瓶颈。可见Redis对于高并发系统的性能提升是非常大的!接入成本不算高,值得学习!实验代码//main.gopackagemainimport("fmt""github.com/go-redis/redis"_"github.com/go-sql-driver/mysql""github.com/jinzhu/gorm""log""net/http""strconv""time")typeEnvelopestruct{Idint`gorm:"primary_key"`CodestringUserIdintCreatedAttime.TimeRewardAt*time.Time}func(Envelope)TableName()string{return"envelope"}func(p*Envelope)BeforeCreate()error{p.CreatedAt=time.Now()returnnil}const(QueueEnvelope="envelope"QueueUser="user")var(db*gorm.DBredisClient*redis.Client)funcinit(){varerrrrdb,err=gorm.Open("mysql","root:root@tcp(localhost:3306)/test?charset=utf8&parseTime=True&loc=Local")iferr!=nil{log.Fatal(err)}iferr=db.DB().Ping();err!=nil{log.Fatal(err)}db.DB().SetMaxOpenConns(100)fmt.Println("databaseconnected.poolsize10")}funcinit(){redisClient=redis.NewClient(&redis.Options{Addr:"localhost:6379",DB:0,PoolSize:100,})if_,err:=redisClient.Ping().Result();err!=nil{log.Fatal(err)}fmt.Println("redisconnected.poolsize100")}//读取代码写入Queuefuncinit(){envelopes:=make([]Envelope,0,100)iferr:=db.Debug().Where("user_id=0").Limit(100).Find(&envelopes).Error;err!=nil{log.Fatal(err)}iflen(envelopes)!=100{log.Fatal("不到100个奖品")}fori:=rangeenvelopes{iferr:=redisClient.LPush(QueueEnvelope,envelopes[i].Code).Err();err!=nil{log.Fatal(err)}}fmt.Println("load100envelopes")}funcmain(){http.HandleFunc("/envelope",func(whttp.ResponseWriter,r*http.Request){uid:=r.Header.Get("x-user-id")ifuid==""{w.WriteHeader(401)_,_=fmt.Fprint(w,"UnAuthorized")return}uidValue,err:=strconv.Atoi(uid)iferr!=nil{w.WriteHeader(400)_,_=fmt.Fprint(w,"BadRequest")return}//检查用户是否抓取了ifresult,err:=redisClient.HIncrBy(QueueUser,uid,1).Result();err!=nil||result!=1{w.WriteHeader(429)_,_=fmt.Fprint(w,"TooManyRequest")返回}//检查是否在队列代码中,err:=redisClient.LPop(QueueEnvelope).Result()iferr!=nil{w.WriteHeader(200)_,_=fmt.Fprint(w,"NoEnvelope")return}//发红包envelope:=&Envelope{}err=db.Where("code=?",code).Take(&envelope).Erroriferr==gorm.ErrRecordNotFound{w.WriteHeader(200)_,_=fmt.Fprint(w,"NoEnvelope")return}iferr!=nil{w.WriteHeader(500)_,_=fmt.Fprint(w,err)return}now:=time.Now()envelope.UserId=uidValueenvelope.RewardAt=&nowrowsAffected:=db.Where("user_id=0").Save(&envelope).RowsAffected//添加user_id=0验证Redis是否真的解决了乱码问题ifrowsAffected==0{fmt.Printf("ascramble.id=%d\n",envelope.Id)w.WriteHeader(500)_,_=fmt.Fprintf(w,"Contentationoccurred.id=%d\n",envelope.Id)re转动n}_,_=fmt.Fprint(w,envelope.Code)})fmt.Println("listenon8080")fmt.Println(http.ListenAndServe(":8080",nil))}