灵魂三连问,什么是事务?什么是Spring事务管理?日常编码有哪些常见坑?
事务目的是保证数据的完整性和一致性。
它的四大特性ACID:
原子性(Atomicity)
原子性是指事务是一个不可分割的工作单位,事务中的操作要么都发生,要么都不发生。
一致性(Consistency)
事务前后数据的完整性必须保持一致。
隔离性(Isolation)
事务的隔离性是多个用户并发访问数据库时,数据库为每一个用户开启的事务,不能被其他事务的操作数据所干扰,多个并发事务之间要相互隔离。
持久性(Durability)
持久性是指一个事务一旦被提交,它对数据库中数据的改变就是永久性的,接下来即使数据库发生故障也不应该对其有任何影响。
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事务核心接口
稍微提一下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方法加事务
controller层调用
@RestController
public class Controller{
@Autowired
private StudentcardService studentcardService;
@RequestMapping(value = "/test/{id}}", method =RequestMethod.GET)
public Response queryStudentCard(@PathVariable("id") String id) {
studentcardService.updateA(id);
}
}
服务层
@Service
public class StudentcardServiceImpl implements StudentcardService {
@Resource
private StudentCardMapper studentCardMapper;
@Override
public void updateA(String id) {
//先去调用内部方法B
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;
}
}
访问后执行结果如下:
总结解释:事务没有起作用,是由于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;
}
}
访问后执行结果如下:
事务起作用了,都没有修改成功!
总结解释:事务可以起作用是由于事务的传播行为导致的,默认事务的传播行为为: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;
}
}
访问后执行结果如下:
由于报错被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
点个“在看”表示朕
已阅