Redis оптимизирует пиковую производительность при высокой степени параллелизма

Redis

Содержание этой статьи

  • Используйте Redis для оптимизации производительности интерфейса при высоких сценариях параллелизма
  • Оптимистическая блокировка базы данных

С приближением Double 11 стали популярными различные рекламные мероприятия, и наиболее распространенные из них включают шипы, захват купонов, групповые бои и так далее.

Основные сценарии, связанные с конкуренцией с высокой степенью параллелизма за один и тот же ресурс, включают всплески и захват купонов.

помещение

правило активности

  • Количество призов ограничено, например 100
  • Неограниченное количество участвующих пользователей
  • Каждый пользователь может участвовать только в 1 seckill

Требования к мероприятию

  • Не больше и не меньше, все 100 призов должны быть разосланы
  • 1 пользователь может получить до 1 приза
  • Следуйте принципу «первым пришел — первым обслужен», пользователи, которые первыми получили призы

Реализация базы данных

Производительность пессимистической блокировки слишком низкая, и в этой статье она не будет обсуждаться, а будут обсуждаться преимущества и недостатки использования оптимистической блокировки для решения проблем с высокой степенью параллелизма.

Структура базы данных

ID Code UserId CreatedAt RewardAt
Идентификатор приза Призовой код ID пользователя время создания выигрышное время
  • UserId равен 0, когда не выигрывает, RewardAt равен NULL
  • Когда выигрыш — это выигрыш, UserId Идентификатор пользователя, RewardAt — это время выигрыша.

реализация оптимистичной блокировки

Оптимистическая блокировка на самом деле не имеет реальной блокировки. Оптимистическая блокировка выполняется с использованием определенного поля данных. Например, пример в этой статье реализуется UserId.

Процесс реализации выглядит следующим образом:

  1. Запросите приз с идентификатором пользователя 0, если он не найден, он не предложит приз

    SELECT * FROM envelope WHERE user_id=0 LIMIT 1
    
  2. Обновите идентификатор пользователя и время выигрыша приза (при условии, что идентификатор приза равен 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
    
  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 следующие (HTTP-сервер, реализованный Golang, размер пула соединений MySQL 100, стресс-тест Jmeter):

  • 500 одновременных 500 Всего запросов Среднее время отклика 331МС Количество успешных выводов 31 Пропускная способность 458,7 / с

Реализация Redis

Видно, что коэффициент конкуренции слишком высок при реализации оптимистической блокировки, что не является рекомендуемым методом реализации.Давайте оптимизируем этот пиковый бизнес с помощью Redis.

Причина высокой производительности Redis

  • Один поток экономит накладные расходы на переключение потоков
  • Операции с памятью Хотя постоянные операции требуют доступа к жесткому диску, они асинхронны и не влияют на работу Redis.
  • используется мультиплексирование ввода-вывода

Процесс реализации

  1. Напишите код приза в базе данных в очередь Redis до начала события

  2. Используйте lpop для извлечения элементов из очереди во время выполнения действия.

  3. Если приобретение прошло успешно, используйте синтаксис UPDATE для выдачи приза.

    UPDATE reward SET user_id=用户ID,reward_at=当前时间 WHERE code='奖品码'
    
  4. Если получение не удалось, в настоящее время нет доступных призов, и будет указано, что вы не выиграли приз.

В случае использования Redis параллельный доступ осуществляется через Redis.lpop()Чтобы гарантировать, что этот метод является атомарным методом, который может гарантировать, что он также будет извлекаться один за другим в случае параллелизма.

стресс тест

Производительность стресс-теста на MacBook Pro 2018 выглядит следующим образом (HTTP-сервер, реализованный Golang, размер пула соединений MySQL 100, партия пула соединений Redis 100, стресс-тест Jmeter):

  • 500 одновременных запросов Всего 500 запросов Среднее время отклика 48 мс Количество успешных распределений 100 Пропускная способность 497,0/с

В заключение

Видно, что производительность Redis стабильна, перерегулирования не будет, а задержка доступа уменьшится примерно в 8 раз, а пропускная способность еще не достигла узкого места.Видно, что улучшение производительности Redis для системы с высоким параллелизмом очень велики! Стоимость доступа не высока, этому стоит научиться!

0.jpeg

экспериментальный код

// 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))
}