面试官:请列举 Spring 的事务会失效的场景

发布时间 2024-01-11 19:12:36作者: 咬到舌头的小蛇

在日常工作中,如果对 Spring 的事务管理功能使用不当,则会造成 Spring 事务不生效的问题。而针对 Spring 事务不生效的问题,也是在跳槽面试中被问的比较频繁的一个问题。
今天,我们就一起梳理下有哪些场景会导致 Spring 事务失效。

Spring 事务失效的8中场景

下面就举例说明这8种失效场景及解决方法

1.使用不支持事务的存储引擎

Spring 事务生效的前提是所连接的数据库要支持事务,如果底层的数据库都不支持事务,则 Spring 的事务肯定会失效。例如,如果使用的数据库为 MySQL,并且选用了 MyISAM 存储引擎,则 Spring 的事务就会失效。

解决方法:使用MySQL中的InnoDB存储引擎就支持事务

2.抛出检查异常导致事务不能正确回滚

以下是一个示例,演示了抛出检查异常导致事务不能正确回滚的情况:

public class UserService {  
  
    @Autowired  
    private JdbcTemplate jdbcTemplate;  
  
    @Transactional(propagation = Propagation.REQUIRES_NEW)  
    public void saveUser(User user) {  
        try {  
            jdbcTemplate.update("INSERT INTO users (name, age) VALUES (?, ?)", user.getName(), user.getAge());  
            // 抛出检查异常,事务将不会回滚  
            throw new Exception("模拟检查异常");  
        } catch (Exception e) {  
            // 异常处理逻辑  
            e.printStackTrace();  
        }  
    }  
}

在上面的示例中,saveUser()方法被标记为@Transactional,并指定了propagation = Propagation.REQUIRES_NEW传播行为。这意味着该方法必须在一个新的事务中运行。如果在执行插入操作后抛出了检查异常(Exception),事务将不会回滚。这是因为检查异常是开发者可以预见的异常,并且开发者通过捕获并处理这些异常来控制程序的流程。因此,事务管理器不会回滚事务,以保持数据库的一致性。

解决方法:使用运行时异常:在Spring框架中,建议使用RuntimeException或其子类作为事务方法中抛出的异常。RuntimeException是未检查异常的子类,因此不会导致事务回滚。相反,检查异常(即那些直接或间接继承自Exception的异常)会导致事务回滚。

3.业务方法内自己 try-catch导致事务不能正确回滚


public class UserService {  
  
    @Autowired  
    private JdbcTemplate jdbcTemplate;  
  
    @Transactional(propagation = Propagation.REQUIRES_NEW)  
    public void saveUser(User user) {  
        try {  
            jdbcTemplate.update("INSERT INTO users (name, age) VALUES (?, ?)", user.getName(), user.getAge());  
            // 模拟抛出异常,但被try-catch捕获并静默处理  
            throw new Exception("模拟异常");  
        } catch (Exception e) {  
            // 异常被捕获并静默处理,事务不会回滚  
            e.printStackTrace();  
        }  
    }  
}

在上面的示例中,saveUser()方法被标记为@Transactional,并指定了propagation = Propagation.REQUIRES_NEW传播行为。这意味着该方法必须在一个新的事务中运行。在try块中,我们执行了一个插入操作,然后模拟抛出了一个异常。这个异常被catch块捕获,并静默处理(只是打印堆栈跟踪)。由于异常被静默处理,事务不会回滚。

解决方法: 要避免这种情况,你应该确保在事务方法中捕获的异常被适当地向外抛出,以便Spring的事务管理器可以检测到异常并回滚事务。你可以选择抛出运行时异常或检查异常,但重要的是要确保异常被正确地传递给调用者,以便于调试和错误处理。

4.非public方法导致的事务时效

当事务方法被标记为非public时,会导致事务失效。这是因为在Spring的声明式事务管理机制中,代理类只能代理public方法。如果方法被声明为非public,代理类无法访问该方法,从而导致事务失效。
以下是一个示例,演示了非public方法导致的事务失效场景:


public class UserService {  
  
    @Autowired  
    private JdbcTemplate jdbcTemplate;  
  
    @Transactional  
    private void saveUser(User user) {  
        jdbcTemplate.update("INSERT INTO users (name, age) VALUES (?, ?)",  user.getName(), user.getAge());  
    }  
}

在上面的示例中,saveUser()方法被声明为private,导致Spring的代理类无法访问该方法。因此,事务失效,并且无法正确地回滚事务。要解决这个问题,你可以将方法声明为public,以确保Spring的代理类可以访问该方法并正确地管理事务。
解决方法: 使用public方法

5.@Transactional没有保证原子行为

当事务方法中存在SELECT方法时,Spring的@Transactional注解无法保证原子性。这是因为SELECT方法不会阻塞,事务的原子性仅仅涵盖INSERT、UPDATE、DELETE、SELECT...FOR UPDATE语句。
以下是一个示例,演示了@Transactional没有保证原子行为导致的事务失效场景:


public class UserService {  
  
    @Autowired  
    private JdbcTemplate jdbcTemplate;  
  
    @Transactional  
    public void updateUser(User user) {  
        // SELECT方法不会阻塞,事务的原子性无法保证  
        User existingUser = jdbcTemplate.queryForObject("SELECT * FROM users WHERE id = ?", user.getId());  
        // 更新操作  
        jdbcTemplate.update("UPDATE users SET name = ? WHERE id = ?", user.getName(), user.getId());  
    }  
}

在上面的示例中,事务方法中包含了一个SELECT方法,用于查询用户信息。然后执行了一个更新操作。由于SELECT方法不会阻塞,事务的原子性无法得到保证。如果其他线程在SELECT和UPDATE之间修改了数据,可能会出现数据不一致的情况。要解决这个问题,你可以考虑使用其他方式来保证原子性,例如使用数据库锁或使用Spring的事务传播行为。
解决方法:

  1. 使用数据库锁:通过数据库锁来保证多个操作在一个事务中的原子性。你可以使用数据库提供的锁机制,例如行锁或表锁,来确保在事务中的操作不会被其他线程干扰。
  2. 修改存储引擎:将数据库的存储引擎改为InnoDB,而不是默认的MyISAM。InnoDB引擎支持事务,并提供了行级锁定和外键约束等特性,可以更好地保证数据的一致性和完整性。
  3. 使用Spring的事务传播行为:通过设置@Transactional注解的propagation属性,你可以指定事务的传播行为。例如,你可以设置propagation = Propagation.REQUIRES_NEW,这样每个事务方法都会运行在一个新的事务中,确保其原子性。
  4. 修改SELECT语句:将SELECT语句替换为SELECT...FOR UPDATE语句。这样,在查询时会对选定的行加锁,直到事务结束时才会释放锁,从而避免了其他线程的干扰。
  5. 使用同步机制:在事务方法中使用同步机制,确保同一时间只有一个线程可以执行该方法。这样可以避免并发争抢资源的情况,保证原子性。

6.AOP切面顺序导致事务不能正确回滚

以下是一个例子,展示了由于Spring AOP切面顺序导致事务不能正确回滚的场景:
假设你有一个服务层方法,使用@Transactional注解进行事务管理。在调用该方法之前,你希望先进行日志记录,以便记录方法的调用信息和参数。因此,你使用了AOP切面来实现日志记录功能。

@Service  
public class UserService {  
    @Transactional  
    public void createUser(User user) {  
        // 业务逻辑代码  
    }  
}
@Aspect  
@Component  
public class LoggingAspect {  
    // 定义日志切面  
}  
@Aspect  
@Component  
public class TransactionAspect {  
    // 定义事务切面  
}

在上述示例中,createUser()方法被标记为@Transactional,用于管理事务。同时,你定义了两个切面:日志切面和事务切面。

日志切面:用于记录方法的调用信息和参数。
事务切面:用于管理事务的开始和回滚。

如果在Spring配置中,日志切面在事务切面前执行,那么当createUser()方法抛出异常时,日志切面可能会先捕获到异常并记录日志,而事务切面可能还没有开始事务。这样,事务切面无法正确地回滚事务,导致数据不一致和其他潜在问题。

解决方法: 为了解决这个问题,你可以在Spring配置中明确指定切面的顺序,确保事务切面在日志切面前执行。你可以使用@Order注解或通过XML配置来定义切面的顺序。例如:

@Aspect  
@Component  
@Order(1) // 定义日志切面的顺序为1  
public class LoggingAspect {  
    // 定义日志切面逻辑  
}  
  
@Aspect  
@Component  
@Order(2) // 定义事务切面的顺序为2  
public class TransactionAspect {  
    // 定义事务切面逻辑  
}

7.调用本类方法导致传播行为失效

在Spring事务中,当一个事务方法调用了本类(同一个类)的其他方法时,可能会导致事务的传播行为失效。
下面是一个示例场景,展示了由于调用本类方法导致事务传播行为失效的问题:

假设你有一个服务类UserService,其中包含两个方法:createUser()updateUser()createUser()方法被标记为@Transactional,用于管理事务。

@Service  
public class UserService {  
  
    @Transactional  
    public void createUser(User user) {  
        // 调用updateUser()方法  
        updateUser(user);  
    }  
  
    public void updateUser(User user) {  
        // 更新用户信息的逻辑代码  
    }  
}

在上述示例中,createUser()方法被标记为@Transactional,并调用了本类的updateUser()方法。这意味着,当createUser()方法执行时,它应该在一个事务的上下文中运行。

然而,由于updateUser()方法没有被标记为@Transactional,它不会在事务的上下文中执行。这意味着,如果在updateUser()方法中发生了异常,事务不会回滚,因为事务的传播行为失效了。

解决办法:你可以将需要事务管理的所有方法都标记为@Transactional,或者使用Spring的事务传播行为来指定事务的传播行为。例如,你可以将@Transactional注解的propagation属性设置为Propagation.REQUIRES_NEW,这样每个事务方法都会运行在一个新的事务中。

@Service  
public class UserService {  
  
    @Transactional(propagation = Propagation.REQUIRES_NEW)  
    public void createUser(User user) {  
        // 调用updateUser()方法  
        updateUser(user);  
    }  
  
    @Transactional(propagation = Propagation.REQUIRES_NEW)  
    public void updateUser(User user) {  
        // 更新用户信息的逻辑代码  
    }  
}

8.@Transactional方法导致的synchronized失效

当你在Spring中使用@Transactional注解时,Spring会为你自动管理事务。但是,@Transactional注解并不会将方法同步化,也就是说,它不会将方法标记为synchronized

以下是一个示例场景,展示了由于@Transactional方法导致的synchronized失效的场景:

假设你有两个服务类UserServiceAUserServiceB,它们都包含一个名为updateUser()的方法,该方法使用synchronized关键字进行同步。

@Service  
public class UserServiceA {  
  
    @Transactional  
    public synchronized void updateUser(User user) {  
        // 更新用户信息的逻辑代码  
    }  
}  
  
@Service  
public class UserServiceB {  
  
    @Transactional  
    public synchronized void updateUser(User user) {  
        // 更新用户信息的逻辑代码  
    }  
}

在上述示例中,updateUser()方法被标记为@Transactionalsynchronized。这意味着在多线程环境中,同一时间只能有一个线程调用该方法。

然而,由于@Transactional注解的存在,Spring会为每个事务创建一个新的事务代理对象。这意味着,当两个线程同时调用UserServiceA.updateUser()UserServiceB.updateUser()方法时,它们实际上是两个不同的方法,而不是同一个方法的两个实例。因此,尽管方法被标记为synchronized,但由于事务代理的存在,这两个方法的同步性失效了。

解决方法:updateUser()方法被标记为@Transactional。为了确保同一时间只有一个线程执行该方法的同步代码块,我们使用了一个同步代码块,将this对象作为锁对象。这样,当一个线程进入同步代码块时,其他线程将会被阻塞,直到第一个线程退出同步代码块。

import org.springframework.transaction.annotation.Transactional;  
  
@Service  
public class UserService {  
  
    @Transactional  
    public void updateUser(User user) {  
        // 同步代码块,确保同一时间只有一个线程执行  
        synchronized (this) {  
            // 更新用户信息的逻辑代码  
        }  
    }  
}

通过使用同步代码块,你可以确保同一时间只有一个线程能够访问共享资源,即使事务代理存在,也不会导致synchronized失效。这种方法适用于简单的同步需求,如果你的应用有更复杂的并发控制需求,可能需要考虑其他同步机制或数据库锁等更高级的解决方案。
image