Говоря о 12 сценариях провала весенней транзакции, это слишком жалко

задняя часть Spring

предисловие

Для студентов, которые занимаются java-разработкой, весенние дела должны быть знакомы.

В некоторых бизнес-сценариях, если запросу требуется одновременная запись данных из нескольких таблиц. Чтобы обеспечить атомарность операций (либо успех, либо отказ одновременно) и избежать несогласованности данных, мы обычно используем транзакции Spring.

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

Но если вы используете его неправильно, он также незаметно поймает вас в ловушку.

Сегодня мы поговорим о некоторых сценариях сбоя транзакции, возможно, вы уже завербованы. Если вы мне не верите, давайте посмотрим.

Недавно я случайно получил заметку о чистке, написанную крупным производителем BAT, которая открыла мне сразу вторую линейку Ren и Du, и я все больше чувствую, что алгоритм не так сложен, как я себе представлял.

Заметки о чистке, написанные боссом BAT, позвольте мне мягко получить предложение

Сделка не вступает в силу

1. Проблемы с правами доступа

Как мы все знаем, в Java существует четыре основных типа разрешений доступа: частные, стандартные, защищенные и общедоступные, причем их разрешения увеличиваются слева направо.

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

@Service
public class UserService {
    
    @Transactional
    private void add(UserModel userModel) {
         saveData(userModel);
         updateData(userModel);
    }
}

Мы видим, что права доступа метода add определены какprivate, что приведет к сбою транзакции. Spring требует, чтобы прокси-метод былpublicиз.

Грубо говоря, вAbstractFallbackTransactionAttributeSourceКатегорияcomputeTransactionAttributeВ методе есть суждение.Если целевой метод не является общедоступным, тоTransactionAttributeВозвращает null, т.е. транзакции не поддерживаются.

protected TransactionAttribute computeTransactionAttribute(Method method, @Nullable Class<?> targetClass) {
    // Don't allow no-public methods as required.
    if (allowPublicMethodsOnly() && !Modifier.isPublic(method.getModifiers())) {
      return null;
    }

    // The method may be on an interface, but we need attributes from the target class.
    // If the target class is null, the method will be unchanged.
    Method specificMethod = AopUtils.getMostSpecificMethod(method, targetClass);

    // First try is the method in the target class.
    TransactionAttribute txAttr = findTransactionAttribute(specificMethod);
    if (txAttr != null) {
      return txAttr;
    }

    // Second try is the transaction attribute on the target class.
    txAttr = findTransactionAttribute(specificMethod.getDeclaringClass());
    if (txAttr != null && ClassUtils.isUserLevelMethod(method)) {
      return txAttr;
    }

    if (specificMethod != method) {
      // Fallback is to look at the original method.
      txAttr = findTransactionAttribute(method);
      if (txAttr != null) {
        return txAttr;
      }
      // Last fallback is the class of the original method.
      txAttr = findTransactionAttribute(method.getDeclaringClass());
      if (txAttr != null && ClassUtils.isUserLevelMethod(method)) {
        return txAttr;
      }
    }
    return null;
  }

То есть, если наш пользовательский метод транзакции (то есть целевой метод), его права доступа неpublic, но если он частный, по умолчанию или защищенный, Spring не будет предоставлять функции транзакций.

2. Метод модифицируется окончательным

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

@Service
public class UserService {

    @Transactional
    public final void add(UserModel userModel){
        saveData(userModel);
        updateData(userModel);
    }
}

Мы видим, что метод add определен какfinal, что приведет к сбою транзакции.

Зачем?

Если вы читали исходный код транзакции Spring, вы, возможно, знаете, что нижний уровень транзакции Spring использует aop, то есть через динамический прокси jdk или cglib, это помогает нам генерировать классы прокси и реализовывать функции транзакций в классах прокси.

Но если метод модифицируется с помощью final, то в его прокси-классе метод не может быть переписан для добавления функции транзакции.

Примечание. Если метод является статическим, он не может стать методом транзакции через динамический прокси.

3. Внутренний вызов метода

Иногда нам нужно вызвать другой метод транзакции в методе класса службы, например:

@Service
public class UserService {

    @Autowired
    private UserMapper userMapper;

    //@Transactional
    public void add(UserModel userModel) {
        userMapper.insertUser(userModel);
        updateStatus(userModel);
    }

    @Transactional
    public void updateStatus(UserModel userModel) {
        doSameThing();
    }
}

Мы видим, что в методе транзакции add непосредственно вызывается метод транзакции updateStatus. Как видно из предыдущего описания, у метода updateStatus есть возможность иметь транзакции, потому что spring aop генерирует прокси-объект, но этот метод напрямую вызывает метод этого объекта, поэтому метод updateStatus не будет генерировать транзакцию.

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

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

3.1 Добавить новый метод службы

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

@Servcie
public class ServiceA {
   @Autowired
   prvate ServiceB serviceB;

   public void save(User user) {
         queryData1();
         queryData2();
         serviceB.doSave(user);
   }
 }

 @Servcie
 public class ServiceB {

    @Transactional(rollbackFor=Exception.class)
    public void doSave(User user) {
       addData1();
       updateData2();
    }

 }

3.2 Внедрите себя в класс обслуживания

Если вы не хотите добавлять еще один класс Service, вы также можете внедрить себя в класс Service. Конкретный код выглядит следующим образом:

@Servcie
public class ServiceA {
   @Autowired
   prvate ServiceA serviceA;

   public void save(User user) {
         queryData1();
         queryData2();
         serviceA.doSave(user);
   }

   @Transactional(rollbackFor=Exception.class)
   public void doSave(User user) {
       addData1();
       updateData2();
    }
 }

У некоторых людей могут возникнуть такие вопросы: Будет ли этот подход вызывать циклические зависимости?

Ответ: нет.

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

3.3 Через класс AopContent

Используйте AopContext.currentProxy(), чтобы получить прокси-объект в классе службы.

Вышеупомянутый метод 2 действительно может решить проблему, но код не выглядит интуитивно понятным, и ту же функцию можно реализовать, используя AOPProxy в классе Service для получения прокси-объекта. Конкретный код выглядит следующим образом:

@Servcie
public class ServiceA {

   public void save(User user) {
         queryData1();
         queryData2();
         ((ServiceA)AopContext.currentProxy()).doSave(user);
   }

   @Transactional(rollbackFor=Exception.class)
   public void doSave(User user) {
       addData1();
       updateData2();
    }
 }

4. Не управляемая пружина

В нашем обычном процессе разработки есть деталь, которую легко упустить из виду. То есть предпосылка использования транзакции Spring такова: для управления Spring необходимо создать экземпляр bean-компонента.

Обычно мы можем автоматически реализовать функции создания экземпляров компонентов и внедрения зависимостей с помощью таких аннотаций, как @Controller, @Service, @Component и @Repository.

Конечно, есть много способов создать экземпляр бина.Заинтересованные друзья могут прочитать еще одну статью, которую я написал ранее»Вы знаете все эти грубые операции @Autowired?

Если однажды вы в спешке разработали класс Service, но забыли добавить аннотацию @Service, например:

//@Service
public class UserService {

    @Transactional
    public void add(UserModel userModel) {
         saveData(userModel);
         updateData(userModel);
    }    
}

Из приведенного выше примера видно, что класс UserService не добавлен.@ServiceAnnotation, то класс не будет передан в управление Spring, поэтому его метод add не будет генерировать транзакции.

5. Многопоточный вызов

В реальной разработке проектов еще достаточно много сценариев использования многопоточности. Возникнет ли проблема, если Spring-транзакции используются в многопоточном сценарии?

@Slf4j
@Service
public class UserService {

    @Autowired
    private UserMapper userMapper;
    @Autowired
    private RoleService roleService;

    @Transactional
    public void add(UserModel userModel) throws Exception {
        userMapper.insertUser(userModel);
        new Thread(() -> {
            roleService.doOtherThing();
        }).start();
    }
}

@Service
public class RoleService {

    @Transactional
    public void doOtherThing() {
        System.out.println("保存role表数据");
    }
}

Из приведенного выше примера видно, что в методе транзакции add вызывается метод транзакции doOtherThing, но метод транзакции doOtherThing вызывается в другом потоке.

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

Если вы читали исходный код транзакций Spring, возможно, вы знаете, что Spring-транзакции реализуются через соединения с базой данных. Карта сохраняется в текущем потоке, ключ — это источник данных, а значение — это подключение к базе данных.

private static final ThreadLocal<Map<Object, Object>> resources =

  new NamedThreadLocal<>("Transactional resources");

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

6. Таблица не поддерживает транзакции

Как мы все знаем, до mysql5 движком базы данных по умолчанию былоmyisam.

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

В некоторых старых проектах он все еще может использоваться.

При создании таблицы вам нужно толькоENGINEпараметр установлен наMyISAMПросто:

CREATE TABLE `category` (
  `id` bigint NOT NULL AUTO_INCREMENT,
  `one_category` varchar(20) COLLATE utf8mb4_bin DEFAULT NULL,
  `two_category` varchar(20) COLLATE utf8mb4_bin DEFAULT NULL,
  `three_category` varchar(20) COLLATE utf8mb4_bin DEFAULT NULL,
  `four_category` varchar(20) COLLATE utf8mb4_bin DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=MyISAM AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin

myisam прост в использовании, но есть фатальная проблема:不支持事务.

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

Кроме того, myisam еще не поддерживает блокировки строк и внешние ключи.

Таким образом, в реальных бизнес-сценариях myisam почти не используется. После mysql5 myisam постепенно ушел со сцены истории, заменившись innodb.

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

7. Сделка не открывается

Иногда основная причина того, что транзакция не вступила в силу, заключается в том, что транзакция не была запущена.

Эта фраза может показаться вам смешной.

Разве открытие транзакции не самая основная функция в проекте?

Почему транзакция еще не началась?

Да, если проект построен, функция транзакций должна быть.

Но если вы строите демонстрационную версию проекта, там только одна таблица, и транзакция этой таблицы не действует. Так что может быть причиной?

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

Если вы используете проект Springboot, вам повезло. Потому что Springboot проходитDataSourceTransactionManagerAutoConfigurationКласс молча начал транзакцию для вас.

Все, что вам нужно сделать, просто настроитьspring.datasourceсопутствующие параметры.

Но если вы все еще используете традиционный проект Spring, вам необходимо вручную настроить параметры, связанные с транзакциями, в файле applicationContext.xml. Если вы забудете настроить, транзакция точно не вступит в силу.

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

   
<!-- 配置事务管理器 --> 
<bean class="org.springframework.jdbc.datasource.DataSourceTransactionManager" id="transactionManager"> 
    <property name="dataSource" ref="dataSource"></property> 
</bean> 
<tx:advice id="advice" transaction-manager="transactionManager"> 
    <tx:attributes> 
        <tx:method name="*" propagation="REQUIRED"/>
    </tx:attributes> 
</tx:advice> 
<!-- 用切点把事务切进去 --> 
<aop:config> 
    <aop:pointcut expression="execution(* com.susan.*.*(..))" id="pointcut"/> 
    <aop:advisor advice-ref="advice" pointcut-ref="pointcut"/> 
</aop:config> 

Говорите молча, если точка входа в теге pointcut соответствует правилу, если совпадение неверное, некоторые типы транзакций не вступят в силу.

Две транзакции не откатываются

1. Характеристики распространения ошибок

На самом деле мы используем@TransactionalПри аннотации можно указатьpropagationпараметр.

Функция этого параметра — указать характеристики распространения транзакции.В настоящее время Spring поддерживает 7 характеристик распространения:

  • REQUIREDЕсли в текущем контексте есть транзакция, присоединиться к транзакции, если транзакции нет, создать транзакцию, которая является значением свойства распространения по умолчанию.
  • SUPPORTSЕсли в текущем контексте есть транзакция, транзакция поддерживается для присоединения к транзакции, если транзакции нет, она выполняется нетранзакционным способом.
  • MANDATORYЕсли в текущем контексте есть транзакция, в противном случае генерируется исключение.
  • REQUIRES_NEWКаждый раз создается новая транзакция, при этом транзакция в контексте приостанавливается.После завершения выполнения текущей новой транзакции контекстная транзакция возобновляется и выполняется.
  • NOT_SUPPORTEDЕсли в текущем контексте есть транзакция, текущая транзакция приостанавливается, а новый метод выполняется в среде без транзакции.
  • NEVERГенерирует исключение, если в текущем контексте есть транзакция, в противном случае выполняет код в нетранзакционном контексте.
  • NESTEDЕсли в текущем контексте есть транзакция, выполняется вложенная транзакция, если транзакции нет, создается новая транзакция.

Если мы неправильно задаем характеристики распространения при ручной настройке параметров распространения, например:

@Service
public class UserService {

    @Transactional(propagation = Propagation.NEVER)
    public void add(UserModel userModel) {
        saveData(userModel);
        updateData(userModel);
    }
}

Мы видим, что функция распространения транзакций метода add определена как Propagation.NEVER, Этот тип функции распространения не поддерживает транзакции, и при наличии транзакции будет выдано исключение.

В настоящее время только эти три характеристики распространения будут создавать новые транзакции: REQUIRED, REQUIRES_NEW, NESTED.

2. Проглотите исключение самостоятельно

Транзакция не будет откатана, самая распространенная проблема: разработчик вручную пытается... поймать исключение в коде. Например:

@Slf4j
@Service
public class UserService {
    
    @Transactional
    public void add(UserModel userModel) {
        try {
            saveData(userModel);
            updateData(userModel);
        } catch (Exception e) {
            log.error(e.getMessage(), e);
        }
    }
}

Spring транзакцию в этом случае конечно же не откатят, потому что разработчик поймал исключение, а не кинул его вручную, иными словами, исключение было проглочено.

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

3. Вручную генерировать другие исключения

Даже если разработчик не поймает исключение вручную, если выброшенное исключение неверно, транзакция Spring не будет отброшена.

@Slf4j
@Service
public class UserService {
    
    @Transactional
    public void add(UserModel userModel) throws Exception {
        try {
             saveData(userModel);
             updateData(userModel);
        } catch (Exception e) {
            log.error(e.getMessage(), e);
            throw new Exception(e);
        }
    }
}

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

Из-за весенних транзакций по умолчанию только откатRuntimeException(исключение во время выполнения) иError(ошибка), для обычного исключения (исключение, не связанное с выполнением) откат не выполняется.

4. Индивидуальное исключение отката

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

Но если значение этого параметра установлено неправильно, это приведет к некоторым необъяснимым проблемам, таким как:

@Slf4j
@Service
public class UserService {
    
    @Transactional(rollbackFor = BusinessException.class)
    public void add(UserModel userModel) throws Exception {
       saveData(userModel);
       updateData(userModel);
    }
}

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

Несмотря на то, что rollbackFor имеет значение по умолчанию, спецификация разработчика Alibaba по-прежнему требует, чтобы разработчики повторно указывали этот параметр.

Почему это?

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

5. Откат большего количества вложенных транзакций

public class UserService {

    @Autowired
    private UserMapper userMapper;

    @Autowired
    private RoleService roleService;

    @Transactional
    public void add(UserModel userModel) throws Exception {
        userMapper.insertUser(userModel);
        roleService.doOtherThing();
    }
}

@Service
public class RoleService {

    @Transactional(propagation = Propagation.NESTED)
    public void doOtherThing() {
        System.out.println("保存role表数据");
    }
}

В этом случае используются вложенные внутренние транзакции.Первоначально при возникновении исключения при вызове метода roleService.doOtherThing будет откатываться только содержимое в методе doOtherThing, а содержимое в userMapper.insertUser не будет откатываться, то есть точка сохранения будет откатана. . Но дело в том, что insertUser тоже откатывается.

why?

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

Как можно просто откатить точку сохранения?

@Slf4j
@Service
public class UserService {

    @Autowired
    private UserMapper userMapper;

    @Autowired
    private RoleService roleService;

    @Transactional
    public void add(UserModel userModel) throws Exception {

        userMapper.insertUser(userModel);
        try {
            roleService.doOtherThing();
        } catch (Exception e) {
            log.error(e.getMessage(), e);
        }
    }
}

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

Три других

1 большая проблема бизнеса

При использовании транзакций spring возникает очень неприятная проблема, то есть проблема больших транзакций.

Обычно мы бы@TransactionalАннотируйте, добавляйте функции транзакций, такие как:

@Service
public class UserService {
    
    @Autowired 
    private RoleService roleService;
    
    @Transactional
    public void add(UserModel userModel) throws Exception {
       query1();
       query2();
       query3();
       roleService.save(userModel);
       update(userModel);
    }
}


@Service
public class RoleService {
    
    @Autowired 
    private RoleService roleService;
    
    @Transactional
    public void save(UserModel userModel) throws Exception {
       query4();
       query5();
       query6();
       saveData(userModel);
    }
}

но@TransactionalАннотации, добавленные к методам, имеют тот недостаток, что весь метод включается в транзакцию.

В приведенном выше примере в классе UserService только эти две строки действительно нуждаются в транзакции:

roleService.save(userModel);
update(userModel);

В классе RoleService транзакция нужна только этой строке:

saveData(userModel);

Текущий способ записи приведет к тому, что все методы запроса будут включены в одну и ту же транзакцию.

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

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

Недавно я случайно получил заметку о чистке, написанную крупным производителем BAT, которая открыла мне сразу вторую линейку Ren и Du, и я все больше чувствую, что алгоритм не так сложен, как я себе представлял.

Заметки о чистке, написанные боссом BAT, позвольте мне мягко получить предложение

2. Программные транзакции

Описанное выше содержание основано на@TransactionalАннотированный, в основном говорящий о проблеме транзакции, мы называем этот вид транзакции:声明式事务.

На самом деле, Spring также предоставляет другой способ создания транзакции, то есть транзакцию, реализованную путем ручного написания кода, мы называем эту транзакцию:编程式事务. Например:


   @Autowired
   private TransactionTemplate transactionTemplate;
   
   ...
   
   public void save(final User user) {
         queryData1();
         queryData2();
         transactionTemplate.execute((status) => {
            addData1();
            updateData2();
            return Boolean.TRUE;
         })
   }

Для поддержки программных транзакций в spring предусмотрен специальный класс TransactionTemplate, который реализует функцию транзакций в своем методе execute.

По сравнению с@TransactionalАннотируя декларативные транзакции, я рекомендую вам использовать его, исходя изTransactionTemplateпрограммные транзакции. Основные причины следующие:

  1. Избегайте проблемы сбоя транзакции из-за проблем Spring AOP.
  2. Объем транзакции можно контролировать с меньшей степенью детализации, что более интуитивно понятно.

Рекомендуется использовать меньше аннотаций @Transactional для запуска транзакций в проектах. Но это не значит, что его нельзя использовать.Если какая-то бизнес-логика в проекте относительно проста и не меняется часто, можно использовать аннотацию @Transactional для открытия транзакции, потому что это проще и более эффективен в разработке, но будьте осторожны с проблемой сбоя транзакций.