Spring 常见问题

1. Spring Bean的线程安全问题

在Spring框架中,Bean的线程安全性是开发中常见的问题。Spring容器中的默认Bean是**单例(singleton)**作用域,这意味着所有线程都共享同一个实例。因此,单例Bean会面临多线程访问时的线程安全问题。

常见问题和解决方案:

  1. 单例模式中的线程安全问题

    • 由于单例模式的Bean会在多个请求中共享同一个实例,因此,当多个线程访问这些Bean时,修改实例中的可变状态可能会导致线程安全问题。
    • 例如,SingletonController类中的number变量是共享的。当多个线程并发访问/api/number1/api/number2时,可能会导致并发冲突。

    解决方案

    • 使用@Scope("prototype")来确保每次请求都会创建新的实例,避免多线程访问时共享同一个实例。或者,将可变的状态使用ThreadLocal进行存储,每个线程使用自己的独立副本。
  2. 原型模式的线程安全

    • 使用@Scope("prototype")可以创建多个实例,每个请求都会创建一个新的Bean实例。例如,在PrototypeController中,number变量在每次请求中都会被重新初始化,因此没有共享状态,也就没有线程安全问题。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    @Scope(value = "prototype")
    @RestController
    public class PrototypeController {

    private int number = 0;

    @GetMapping(value = "/api/v2/number1")
    public int number1() {
    number ++;
    System.out.println(this.number);
    return this.number;
    }

    @GetMapping(value = "/api/v2/number2")
    public int number2() {
    number ++;
    System.out.println(this.number);
    return this.number;
    }
    }
  3. 线程安全控制:使用ThreadLocal来确保每个线程都能拥有独立的变量副本。例如,在ThreadSafeController中使用ThreadLocal来保证每个线程的numberThreadLocal变量独立。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    @RestController
    public class ThreadSafeController {

    private ThreadLocal<Integer> numberThreadLocal = ThreadLocal.withInitial(() -> 0);

    @GetMapping(value = "/api/v3/number1")
    public int number() {
    numberThreadLocal.set(1);
    System.out.println(numberThreadLocal.get());
    return numberThreadLocal.get();
    }

    @GetMapping(value = "/api/v3/number2")
    public int number2() {
    System.out.println(numberThreadLocal.get());
    return numberThreadLocal.get();
    }
    }

2. Spring事务管理

Spring提供了@Transactional注解来实现事务管理,但是有一些常见的错误和注意事项。

常见事务管理问题和解决方案:

  1. 事务回滚

    • 默认情况下,Spring只会在遇到RuntimeException及其子类时回滚事务。如果抛出了IOException等受检查异常(checked exception),则不会触发回滚。

    解决方案

    • 如果需要在IOException等异常时回滚事务,可以通过@TransactionalrollbackFor属性来指定回滚规则。
    1
    2
    3
    4
    @Transactional(rollbackFor = IOException.class)
    public int reduceStore(Integer id) throws IOException {
    // 代码逻辑
    }
    • 如图所示,如果方法抛出了IOException,需要显式声明rollbackFor = IOException.class才能触发事务回滚。
  2. 多线程中的事务失效

    • 事务通常是绑定到当前线程的。在reduceStore方法中,如果开启了一个新线程来执行代码(如使用new Thread(() -> {...}).start();),事务不会随着新线程执行,因为Spring事务管理是基于代理的,不会跨线程传播。

    解决方案

    • 避免在事务方法中启动新线程,或者在新线程中手动管理事务。如果需要跨线程事务支持,可能需要使用分布式事务或手动控制事务边界。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    @Transactional(rollbackFor = Exception.class)
    public int reduceStore(Integer id) {
    int update = 0;

    Goods goods = goodsDao.queryById(id);
    Integer store = goods.getStore();
    if (store > 0) {
    update = goodsDao.updateStore(id);
    }

    new Thread(() -> {
    // 在此处启动新线程时,事务不会传播到新线程
    int b = 10 / 0;
    }).start();

    return update;
    }
  3. 传播行为

    • Spring的事务传播行为决定了方法调用时事务的传播方式。例如,Propagation.NOT_SUPPORTED表示当前方法执行时不支持事务,这意味着即使方法是在一个事务中被调用的,它也会在没有事务的情况下执行。
    1
    2
    3
    4
    @Transactional(propagation = Propagation.NOT_SUPPORTED)
    public int m(Integer id) {
    // 代码逻辑
    }
    • 事务传播行为的使用场景:如果方法

需要执行的操作与事务无关,可以使用NOT_SUPPORTED。这确保了当前方法不会影响事务的状态,从而可以避免不必要的事务开销。

3. Spring事务失败的常见原因

  1. 异常类型错误

    • 默认回滚:Spring默认只会在抛出RuntimeException及其子类时触发事务回滚。而checked exception(受检查的异常)默认不会触发回滚。
    • 解决方法:如果希望Spring回滚受检查的异常,可以使用@Transactional(rollbackFor = Exception.class)显式指定需要回滚的异常类型。
    1
    2
    3
    4
    @Transactional(rollbackFor = IOException.class)
    public void processFile() throws IOException {
    // 如果发生IOException,将触发事务回滚
    }
  2. 方法或类上没有标注@Transactional注解

    • Spring事务管理只会对标有@Transactional的类或方法生效。如果方法没有标注@Transactional,Spring不会管理该方法的事务。
    • 解决方法:确保所有需要事务管理的方法都标注了@Transactional注解。
    1
    2
    3
    4
    @Transactional
    public void transferMoney() {
    // 方法会受到Spring事务管理
    }
  3. 同一类中,方法内部自调用

    • 由于Spring事务是基于代理模式实现的,事务管理是通过代理对象拦截方法调用的。如果方法内部自调用,Spring的代理无法拦截,因此事务不会生效。
    • 解决方法:避免在同一个类内自调用事务方法,或者在不同的类中调用方法来触发事务。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    @Transactional
    public void methodA() {
    this.methodB(); // 事务不会生效,因为是自调用
    }

    @Transactional
    public void methodB() {
    // 事务逻辑
    }
  4. 事务方法不是public

    • Spring事务管理只能拦截public方法,因为事务代理是通过CGLIB或JDK动态代理实现的,而代理对象只能拦截public方法。
    • 解决方法:确保所有需要事务管理的方法是public的。
    1
    2
    3
    4
    @Transactional
    private void privateMethod() { // 事务不会生效,因为方法是私有的
    // 事务逻辑
    }
  5. 多线程调用

    • 事务是绑定到当前线程的,如果在事务方法中启动新线程,事务无法传播到新线程,导致事务失效。
    • 解决方法:避免在事务方法中创建新线程,或者在新线程中手动管理事务。
    1
    2
    3
    4
    5
    6
    @Transactional
    public void processOrder() {
    new Thread(() -> {
    // 新线程中的事务不会生效
    }).start();
    }
  6. 异常被try...catch捕获

    • 如果事务方法中的异常被try...catch捕获,Spring事务管理就无法感知到异常,从而无法触发回滚。
    • 解决方法:不要在事务方法中捕获不必要的异常,或者捕获后确保异常被重新抛出。
    1
    2
    3
    4
    5
    6
    7
    8
    @Transactional
    public void process() {
    try {
    int result = 10 / 0;
    } catch (Exception e) {
    e.printStackTrace(); // 捕获异常会导致事务失效
    }
    }
  7. 手动抛了别的异常

    • Spring事务默认只会回滚RuntimeExceptionError,如果手动抛出其他异常(如IOException),默认不会触发事务回滚。
    • 解决方法:确保事务回滚规则适配你抛出的异常,或者使用@TransactionalrollbackFor指定回滚类型。
    1
    2
    3
    4
    @Transactional(rollbackFor = IOException.class)
    public void processFile() throws IOException {
    // 手动抛出IOException时,事务会回滚
    }
  8. 事务方法所在的Bean未被Spring容器管理

    • 事务是由Spring容器管理的,如果事务方法所在的Bean没有被Spring管理(例如直接通过new创建的对象),则事务管理不会生效。
    • 解决方法:确保事务方法所在的类是由Spring容器管理的Bean,不能直接通过new实例化。
  9. 方法的事务传播类型不支持事务

    • Spring事务提供了多种传播行为,例如Propagation.NOT_SUPPORTED表示当前方法不支持事务。如果方法设置了不支持事务的传播行为,事务管理就会失效。
    • 解决方法:检查方法的传播行为,确保需要事务的操作使用合适的传播行为。
    1
    2
    3
    4
    @Transactional(propagation = Propagation.NOT_SUPPORTED)
    public void doNotSupportTransaction() {
    // 当前方法不参与事务
    }
  10. 表的数据库引擎不支持事务

  • 某些数据库引擎(例如MySQL的MyISAM引擎)不支持事务,因此即使Spring事务管理正常配置,事务也不会生效。
  • 解决方法:确保使用支持事务的数据库引擎(例如MySQL的InnoDB引擎)。
1
2
-- 确保使用支持事务的数据库引擎,如 InnoDB
ALTER TABLE your_table ENGINE = InnoDB;

4. Spring事务回滚机制与异常

Spring事务的回滚机制默认只会在遇到RuntimeException时回滚。如果想让某些受检查的异常(如IOException)触发回滚,需要显式指定rollbackFor属性。

Spring默认回滚的异常

  • 默认情况下,@Transactional只会回滚RuntimeException及其子类。

显式指定回滚的异常

  • 如果方法中会抛出受检查异常,如IOException,可以通过@Transactional(rollbackFor = IOException.class)指定回滚。
1
2
3
4
@Transactional(rollbackFor = IOException.class)
public int processOrder() throws IOException {
// 代码逻辑
}