Преамбула
Для внешнего интерфейса сценарии «параллельности» встречаются редко.В этой статье мы поговорим о распространенных сценариях seckill и о том, какие технологии будут использоваться, когда реальное онлайн-приложение узла сталкивается с «параллелизмом».
стек технологий
Пример базы данных кода в этой статье основан на MongoDB, а кэш — на Redis.
Сценарий 1: Получение купонов
Правила: Пользователь может претендовать только на один купон.
Прежде всего, наша идея состоит в том, чтобы использовать таблицу записей для сохранения записи купона пользователя и проверки того, получил ли пользователь купон, когда пользователь получает купон.
Структура записей следующая
new Schema({
// 用户id
userId: {
type: String,
required: true,
},
});
Бизнес-процесс также прост:
Реализация MongoDB
Пример кода выглядит следующим образом:
async grantCoupon(userId: string) {
const record = await this.recordsModel.findOne({
userId,
});
if (record) {
return false;
} else {
this.grantCoupon();
this.recordModel.create({
userId,
});
}
}
Почтальон тест, вроде без проблем. Далее рассматриваем параллельные сценарии.Например "пользователь" не будет послушно нажимать на кнопку и ждать выдачи купона, а нажимать быстро, или использовать инструмент для параллельного запроса интерфейса купона.Не будет ли проблем с нашим программа? (Внешнего интерфейса проблемы параллелизма можно избежать, загрузив, но интерфейс должен быть заблокирован, чтобы предотвратить хакерские атаки)
В результате пользователь может получить несколько купонов. проблема в查询recordsа также新增领券记录, эти два шага выполняются отдельно, то есть существует момент времени: запрашивается, что пользователь А не имеет записи о получении купонов, и пользователь А снова запрашивает интерфейс после выдачи купона.В это время данные операция вставки записей в таблицу не завершена, в результате чего возникает проблема повторной выдачи.
Решение также очень простое, то есть как заставить запрос и оператор вставки выполняться вместе, исключая асинхронный процесс в середине. мангуст дает намfindOneAndUpdate, то есть найти и изменить, давайте взглянем на переписанный оператор:
async grantCoupon(userId: string) {
const record = await this.recordModel.findOneAndUpdate({
userId,
}, {
$setOnInsert: {
userId,
},
}, {
new: false,
upsert: true,
});
if (! record) {
this.grantCoupon();
}
}
По сути, это атомарная операция монго.Первый параметр — оператор запроса, запрашивающий запись userId, второй параметр $setOnInsert указывает поле, вставляемое при добавлении, а третий параметр upsert=true указывает, что если запись запроса Если он не существует, он будет создан, new=false означает возврат запрошенной записи вместо измененной записи. Затем нам нужно только решить, что запись запроса не существует, а затем выполнить логику освобождения, а оператор вставки выполняется вместе с оператором запроса. Даже если в это время будут поступать параллельные запросы, следующий запрос будет после последнего оператора вставки.
Атомный означает «частицы, которые не могут быть далее разделены». Атомарная операция означает «операцию или серию операций, которые нельзя прервать», и невозможно, чтобы две атомарные операции одновременно воздействовали на одну и ту же переменную.
Реализация Redis
Для этой логики очень подходит не только MongoDB, но и redis, реализуем его с помощью redis:
async grantCoupon(userId: string) {
const result = await this.redis.setnx(userId, 'true');
if (result === 1) {
this.grantCoupon();
}
}
Точно так же setnx — это атомарная операция redis, что означает: если у ключа нет значения, установите значение, если значение есть, оно не будет обработано и вызовет ошибку. Это просто для демонстрации параллельной обработки, также необходимо учитывать фактическую онлайн-службу:
- Значение ключа не может использоваться в конфликте с другими приложениями, такими как
应用名称+功能名称+userId - После того, как служба отключается, ключ redis необходимо очистить или напрямую добавить время истечения в третий параметр setnx.
- Данные Redis находятся только в памяти, а запись о выдаче купонов нужно хранить на складе
Сценарий 2. Ограничения инвентаря
Правила: общий запас купонов фиксирован, количество купонов, которые может получить один пользователь, не ограничено.
В приведенном выше примере аналогичного параллелизма также очень легко добиться, перейдите непосредственно к коду
Реализация MongoDB
использоватьstocksТаблица для записи количества выданных купонов, конечно же, нам нужно поле CouponId для идентификации этой записи.
Структура таблицы:
new Schema({
/* 券标识 */
couponId: {
type: String,
required: true,
},
/* 已发放数量 */
count: {
type: Number,
default: 0,
},
});
Логика выпуска:
async grantCoupon(userId: string) {
const couponId = 'coupon-1'; // 券标识
const total = 100; // 总库存
const result = await this.stockModel.findOneAndUpdate({
couponId,
}, {
$inc: {
count: 1,
},
$setOnInsert: {
couponId,
},
}, {
new: true, // 返回modify后结果
upsert: true, // 不存在则新增
});
if (result.count <= total) {
this.grantCoupon();
}
}
Реализация Redis
incr: атомарная операция, значение ключа равно +1, если значение не существует, оно будет инициализировано до 0;
async grantCoupon(userId: string) {
const total = 100; // 总库存
const result = await this.redis.incr('coupon-1');
if (result <= total) {
this.grantCoupon();
}
}
Подумай о проблеме, после того, как весь инвентарь будет израсходован,countБудут ли добавлены поля? Как это должно быть оптимизировано?
Сцена третья: пользователи получают лимит запаса купонов +
Правила: Пользователь может получить только один купон, а общий инвентарь ограничен.
Разобрать
Чтобы решить проблему «один пользователь может получить только один» или «общий лимит запасов», мы можем использовать атомарные операции для решения этой проблемы. Когда есть два условия, можем ли мы реализовать одно? Подобно атомарным операциям, «один пользователь может получить только один .» Операция слияния «Один лист» и «Общий лимит запасов» или «транзакция», более похожая на базу данных.
Транзакция базы данных — это последовательность операций с базой данных, которые обращаются к различным элементам данных и, возможно, оперируют ими.Эти операции либо выполняются все, либо не выполняются вообще и представляют собой неотделимую единицу работы. Транзакция состоит из всех операций с базой данных, выполненных между началом и концом транзакции.
mongoDB поддерживает транзакции с версии 4.0, но здесь в качестве демонстрации мы по-прежнему используем логику кода для управления параллелизмом.
Бизнес-логика:
Код:
async grantCoupon(userId: string) {
const couponId = 'coupon-1';// 券标识
const totalStock = 100;// 总库存
// 查询用户是否已领过券
const recordByFind = await this.recordModel.findOne({
couponId,
userId,
});
if (recordByFind) {
return '每位用户只能领一张';
}
// 查询已发放数量
const grantedCount = await this.stockModel.findOne({
couponId,
});
if (grantedCount >= totalStock) {
return '超过库存限制';
}
// 原子操作:已发放数量+1,并返回+1后的结果
const result = await this.stockModel.findOneAndUpdate({
couponId,
}, {
$inc: {
count: 1,
},
$setOnInsert: {
couponId,
},
}, {
new: true, // 返回modify后结果
upsert: true, // 如果不存在就新增
});
// 根据+1后的的结果判断是否超出库存
if (result.count > totalStock) {
// 超出后执行-1操作,保证数据库中记录的已发放数量准确。
this.stockModel.findOneAndUpdate({
couponId,
}, {
$inc: {
count: -1,
},
});
return '超过库存限制';
}
// 原子操作:records表新增用户领券记录,并返回新增前的查询结果
const recordBeforeModify = await this.recordModel.findOneAndUpdate({
couponId,
userId,
}, {
$setOnInsert: {
userId,
},
}, {
new: false, // 返回modify后结果
upsert: true, // 如果不存在就新增
});
if (recordBeforeModify) {
// 超出后执行-1操作,保证数据库中记录的已发放数量准确。
this.stockModel.findOneAndUpdate({
couponId,
}, {
$inc: {
count: -1,
},
});
return '每位用户只能领一张';
}
// 上述条件都满足,才执行发放操作
this.grantCoupon();
}
На самом деле, мы можем оставить первые два записи запросов и инвентаризации запросов, и результат не будет проблемой. С точки зрения оптимизации базы данных, очевидна, что изменения являются более трудоемкими, чем запрашивания, и инвентарь ограничен. В конце концов инвентарь потребляется, и последующие запросы будут завершены в первых двух этапах логики.
- При каких обстоятельствах он перейдет на левую ветвь шага 3?
Пример сценария: Остался только 1 запас. В это время пользователь А и пользователь Б запрашивают одновременно. В это время А немного быстрее. После запаса + 1 = 100, запас Б + 1 = 101;
- При каких обстоятельствах он перейдет на левую ветвь шага 4?
Пример сценария: пользователь А отправляет два запроса одновременно, а запас +1 меньше 100, тогда более быстрый запрос будет успешным, а другой запросит существующую запись купона.
- Мысль: при каких обстоятельствах может случиться так, что пользователь, запросивший первым, не получил купон, а пользователь, пришедший позже, мог получить купон?
Осталось еще 4 запаса, и пользователь А инициирует большое количество запросов, что в конечном итоге приводит к тому, что выданный запас, зарегистрированный в базе данных, превышает 100, и выполняются все операции -1. В это время пользователи B, C , и D также запрашивают одновременно, и избыточный инвентарь будет возвращен.После завершения операции отката инвентаря последующие запросы пользователей E, F и G показывают, что инвентарь все еще есть, и купоны успешно получены. Конечно, это только теоретически возможная ситуация.
Суммировать
На самом деле, чтобы спроектировать систему seckill, необходимо рассмотреть множество ситуаций. Например, при крупномасштабной деятельности seckill в электронной коммерции одновременно выполняются десятки тысяч одновременных запросов, и сервер может быть не в состоянии их поддерживать.Он может напрямую отклонять некоторые пользовательские запросы на уровне шлюза, чтобы уменьшить нагрузка на сервер, или комбинировать очереди сообщений Kafka, или использовать такие технологии, как динамическое расширение.
разное
Если в вышеизложенном есть ошибки, поправьте меня.
Предыдущий:Визуальный строительный проект H5 lowcode