Redis — крупная авария, вызванная распределенными блокировками

Java

предисловие

В использовании распределенных блокировок на основе Redis сегодня нет ничего нового. Эта статья в основном основана на анализе и решении аварий, вызванных распределенной блокировкой Redis в нашем реальном проекте.
задний план: Панический заказ в нашем проекте решается распределенными блокировками. Однажды операция организовала спешку, чтобы купить Feitian Moutai со 100 бутылками на складе, но она была перепродана! Вы знаете, мало летающих Маотай на этой земле! ! ! Авария была обозначена как крупная авария уровня P0 ... только для того, чтобы ее приняли. Производительность всей команды проекта была вычтена~~ После аварии технический директор назвал меня и попросил взять на себя ответственность за то, чтобы справиться с этим, хорошо, зарядка~

место происшествия

Немного разобравшись, я узнал, что такого интерфейса активности по скупке никогда раньше не было, но почему на этот раз он перепродан? Причина в том, что предыдущие продукты распродажи не были дефицитным товаром, и это событие фактически являетсяФейтян Мутай, Благодаря анализу данных захороненных точек, данные в основном удвоились, и можно себе представить энтузиазм деятельности! Говорить особо нечего, сразу переходим к основному коду, а конфиденциальная часть обрабатывается в псевдокоде. . .

public SeckillActivityRequestVO seckillHandle(SeckillActivityRequestVO request) {
SeckillActivityRequestVO response;
    String key = "key:" + request.getSeckillId;
    try {
        Boolean lockFlag = redisTemplate.opsForValue().setIfAbsent(key, "val", 10, TimeUnit.SECONDS);
        if (lockFlag) {
            // HTTP请求用户服务进行用户相关的校验
            // 用户活动校验
            
            // 库存校验
            Object stock = redisTemplate.opsForHash().get(key+":info", "stock");
            assert stock != null;
            if (Integer.parseInt(stock.toString()) <= 0) {
                // 业务异常
            } else {
                redisTemplate.opsForHash().increment(key+":info", "stock", -1);
                // 生成订单
                // 发布订单创建成功事件
                // 构建响应VO
            }
        }
    } finally {
        // 释放锁
        stringRedisTemplate.delete("key");
        // 构建响应VO
    }
    return response;
}

Вышеприведенный код гарантирует, что бизнес-логика имеет достаточное время выполнения в течение срока действия распределенной блокировки, равного 10 с; блок try-finally используется для гарантии того, что блокировка будет снята вовремя. Инвентаризация также проверяется в рамках бизнес-кода. Выглядит очень безопасно~ Не волнуйтесь, продолжайте анализировать. . .

причина аварии

Активность Feitian Moutai привлекла большое количество новых пользователей, которые загрузили и зарегистрировали наше приложение, среди них есть много шерстяных вечеринок, которые используют профессиональные средства для регистрации новых пользователей, чтобы получать заказы на шерсть и щетки. Конечно, наша пользовательская система была подготовлена ​​заранее, и доступ к аутентификации «человек-машина» Alibaba Cloud, трехфакторной аутентификации, собственной системе контроля рисков и другим восемнадцати боевым искусствам заблокировал большое количество нелегальных пользователей. Не могу не поставить лайк~
Но именно из-за этого пользовательский сервис находится под высокой операционной нагрузкой..
В тот момент, когда началась активация snap-up, на пользовательский сервис попало большое количество запросов на проверку пользователей. Шлюз обслуживания пользователей имеет короткую задержку ответа, время ответа на некоторые запросы превышает 10 с. Однако из-за тайм-аута ответа на HTTP-запросы мы установили его равным 30 с, что приводит к блокировке интерфейса на контрольной точке пользователя. После 10 с , распределенная блокировка истекла.В это время, если поступает новый запрос, блокировка может быть получена, то есть блокировка перезаписывается. После того, как эти заблокированные интерфейсы будут выполнены, будет выполнена логика снятия блокировки, которая снимет блокировки других потоков, так что новые запросы также могут конкурировать за блокировку~ Это действительно очень плохой цикл.
В настоящее время мы можем полагаться только на проверку запасов, но проверка запасов не является неатомарной, используяget and compareПуть,Вот так и произошла трагедия перепроданности.~~~

анализ аварий

После тщательного анализа можно обнаружить, что этот интерфейс моментального доступа имеет серьезные риски безопасности в сценариях с высоким параллелизмом, в основном в трех местах:

  • Отказоустойчивость других системных рисков отсутствует
    Задержка ответа шлюза из-за жесткого обслуживания пользователей, но ничего не может с этим поделать, это перепроданопредохранитель.
  • Кажущиеся безопасными распределенные блокировки на самом деле вовсе не безопасны.
    Хотя используется метод установки значения ключа [EX секунд] [PX миллисекунд] [NX|XX], если поток A выполняется в течение длительного времени и не успевает освободиться, блокировка истечет, и поток B может получить блокировку. замок в это время. Когда поток A завершает выполнение, освобождение блокировки фактически освобождает блокировку потока B. В это время поток C может снова получить блокировку, и в это время, если поток B завершает снятие блокировки, фактически это блокировка, установленная освобожденным потоком C. это перепроданопрямая причина.
  • Неатомарная инвентаризация
    Неатомарная проверка инвентаризации приводит к неточным результатам проверки инвентаризации в параллельных сценариях. это перепроданоосновная причина.

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

решение

Как только мы узнаем причину, мы можем назначить правильное лекарство.

Реализовать относительно безопасные распределенные блокировки

Относительно безопасное определение: set и del сопоставляются один к одному, и нет случая, когда другие существующие блокировки являются del. С практической точки зрения, даже если set и del могут быть отображены один за другим, нельзя гарантировать абсолютную безопасность бизнеса. Потому что время истечения блокировки всегда ограничено, если только время истечения не установлено или время истечения не установлено очень долго, но это также приведет к другим проблемам. Так что это не имеет смысла.
Чтобы получить относительно безопасную распределенную блокировку, вы должны полагаться на значение ключа. При снятии блокировки уникальность значения гарантирует, что оно не будет удалено. Реализуем на основе LUA скриптаАтомный получить и сравнить,следующее:

public void safedUnLock(String key, String val) {
    String luaScript = "local in = ARGV[1] local curr=redis.call('get', KEYS[1]) if in==curr then redis.call('del', KEYS[1]) end return 'OK'"";
    RedisScript<String> redisScript = RedisScript.of(luaScript);
    redisTemplate.execute(redisScript, Collections.singletonList(key), Collections.singleton(val));
}

Мы используем скрипт LUA для безопасной разблокировки.

Обеспечивает безопасную проверку запасов

Если у нас есть более глубокое понимание параллелизма, мы обнаружим, что такие операции, как получение и сравнение/чтение и сохранение, не являются атомарными. Если мы хотим добиться атомарности, мы также можем сделать это с помощью LUA-скриптов. Но в нашем случае, поскольку событие snap-up может размещать только одну бутылку за заказ, его можно реализовать не на основе LUA-скрипта, а на основе атомарности самого redis. причина в следующем:

// redis会返回操作之后的结果,这个过程是原子性的
Long currStock = redisTemplate.opsForHash().increment("key", "stock", -1);

Нашел нет, инвентаризация в коде совершенно "лишняя".

Код после доработки

После проведенного выше анализа мы решили создать новый класс DistributedLocker для работы с распределенными блокировками.

public SeckillActivityRequestVO seckillHandle(SeckillActivityRequestVO request) {
SeckillActivityRequestVO response;
    String key = "key:" + request.getSeckillId();
    String val = UUID.randomUUID().toString();
    try {
        Boolean lockFlag = distributedLocker.lock(key, val, 10, TimeUnit.SECONDS);
        if (!lockFlag) {
            // 业务异常
        }

        // 用户活动校验
        // 库存校验,基于redis本身的原子性来保证
        Long currStock = stringRedisTemplate.opsForHash().increment(key + ":info", "stock", -1);
        if (currStock < 0) { // 说明库存已经扣减完了。
            // 业务异常。
            log.error("[抢购下单] 无库存");
        } else {
            // 生成订单
            // 发布订单创建成功事件
            // 构建响应
        }
    } finally {
        distributedLocker.safedUnLock(key, val);
        // 构建响应
    }
    return response;
}

Глубокое мышление

Нужна ли распределенная блокировка?

После улучшения мы действительно можем обнаружить, что мы можем гарантировать, что мы не будем перепроданы, используя атомарную дедукцию самого redis. правильный. Однако, если такой блокировки нет, все запросы будут проходить через бизнес-логику, из-за зависимости от других систем нагрузка на другие системы в это время будет возрастать. Это увеличит потерю производительности и нестабильность обслуживания больше, чем выигрыш. На основе распределенных блокировок некоторый трафик может быть в определенной степени перехвачен.

Выбор распределенных замков

Некоторые люди предлагают использовать RedLock для реализации распределенных блокировок. RedLock более надежен, но за счет некоторой производительности. В этом сценарии повышение надежности гораздо менее рентабельно, чем повышение производительности. Для сценариев с чрезвычайно высокими требованиями к надежности можно использовать RedLock.

Стоит ли снова думать о распределенных блокировках?

Так как баг нужно срочно исправить и запустить, мы его оптимизировали и провели стресс-тест в тестовой среде, и сразу развернули в онлайн. Получается, что эта оптимизация прошла успешно, производительность немного улучшилась, а в случае сбоя распределенной блокировки нет ситуации перепроданности.
Однако есть ли еще возможности для оптимизации? немного!
Поскольку служба развернута в кластере, мы можем равномерно распределять инвентаризацию на каждый сервер в кластере и уведомлять каждый сервер в кластере посредством широковещательной рассылки. Уровень шлюза использует алгоритм хеширования на основе идентификатора пользователя, чтобы определить, на какой сервер отправляется запрос. Таким образом, вывод и оценка инвентаризации могут быть реализованы на основе кеша приложения. Производительность была улучшена!


// 通过消息提前初始化好,借助ConcurrentHashMap实现高效线程安全
private static ConcurrentHashMap<Long, Boolean> SECKILL_FLAG_MAP = new ConcurrentHashMap<>();
// 通过消息提前设置好。由于AtomicInteger本身具备原子性,因此这里可以直接使用HashMap
private static Map<Long, AtomicInteger> SECKILL_STOCK_MAP = new HashMap<>();

...

public SeckillActivityRequestVO seckillHandle(SeckillActivityRequestVO request) {
SeckillActivityRequestVO response;

    Long seckillId = request.getSeckillId();
    if(!SECKILL_FLAG_MAP.get(requestseckillId)) {
        // 业务异常
    }
     // 用户活动校验
     // 库存校验
    if(SECKILL_STOCK_MAP.get(seckillId).decrementAndGet() < 0) {
        SECKILL_FLAG_MAP.put(seckillId, false);
        // 业务异常
    }
    // 生成订单
    // 发布订单创建成功事件
    // 构建响应
    return response;
}

Благодаря приведенному выше преобразованию нам вообще не нужно полагаться на Redis. Как производительность, так и безопасность могут быть улучшены!
Конечно, это решение не учитывает сложные сценарии, такие как динамическое расширение и сокращение машин, Если их все же нужно учитывать, лучше сразу рассмотреть решение распределенных блокировок.

Суммировать

Перепродажа дефицитных товаров, безусловно, является крупной аварией. Если количество перепроданности велико, это даже окажет очень серьезное деловое и социальное влияние на платформу. После этой аварии я понял, что ни одна строчка кода в проекте не должна восприниматься легкомысленно, иначе в некоторых сценариях эти нормальные рабочие коды станут фатальными убийцами! Для застройщика при разработке схемы разработки необходимо полностью учитывать схему. Как можно хорошо продумать план? Просто продолжайте учиться!