设为首页 - 加入收藏 伊春站长网 (http://www.0458zz.com)- 国内知名站长资讯网站,提供最新最全的站长资讯,创业经验,网站建设等!
热搜: 2018 腾讯 模式 实现
当前位置: 首页 > 运营中心 > 建站资源 > 优化 > 正文

Redis优化高并发下的秒杀性能

发布时间:2019-11-04 22:36 所属栏目:[优化] 来源:xialeistudio
导读:本文内容 使用Redis优化高并发场景下的接口性能 数据库乐观锁 随着双11的临近,各种促销活动开始变得热门起来,比较主流的有秒杀、抢优惠券、拼团等等。 涉及到高并发争抢同一个资源的主要场景有秒杀和抢优惠券。 前提 活动规则 奖品数量有限,比如100个

?本文内容

  • 使用Redis优化高并发场景下的接口性能
  • 数据库乐观锁

Redis优化高并发下的秒杀性能

随着双11的临近,各种促销活动开始变得热门起来,比较主流的有秒杀、抢优惠券、拼团等等。

涉及到高并发争抢同一个资源的主要场景有秒杀和抢优惠券。

前提

活动规则

  • 奖品数量有限,比如100个
  • 不限制参与用户数
  • 每个用户只能参与1次秒杀

活动要求

  • 不能多发,也不能少发,100个奖品要全部发出去
  • 1个用户最多抢1个奖品
  • 遵循先到先得原则,先来的用户有奖品

数据库实现

悲观锁性能太差,本文不予讨论,讨论一下使用乐观锁解决高并发问题的优缺点。

数据库结构

Redis优化高并发下的秒杀性能

  • 未中奖时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个未中奖的奖品(发生并发问题)
  2. 将奖品的中奖用户更新为用户1,更新条件只有ID=奖品ID
  3. 上述SQL执行是成功的,影响行数也是1,此时接口会返回用户1中奖
  4. 接下来将中奖用户更新为用户2,更新条件也只有ID=奖品ID
  5. 由于是同一个奖品,已经发给用户1的奖品会重新发放给用户2,此时影响行数为1,接口返回用户2也中奖
  6. 所以该奖品的最终结果是发放给用户2
  7. 用户1就会过来投诉活动方了,因为抽奖接口返回用户1中奖,但他的奖品被抢了,此时活动方只能赔钱了

添加乐观锁之后的抽奖流程

  1. 更新用户1时的条件为id=红包ID AND user_id=0 ,由于此时红包未分配给任何人,用户1更新成功,接口返回用户1中奖
  2. 当更新用户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

结论

可以看到Redis的表现是稳定的,不会出现超发,且访问延迟少了8倍左右,吞吐量还没达到瓶颈,可以看出Redis对于高并发系统的性能提升是非常大的!接入成本也不算高,值得学习!

实验代码

  1. //?main.go?
  2. package?main?
  3. ?
  4. import?(?
  5. ????"fmt"?
  6. ????"github.com/go-redis/redis"?
  7. ????_?"github.com/go-sql-driver/mysql"?
  8. ????"github.com/jinzhu/gorm"?
  9. ????"log"?
  10. ????"net/http"?
  11. ????"strconv"?
  12. ????"time"?
  13. )?
  14. ?
  15. type?Envelope?struct?{?
  16. ????Id????????int?`gorm:"primary_key"`?
  17. ????Code??????string?
  18. ????UserId????int?
  19. ????CreatedAt?time.Time?
  20. ????RewardAt??*time.Time?
  21. }?
  22. ?
  23. func?(Envelope)?TableName()?string?{?
  24. ????return?"envelope"?
  25. }?
  26. ?
  27. func?(p?*Envelope)?BeforeCreate()?error?{?
  28. ????p.CreatedAt?=?time.Now()?
  29. ????return?nil?
  30. }?
  31. ?
  32. const?(?
  33. ????QueueEnvelope?=?"envelope"?
  34. ????QueueUser?????=?"user"?
  35. )?
  36. ?
  37. var?(?
  38. ????db??????????*gorm.DB?
  39. ????redisClient?*redis.Client?
  40. )?
  41. ?
  42. func?init()?{?
  43. ????var?err?error?
  44. ????db,?err?=?gorm.Open("mysql",?"root:root@tcp(localhost:3306)/test?charset=utf8&parseTime=True&loc=Local")?
  45. ????if?err?!=?nil?{?
  46. ????????log.Fatal(err)?
  47. ????}?
  48. ????if?err?=?db.DB().Ping();?err?!=?nil?{?
  49. ????????log.Fatal(err)?
  50. ????}?
  51. ????db.DB().SetMaxOpenConns(100)?
  52. ????fmt.Println("database?connected.?pool?size?10")?
  53. }?
  54. ?
  55. func?init()?{?
  56. ????redisClient?=?redis.NewClient(&redis.Options{?
  57. ????????Addr:?????"localhost:6379",?
  58. ????????DB:???????0,?
  59. ????????PoolSize:?100,?
  60. ????})?
  61. ????if?_,?err?:=?redisClient.Ping().Result();?err?!=?nil?{?
  62. ????????log.Fatal(err)?
  63. ????}?
  64. ????fmt.Println("redis?connected.?pool?size?100")?
  65. }?
  66. ?
  67. //?读取Code写入Queue?
  68. func?init()?{?
  69. ????envelopes?:=?make([]Envelope,?0,?100)?
  70. ????if?err?:=?db.Debug().Where("user_id=0").Limit(100).Find(&envelopes).Error;?err?!=?nil?{?
  71. ????????log.Fatal(err)?
  72. ????}?
  73. ????if?len(envelopes)?!=?100?{?
  74. ????????log.Fatal("不足100个奖品")?
  75. ????}?
  76. ????for?i?:=?range?envelopes?{?
  77. ????????if?err?:=?redisClient.LPush(QueueEnvelope,?envelopes[i].Code).Err();?err?!=?nil?{?
  78. ????????????log.Fatal(err)?
  79. ????????}?
  80. ????}?
  81. ????fmt.Println("load?100?envelopes")?
  82. }?
  83. ?
  84. func?main()?{?
  85. ????http.HandleFunc("/envelope",?func(w?http.ResponseWriter,?r?*http.Request)?{?
  86. ????????uid?:=?r.Header.Get("x-user-id")?
  87. ????????if?uid?==?""?{?
  88. ????????????w.WriteHeader(401)?
  89. ????????????_,?_?=?fmt.Fprint(w,?"UnAuthorized")?
  90. ????????????return?
  91. ????????}?
  92. ????????uidValue,?err?:=?strconv.Atoi(uid)?
  93. ????????if?err?!=?nil?{?
  94. ????????????w.WriteHeader(400)?
  95. ????????????_,?_?=?fmt.Fprint(w,?"Bad?Request")?
  96. ????????????return?
  97. ????????}?
  98. ????????//?检测用户是否抢过了?
  99. ????????if?result,?err?:=?redisClient.HIncrBy(QueueUser,?uid,?1).Result();?err?!=?nil?||?result?!=?1?{?
  100. ????????????w.WriteHeader(429)?
  101. ????????????_,?_?=?fmt.Fprint(w,?"Too?Many?Request")?
  102. ????????????return?
  103. ????????}?
  104. ????????//?检测是否在队列中?
  105. ????????code,?err?:=?redisClient.LPop(QueueEnvelope).Result()?
  106. ????????if?err?!=?nil?{?
  107. ????????????w.WriteHeader(200)?
  108. ????????????_,?_?=?fmt.Fprint(w,?"No?Envelope")?
  109. ????????????return?
  110. ????????}?
  111. ????????//?发放红包?
  112. ????????envelope?:=?&Envelope{}?
  113. ????????err?=?db.Where("code=?",?code).Take(&envelope).Error?
  114. ????????if?err?==?gorm.ErrRecordNotFound?{?
  115. ????????????w.WriteHeader(200)?
  116. ????????????_,?_?=?fmt.Fprint(w,?"No?Envelope")?
  117. ????????????return?
  118. ????????}?
  119. ????????if?err?!=?nil?{?
  120. ????????????w.WriteHeader(500)?
  121. ????????????_,?_?=?fmt.Fprint(w,?err)?
  122. ????????????return?
  123. ????????}?
  124. ????????now?:=?time.Now()?
  125. ????????envelope.UserId?=?uidValue?
  126. ????????envelope.RewardAt?=?&now?
  127. ????????rowsAffected?:=?db.Where("user_id=0").Save(&envelope).RowsAffected?//?添加user_id=0来验证Redis是否真的解决争抢问题?
  128. ????????if?rowsAffected?==?0?{?
  129. ????????????fmt.Printf("发生争抢.?id=%d\n",?envelope.Id)?
  130. ????????????w.WriteHeader(500)?
  131. ????????????_,?_?=?fmt.Fprintf(w,?"发生争抢.?id=%d\n",?envelope.Id)?
  132. ????????????return?
  133. ????????}?
  134. ????????_,?_?=?fmt.Fprint(w,?envelope.Code)?
  135. ????})?
  136. ?
  137. ????fmt.Println("listen?on?8080")?
  138. ????fmt.Println(http.ListenAndServe(":8080",?nil))?
  139. }?

【免责声明】本站内容转载自互联网,其相关言论仅代表作者个人观点绝非权威,不代表本站立场。如您发现内容存在版权问题,请提交相关链接至邮箱:bqsm@foxmail.com,我们将及时予以处理。

网友评论
推荐文章