N Szenarien von Spring-Transaktionsfehlern

Das Spring-Team empfiehlt die Verwendung der Annotation @Transactional für bestimmte Klassen (oder Methoden von Klassen) und nicht für Schnittstellen, die die Klasse implementiert.

Die Verwendung der Annotation @Transactional auf einer Schnittstelle wird nur wirksam, wenn Sie einen schnittstellenbasierten Proxy einrichten.

Da Anmerkungen nicht vererbt werden, bedeutet dies, dass bei Verwendung von klassenbasierten Proxys Transaktionseinstellungen nicht von klassenbasierten Proxys erkannt werden und Objekte nicht von Transaktionsproxys umschlossen werden.

  1. Die Datenbank-Engine unterstützt keine Transaktionen.

Beispielsweise unterstützt die häufig verwendete MySQL-Engine MyISAM keine Transaktionsoperationen, während die InnoDB-Engine Transaktionen unterstützen kann.

Vor MySQL 5 war MySAM die Standard-Datenbank-Engine. Seine Vorteile: Indexdateien und Datendateien werden separat gespeichert, und die Leistung ist besser als InnoDB für Einzeltabellenoperationen mit mehr Abfragen und weniger Schreibvorgängen.

In einigen alten Projekten kann es noch verwendet werden.

Beim Erstellen einer Tabelle müssen Sie nur den Parameter ENGINE auf MyISAM setzen :

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 ist einfach zu bedienen, aber es gibt ein sehr fatales Problem: Es unterstützt keine Transaktionen.

Wenn es sich nur um eine einzelne Tabellenoperation handelt, ist es in Ordnung, und es wird nicht zu viele Probleme geben. Wenn Sie jedoch über mehrere Tabellen hinweg arbeiten müssen, da Transaktionen nicht unterstützt werden, sind die Daten wahrscheinlich unvollständig.

Außerdem unterstützt myisam keine Zeilensperren und Fremdschlüssel.

In tatsächlichen Geschäftsszenarien wird myisam also nicht viel verwendet. Nach mysql5 hat sich myisam allmählich von der Bühne der Geschichte zurückgezogen und durch innodb ersetzt.

Während des Entwicklungsprozesses stellen wir manchmal fest, dass die Transaktion einer bestimmten Tabelle nicht wirksam wurde. Dies muss nicht unbedingt an der Frühlingstransaktion liegen. Am besten überprüfen Sie, ob die von Ihnen verwendete Tabelle Transaktionen unterstützt.
  1. Die Eingabemethode muss öffentlich sein

Wie wir alle wissen, gibt es vier Haupttypen von Zugriffsberechtigungen für Java: privat, standardmäßig, geschützt und öffentlich, und ihre Berechtigungen nehmen von links nach rechts zu.

Berechtigungen

innerhalb der Klasse

Gleiches Paket

verschiedene Brötchensorten

anderes Paket, keine Unterklasse

Privatgelände

×

×

×

Standard

×

×

geschützt

×

öffentlich

Wenn wir jedoch während des Entwicklungsprozesses falsche Zugriffsberechtigungen für bestimmte Transaktionsmethoden definieren, führt dies zu Problemen mit der Transaktionsfunktion, z. B.:

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

Wir können sehen, dass die Zugriffsberechtigung der add-Methode als privat definiert ist, was dazu führt, dass die Transaktion fehlschlägt, und spring erfordert, dass die Proxy-Methode öffentlich sein muss.

Um es ganz klar zu sagen, es gibt ein Urteil in der computeTransactionAttribute-Methode der AbstractFallbackTransactionAttributeSource-Klasse: Wenn die Zielmethode nicht öffentlich ist, gibt TransactionAttribute null zurück, das heißt, Transaktionen werden nicht unterstützt.

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;
  }

Das heißt, wenn unsere benutzerdefinierte Transaktionsmethode (d. h. die Zielmethode) andere Zugriffsrechte als public , aber private, default oder protected hat, stellt spring keine Transaktionsfunktionen bereit.

3. Die Methode wird mit final modifiziert

Manchmal möchte eine Methode nicht von Unterklassen umgeschrieben werden, dann kann die Methode als final definiert werden. Es ist kein Problem, gewöhnliche Methoden wie diese zu definieren, aber wenn die Transaktionsmethode als final definiert ist, zum Beispiel:

@Service
public class UserService {

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

Wir können sehen, dass die add-Methode als final definiert ist, was dazu führt, dass die Transaktion fehlschlägt.

Warum?

Wenn Sie den Quellcode von Spring Transactions gelesen haben, wissen Sie vielleicht, dass aop am Ende von Spring Transactions verwendet wird, d. h. über jdk dynamic proxy oder cglib hilft es uns, Proxy-Klassen zu generieren und Transaktionsfunktionen in Proxy-Klassen zu implementieren.

Wenn eine Methode jedoch mit final geändert wird, kann die Methode in ihrer Proxy-Klasse nicht umgeschrieben werden, um Transaktionsfunktionen hinzuzufügen.

Hinweis: Wenn eine Methode statisch ist, kann sie auch nicht über einen dynamischen Proxy zu einer Transaktionsmethode werden.

4. Methodeninterner Aufruf

Manchmal müssen wir eine andere Transaktionsmethode in einer Methode einer Service-Klasse aufrufen, wie zum Beispiel:

@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生成代理了对象,同一个类中方法内部调用,绕过了代理对象,所以事务失效。

由此可见,在同一个类中的方法直接内部调用,会导致事务失效。

那么问题来了,如果有些场景,确实想在同一个类的某个方法中,调用它自己的另外一个方法,该怎么办呢?

4.1 新加一个Service方法

这个方法非常简单,只需要新加一个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();
    }

 }

4.2 在该Service类中注入自己

如果不想再新加一个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();
    }
 }

可能有些人可能会有这样的疑问:这种做法会不会出现循环依赖问题?

答案:不会。

参考文章

https://blog.csdn.net/JiuQianWan/article/details/126031360

4.3 调用同一个类中方法前,先获取代理对象。

  1. 先引入aspactj的依赖:

          <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-aop</artifactId>
        </dependency>
 在调用b,c方法之前通过切面重新获得此对象的代理AServiceImpl aService = AopContext.currentProxy();
 可以解决这个问题。
 代码如下:
 class AServiceImpl{

    @Transactional
    public void a() {
        AServiceImpl aService = AopContext.currentProxy();
       aService.b();
       aService.c();
    }
    @Transactional(propagation = Propagation.REQUIRED,timeout = 2)
    public void b() {
        System.out.println("b");
    }

    @Transactional(propagation = Propagation.REQUIRES_NEW,timeout = 2)
    public void c() {
        System.out.println("c");
    }
  }

5.未被spring管理

在我们平时开发过程中,有个细节很容易被忽略。即使用spring事务的前提是:对象要被spring管理,需要创建bean实例。

通常情况下,我们通过@Controller、@Service、@Component、@Repository等注解,可以自动实现bean实例化和依赖注入的功能。

如果有一天,你匆匆忙忙的开发了一个Service类,但忘了加@Service注解,比如:

//@Service
public class UserService {

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

从上面的例子,我们可以看到UserService类没有加@Service注解,那么该类不会交给spring管理,所以它的add方法也不会生成事务。

6.多线程调用

在实际项目开发中,多线程的使用场景还是挺多的。如果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的事务是通过数据库连接来实现的。当前线程中保存了一个map,key是数据源,value是数据库连接。

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

  new NamedThreadLocal<>("Transactional resources");

我们说的同一个事务,其实是指同一个数据库连接,只有拥有同一个数据库连接才能同时提交和回滚。如果在不同的线程,拿到的数据库连接肯定是不一样的,所以是不同的事务。

7.未开启事务

有时候,事务没有生效的根本原因是没有开启事务。

你看到这句话可能会觉得好笑。

开启事务不是一个项目中,最最最基本的功能吗?

为什么还会没有开启事务?

没错,如果项目已经搭建好了,事务功能肯定是有的。

但如果你是在搭建项目demo的时候,只有一张表,而这张表的事务没有生效。那么会是什么原因造成的呢?

当然原因有很多,但没有开启事务,这个原因极其容易被忽略。

如果你使用的是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标签中的切入点匹配规则,配错了的话,有些类的事务也不会生效。

8.错误的传播特性

其实,我们在使用@Transactional注解时,是可以指定propagation参数的。

该参数的作用是指定事务的传播特性,spring目前支持7种传播特性:

REQUIRED 如果当前上下文中存在事务,那么加入该事务,如果不存在事务,创建一个事务,这是默认的传播属性值。

SUPPORTS 如果当前上下文存在事务,则支持事务加入事务,如果不存在事务,则使用非事务的方式执行。

MANDATORY 如果当前上下文中存在事务,否则抛出异常。

REQUIRES_NEW 每次都会新建一个事务,并且同时将上下文中的事务挂起,执行当前新建事务完成以后,上下文事务恢复再执行。

NOT_SUPPORTED 如果当前上下文中存在事务,则挂起当前事务,然后新的方法在没有事务的环境中执行。

NEVER 如果当前上下文中存在事务,则抛出异常,否则在无事务环境上执行代码。

NESTED 如果当前上下文中存在事务,则嵌套事务执行,如果不存在事务,则新建事务。

如果我们在手动设置propagation参数的时候,把传播特性设置错了,比如:

@Service
public class UserService {

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

我们可以看到add方法的事务传播特性定义成了Propagation.NEVER,这种类型的传播特性不支持事务,如果有事务则会抛异常。

目前只有这三种传播特性才会创建新事务:REQUIRED,REQUIRES_NEW,NESTED。

9.自己吞了异常

事务不会回滚,最常见的问题是:开发者在代码中手动try…catch了异常。比如:

@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认为程序是正常的。

10.手动抛了别的异常

即使开发者没有手动捕获异常,但如果抛的异常不正确,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);
        }
    }
}

上面的这种情况,开发人员自己捕获了异常,又手动抛出了异常:Exception,事务同样不会回滚。

因为spring事务,默认情况下只会回滚RuntimeException(运行时异常)和Error(错误),对于普通的Exception(非运行时异常),它不会回滚。

Java中的异常

https://blog.csdn.net/weixin_54401017/article/details/129628520?csdn_share_tail=%7B%22type%22%3A%22blog%22%2C%22rType%22%3A%22article%22%2C%22rId%22%3A%22129628520%22%2C%22source%22%3A%22weixin_54401017%22%7D

11.自定义了回滚异常

在使用@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有默认值,但阿里巴巴开发者规范中,还是要求开发者重新指定该参数。

rollbackFor默认值为UncheckedException,包括了RuntimeException和Error.

当我们直接使用@Transactional不指定rollbackFor时,Exception及其子类都不会触发回滚。

所以,建议一般情况下,将该参数设置成:Exception或Throwable。

12.嵌套事务回滚多了

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方法出现了异常,没有手动捕获,会继续往上抛,到外层add方法的代理方法中捕获了异常。所以,这种情况是直接回滚了整个事务,不只回滚单个保存点。

怎么样才能只回滚保存点呢?

@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);
        }
    }
}

因为doOtherThing方法出现了异常,没有手动捕获,会继续往上抛,到外层add方法的代理方法中捕获了异常。所以,这种情况是直接回滚了整个事务,不只回滚单个保存点。

Guess you like

Origin blog.csdn.net/weixin_54401017/article/details/129598621