前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Redis优化高并发下的秒杀性能

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

作者头像
Lemon黄
发布2019-11-01 16:08:30
1.6K0
发布2019-11-01 16:08:30
举报
文章被收录于专栏:Lemon黄Lemon黄

作者:xialeistudio 来源:developer.51cto.com

本文内容

  • 使用Redis优化高并发场景下的接口性能
  • 数据库乐观锁
随着双11的临近,各种促销活动开始变得热门起来,比较主流的有秒杀、抢优惠券、拼团等等。

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

前提

  1. 活动规则
  • 奖品数量有限,比如100个
  • 不限制参与用户数
  • 每个用户只能参与1次秒杀
  1. 活动要求
  • 不能多发,也不能少发,100个奖品要全部发出去
  • 1个用户最多抢1个奖品
  • 遵循先到先得原则,先来的用户有奖品

数据库实现

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

1. 数据库结构
2. 乐观锁实现

乐观锁实际上并不存在真正的锁,乐观锁是利用数据的某个字段来做的,比如本文的例子就是以UserId来实现的。

实现流程如下:

  1. 查询UserId为0的奖品,如果未找到则提示无奖品
代码语言:javascript
复制
SELECT * FROM envelope WHERE user_id=0 LIMIT 1
  1. 更新奖品的用户ID和中奖时间(假设奖品ID为1,中奖用户ID为100,当前时间为2019-10-29 12:00:00),这里的user_id=0就是我们的乐观锁了。
代码语言:javascript
复制
UPDATE envelope SET user_id=100, reward_at='2019-10-29 12:00:00' WHERE user_id=0 AND id=1 
  1. 检测UPDATE语句的执行返回值,如果返回1证明中奖成功,否则证明该奖品被其他人抢了
3. 为什么要添加乐观锁

正常情况下获取奖品、然后把奖品更新给指定用户是没问题的。如果不添加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中奖,但他的奖品被抢了,此时活动方只能赔钱了
4. 添加乐观锁之后的抽奖流程

更新用户1时的条件为id=红包ID AND user_id=0 ,由于此时红包未分配给任何人,用户1更新成功,接口返回用户1中奖 当更新用户2时更新条件为id=红包ID AND user_id=0,由于此时该红包已经分配给用户1了,所以该条件不会更新任何记录,接口返回用户2中奖

5.乐观锁优缺点

优点

  • 性能尚可,因为无锁
  • 不会超发

缺点

  • 通常不满足“先到先得”的活动规则,一旦发生并发,就会发生未中奖的情况,此时奖品库还有奖品
5. 压测

在MacBook Pro 2018上的压测表现如下(Golang实现的HTTP服务器,MySQL连接池大小100,Jmeter压测):

  • 500并发 500总请求数 平均响应时间331ms 发放成功数为31 吞吐量458.7/s

Redis实现

可以看到乐观锁的实现下争抢比太高,不是推荐的实现方法,下面通过Redis来优化这个秒杀业务。

1.Redis高性能的原因
  • 单线程 省去了线程切换开销
  • 基于内存的操作 虽然持久化操作涉及到硬盘访问,但是那是异步的,不会影响Redis的业务
  • 使用了IO多路复用
2.实现流程
  1. 活动开始前将数据库中奖品的code写入Redis队列中
  2. 活动进行时使用lpop弹出队列中的元素
  3. 如果获取成功,则使用UPDATE语法发放奖品
代码语言:javascript
复制
UPDATE reward SET user_id=用户ID,reward_at=当前时间 WHERE code='奖品码' 
  1. 如果获取失败,则当前无可用奖品,提示未中奖即可

使用Redis的情况下并发访问是通过Redis的lpop()来保证的,该方法是原子方法,可以保证并发情况下也是一个个弹出的。

3. 压测

在MacBook Pro 2018上的压测表现如下(Golang实现的HTTP服务器,MySQL连接池大小100,Redis连接池代销100,Jmeter压测):

  • 500并发 500总请求数 平均响应时间48ms 发放成功数100 吞吐量497.0/s

结论

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

实验代码

代码语言:javascript
复制
// main.go
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))
}
本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2019-10-30,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 Lemon黄 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 前提
  • 数据库实现
    • 1. 数据库结构
      • 2. 乐观锁实现
        • 3. 为什么要添加乐观锁
          • 4. 添加乐观锁之后的抽奖流程
            • 5.乐观锁优缺点
              • 5. 压测
              • Redis实现
                • 1.Redis高性能的原因
                  • 2.实现流程
                    • 3. 压测
                    • 结论
                    • 实验代码
                    相关产品与服务
                    云数据库 Redis
                    腾讯云数据库 Redis(TencentDB for Redis)是腾讯云打造的兼容 Redis 协议的缓存和存储服务。丰富的数据结构能帮助您完成不同类型的业务场景开发。支持主从热备,提供自动容灾切换、数据备份、故障迁移、实例监控、在线扩容、数据回档等全套的数据库服务。
                    领券
                    问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档