Как выполнять распределенные транзакции на практике

Java

задний план

Год назад я написал статью о распределенных транзакциях:Если кто-то снова спросит вас о распределенных транзакциях, киньте ему это, В этой статье я подробно представил, что такое распределенные транзакции, и каковы общие решения для реализации распределенных транзакций, но многие из них носят теоретический характер, и у многих читателей все еще может быть небольшой пробел в их реальном использовании в реальном бою. Итак, в предыдущих обновлениях статьи я представил много контента о Seata (платформа распределенных транзакций с открытым исходным кодом Alibaba).Если вы не знакомы с Seata, вы можете прочитать следующий контент:

Seata предоставила нам два распределенных режима реализации:

  • В: автоматический режим, мы записываем андолог запуска sql для завершения автоматического повтора при сбое транзакции.
  • TCC: режим TCC, этот режим компенсирует сценарий, при котором наш режим AT может поддерживать только базу данных ACID.

В большинстве случаев Seata достаточно, но во многих случаях мы не можем выбрать платформу TCC, такую ​​​​как Seata, в разных сценариях:

  • Трансформация сложна.В настоящее время не так много коммуникационных фреймворков, поддерживаемых Seata, только Dubbo и Spring-Cloud-Alibaba.Если используются другие фреймворки или используется простой HTTP, даже некоторые компании могут не поддерживать Trace в текущей системе .
  • Стоимость обслуживания высока. Для обслуживания Seata требуется отдельный кластер. Как правило, компаниям необходимо выделять определенные ресурсы (кадровые ресурсы, машинные ресурсы) для управления и обслуживания Seata. Во многих случаях невозможно потратить такие большие затраты на несколько распределенные транзакции, конечно, эту часть можно решить, перейдя в облако в будущем.

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

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

проблема

Чтобы лучше объяснить, как выполнять распределенные транзакции в реальном бою, вот пример, с которым все знакомы: когда пользователи размещают заказ, они могут выбрать три вида активов, а именно: баланс сохраненной стоимости, баллы и купоны. приложение может его видеть, и эту сцену можно сопоставить с 4 сервисами в нашем бэкенде, как показано на следующем рисунке:

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

  • Шаг 1: Создайте статус заказа как Инициализированный и проверьте, достаточно ли всех ресурсов пользователя.
  • Шаг 2: Оплатите сохраненную сумму
  • Шаг 3: Платежный ваучер
  • Шаг 4: Оплатите золотые монеты
  • Шаг 5. Обновите статус заказа на «Выполнен».

Простых строк здесь почти 4. Многие люди вложат эти 5 шагов непосредственно в транзакцию, то есть добавят аннотацию @Transactional, но на самом деле добавление этой аннотации не только не играет транзакционной роли, но и делает нашу транзакция превратилась в длинную транзакцию. Шаги 2-4 здесь — все удаленные вызовы RPC. Как только в RPC произойдет тайм-аут, наше соединение с базой данных будет удерживаться в течение длительного времени и не будет разорвано, что может привести к лавине нашей системы.

Так как добавлять сюда транзакции бесполезно, мы видим, какие проблемы возникнут: если платеж на шаге 2 пройдет успешно, а на шаге 3 не получится, это приведет к несогласованности данных. На самом деле, у многих людей будет менталитет случайности. По умолчанию наши шаги 2-4 будут успешными. Если есть проблема, мы исправим ее вручную. Стоимость ручного ремонта слишком высока, вы думаете, если вы выезжаете на улицу и вдруг попросите восстановить данные, вас будет рвать кровью от злости? Поэтому мы шаг за шагом научим вас, как постепенно оптимизировать эту бизнес-логику, чтобы обеспечить согласованность наших данных.

метод

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

повторить запись

Давайте подумаем, на что опирается наш откат транзакции mysql? Полагаясь на undolog, наш undolog сохраняет версию данных перед транзакцией, поэтому при откате мы можем напрямую использовать эту версию данных для отката. Здесь нам сначала нужно добавить наши записи повторов, нам не нужно вызывать undolog, нам нужно добавить таблицу записей транзакций в каждый ресурсный сервис:

CREATE TABLE `transaction_record` (
  `orderId` int(11) unsigned NOT NULL AUTO_INCREMENT,
  `op_total` int(11) NOT NULL COMMENT '本次操作资源操作数量',
  `status` int(11) NOT NULL COMMENT '1:代表支付成功 2:代表支付取消',
  `resource_id` int(11) NOT NULL COMMENT '本次操作资源的Id',
  `user_id` int(11) NOT NULL COMMENT '本次操作资源的用户Id',
  PRIMARY KEY (`orderId`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

В нашей распределенной транзакции есть глобальный идентификатор транзакции, и мыorderIdЕго можно хорошо адаптировать к этой роли.Здесь нам нужно записать этот OrderId в таблицу записей транзакций каждого ресурса, чтобы связать с глобальной транзакцией, и мы напрямую используем его в качестве первичного ключа здесь, что также показывает, что эта таблица будет только появится в одноразовом глобальном идентификаторе транзакции. здесьop_totalОн используется для записи количества ресурсов в этой операции для последующих откатов, даже если мы не делаем откат, мы также можем использовать его для последующих запросов записи.statusОн используется для записи состояния нашей текущей записи.Здесь используются два состояния.Мы можем расширить больше состояний и решить больше проблем с распределенными транзакциями позже.

С этой записью повтора нам нужно только записать transaction_record нашего текущего ресурса при каждом выполнении и откатить все ресурсы в соответствии с нашим OrderId при откате.После нашей оптимизации код может быть следующим:

        int orderId = createInitOrder();
        checkResourceEnough();
        try {
            accountService.payAccount(orderId, userId, opTotal);
            coinService.payCoin(orderId, userId, opTotal);
            couponService.payCoupon(orderId, userId, couponId);
            updateOrderStatus(orderId, PAID);
        }catch (Exception e){
            //这里进行回滚
            accountService.rollback(orderId, userId);
            coinService.rollback(orderId, userId);
            couponService.rollback(orderId, userId);
            updateOrderStatus(orderId, FAILED);
        }

Здесь мы передадим созданный порядок инициализации в качестве параметра в нашу служебную запись ресурса и, наконец, обновим статус.Если произойдет исключение, то нам нужноруководствоОткатиться и превратить данные заказа в FAILED, а основанием для отката является наш ID заказа. Псевдокод для нашей оплаты и отката выглядит следующим образом:

    @Transactional
    void payAccount(int orderId, int userId, int opTotal){
        account.payAccount(userId, opTotal); // 实际的去我们account表扣减
        transactionRecordStorage.save(orderId, userId, opTotal, account.getId()); //保存事务记录表
    }
    @Transactional
    void rollback(int orderId, int userId){
        TransactionRecord tr = transactionRecordStorage.get(orderId); //从记录表中查询
        account.rollbackBytr(tr); // 根据记录回滚
    }

Вариант тут относительно простой, а проблем еще больше, об оптимизации поговорим позже.

механизм повторной попытки

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

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

  • Запланированные задачи: Запланированные задачи являются нашим наиболее распространенным механизмом повторных попыток. В основном, все структуры распределенных транзакций также выполняются через запланированные задачи. Здесь нам нужно использовать распределенные запланированные задачи. Распределенные запланированные задачи могут использовать одну машину. Задача + распределенная блокировка или прямое использование ПО промежуточного слоя для распределенных задач с открытым исходным кодом, такое как elastic-job. В логике распределенной задачи каждый раз, когда мы запрашиваем статус нашего заказа — init и время создания более одной минуты, мы откатываем его и устанавливаем статус заказа в FAILED после завершения отката.
  • Очередь сообщений: в настоящее время мы используем очередь сообщений в нашем бизнесе и помещаем операцию заказа в очередь сообщений, чтобы сделать это.Если у нас есть различные исключения, то мы полагаемся на механизм повторных попыток очереди сообщений.Вообще говоря, текущая очередь в настоящее время Попробовать еще раз, а затем бросить ее в очередь недоставленных сообщений, чтобы повторить попытку. Логику здесь нужно менять.Когда мы создаем заказ, возможно, что заказ уже существует.Если он есть, то мы судим, следует ли откатить его статус (init+1min) напрямую, а если да, то напрямую 1min. Почему мы выбрали очередь сообщений для повторной попытки? Поскольку наша бизнес-логика основана на очередях сообщений, нам не нужно вводить задачи с таймером, мы можем напрямую полагаться на очереди сообщений.

идемпотент

О том, является ли опыт программиста сложным, можно судить по тому, может ли он учитывать идемпотентность при написании кода. Многие молодые программисты даже не задумываются о существовании идемпотентности и даже не знают, что такое идемпотентность. Вот объяснение концепции идемпотентности: вы можете просто думать, что влияние любого количества выполнений такое же, как влияние одного выполнения.

Зачем нам нужна идемпотентность при выполнении распределенных транзакций? Вы можете подумать, что если машина выйдет из строя при выполнении операции отката, то сработает описанный выше механизм повторных попыток, например, наш купонный ресурс был откатан, но когда мы повторяем операцию, я не знаю, что купон исчерпан. был откатан. Если он откатился, попробуйте в это время снова откатить купон. Что будет, если не выполнить идемпотентную операцию? Это может привести к увеличению пользовательских активов, что принесет много убытков компания.

Итак, идемпотентность очень важна при повторных попытках. Что является ключом к достижению идемпотентности? Мы хотим сделать несколько операций такими же, как одну операцию, тогда нам нужно только сравнить, какая была сделана первая, и через что проходит эта метка? Здесь мы можем использовать средства перехода нашего конечного автомата для завершения маркировки. Недостаточно просто отметить это здесь, поэтому здесь мы используем пример, иллюстрирующий простую оптимизацию приведенного выше отката:

    @Transactional
    void rollback(int orderId, int userId){
        TransactionRecord tr = transactionRecordStorage.get(orderId);
        if(tr.isCanceled()){
            return; //如果已经被取消了那么直接返回
        }
        //从记录表中查询
        account.rollbackBytr(tr); // 根据记录回滚
    }

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

Так как же этого избежать? Умные учащиеся сразу подумают об использовании распределенных блокировок.Когда дело доходит до распределенных блокировок, они сразу же думают о блокировках Redis, ZK и т. д. Я также представил в этой статье:Разговор о распределенных блокировках, но здесь мы можем напрямую использовать блокировку строки базы данных, то есть использовать следующую инструкцию sql для запроса:

select * from transaction where orderId = "#{orderId}" for update;

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

  • Если он пуст, мы можем вернуться напрямую.
  • Если он пуст, мы сохраняем TransactionRecord, статус которого является состоянием выполнения пустого отката.

Первая стратегия выше относительно проста, но здесь нам нужно выбрать вторую стратегию, почему, потому что нам также нужно предотвратить одну вещь: антиподвес, давайте поговорим об идемпотенте отката, если при первом откате происходит перегрузка сети, то Здесь мы заменяем откат с блокировкой, когда мы сделали первый платеж, из-за чего платеж дошел до нашего клиента после отката. Если мы примем первый метод, наш заблокированный запрос на оплату не сможет воспринять всю транзакцию из-за отката. , а затем продолжить платить, чтобы нашу плату никогда не откатили, которая висит. Итак, мы принимаем здесь вторую стратегию, сохраняем запись, мы также проверим, есть ли эта запись в плате, поэтому оптимизированный код:

    @Transactional
    void payAccount(int orderId, int userId, int opTotal){
        TransactionRecord tr = transactionRecordStorage.getForUpdate(orderId);
        if(tr != null){
            return; //如果已经有数据了,这里直接返回
        }
        account.payAccount(userId, opTotal); // 实际的去我们account表扣减
        transactionRecordStorage.save(orderId, userId, opTotal, account.getId()); //保存事务记录表
    }
    @Transactional
    void rollback(int orderId, int userId){
         TransactionRecord tr = transactionRecordStorage.getForUpdate(orderId);
        if(tr == null){
            saveNullCancelTr(orderId, userId); //保存空回滚的记录
        }
        if(tr.isCanceled() || tr.isNullCancel()){
            return; //如果已经被取消了那么直接返回
        }
        //从记录表中查询
        account.rollbackBytr(tr); // 根据记录回滚
    }

Суммировать

На данный момент мы в основном завершили построение распределенных транзакций, таким образом, мы можем в основном решить бизнес-задачи, связанные с распределенными транзакциями в будущем. Здесь мы возвращаемся к нашим трем основным пунктам:

  • Повторить регистрацию: Сохранено путем регистрации данных.
  • Механизм повтора: повтор запланированных задач или очередей сообщений.
  • Идемпотент: добавьте блокировки строк базы данных через конечный автомат.

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

Если вы считаете, что эта статья полезна для вас, то ваше внимание и пересылка - самая большая поддержка для меня, O(∩_∩)O: