vlambda博客
学习文章列表

灵魂三连问,什么是事务?什么是Spring事务管理?日常编码有哪些常见坑?



灵魂三连问,什么是事务?什么是Spring事务管理?日常编码有哪些常见坑?

 



  什么是事务?



     事务目的是保证数据的完整性一致性

它的四大特性ACID:

    原子性(Atomicity)

    原子性是指事务是一个不可分割的工作单位,事务中的操作要么都发生,要么都不发生。

    一致性(Consistency)

    事务前后数据的完整性必须保持一致。

    隔离性(Isolation)

    事务的隔离性是多个用户并发访问数据库时,数据库为每一个用户开启的事务,不能被其他事务的操作数据所干扰,多个并发事务之间要相互隔离。

    持久性(Durability)

    持久性是指一个事务一旦被提交,它对数据库中数据的改变就是永久性的,接下来即使数据库发生故障也不应该对其有任何影响。




什么是Spring事务?



    Spring事务的本质其实就是数据库对事务的支持,没有数据库的事务支持,spring是无法提供事务功能的。Spring事务默认的隔离等级为数据库的隔离等级。mysql是可重复读,oracle是读已提交。


Spring事务分类

  • 编码事务:硬编码实现事务,在需要事务的代码中手动开启事务,事务结束回滚或者提交。

  • 声明式事务:基于Spring AOP,通过切面完成事务管理。可以通过Xml定义切点、切面、通知等。也可以在需要事务的方法通过注解@Transactional完成。

  • @Transaction具有八个参数:


    参数名称

    功能描述

    readOnly

    该属性用于设置当前事务是否为只读事务,设置为true表示只读,false则表示可读写,默认值为false。例如:@Transactional(readOnly=true)

    rollbackFor

    该属性用于设置需要进行回滚的异常类数组,当方法中抛出指定异常数组中的异常时,则进行事务回滚。例如:

    指定单一异常类:@Transactional(rollbackFor=RuntimeException.class)

    指定多个异常类:@Transactional(rollbackFor={RuntimeException.class, Exception.class})

    rollbackForClassName

    该属性用于设置需要进行回滚的异常类名称数组,当方法中抛出指定异常名称数组中的异常时,则进行事务回滚。例如:

    指定单一异常类名称:@Transactional(rollbackForClassName="RuntimeException")

    指定多个异常类名称:@Transactional(rollbackForClassName={"RuntimeException","Exception"})

    noRollbackFor

    该属性用于设置不需要进行回滚的异常类数组,当方法中抛出指定异常数组中的异常时,不进行事务回滚。例如:

    指定单一异常类:@Transactional(noRollbackFor=RuntimeException.class)

    指定多个异常类:@Transactional(noRollbackFor={RuntimeException.class, Exception.class})

    noRollbackForClassName

    该属性用于设置不需要进行回滚的异常类名称数组,当方法中抛出指定异常名称数组中的异常时,不进行事务回滚。例如:

    指定单一异常类名称:@Transactional(noRollbackForClassName="RuntimeException")

    指定多个异常类名称:

    @Transactional(noRollbackForClassName={"RuntimeException","Exception"})

    propagation

    该属性用于设置事务的传播行为,

    例如:@Transactional(propagation=Propagation.NOT_SUPPORTED,readOnly=true)

    isolation

    该属性用于设置底层数据库的事务隔离级别,事务隔离级别用于处理多事务并发的情况,通常使用数据库的默认隔离级别即可,基本不需要进行设置

    timeout

    该属性用于设置事务的超时秒数,默认值为-1表示永不超时

Spring事务核心接口

灵魂三连问,什么是事务?什么是Spring事务管理?日常编码有哪些常见坑?


  

 稍微提一下Springboot事务管理


        新建的Spring Boot项目中,一般都会引用spring-boot-starter或者spring-boot-starter-web,而这两个起步依赖中都已经包含了对于spring-boot-starter-jdbc或spring-boot-starter-data-jpa的依赖.当我们使用了这两个依赖的时候,框架会自动默认分别注入

DataSourceTransactionManager

JpaTransactionManager

所以我们不需要任何额外配置就可以用@Transactional注解进行事务的使用.




开发中常见坑



常见坑点1:

@Transactional

public int insertUser(User user) throws Exception

{

// 新增用户信息

int rows = userMapper.insertUser(user);

// 新增用户岗位关联

insertUserPost(user);

// 新增用户与角色管理

insertUserRole(user);

// 模拟抛出SQLException异常

boolean flag = true;

if (flag)

{

throw new SQLException("发生异常了..");

}

return rows;

}


原因:遇到检查异常时,事务开启,也无法回滚。例如下面这段代码,用户依旧增加成功,并没有因为后面遇到检查异常而回滚!!


总结解释:在spring的文档中说道,spring声明式事务管理默认对非检查型异常和运行时异常进行事务回滚,而对检查型异常则不进行回滚操作。可以在@Transactional 注解里使用 rollbackFor 属性明确指定异常。


例如下面这样,就可以正常回滚:

@Transactional(rollbackFor=Exception.class)

public int insertUser(User user) throws Exception

{

// 新增用户信息

int rows = userMapper.insertUser(user);

// 新增用户岗位关联

insertUserPost(user);

// 新增用户与角色管理

insertUserRole(user);

// 模拟抛出SQLException异常

boolean flag = true;

if (flag)

{

throw new SQLException("发生异常了..");

}

return rows;

}

常见坑点2:

    在业务层捕捉异常后,发现事务不生效。这是许多新手都会犯的一个错误,在业务层手工捕捉并处理了异常,你都把异常“吃”掉了,Spring自然不知道这里有错,更不会主动去回滚数据。
例如:下面这段代码直接导致用户新增的事务回滚没有生效。

@Transactional

public int insertUser(User user) throws Exception

{

// 新增用户信息

int rows = userMapper.insertUser(user);

// 新增用户岗位关联

insertUserPost(user);

// 新增用户与角色管理

insertUserRole(user);

// 模拟抛出SQLException异常

boolean flag = true;

if (flag)

{

try

{

// 谨慎:尽量不要在业务层捕捉异常并处理

throw new SQLException("发生异常了..");

}

catch (Exception e)

{

e.printStackTrace();

}

}

return rows;

}

推荐做法:在业务层统一抛出异常,然后在控制层统一处理。

@Transactional

public int insertUser(User user) throws Exception

{

// 新增用户信息

int rows = userMapper.insertUser(user);

// 新增用户岗位关联

insertUserPost(user);

// 新增用户与角色管理

insertUserRole(user);

// 模拟抛出SQLException异常

boolean flag = true;

if (flag)

{

throw new RuntimeException("发生异常了..");

}

return rows;

}

常见坑点3 :同service中相互调用

    示例1:A方法无事务,B方法加事务

  1. controller层调用

  2. @RestController

  3. public class Controller{

  4.     

  5.     @Autowired

  6.     private StudentcardService studentcardService;  

  7.   

  8.     @RequestMapping(value = "/test/{id}}", method      =RequestMethod.GET)

  9.     public Response queryStudentCard(@PathVariable("id") String id) {

  10.     studentcardService.updateA(id);

  11.     }

  12. }

  13. 服务层

  14. @Service

  15. public class StudentcardServiceImpl implements StudentcardService {

  16. @Resource

  17. private StudentCardMapper studentCardMapper;


  18. @Override

  19. public void updateA(String id) {

  20. //先去调用内部方法B

  21. this.updateB(id);

  22. StudentCard sc =new StudentCard();

  23. sc.setScId(id);

  24. //修改问题字段

  25. sc.setQuestion("AAAAA");

  26. studentCardMapper.update(sc);

  27. }


  28. @Override

  29. @Transactional

  30. public void updateB(String id) {

  31. StudentCard sc =new StudentCard();

  32. sc.setScId(id);

  33. //修改答案字段

  34. sc.setAnswer("BBBBB");

  35. studentCardMapper.update(sc);

  36. //修改完数据后报错


  37. double i=1/0;

  38. }

  39. }

访问后执行结果如下:

灵魂三连问,什么是事务?什么是Spring事务管理?日常编码有哪些常见坑?

 总结解释事务没有起作用,是由于Spring事务本质是基于AOP代理来实现的,当Controller调用Service的方法A是基于proxy的,所以会切入,但是方法A在调用方法B时,属于类内部调用,即使方法B上加上了@Transactional注解,但没有Spring代理了,所以不受事务控制,自然事务不会生效。

示例2:将A方法加事务,B方法不加事务

@Service

public class StudentcardServiceImpl implements StudentcardService {

@Resource

private StudentCardMapper studentCardMapper;


@Override

@Transactional

public void updateA(String id) {

//先去调用内部方法B

this.updateB(id);

StudentCard sc =new StudentCard();

sc.setScId(id);

//修改问题字段

sc.setQuestion("AAAAA");

studentCardMapper.update(sc);

}


@Override

public void updateB(String id) {

StudentCard sc =new StudentCard();

sc.setScId(id);

//修改答案字段

sc.setAnswer("BBBBB");

studentCardMapper.update(sc);

//修改完数据后报错

double i=1/0;

}

}

访问后执行结果如下:

灵魂三连问,什么是事务?什么是Spring事务管理?日常编码有哪些常见坑?

   事务起作用了,都没有修改成功!

总结解释:事务可以起作用是由于事务的传播行为导致的,默认事务的传播行为为:PROPAGATION_REQUIRED 。方法A标注了注解@Transactional ,执行的时候传播给方法B,因为方法A开启了事务,线程内的connection的属性autoCommit=false,并且执行到方法B时,事务传播依然是生效的,得到的还是方法A的connection,autoCommit还是为false,所以事务生效;反之,如果方法A没有注解@Transactional 时,是不受事务管理的,autoCommit=true,那么传播给方法B的也为true,执行完自动提交,即使B标注了@Transactional 事务也是不起作用的。

示例3:A方法加事务,B方法没有事务,但是在A调用B方法时用try进行包裹,B方法中有错误

@Service

public class StudentcardServiceImpl implements StudentcardService {


@Resource

private StudentCardMapper studentCardMapper;


@Override

@Transactional

public void updateA(String id) {

//先去调用内部方法B

try {

this.updateB(id);

}catch (Exception e){}

StudentCard sc =new StudentCard();

sc.setScId(id);

//修改问题字段

sc.setQuestion("AAAAA");

studentCardMapper.update(sc);

}


@Override

public void updateB(String id) {

StudentCard sc =new StudentCard();

sc.setScId(id);

//修改答案字段

sc.setAnswer("BBBBB");

studentCardMapper.update(sc);

//修改完数据后报错

double i=1/0;

}

}

访问后执行结果如下:

灵魂三连问,什么是事务?什么是Spring事务管理?日常编码有哪些常见坑?

        由于报错被try包起来了,所以数据都插入了!那如果将报错信息放到执行完方法B后呢,会怎样呢?

示例4:A方法加事务,B方法没有事务,但是在A调用B方法时用try进行包裹,A方法中有错误

@Service

public class StudentcardServiceImpl implements StudentcardService {


@Resource

private StudentCardMapper studentCardMapper;


@Override

@Transactional

public void updateA(String id) {

//先去调用内部方法B

try {

this.updateB(id);

}catch (Exception e){}

StudentCard sc =new StudentCard();

sc.setScId(id);

//修改问题字段

sc.setQuestion("AAAAA");

studentCardMapper.update(sc);

//修改完数据后报错

double i=1/0;

}


@Override

public void updateB(String id) {

StudentCard sc =new StudentCard();

sc.setScId(id);

//修改答案字段


sc.setAnswer("BBBBB");

studentCardMapper.update(sc);

}

}

访问后执行结果如下:

数据都没有插入呢!这是因为在事务提交前报错了,事务全部rollback了,下面言归正传,示例1为何不能成功呢?于是查询各种资料终于找到了缘由,并对示例1进行改造

示例5:A方法无事务,B方法加事务

@Service

@EnableAspectJAutoProxy(proxyTargetClass = true, exposeProxy = true)

public class StudentcardServiceImpl implements StudentcardService {


@Resource

private StudentCardMapper studentCardMapper;


@Override

public void updateA(String id) {

//先去调用内部方法B

StudentcardService studentcardService = (StudentcardService) AopContext.currentProxy();

studentcardService.updateB(id);

//this.updateB(id);

StudentCard sc =new StudentCard();

sc.setScId(id);

//修改问题字段

sc.setQuestion("AAAAA");

studentCardMapper.update(sc);

}


@Override

@Transactional

public void updateB(String id) {

StudentCard sc =new StudentCard();

sc.setScId(id);

//修改答案字段

sc.setAnswer("BBBBB");

studentCardMapper.update(sc);

//修改完数据后报错

double i=1/0;

}

}


访问后执行结果如下:

数据没有进行修改,事务起作用了!

    总结解释:事务又可以起作用的,是由于我们在方法A调用方法B时,先获取到了Service的当前代理,然后用当前代理去调用方法B,所以事务当然会生效了~





其他事务失效场景




1.数据库引擎不支持事务

        对于Mysql数据库而言innodb支持事务,myisam不支持

2.Service类没有交给Spring管理

        spring事务是基于AOP,Service的方法必须是public,private、final、static方法不生效(private方法是用户私有的方法,用户自己去维护,static方法是类方法,不是spring 的bean)

3.必须在同一个线程里





知识拓展




spring @Transactional注解参数详解

https://www.cnblogs.com/caoyc/p/5632963.html

java中代理,静态代理,动态代理以及spring aop代理方式,实现原理统一汇总

https://www.cnblogs.com/shamo89/p/9932075.html



点个“在看”表示朕

已阅