Spring 事务常见错误

Spring 在初始化时,会通过扫描拦截对事务的方法进行增强。如果目标方法存在事务,Spring 就会创建一个 Bean 对应的代理(Proxy)对象,并进行相关的事务处理操作。

1.unchecked 异常与事务回滚

新增学生实例:


public class Student implements Serializable {
    private Integer id;
    private String realname;
    }
}


@Mapper
public interface StudentMapper {
    @Insert("INSERT INTO `student`(`realname`) VALUES (#{realname})")
    void saveStudent(Student student);
}


CREATE TABLE `student` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `realname` varchar(255) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;



@Service
public class StudentService {
    @Autowired
    private StudentMapper studentMapper;

    @Transactional
    public void saveStudent(String realname) throws Exception {
        Student student = new Student();
        student.setRealname(realname);
        studentMapper.saveStudent(student);
        if (student.getRealname().equals("小明")) {
            throw new Exception("该学生已存在");
        }
    }
}


public class AppConfig {
    public static void main(String[] args) throws Exception {
        ApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);
        StudentService studentService = (StudentService) context.getBean("studentService");
        studentService.saveStudent("小明");
    }
}

异常确实被抛出来,但是检查数据库,你会发现数据库里插入了一条新的记录。

我们的常规思维可能是:在 Spring 里,抛出异常,就会导致事务回滚,而回滚以后,是不应该有数据存入数据库才对啊。而在这个案例中,异常也抛了,回滚却没有如期而至,这是什么原因呢?

Spring 在处理事务过程中,并不会对 Exception 进行回滚,而会对 RuntimeException 或者 Error 进行回滚


@Service
public class StudentService {
    @Autowired
    private StudentMapper studentMapper;

    @Transactional
    public void saveStudent(String realname) throws Exception {
        Student student = new Student();
        student.setRealname(realname);
        studentMapper.saveStudent(student);
        if (student.getRealname().equals("小明")) {
            throw new RuntimeException("该用户已存在");
        }
    }

// 也可以使用:
@Transactional(rollbackFor = Exception.class)

2.试图给 private 方法添加事务


@Service
public class StudentService {
    @Autowired
    private StudentMapper studentMapper;

    @Autowired
    private StudentService studentService;

    public void saveStudent(String realname) throws Exception {
        Student student = new Student();
        student.setRealname(realname);
        studentService.doSaveStudent(student);
    }

    @Transactional
    private void doSaveStudent(Student student) throws Exception {
        studentMapper.saveStudent(student);
        if (student.getRealname().equals("小明")) {
            throw new RuntimeException("该用户已存在");
        }
    }
}

以上代码异常正常抛出,事务却没有回滚

  • 只有当注解为事务的方法被声明为 public 的时候,才会被 Spring 处理。
  • 调用这个加了事务注解的方法,必须是调用被 Spring AOP 代理过的方法,也就是不能通过类的内部调用或者通过 this 的方式调用
    修正:

@Service
public class StudentService {
    @Autowired
    private StudentMapper studentMapper;

    @Autowired
    private StudentService studentService;

    public void saveStudent(String realname) throws Exception {
        Student student = new Student();
        student.setRealname(realname);
        studentService.doSaveStudent(student);
    }

    @Transactional
    public void doSaveStudent(Student student) throws Exception {
        studentMapper.saveStudent(student);
        if (student.getRealname().equals("小明")) {
            throw new RuntimeException("该学生已存在");
        }
    }
}

3.嵌套事务回滚错误

假设我们需要对这个功能继续进行扩展,当学生注册完成后,需要给这个学生登记一门英语必修课,并更新这门课的登记学生数。


CREATE TABLE `course` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `course_name` varchar(64) DEFAULT NULL,
  `number` int(11) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;


CREATE TABLE `student_course` (
  `student_id` int(11) NOT NULL,
  `course_id` int(11) NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8;


// 1.新增学生选课记录

@Mapper
public interface StudentCourseMapper {
    @Insert("INSERT INTO `student_course`(`student_id`, `course_id`) VALUES (#{studentId}, #{courseId})")
    void saveStudentCourse(@Param("studentId") Integer studentId, @Param("courseId") Integer courseId);
}


// 2.课程登记学生数 + 1
@Mapper
public interface CourseMapper {
    @Update("update `course` set number = number + 1 where id = #{id}")
    void addCourseNumber(int courseId);
}


@Service
public class CourseService {
    @Autowired
    private CourseMapper courseMapper;

    @Autowired
    private StudentCourseMapper studentCourseMapper;

    //注意这个方法标记了“Transactional”
    @Transactional(rollbackFor = Exception.class)
    public void regCourse(int studentId) throws Exception {
        studentCourseMapper.saveStudentCourse(studentId, 1);
        courseMapper.addCourseNumber(1);
    }
}



@Service
public class StudentService {
  //省略非关键代码
  @Transactional(rollbackFor = Exception.class)
  public void saveStudent(String realname) throws Exception {
      Student student = new Student();
      student.setRealname(realname);
      studentService.doSaveStudent(student);
      try {
          courseService.regCourse(student.getId());
      } catch (Exception e) {
          e.printStackTrace();
      }
  }
  //省略非关键代码
}



@Transactional(rollbackFor = Exception.class)
public void regCourse(int studentId) throws Exception {
    studentCourseMapper.saveStudentCourse(studentId, 1);
    courseMapper.addCourseNumber(1);
    throw new Exception("注册失败");
}

最后的结果是,学生和选课的信息都被回滚了,显然这并不符合我们的预期。我们期待的结果是即便内部事务 regCourse() 发生异常,外部事务 saveStudent() 俘获该异常后,内部事务应自行回滚,不影响外部事务。

  • Spring 声明式的事务处理中,有一个属性 propagation,表示打算对这些方法怎么使用事务,即一个带事务的方法调用了另一个带事务的方法,被调用的方法它怎么处理自己事务和调用方法事务之间的关系。
  • propagation 有 7 种配置:REQUIRED、SUPPORTS、MANDATORY、REQUIRES_NEW、NOT_SUPPORTED、NEVER、NESTED。默认是 REQUIRED,它的含义是:如果本来有事务,则加入该事务,如果没有事务,则创建新的事务
  • 结合我们的伪代码示例,因为在 saveStudent() 上声明了一个外部的事务,就已经存在一个事务了,在 propagation 值为默认的 REQUIRED 的情况下, regCourse() 就会加入到已有的事务中,两个方法共用一个事务。

Spring 在处理事务过程中,有个默认的传播属性 REQUIRED,在整个事务的调用链上,任何一个环节抛出的异常都会导致全局回滚。知道了这个结论,修改方法也就很简单了,我们只需要对传播属性进行修改,把类型改成 REQUIRES_NEW 就可以了


@Transactional(rollbackFor = Exception.class, propagation = Propagation.REQUIRES_NEW)
public void regCourse(int studentId) throws Exception {
    studentCourseMapper.saveStudentCourse(studentId, 1);
    courseMapper.addCourseNumber(1);
    throw new Exception("注册失败");
}
  • 当子事务声明为 Propagation.REQUIRES_NEW 时,在 TransactionAspectSupport.invokeWithinTransaction() 中调用 createTransactionIfNecessary() 就会创建一个新的事务,独立于外层事务。
  • 而在 AbstractPlatformTransactionManager.processRollback() 进行 rollback 处理时,因为 status.isNewTransaction() 会因为它处于一个新的事务中而返回 true,所以它走入到了另一个分支,执行了 doRollback() 操作,让这个子事务单独回滚,不会影响到主事务

4.多数据源间切换之谜

新需求又来了,每个学生注册的时候,需要给他们发一张校园卡,并给校园卡里充入 50 元钱。但是这个校园卡管理系统是一个第三方系统,使用的是另一套数据库,这样我们就需要在一个事务中同时操作两个数据库。


CREATE TABLE `card` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `student_id` int(11) DEFAULT NULL,
  `balance` int(11) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;


public class Card {
    private Integer id;
    private Integer studentId;
    private Integer balance;
    //省略 Get/Set 方法
}


@Mapper
public interface CardMapper {
    @Insert("INSERT INTO `card`(`student_id`, `balance`) VALUES (#{studentId}, #{balance})")
    @Options(useGeneratedKeys = true, keyProperty = "id")
    int saveCard(Card card);
}


@Service
public class CardService {
    @Autowired
    private CardMapper cardMapper;

    @Transactional
    public void createCard(int studentId) throws Exception {
        Card card = new Card();
        card.setStudentId(studentId);
        card.setBalance(50);
        cardMapper.saveCard(card);
    }
}

学生注册和发卡都要在一个事务里完成,但是我们都默认只会连一个数据源,之前我们一直连的都是学生信息这个数据源,在这里,我们还需要对校园卡的数据源进行操作。于是,我们需要在一个事务里完成对两个数据源的操作

在 Spring 里有这样一个抽象类 AbstractRoutingDataSource,这个类相当于 DataSource 的路由中介,在运行时根据某种 key 值来动态切换到所需的 DataSource 上。通过实现这个类就可以实现我们期望的动态数据源切换。

修正:


public class MyDataSource extends AbstractRoutingDataSource {
    private static final ThreadLocal<String> key = new ThreadLocal<String>();

    @Override
    protected Object determineCurrentLookupKey() {
        return key.get();
    }

    public static void setDataSource(String dataSource) {
        key.set(dataSource);
    }

    public static String getDatasource() {
        return key.get();
    }

    public static void clearDataSource() {
        key.remove();
    }
}




public class JdbcConfig {
    //省略非关键代码
    @Value("${card.driver}")
    private String cardDriver;

    @Value("${card.url}")
    private String cardUrl;

    @Value("${card.username}")
    private String cardUsername;

    @Value("${card.password}")
    private String cardPassword;

    @Autowired
    @Qualifier("dataSourceCard")
    private DataSource dataSourceCard;

    @Autowired
    @Qualifier("dataSourceCore")
    private DataSource dataSourceCore;

    //省略非关键代码

    @Bean(name = "dataSourceCore")
    public DataSource createCoreDataSource() {
        DriverManagerDataSource ds = new DriverManagerDataSource();
        ds.setDriverClassName(driver);
        ds.setUrl(url);
        ds.setUsername(username);
        ds.setPassword(password);
        return ds;
    }

    @Bean(name = "dataSourceCard")
    public DataSource createCardDataSource() {
        DriverManagerDataSource ds = new DriverManagerDataSource();
        ds.setDriverClassName(cardDriver);
        ds.setUrl(cardUrl);
        ds.setUsername(cardUsername);
        ds.setPassword(cardPassword);
        return ds;
    }

    @Bean(name = "dataSource")
    public MyDataSource createDataSource() {
        MyDataSource myDataSource = new MyDataSource();
        Map<Object, Object> map = new HashMap<>();
        map.put("core", dataSourceCore);
        map.put("card", dataSourceCard);
        myDataSource.setTargetDataSources(map);
        myDataSource.setDefaultTargetDataSource(dataSourceCore);
        return myDataSource;
    }

    //省略非关键代码
}




@Documented
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface DataSource {
    String value();

    String core = "core";

    String card = "card";
}




@Aspect
@Service
@Order(1)
public class DataSourceSwitch {
    @Around("execution(* com.spring.puzzle.others.transaction.example3.CardService.*(..))")
    public void around(ProceedingJoinPoint point) throws Throwable {
        Signature signature = point.getSignature();
        MethodSignature methodSignature = (MethodSignature) signature;
        Method method = methodSignature.getMethod();
        if (method.isAnnotationPresent(DataSource.class)) {
            DataSource dataSource = method.getAnnotation(DataSource.class);
            MyDataSource.setDataSource(dataSource.value());
            System.out.println("数据源切换至:" + MyDataSource.getDatasource());
        }
        point.proceed();
        MyDataSource.clearDataSource();
        System.out.println("数据源已移除!");
    }
}



@Service
public class CardService {
    @Autowired
    private CardMapper cardMapper;

    @Transactional(propagation = Propagation.REQUIRES_NEW)
    @DataSource(DataSource.card)
    public void createCard(int studentId) throws Exception {
        Card card = new Card();
        card.setStudentId(studentId);
        card.setBalance(50);
        cardMapper.saveCard(card);
    }
}



@Transactional(rollbackFor = Exception.class)
public void saveStudent(String realname) throws Exception {
    Student student = new Student();
    student.setRealname(realname);
    studentService.doSaveStudent(student);
    try {
        courseService.regCourse(student.getId());
        cardService.createCard(student.getId());
    } catch (Exception e) {
        e.printStackTrace();
    }
}

总结:

  • Spring 支持声明式事务机制,它通过在方法上加上 @Transactional,表明该方法需要事务支持。于是,在加载的时候,根据 @Transactional 中的属性,决定对该事务采取什么样的策略;
  • @Transactional 对 private 方法不生效,所以我们应该把需要支持事务的方法声明为 public 类型;
  • Spring 处理事务的时候,默认只对 RuntimeException 和 Error 回滚,不会对 Exception 回滚,如果有特殊需要,需要额外声明,例如指明 Transactional 的属性 rollbackFor 为 Exception.class。
  • Spring 在事务处理中有一个很重要的属性 Propagation,主要用来配置当前需要执行的方法如何使用事务,以及与其它事务之间的关系。
  • Spring 默认的传播属性是 REQUIRED,在有事务状态下执行,如果当前没有事务,则创建新的事务
  • Spring 事务是可以对多个数据源生效,它提供了一个抽象类 AbstractRoutingDataSource,通过实现这个抽象类,我们可以实现自定义的数据库切换。
  • 3
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
当涉及到Spring事务的面试问题时,以下是一些常见的问题和参考答案: 1. 什么是Spring事务管理? Spring事务管理是一种将事务逻辑与业务逻辑分离的方式。它提供了一种简单的方法来管理数据库事务,并确保在发生错误或异常的情况下进行回滚或提交。 2. Spring事务管理的主要特性是什么? 主要特性包括: - 声明式事务管理:使用注解或XML配置来声明事务 - 编程式事务管理:在代码中使用编程方式控制事务 - 隔离级别:定义事务之间的隔离级别,如读未提交,读已提交,可重复读和串行化 - 传播行为:定义事务方法之间的调用关系,如PROPAGATION_REQUIRED和PROPAGATION_REQUIRES_NEW - 回滚规则:定义哪些异常会导致事务回滚 3. Spring事务管理有哪些常见的实现方式? 常见的实现方式包括: - 基于注解的事务管理:使用@Transactional注解来标记需要进行事务管理的方法 - 基于XML配置的事务管理:通过配置<tx:advice>和<tx:attributes>元素来进行事务管理 4. 什么是事务传播行为?Spring提供了哪些事务传播行为? 事务传播行为定义了事务方法之间的调用关系。Spring提供了多种事务传播行为,包括: - REQUIRED:如果当前存在事务,则加入该事务,否则新建一个事务。 - REQUIRES_NEW:无论当前是否存在事务,都创建一个新的事务,如果当前存在事务,则挂起当前事务。 - SUPPORTS:如果当前存在事务,则加入该事务,否则以非事务方式执行。 - NOT_SUPPORTED:以非事务方式执行操作,如果当前存在事务,则挂起当前事务。 - MANDATORY:如果当前存在事务,则加入该事务,否则抛出异常。 - NEVER:以非事务方式执行操作,如果当前存在事务,则抛出异常。 5. Spring如何处理事务的并发问题? Spring使用数据库的事务隔离级别来处理并发问题。常见的隔离级别包括: - READ_UNCOMMITTED:允许读取未提交的数据,可能导致脏读、不可重复读和幻读问题。 - READ_COMMITTED:只能读取已提交的数据,可以避免脏读问题,但仍可能出现不可重复读和幻读问题。 - REPEATABLE_READ:保证同一事务内多次读取的数据是一致的,可以避免脏读和不可重复读问题,但仍可能出现幻读问题。 - SERIALIZABLE:最高级别的隔离级别,保证所有并发事务按照顺序依次执行,可以避免脏读、不可重复读和幻读问题,但性能较差。 这些是一些常见Spring事务管理的面试问题和参考答案。当然,在面试过程中还可能有其他相关的问题,因此建议对Spring事务管理的原理和用法进行深入了解,准备充分以回答更多的问题。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值