Сосредоточьтесь на PHP, MySQL, Linux и фронтенд-разработке, если вам интересно, спасибо за внимание! ! ! Статьи организованы вGitHub,GiteeОсновные технологии включают PHP, Redis, MySQL, JavaScript, HTML и CSS, Linux, Java, Golang, Linux и инструментальные ресурсы, а также другие связанные теоретические знания, вопросы для интервью и практический контент.
Актуальное заявление
В недавней маркетинговой деятельности по проекту коллега использует Redis для управления товарными запасами. В процессе измерения давления было обнаружено наличие перепроданности. Вот краткое изложение того, как правильно использовать Redis для решения проблемы перепроданности.
Шаги демонстрации
Я не буду вам напрямую объяснять, как добиться безопасной и эффективной распределенной блокировки. Вместо этого он шаг за шагом реализует блокировки различными способами, обнаруживает недостатки каждой блокировки и способы ее оптимизации для этого типа блокировки и, наконец, достигает эффективной и безопасной распределенной блокировки.
первый сценарий
Этот сценарий используется с Redis для хранения количества предметов. Сначала получите инвентарь, определите инвентарь, если инвентарь больше 0, затем уменьшите 1, затем обновите данные о запасах Redis. Грубое диаграмма выглядит следующим образом:
-
Когда придет первый запрос, перейдите к определению инвентарного количества Redis. Затем уменьшите запасы продукта на единицу, а затем обновите количество запасов.
-
Когда между первым запросом, обрабатывающим логику инвентаризации, приходит второй запрос, с той же логикой, чтобы прочитать инвентарь Redis, определить количество инвентаря и затем выполнить операцию сокращения инвентаря. В настоящее время товары, которыми они оперируют, на самом деле являются одним и тем же товаром.
-
Тогда с такой логикой при таком большом количестве запросов в seckill легко реальное количество проданных товаров будет намного больше, чем количество товаров на складе.
public function demo1(ResponseInterface $response)
{
$application = ApplicationContext::getContainer();
$redisClient = $application->get(Redis::class);
/** @var int $goodsStock 商品当前库存*/
$goodsStock = $redisClient->get($this->goodsKey);
if ($goodsStock > 0) {
$redisClient->decr($this->goodsKey);
// TODO 执行额外业务逻辑
return $response->json(['msg' => '秒杀成功'])->withStatus(200);
}
return $response->json(['msg' => '秒杀失败,商品库存不足。'])->withStatus(500);
}
анализ проблемы:
-
Этот метод использует Redis для управления товарными запасами и снижает нагрузку на MySQL.
-
Предполагая, что в данный момент запас равен 1, первый запрос находится в процессе определения того, что запас больше 0, и сокращения запаса. При повторном запросе на чтение данных обнаруживается, что товарный запас больше 0. Оба будут выполнять логику мгновенного уничтожения, но есть только один инвентарь, и он столкнется с ситуацией перепроданности.
-
На этом этапе давайте представим, что если мы можем разрешить только один запрос для обработки инвентаря, а другие запросы могут ждать только до окончания предыдущего запроса для получения инвентаря продукта, можем ли мы достичь перепроданности? Это реализация механизма блокировки, упомянутого в следующих сценариях.
второй сценарий
Используя блокировку файла, после того, как придет первый запрос, откройте блокировку файла. После того, как бизнес обработан, текущая блокировка файла снимается, а затем обрабатывается следующий запрос, зацикливаясь по очереди. Гарантируется, что из всех текущих запросов только один запрос обрабатывает инвентарь. После обработки запроса блокировка снимается.
-
Используя файловые блокировки, делается запрос на блокировку файла. В этот момент другой запрос будет заблокирован, и следующий запрос не будет выполнен до тех пор, пока предыдущий запрос не освободит файл блокировки.
-
Все запросы похожи на очередь, предыдущий входит в очередь первым, а другой входит в очередь последним, и они обрабатываются в порядке FIFO.
public function demo3(ResponseInterface $response)
{
$fp = fopen("/tmp/lock.txt", "r+");
try {
if (flock($fp, LOCK_EX)) { // 进行排它型锁定
$application = ApplicationContext::getContainer();
$redisClient = $application->get(Redis::class);
/** @var int $goodsStock 商品当前库存*/
$goodsStock = $redisClient->get($this->goodsKey);
if ($goodsStock > 0) {
$redisClient->decr($this->goodsKey);
// TODO 处理额外的业务逻辑
$result = true; // 业务逻辑处理最终结果
flock($fp, LOCK_UN); // 释放锁定
fclose($fp);
if ($result) {
return $response->json(['msg' => '秒杀成功'])->withStatus(200);
}
return $response->json(['msg' => '秒杀失败'])->withStatus(200);
} else {
flock($fp, LOCK_UN); // 释放锁定
fclose($fp);
return $response->json(['msg' => '库存不足,秒杀失败。'])->withStatus(500);
}
} else {
fclose($fp);
return $response->json(['msg' => '活动过于火爆,抢购的人过多,请稍后重试。'])->withStatus(500);
}
} catch (\Exception $exception) {
fclose($fp);
return $response->json(['msg' => '系统异常'])->withStatus(500);
} finally {
fclose($fp);
}
}
анализ проблемы:
-
Если используются файловые блокировки, открытие и снятие блокировки занимают много времени. В бизнес-сценарии seckill приходит большое количество запросов, и большинству пользователей легко находиться в процессе ожидания запросов.
-
Когда блокировка файла открыта, это для текущего сервера. Если наш проект относится к распределенному развертыванию, вышеуказанная блокировка может быть заблокирована только для текущего сервера, а не для запроса. Как показано на рисунке ниже: Легко иметь несколько многосерверных множественных блокировок в любое время.
третий сценарий
Решение состоит в том, чтобы сначала хранить инвентарь товаров в Redis, а при выполнении запроса уменьшать указанный выше инвентарь на 1. Если инвентарь, возвращаемый Redis, меньше 0, это означает, что текущий всплеск не работает. Он в основном использует однопоточную запись Redis. Убедитесь, что только один поток выполняет каждую запись в Redis.
public function demo2(ResponseInterface $response)
{
$application = ApplicationContext::getContainer();
$redisClient = $application->get(Redis::class);
/** @var int $goodsStock Redis减少1后的库存数据 */
$goodsStock = $redisClient->decr($this->goodsKey);
if ($goodsStock > 0) {
// TODO 执行额外业务逻辑
$result = true;// 业务处理的结果
if ($result) {
return $response->json(['msg' => '秒杀成功'])->withStatus(200);
} else {
$redisClient->incr($this->goodsKey);// 将减少的库存进行增加1
return $response->json(['msg' => '秒杀失败'])->withStatus(500);
}
}
return $response->json(['msg' => '秒杀失败,商品库存不足。'])->withStatus(500);
}
анализ проблемы:
-
Хотя в этом решении используется однопоточная модель Redis, оно позволяет избежать перепроданности. Когда инвентарь равен 0, запрос seckill уменьшит инвентарь на 1, а окончательные данные кеша Redis точно будут меньше 0.
-
В этом решении количество seckills пользователей не соответствует фактическому количеству seckill-продуктов. Как показано в приведенном выше коде, когда результат бизнес-обработки равен FALSE, добавьте в Redis 1. Если во время процесса добавления 1 возникнет исключение, количество продуктов будет несогласованным, если добавление не будет успешным.
четвертый сценарий
Благодаря анализу вышеуказанных трех ситуаций можно сделать вывод, что ситуация блокировки файлов является лучшим решением. Но файловые замки не могут решить опорожнение распределенных развертываний. В это время мы можем использовать Setnx Redis и истекает для реализации распределенных замков. Команда setnx сначала устанавливает замок и истекает добавляет тайм-аут в замок.
public function demo4(ResponseInterface $response)
{
$application = ApplicationContext::getContainer();
$redisClient = $application->get(Redis::class);
if ($redisClient->setnx($this->goodsKey, 1)) {
// 假设该执行下面的操作时服务器宕机
$redisClient->expire($this->goodsKey, 10);
// TODO 处理业务逻辑
$result = true;// 处理业务逻辑的结果
// 删除锁
$redisClient->del($this->goodsKey);
if ($result) {
return $response->json(['msg' => '秒杀成功。'])->withStatus(200);
}
return $response->json(['msg' => '秒杀失败。'])->withStatus(500);
}
return $response->json(['msg' => '系统异常,请重试。'])->withStatus(500);
}
анализ проблемы:
-
Из приведенного выше примера кода мы почувствуем, что с этим методом нет проблем. Добавьте блокировку и снимите блокировку. Но подумайте об этом, после того как команда setnx добавила блокировку, произошло исключение, когда для блокировки было установлено время истечения (expire), а время истечения не было добавлено в блокировку нормально. Этот замок еще на месте?
-
Следовательно, описанная выше ситуация для достижения распределенной блокировки Redis не удовлетворяет атомарности.
пятый сценарий
В четвертом сценарии Redis используется для реализации распределенных блокировок. Однако распределенная блокировка не является атомарной.К счастью, Redis предоставляет комбинированную версию двух команд, которая позволяет достичь атомарности. Установлено(ключ, значение, ['nx', 'ex' => 'срок действия']).
public function demo5(ResponseInterface $response)
{
$application = ApplicationContext::getContainer();
$redisClient = $application->get(Redis::class);
if ($redisClient->set($this->goodsKey, 1, ['nx', 'ex' => 10])) {
try {
// TODO 处理秒杀业务
$result = true;// 处理业务逻辑的结果
$redisClient->del($this->goodsKey);
if ($result) {
return $response->json(['msg' => '秒杀成功。'])->withStatus(200);
} else {
return $response->json(['msg' => '秒杀失败。'])->withStatus(200);
}
} catch (\Exception $exception) {
$redisClient->del($this->goodsKey);
} finally {
$redisClient->del($this->goodsKey);
}
}
return $response->json(['msg' => '系统异常,请重试。'])->withStatus(500);
}
анализ проблемы:
-
Благодаря пошаговому продвижению вы можете подумать, что пятый сценарий Redis должен быть бесшовным для достижения распространения. Мы внимательно наблюдаем за местом, где разыгрывается TODO, а также там, где обрабатывается бизнес-логика. Что делать, если бизнес-логика превышает настройку кэша, равную 10 секундам?
-
Если логическая обработка превышает 10 секунд, второй запрос seckill может нормально обрабатывать свой бизнес-запрос. Так уж получилось, что после выполнения бизнес-логики первого запроса блокировка Redis второго запроса будет удалена, если блокировка Redis подлежит удалению. Третий запрос будет выполняться нормально.Согласно этой логике, блокировка Redis является недопустимой блокировкой?
-
Эта ситуация приведет к тому, что текущий запрос удалит собственную блокировку при удалении блокировки Redis. Если мы делаем проверку при удалении замков, мы можем удалить только свои собственные замки, чтобы увидеть, работает ли эта схема? Далее рассмотрим шестой случай.
шестой сценарий
Для пятого вышеприведенного случая это решение добавляет запрошенную оценку уникального идентификатора при удалении. То есть удаляется только идентификатор, когда блокировка добавляется сама по себе.
public function demo6(ResponseInterface $response)
{
$application = ApplicationContext::getContainer();
$redisClient = $application->get(Redis::class);
/** @var string $client 当前请求的唯一标识*/
$client = md5((string)mt_rand(100000, 100000000000000000).uniqid());
if ($redisClient->set($this->goodsKey, $client, ['nx', 'ex' => 10])) {
try {
// TODO 处理秒杀业务逻辑
$result = true;// 处理业务逻辑的结果
$redisClient->del($this->goodsKey);
if ($result) {
return $response->json(['msg' => '秒杀成功'])->withStatus(200);
}
return $response->json(['msg' => '秒杀失败'])->withStatus(500);
} catch (\Exception $exception) {
if ($redisClient->get($this->goodsKey) == $client) {
// 此处存在时间差
$redisClient->del($this->goodsKey);
}
} finally {
if ($redisClient->get($this->goodsKey) == $client) {
// 此处存在时间差
$redisClient->del($this->goodsKey);
}
}
}
return $response->json(['msg' => '请稍后重试'])->withStatus(500);
}
анализ проблемы
-
Анализируя вышеописанную ситуацию, кажется, что проблемы нет вообще. Однако внимательно вы можете увидеть, где я добавил комментарий «Здесь разница во времени». Если Redis считывает кеш и определяет, что уникальный идентификатор запроса непротиворечив, при выполнении блокировки удаления происходит блокировка, колебания сети и т. д. После истечения срока действия блокировки выполняется команда del. Является ли удаленная блокировка по-прежнему запрошенной в данный момент блокировкой?
-
Чтобы удалить блокировку в это время, это определенно не запрошенная в данный момент блокировка. но блокировка для следующего запроса. В этом случае также возникает ситуация, когда блокировка недействительна?
краткое изложение проблемы
Из приведенных выше нескольких демонстраций кода видно, что большая проблема заключается в снятии блокировки с Redis, поскольку это не атомарная операция. В сочетании с шестым случаем, если мы можем гарантировать, что снятие блокировки является атомарным, и добавление блокировки также является атомарным, можем ли мы правильно гарантировать, что с нашей блокировкой распределения проблем не возникнет?
-
При добавлении блокировок для достижения атомарных операций мы можем использовать собственные команды Redis.
-
При снятии блокировки удаляется только добавленная сама собой блокировка, что мы и решили в шестом сценарии.
-
Далее вам нужно только рассмотреть атомарную операцию при снятии блокировки. Поскольку в Redis изначально нет такой команды, нам нужно использовать операции lua для достижения атомарности.
Выполнение
Открыв официальный сайт, вы можете увидеть, что официальный сайт предоставляет несколько клиентов, реализованных распределенными блокировками, которые можно использовать напрямую.Адрес официального сайта, вот клиент, который я используюrtckit/reactphp-redlock. Конкретный метод установки можно использовать напрямую в соответствии с документацией. Вот краткое описание двух методов вызова.
первый способ
public function demo7()
{
/** @var Factory $factory 初始化一个Redis实例*/
$factory = new \Clue\React\Redis\Factory();
$client = $factory->createLazyClient('127.0.0.1');
/** @var Custodian $custodian 初始化一个锁监听器*/
$custodian = new \RTCKit\React\Redlock\Custodian($client);
$custodian->acquire('MyResource', 60, 'r4nd0m_token')
->then(function (?Lock $lock) use ($custodian) {
if (is_null($lock)) {
// 获取锁失败
} else {
// 添加一个10s生命周期的锁
// TODO 处理业务逻辑
// 释放锁
$custodian->release($lock);
}
});
}
Общая логика этого метода аналогична логике нашего шестого решения, все из которых используют Redis.set + nx
Команда реализует атомную блокировку, а затем устанавливает случайную строку в дополнительный замок, который используется для того, чтобы иметь дело с тем, что когда выделяется текущий замок, не могут быть освобождены другие люди. Большая разница в использованииrelease
При снятии блокировки этот метод вызывает сценарий lua для снятия блокировки. Освобождение блокировки гарантированно будет атомарным. Ниже приведен примерный скриншот снятия блокировки.
// lua脚本
public const RELEASE_SCRIPT = <<<EOD
if redis.call("get", KEYS[1]) == ARGV[1] then
return redis.call("del", KEYS[1])
else
return 0
end
EOD;
public function release(Lock $lock): PromiseInterface
{
/** @psalm-suppress InvalidScalarArgument */
return $this->client->eval(self::RELEASE_SCRIPT, 1, $lock->getResource(), $lock->getToken())
->then(function (?string $reply): bool {
return $reply === '1';
});
}
второй способ
Второй способ мало чем отличается от первого, это не что иное, как добавление одного自旋锁
. Продолжайте получать блокировку и отказывайтесь от текущего запроса, если он не получен.
public function demo8()
{
/** @var Factory $factory 初始化一个Redis实例*/
$factory = new \Clue\React\Redis\Factory();
$client = $factory->createLazyClient('127.0.0.1');
/** @var Custodian $custodian 初始化一个锁监听器*/
$custodian = new \RTCKit\React\Redlock\Custodian($client);
$custodian->spin(100, 0.5, 'HotResource', 10, 'r4nd0m_token')
->then(function (?Lock $lock) use ($custodian) : void {
if (is_null($lock)) {
// 将进行100次的场次,每一次间隔0.5秒去获取锁,如果没有获取到锁。则放弃加锁请求。
} else {
// 添加一个10s生命周期的锁
// TODO 处理业务逻辑
// 释放锁
$custodian->release($lock);
}
});
}
блокировка спина
Спин-блокировка, также известная как спин-блокировка, представляет собой механизм блокировки, предлагаемый для защиты общих ресурсов. Спин-блокировки аналогичны блокировкам мьютекса, обе из которых предназначены для разрешения взаимоисключающего использования ресурса.
Будь то мьютекс или самоблокирующийся, в любое время до одного держателя может быть только один исполнительный блок для получения блокировки. Но оба немного отличаются по механизму планирования. Для мьютекса, если ресурс был занят, заявитель на ресурс может только войти в состояние сна. Тем не менее, блокировка вращения не вызывает сон вызывающего, и если блокировка вращения была сохранена другими исполнительными блоками, вызывающий объект зацикливался, чтобы увидеть, снял ли держатель спиральной блокировки блокировку, «вращать». теперь назван.
Суммировать
На самом деле, с помощью приведенных выше решений, если вы будете осторожны, вы все равно можете найти много проблем.
-
Параллелизм сам по себе может быть многопоточным методом обработки После того, как мы добавим здесь блокировки, станет ли параллельная обработка последовательной обработкой? Снижена так называемая высокая производительность шипа.
-
Возможно ли указанное выше решение по-прежнему применимо в репликации Redis master-slave, кластеризации и других решениях архитектуры развертывания?
-
Многие говорят, что zookeeper больше подходит для сценариев с распределенными блокировками, где zookeeper потребляет больше, чем Redis?
Со всевозможными вопросами мы увидимся в следующей статье. Нравится, интересно, прошу обратить внимание на мои статьи. В статье есть недочеты, исправления также приветствуются.