点击上方“芋道源码”,选择“设为星标”
管她前浪,还是后浪?
能浪的浪,才是好浪!
每天 10:33?更新文章,每天掉亿点点头发...
源码精品专栏
对于从事java开发工作的同学来说,spring的事务肯定再熟悉不过了。在某些业务场景下,如果同时有多张表的写入操作,为了保证操作的原子性(要么同时成功,要么同时失败)避免数据不一致的情况,我们一般都会使用spring事务。
没错,spring事务大多数情况下,可以满足我们的业务需求。但是今天我要告诉大家的是,它有很多坑,稍不注意事务就会失效。
不信,我们一起看看。
@Service
public?class?UserService?{
????@Autowired
????private?UserMapper?userMapper;
????
????@Transactional
????private?void?add(UserModel?userModel)?{
????????userMapper.insertUser(userModel);
????}
}
我们可以看到add方法的访问权限被定义成了private,这样会导致事务失效,spring要求被代理方法必须是public的。
AbstractFallbackTransactionAttributeSource类的computeTransactionAttribute方法中有个判断,如果目标方法不是public,则TransactionAttribute返回null,即不支持事务。
protected?TransactionAttribute?computeTransactionAttribute(Method?method,?@Nullable?Class<?>?targetClass)?{
????//?Don't?allow?no-public?methods?as?required.
????if?(allowPublicMethodsOnly()?&&?!Modifier.isPublic(method.getModifiers()))?{
??????return?null;
????}
????//?The?method?may?be?on?an?interface,?but?we?need?attributes?from?the?target?class.
????//?If?the?target?class?is?null,?the?method?will?be?unchanged.
????Method?specificMethod?=?AopUtils.getMostSpecificMethod(method,?targetClass);
????//?First?try?is?the?method?in?the?target?class.
????TransactionAttribute?txAttr?=?findTransactionAttribute(specificMethod);
????if?(txAttr?!=?null)?{
??????return?txAttr;
????}
????//?Second?try?is?the?transaction?attribute?on?the?target?class.
????txAttr?=?findTransactionAttribute(specificMethod.getDeclaringClass());
????if?(txAttr?!=?null?&&?ClassUtils.isUserLevelMethod(method))?{
??????return?txAttr;
????}
????if?(specificMethod?!=?method)?{
??????//?Fallback?is?to?look?at?the?original?method.
??????txAttr?=?findTransactionAttribute(method);
??????if?(txAttr?!=?null)?{
????????return?txAttr;
??????}
??????//?Last?fallback?is?the?class?of?the?original?method.
??????txAttr?=?findTransactionAttribute(method.getDeclaringClass());
??????if?(txAttr?!=?null?&&?ClassUtils.isUserLevelMethod(method))?{
????????return?txAttr;
??????}
????}
????return?null;
??}
基于 Spring Boot + MyBatis Plus + Vue & Element 实现的后台管理系统 + 用户小程序,支持 RBAC 动态权限、多租户、数据权限、工作流、三方登录、支付、短信、商城等功能
@Service
public?class?UserService?{
????@Autowired
????private?UserMapper?userMapper;
????@Transactional
????public?final?void?add(UserModel?userModel)?{
????????userMapper.insertUser(userModel);
????}
}
我们可以看到add方法被定义成了final的,这样会导致spring aop生成的代理对象不能复写该方法,而让事务失效。
基于 Spring Cloud Alibaba + Gateway + Nacos + RocketMQ + Vue & Element 实现的后台管理系统 + 用户小程序,支持 RBAC 动态权限、多租户、数据权限、工作流、三方登录、支付、短信、商城等功能
@Service
public?class?UserService?{
????@Autowired
????private?UserMapper?userMapper;
????@Transactional
????public?void?add(UserModel?userModel)?{
????????userMapper.insertUser(userModel);
????????updateStatus(userModel);
????}
????@Transactional
????public?void?updateStatus(UserModel?userModel)?{
????????//?doSameThing();
????}
}
我们看到在事务方法add中,直接调用事务方法updateStatus。从前面介绍的内容可以知道,updateStatus方法拥有事务的能力是因为spring aop生成代理了对象,但是这种方法直接调用了this对象的方法,所以updateStatus方法不会生成事务。
//@Service
public?class?UserService?{
????@Autowired
????private?UserMapper?userMapper;
????@Transactional
????public?void?add(UserModel?userModel)?{
????????userMapper.insertUser(userModel);
????}
}
我们可以看到UserService类没有定义@Service注解,即没有交给spring管理bean实例,所以它的add方法也不会生成事务。
@Service
public?class?UserService?{
????@Autowired
????private?UserMapper?userMapper;
????@Transactional(propagation?=?Propagation.NEVER)
????public?void?add(UserModel?userModel)?{
????????userMapper.insertUser(userModel);
????}
}
我们可以看到add方法的事务传播特性定义成了Propagation.NEVER,这种类型的传播特性不支持事务,如果有事务则会抛异常。只有这三种传播特性才会创建新事务:PROPAGATION_REQUIRED,PROPAGATION_REQUIRES_NEW,PROPAGATION_NESTED。
msql8以前的版本数据库引擎是支持myslam和innerdb的。我以前也用过,对应查多写少的单表操作,可能会把表的数据库引擎定义成myslam,这样可以提升查询效率。但是,要千万记得一件事情,myslam只支持表锁,并且不支持事务。所以,对这类表的写入操作事务会失效。
@Slf4j
@Service
public?class?UserService?{
????@Autowired
????private?UserMapper?userMapper;
????
????@Transactional
????public?void?add(UserModel?userModel)?{
????????try?{
????????????userMapper.insertUser(userModel);
????????}?catch?(Exception?e)?{
????????????log.error(e.getMessage(),?e);
????????}
????}
}
这种情况下事务不会回滚,因为开发者自己捕获了异常,又没有抛出。事务的AOP无法捕获异常,导致即使出现了异常,事务也不会回滚。
@Slf4j
@Service
public?class?UserService?{
????@Autowired
????private?UserMapper?userMapper;
????
????@Transactional
????public?void?add(UserModel?userModel)?throws?Exception?{
????????try?{
????????????userMapper.insertUser(userModel);
????????}?catch?(Exception?e)?{
????????????log.error(e.getMessage(),?e);
????????????throw?new?Exception(e);
????????}
????}
}
这种情况下,开发人员自己捕获了异常,又抛出了异常:Exception,事务也不会回滚。因为spring事务,默认情况下只会回滚RuntimeException(运行时异常)和Error(错误),不会回滚Exception。
@Slf4j
@Service
public?class?UserService?{
????@Autowired
????private?UserMapper?userMapper;
????@Autowired
????private?RoleService?roleService;
????@Transactional
????public?void?add(UserModel?userModel)?throws?Exception?{
????????userMapper.insertUser(userModel);
????????new?Thread(()?->?{
????????????roleService.doOtherThing();
????????}).start();
????}
}
@Service
public?class?RoleService?{
????@Transactional
????public?void?doOtherThing()?{
????????System.out.println("保存role表数据");
????}
}
我们可以看到事务方法add中,调用了事务方法doOtherThing,但是事务方法doOtherThing是在另外一个线程中调用的,这样会导致两个事务方法不在同一个线程中,获取到的数据库连接不一样,从而是两个不同的事务。如果想doOtherThing方法中抛了异常,add方法也回滚是不可能的。
如果看过spring事务源码的朋友,可能会知道spring的事务是通过数据库连接来实现的。当前线程中保存了一个map,key是数据源,value是数据库连接。
private?static?final?ThreadLocal<Map<Object,?Object>>?resources?=
??????new?NamedThreadLocal<>("Transactional?resources");
我们说的同一个事务,其实是指同一个数据库连接,只有拥有同一个数据库连接才能同时提交和回滚。如果在不同的线程,拿到的数据库连接肯定是不一样的,所以是不同的事务。
public?class?UserService?{
????@Autowired
????private?UserMapper?userMapper;
????@Autowired
????private?RoleService?roleService;
????@Transactional
????public?void?add(UserModel?userModel)?throws?Exception?{
????????userMapper.insertUser(userModel);
????????roleService.doOtherThing();
????}
}
@Service
public?class?RoleService?{
????@Transactional(propagation?=?Propagation.NESTED)
????public?void?doOtherThing()?{
????????System.out.println("保存role表数据");
????}
}
这种情况使用了嵌套的内部事务,原本是希望调用roleService.doOtherThing方法时,如果出现了异常,只回滚doOtherThing方法里的内容,不回滚 userMapper.insertUser里的内容,即回滚保存点。。但事实是,insertUser也回滚了。
why?
因为doOtherThing方法出现了异常,没有手动捕获,会继续往上抛,到外层add方法的代理方法中捕获了异常。所以,这种情况是直接回滚了整个事务,不只回滚单个保存点。
怎么样才能只回滚保存点呢?
@Slf4j
@Service
public?class?UserService?{
????@Autowired
????private?UserMapper?userMapper;
????@Autowired
????private?RoleService?roleService;
????@Transactional
????public?void?add(UserModel?userModel)?throws?Exception?{
????????userMapper.insertUser(userModel);
????????try?{
????????????roleService.doOtherThing();
????????}?catch?(Exception?e)?{
????????????log.error(e.getMessage(),?e);
????????}
????}
}
在代码中手动把内部嵌套事务放在try/catch中,并且不继续往抛异常。
介绍到这里,你会发现spring事务的坑还是挺多的~
欢迎加入我的知识星球,一起探讨架构,交流源码。加入方式,长按下方二维码噢:
已在知识星球更新源码解析如下:
最近更新《芋道 SpringBoot 2.X 入门》系列,已经 101 余篇,覆盖了?MyBatis、Redis、MongoDB、ES、分库分表、读写分离、SpringMVC、Webflux、权限、WebSocket、Dubbo、RabbitMQ、RocketMQ、Kafka、性能测试等等内容。
提供近 3W 行代码的 SpringBoot 示例,以及超 4W 行代码的电商微服务项目。
获取方式:点“在看”,关注公众号并回复?666?领取,更多内容陆续奉上。
文章有帮助的话,在看,转发吧。谢谢支持哟 (*^__^*)