Spring事物失效的几种场景
1. 业务概述
日常开发中事物的使用非常常见,就是为了保证数据库的四大特性ACID,原子,一致,隔离,持久中的原子性,尤其是一个接口涉及多个操作,又要保证操作的数据原子性,事物就是解决此问题的,但是在使用过程中会存在一些事物失效的场景。
2. 异常场景
2.1 事务不生效
2.1.1 访问权限问题
众所周知,java的访问权限主要有四种:private、default、protected、public,它们的权限从左到右,依次变大。
但如果我们在开发过程中,把有某些事务方法,定义了错误的访问权限,就会导致事务功能出问题,例如:
/**
* 事务不生效
* 1.访问权限问题。
* private void add(Goods goods) {}
* 虽然这种情况理论上存在,但是实际开发中绝对不会存在!!!
* 因为private修饰的方法其他类调用不了,编译就会不通过。
*/
@Override
@Transactional(rollbackFor = Exception.class)
//private void addPrivate(Goods goods) {
public void addPrivate(Goods goods) {
insert(goods);
goods.setGoodsName(goods.getGoodsName() + "up");
updateException(goods);
}
2.1.2 方法用final修饰
有时候,某个方法不想被子类重新,这时可以将该方法定义成final的。普通方法这样定义是没问题的,但如果将事务方法定义成final,例如:
/**
* 事务不生效
* 2. 方法用final修饰
* 提示:Methods annotated with '@Transactional' must be overridable
* 虽然有提示事物注解不能有final修饰,但是可以编译通过
* 事物不会回滚,新增成功,更新失败
*/
@Override
@Transactional(rollbackFor = Exception.class)
public final void addFinal(Goods goods) {
insert(goods);
goods.setGoodsName(goods.getGoodsName() + "up");
updateException(goods);
}
2.1.3 方法内部调用
有时候我们需要在某个Service类的某个方法中,调用另外一个事务方法,比如:
/**
* 事务不生效
* 3.方法内部调用
* 实现:有时候我们需要在某个Service类的某个方法中,调用另外一个事务方法
* 验证:事物不会回滚,新增成功,更新失败
* 如果有些场景,确实想在同一个类的某个方法中,调用它自己的另外一个方法,该怎么办呢?
* 新加一个Service方法
*/
@Override
@Transactional(rollbackFor = Exception.class)
public void addMethod(Goods goods) {
insert(goods);
goods.setGoodsName(goods.getGoodsName() + "up");
// 事物修改
updateTransactional(goods);
}
这个方法非常简单,只需要新加一个Service方法,把@Transactional注解加到新Service方法上,把需要事务执行的代码移到新方法中。具体代码如下:
@Servcie
public class ServiceA {
@Autowired
prvate ServiceB serviceB;
public void save(User user) {
queryData1();
queryData2();
serviceB.doSave(user);
}
}
@Servcie
public class ServiceB {
@Transactional(rollbackFor=Exception.class)
public void doSave(User user) {
addData1();
updateData2();
}
}
2.1.4 未被spring管理
在我们平时开发过程中,有个细节很容易被忽略。即使用spring事务的前提是:对象要被spring管理,需要创建bean实例。
通常情况下,我们通过@Controller、@Service、@Component、@Repository等注解,可以自动实现bean实例化和依赖注入的功能。
/**
* 事务不生效
* 4.未被spring管理
* 实现:对象要被spring管理,需要创建bean实例。去掉@Controller、@Service、@Component、@Repository等注解,可以自动实现bean实例化和依赖注入的功能。
* 验证:这种情况下比如去掉@Service,在Controller或者其他service都无法注入,肯定异常,一般不会发生
*/
@Override
@Transactional(rollbackFor = Exception.class)
public void addNoSpring(Goods goods) {
insert(goods);
goods.setGoodsName(goods.getGoodsName() + "up");
update(goods);
}
2.1.5 多线程调用
在实际项目开发中,多线程的使用场景还是挺多的。如果spring事务用在多线程场景中
/**
* 事务不生效
* 5.多线程调用
* 实现:多线程情况下第一一条还没抛出异常,第二条可能已经成功提交,所以在事物内多线程操作事物失效
* 验证:事物不会回滚,新增成功,更新失败
*/
@Override
@Transactional(rollbackFor = Exception.class)
public void addMulThread(Goods goods) {
for (int i = 10; i < 15; i++) {
int finalI = i;
goods.setId((long) finalI);
goods.setGoodsName(goods.getGoodsName() + finalI);
// 异常数据
if (i == 11 || i == 12) {
pool.execute(() -> {
insertException(goods);
});
}
//正常数据
pool.execute(() -> {
insert(goods);
});
}
}
2.1.6 表不支持事务
周所周知,在mysql5之前,默认的数据库引擎是myisam。
它的好处就不用多说了:索引文件和数据文件是分开存储的,对于查多写少的单表操作,性能比innodb更好。
有些老项目中,可能还在用它。
在创建表的时候,只需要把ENGINE参数设置成MyISAM即可:
CREATE TABLE `category` (
`id` bigint NOT NULL AUTO_INCREMENT,
`one_category` varchar(20) COLLATE utf8mb4_bin DEFAULT NULL,
`two_category` varchar(20) COLLATE utf8mb4_bin DEFAULT NULL,
`three_category` varchar(20) COLLATE utf8mb4_bin DEFAULT NULL,
`four_category` varchar(20) COLLATE utf8mb4_bin DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=MyISAM AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin
myisam好用,但有个很致命的问题是:不支持事务。
如果只是单表操作还好,不会出现太大的问题。但如果需要跨多张表操作,由于其不支持事务,数据极有可能会出现不完整的情况。
此外,myisam还不支持行锁和外键。
所以在实际业务场景中,myisam使用的并不多。在mysql5以后,myisam已经逐渐退出了历史的舞台,取而代之的是innodb。
2.1.7 未开启事务
如果你使用的是springboot项目,那么你很幸运。因为springboot通过DataSourceTransactionManagerAutoConfiguration类,已经默默的帮你开启了事务。
2.2 事务不回滚
2.2.1 错误的传播特性
其实,我们在使用@Transactional注解时,是可以指定propagation参数的。
该参数的作用是指定事务的传播特性,spring目前支持7种传播特性:
REQUIRED 如果当前上下文中存在事务,那么加入该事务,如果不存在事务,创建一个事务,这是默认的传播属性值。
SUPPORTS 如果当前上下文存在事务,则支持事务加入事务,如果不存在事务,则使用非事务的方式执行。
MANDATORY 如果当前上下文中存在事务,否则抛出异常。
REQUIRES_NEW 每次都会新建一个事务,并且同时将上下文中的事务挂起,执行当前新建事务完成以后,上下文事务恢复再执行。
NOT_SUPPORTED 如果当前上下文中存在事务,则挂起当前事务,然后新的方法在没有事务的环境中执行。
NEVER 如果当前上下文中存在事务,则抛出异常,否则在无事务环境上执行代码。
NESTED 如果当前上下文中存在事务,则嵌套事务执行,如果不存在事务,则新建事务。
如果我们在手动设置propagation参数的时候,把传播特性设置错了,比如:
/**
* 事务不生效
* 7.未开启事务
* 实现:如果你使用的是springboot项目,那么你很幸运。因为springboot通过DataSourceTransactionManagerAutoConfiguration类,已经默默的帮你开启了事务。
* 8.我们在使用@Transactional注解时,是可以指定propagation参数的。
* 验证:事物不会回滚,新增成功,更新失败
*/
@Override
@Transactional(rollbackFor = Exception.class, propagation = Propagation.NEVER)
public void addSpread(Goods goods) {
insert(goods);
goods.setGoodsName(goods.getGoodsName() + "up");
// 由于指定了事物传播属性,所以never下不会回滚
updateException(goods);
}
2.2.2 捕获异常
/**
* 事务不生效
* 2.捕获异常
* 实现:捕获异常后没有抛出,事物检测不到异常自然不会回滚
* 验证:事物不会回滚,新增成功,更新失败
*/
@Override
@Transactional(rollbackFor = Exception.class)
public void addTry(Goods goods) {
try {
insert(goods);
goods.setGoodsName(goods.getGoodsName() + "up");
// 由于指定了事物传播属性,所以never下不会回滚
updateException(goods);
} catch (Exception e) {
System.out.println("异常啦:" + e);
}
}
2.2.3 手动抛了别的异常
即使开发者没有手动捕获异常,但如果抛的异常不正确,spring事务也不会回滚。
/**
* 事务不生效
* 3.手动抛了别的异常
* 实现:手动捕获异常后抛出,事物检测不到异常,事物不会滚
* 验证:事物不会回滚,新增成功,更新失败
*/
@Override
@Transactional(rollbackFor = Exception.class)
public void addThrow(Goods goods) throws Exception {
try {
insert(goods);
goods.setGoodsName(goods.getGoodsName() + "up");
// 由于指定了事物传播属性,所以never下不会回滚
updateException(goods);
} catch (Exception e) {
System.out.println("异常啦:" + e);
throw new Exception(e);
}
}
2.2.4 自定义了回滚异常
在使用@Transactional注解声明事务时,有时我们想自定义回滚的异常,spring也是支持的。可以通过设置rollbackFor参数,来完成这个功能。
/**
* 事务不生效
* 4.自定义了回滚异常
* 实现:指定或者自定义指定异常,事物只检测指定异常,其他异常不会滚
* 验证:事物不会回滚,新增成功,更新失败
*/
@Override
@Transactional(rollbackFor = ArrayIndexOutOfBoundsException.class)
public void addRollBack(Goods goods) {
insert(goods);
goods.setGoodsName(goods.getGoodsName() + "up");
updateException(goods);
}
2.2.5 嵌套事务回滚多了
/**
* 事务不生效
* 5.嵌套事务回滚多了
* 实现:
* 验证:事物不会回滚,新增成功,更新失败
*/
@Override
@Transactional(rollbackFor = Exception.class)
public void addNested(Goods goods) {
insert(goods);
goods.setGoodsName(goods.getGoodsName() + "up");
updateException(goods);
}
2.3 其他情况
2.3.1 大事务问题
事物内容涉及到比较多的操作,导致超时问题等。
2.3.2 编程试事物
事物分为编程式事务与声明式事务,编程是比较灵狐,手动代码里处理,但是耦合性太强,不移维护。
@Autowired
private TransactionTemplate transactionTemplate;
public void ad(){
transactionTemplate.execute(()->{
// 执行操作
})
}
3. 实现验证
3.1 GoodsController
package com.example.category.controller;
import com.example.category.entity.Goods;
import com.example.category.service.GoodsService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.web.bind.annotation.*;
import com.example.category.entity.Response;
import javax.annotation.Resource;
/**
* 商品表(Goods)控制层
* 事务不生效测试
*
* @author zrj
* @since 2021-09-05 10:48:26
*/
@RestController
@Api(tags = "商品管理", description = "商品管理")
@RequestMapping("/goods")
public class GoodsController {
@Resource
private GoodsService goodsService;
/**
* 事物新增商品
*/
@ApiOperation(value = "事物新增商品")
@RequestMapping(value = "add", method = RequestMethod.POST)
public Response<Integer> add(@RequestBody Goods goods) {
//正常事物
//goodsService.addNormal(goods);
//1访问权限问题
//goodsService.addPrivate(goods);
//2方法用final修饰
//goodsService.addFinal(goods);
//3方法内部调用
//goodsService.addMethod(goods);
//4未被spring管理
//goodsService.addNoSpring(goods);
//5多线程调用
//goodsService.addMulThread(goods);
//6表不支持事物
//goodsService.addTableNoSupport(goods);
//7事物传播错误
//goodsService.addSpread(goods);
//8捕获异常
goodsService.addTry(goods);
//9手动抛出异常
//goodsService.addThrow(goods);
//10自定义回滚
//goodsService.addRollBack(goods);
//11嵌套事物回滚异常
//goodsService.addNested(goods);
return Response.success("新增成功", null);
}
/**
* 新增商品
*/
@ApiOperation(value = "新增商品")
@RequestMapping(value = "insert", method = RequestMethod.POST)
public Response<Integer> insert(@RequestBody Goods goods) {
int result = goodsService.insert(goods);
if (result > 0) {
return Response.success("新增成功", result);
}
return Response.fail("新增失败");
}
/**
* 修改商品
*/
@ApiOperation(value = "修改商品")
@RequestMapping(value = "update", method = RequestMethod.PUT)
public Response<Integer> update(@RequestBody Goods goods) {
int result = goodsService.update(goods);
if (result > 0) {
return Response.success("修改成功", result);
}
return Response.fail("修改失败");
}
}
3.2 GoodsService
package com.example.category.service;
import com.example.category.entity.Goods;
import java.util.List;
import java.util.Map;
/**
* 商品服务接口
*
* @author zrj
* @since 2021-09-05
*/
public interface GoodsService {
/**
* 正常事物
*/
void addNormal(Goods goods);
/**
* 访问权限问题
*/
void addPrivate(Goods goods);
/**
* 方法用final修饰
*/
void addFinal(Goods goods);
/**
* 方法内部调用
*/
void addMethod(Goods goods);
/**
* 未被spring管理
*/
void addNoSpring(Goods goods);
/**
* 多线程调用
*/
void addMulThread(Goods goods);
/**
* 表不支持事物
*/
void addTableNoSupport(Goods goods);
/**
* 错误事物传播特性
*/
void addSpread(Goods goods);
/**
* 捕获异常
*/
void addTry(Goods goods);
/**
* 手动抛出异常
*/
void addThrow(Goods goods) throws Exception;
/**
* 自定义回归异常
*/
void addRollBack(Goods goods);
/**
* 嵌套事物回滚异常
*/
void addNested(Goods goods);
/**
* 新增数据
*/
int insert(Goods goods);
/**
* 异常新增数据
*/
int insertException(Goods goods);
/**
* 修改数据
*/
int update(Goods goods);
/**
* 异常修改数据
*/
int updateException(Goods goods);
/**
* 事物修改数据
*/
int updateTransactional(Goods goods);
}
3.3 GoodsServiceImpl
package com.example.category.service.impl;
import com.example.category.entity.Goods;
import com.example.category.mapper.GoodsMapper;
import com.example.category.service.GoodsService;
import com.google.common.util.concurrent.ThreadFactoryBuilder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import javax.annotation.Resource;
import java.util.concurrent.*;
/**
* 商品服务实现类
*
* @author zrj
* @since 2021-09-05
*/
@Service("goodsService")
public class GoodsServiceImpl implements GoodsService {
// 自定义线程名称
private static ThreadFactory namedThreadFactory = new ThreadFactoryBuilder().setNameFormat("thread-%d").build();
// 定义线程池
private static ExecutorService pool = new ThreadPoolExecutor(30, 200, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<>(1024), namedThreadFactory, new ThreadPoolExecutor.AbortPolicy());
@Resource
private GoodsMapper goodsMapper;
/**
* 正常事务
*/
@Override
@Transactional(rollbackFor = Exception.class)
public void addNormal(Goods goods) {
insert(goods);
goods.setGoodsName(goods.getGoodsName() + "up");
update(goods);
}
/**
* 事务不生效
* 1.访问权限问题。
* private void add(Goods goods) {}
* 虽然这种情况理论上存在,但是实际开发中绝对不会存在!!!
* 因为private修饰的方法其他类调用不了,编译就会不通过。
*/
@Override
@Transactional(rollbackFor = Exception.class)
//private void addPrivate(Goods goods) {
public void addPrivate(Goods goods) {
insert(goods);
goods.setGoodsName(goods.getGoodsName() + "up");
updateException(goods);
}
/**
* 事务不生效
* 2. 方法用final修饰
* 提示:Methods annotated with '@Transactional' must be overridable
* 虽然有提示事物注解不能有final修饰,但是可以编译通过
* 事物不会回滚,新增成功,更新失败
*/
@Override
@Transactional(rollbackFor = Exception.class)
public final void addFinal(Goods goods) {
insert(goods);
goods.setGoodsName(goods.getGoodsName() + "up");
updateException(goods);
}
/**
* 事务不生效
* 3.方法内部调用
* 实现:有时候我们需要在某个Service类的某个方法中,调用另外一个事务方法
* 验证:事物不会回滚,新增成功,更新失败
* 如果有些场景,确实想在同一个类的某个方法中,调用它自己的另外一个方法,该怎么办呢?
* 新加一个Service方法
*/
@Override
@Transactional(rollbackFor = Exception.class)
public void addMethod(Goods goods) {
insert(goods);
goods.setGoodsName(goods.getGoodsName() + "up");
// 事物修改
updateTransactional(goods);
}
/**
* 事务不生效
* 4.未被spring管理
* 实现:对象要被spring管理,需要创建bean实例。去掉@Controller、@Service、@Component、@Repository等注解,可以自动实现bean实例化和依赖注入的功能。
* 验证:这种情况下比如去掉@Service,在Controller或者其他service都无法注入,肯定异常,一般不会发生
*/
@Override
@Transactional(rollbackFor = Exception.class)
public void addNoSpring(Goods goods) {
insert(goods);
goods.setGoodsName(goods.getGoodsName() + "up");
update(goods);
}
/**
* 事务不生效
* 5.多线程调用
* 实现:多线程情况下第一一条还没抛出异常,第二条可能已经成功提交,所以在事物内多线程操作事物失效
* 验证:事物不会回滚,新增成功,更新失败
*/
@Override
@Transactional(rollbackFor = Exception.class)
public void addMulThread(Goods goods) {
for (int i = 10; i < 15; i++) {
int finalI = i;
goods.setId((long) finalI);
goods.setGoodsName(goods.getGoodsName() + finalI);
// 异常数据
if (i == 11 || i == 12) {
pool.execute(() -> {
insertException(goods);
});
}
//正常数据
pool.execute(() -> {
insert(goods);
});
}
}
/**
* 事务不生效
* 6.表不支持事务
* 实现:在创建表的时候,只需要把ENGINE参数设置成MyISAM即可:
* 验证:事物不会回滚,新增成功,更新失败
*/
@Override
@Transactional(rollbackFor = Exception.class)
public void addTableNoSupport(Goods goods) {
insert(goods);
goods.setGoodsName(goods.getGoodsName() + "up");
update(goods);
}
/**
* 事务不生效
* 7.未开启事务
* 实现:如果你使用的是springboot项目,那么你很幸运。因为springboot通过DataSourceTransactionManagerAutoConfiguration类,已经默默的帮你开启了事务。
* 8.我们在使用@Transactional注解时,是可以指定propagation参数的。
* 验证:事物不会回滚,新增成功,更新失败
*/
@Override
@Transactional(rollbackFor = Exception.class, propagation = Propagation.NEVER)
public void addSpread(Goods goods) {
insert(goods);
goods.setGoodsName(goods.getGoodsName() + "up");
// 由于指定了事物传播属性,所以never下不会回滚
updateException(goods);
}
/**
* 事务不生效
* 2.捕获异常
* 实现:捕获异常后没有抛出,事物检测不到异常自然不会回滚
* 验证:事物不会回滚,新增成功,更新失败
*/
@Override
@Transactional(rollbackFor = Exception.class)
public void addTry(Goods goods) {
try {
insert(goods);
goods.setGoodsName(goods.getGoodsName() + "up");
// 由于指定了事物传播属性,所以never下不会回滚
updateException(goods);
} catch (Exception e) {
System.out.println("异常啦:" + e);
}
}
/**
* 事务不生效
* 3.手动抛了别的异常
* 实现:手动捕获异常后抛出,事物检测不到异常,事物不会滚
* 验证:事物不会回滚,新增成功,更新失败
*/
@Override
@Transactional(rollbackFor = Exception.class)
public void addThrow(Goods goods) throws Exception {
try {
insert(goods);
goods.setGoodsName(goods.getGoodsName() + "up");
// 由于指定了事物传播属性,所以never下不会回滚
updateException(goods);
} catch (Exception e) {
System.out.println("异常啦:" + e);
throw new Exception(e);
}
}
/**
* 事务不生效
* 4.自定义了回滚异常
* 实现:指定或者自定义指定异常,事物只检测指定异常,其他异常不会滚
* 验证:事物不会回滚,新增成功,更新失败
*/
@Override
@Transactional(rollbackFor = ArrayIndexOutOfBoundsException.class)
public void addRollBack(Goods goods) {
insert(goods);
goods.setGoodsName(goods.getGoodsName() + "up");
updateException(goods);
}
/**
* 事务不生效
* 5.嵌套事务回滚多了
* 实现:
* 验证:事物不会回滚,新增成功,更新失败
*/
@Override
@Transactional(rollbackFor = Exception.class)
public void addNested(Goods goods) {
insert(goods);
goods.setGoodsName(goods.getGoodsName() + "up");
updateException(goods);
}
/**
* 新增数据
*/
@Override
public int insert(Goods goods) {
return this.goodsMapper.insert(goods);
}
/**
* 异常新增数据
*/
@Override
public int insertException(Goods goods) {
int result = 0;
try {
int res = 1 / 0;
this.goodsMapper.insert(goods);
} catch (Exception e) {
throw new RuntimeException("更新失败啦,哈哈哈");
}
return result;
}
/**
* 正常修改数据
*/
@Override
public int update(Goods goods) {
return this.goodsMapper.update(goods);
}
/**
* 异常修改数据
*/
@Override
public int updateException(Goods goods) {
int result = 0;
try {
int res = 1 / 0;
this.goodsMapper.update(goods);
} catch (Exception e) {
throw new RuntimeException("更新失败啦,哈哈哈");
}
return result;
}
/**
* 事物修改数据
*/
@Override
@Transactional(rollbackFor = Exception.class)
public int updateTransactional(Goods goods) {
int result = 0;
try {
int res = 1 / 0;
this.goodsMapper.update(goods);
} catch (Exception e) {
throw new RuntimeException("更新失败啦,哈哈哈");
}
return result;
}
}