Содержание этой статьи
- Используйте Redis для оптимизации производительности интерфейса при высоких сценариях параллелизма
- Оптимистическая блокировка базы данных
С приближением Double 11 стали популярными различные рекламные мероприятия, и наиболее распространенные из них включают шипы, захват купонов, групповые бои и так далее.
Основные сценарии, связанные с конкуренцией с высокой степенью параллелизма за один и тот же ресурс, включают всплески и захват купонов.
помещение
правило активности
- Количество призов ограничено, например 100
- Неограниченное количество участвующих пользователей
- Каждый пользователь может участвовать только в 1 seckill
Требования к мероприятию
- Не больше и не меньше, все 100 призов должны быть разосланы
- 1 пользователь может получить до 1 приза
- Следуйте принципу «первым пришел — первым обслужен», пользователи, которые первыми получили призы
Реализация базы данных
Производительность пессимистической блокировки слишком низкая, и в этой статье она не будет обсуждаться, а будут обсуждаться преимущества и недостатки использования оптимистической блокировки для решения проблем с высокой степенью параллелизма.
Структура базы данных
ID | Code | UserId | CreatedAt | RewardAt |
---|---|---|---|---|
Идентификатор приза | Призовой код | ID пользователя | время создания | выигрышное время |
- UserId равен 0, когда не выигрывает, RewardAt равен NULL
- Когда выигрыш — это выигрыш, UserId Идентификатор пользователя, RewardAt — это время выигрыша.
реализация оптимистичной блокировки
Оптимистическая блокировка на самом деле не имеет реальной блокировки. Оптимистическая блокировка выполняется с использованием определенного поля данных. Например, пример в этой статье реализуется UserId.
Процесс реализации выглядит следующим образом:
-
Запросите приз с идентификатором пользователя 0, если он не найден, он не предложит приз
SELECT * FROM envelope WHERE user_id=0 LIMIT 1
-
Обновите идентификатор пользователя и время выигрыша приза (при условии, что идентификатор приза равен 1, идентификатор пользователя-победителя равен 100, а текущее время — 2019-10-29 12:00:00), где user_id=0 — наша оптимистичная блокировка. .
UPDATE envelope SET user_id=100, reward_at='2019-10-29 12:00:00' WHERE user_id=0 AND id=1
-
Проверьте значение, возвращаемое выполнением оператора 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 следующие (HTTP-сервер, реализованный Golang, размер пула соединений MySQL 100, стресс-тест Jmeter):
- 500 одновременных 500 Всего запросов Среднее время отклика 331МС Количество успешных выводов 31 Пропускная способность 458,7 / с
Реализация Redis
Видно, что коэффициент конкуренции слишком высок при реализации оптимистической блокировки, что не является рекомендуемым методом реализации.Давайте оптимизируем этот пиковый бизнес с помощью Redis.
Причина высокой производительности Redis
- Один поток экономит накладные расходы на переключение потоков
- Операции с памятью Хотя постоянные операции требуют доступа к жесткому диску, они асинхронны и не влияют на работу Redis.
- используется мультиплексирование ввода-вывода
Процесс реализации
-
Напишите код приза в базе данных в очередь Redis до начала события
-
Используйте lpop для извлечения элементов из очереди во время выполнения действия.
-
Если приобретение прошло успешно, используйте синтаксис UPDATE для выдачи приза.
UPDATE reward SET user_id=用户ID,reward_at=当前时间 WHERE code='奖品码'
-
Если получение не удалось, в настоящее время нет доступных призов, и будет указано, что вы не выиграли приз.
В случае использования Redis параллельный доступ осуществляется через Redis.lpop()
Чтобы гарантировать, что этот метод является атомарным методом, который может гарантировать, что он также будет извлекаться один за другим в случае параллелизма.
стресс тест
Производительность стресс-теста на MacBook Pro 2018 выглядит следующим образом (HTTP-сервер, реализованный Golang, размер пула соединений MySQL 100, партия пула соединений Redis 100, стресс-тест Jmeter):
- 500 одновременных запросов Всего 500 запросов Среднее время отклика 48 мс Количество успешных распределений 100 Пропускная способность 497,0/с
В заключение
Видно, что производительность Redis стабильна, перерегулирования не будет, а задержка доступа уменьшится примерно в 8 раз, а пропускная способность еще не достигла узкого места.Видно, что улучшение производительности Redis для системы с высоким параллелизмом очень велики! Стоимость доступа не высока, этому стоит научиться!
экспериментальный код
// 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))
}