spring事务和数据库事务的关系和相关知识总结(拓展:redis事务)

前言:首先,事务这个概念是数据库层面的,数据库事务和spring事务本质上其实都是同一个概念,spring的事务是对数据库的事务的封装,最后本质的实现还是在数据库,假如数据库不支持事务的话,spring的事务是没有作用的。数据库的事务说简单就只有开启,回滚和关闭,spring对数据库事务的包装,原理就是拿一个数据连接,根据spring的事务配置,操作这个数据连接对数据库进行事务开启,回滚或关闭操作。但是spring除了实现这些,还配合spring的传播行为对事务进行了更广泛的管理。其实这里还有个重要的点,那就是事务中涉及的隔离级别,以及spring如何对数据库的隔离级别进行封装。事务与隔离级别放在一起理解会更好些。

文章目录

  • 一、spring事务的实现方式和原理以及隔离级别
    • 1、spring事务的实现方式
    • 2、spring事务隔离级别
  • 二、spring事务传播机制
  • 三、spring事务什么时候会失效
  • 四、案例&注意点&大坑
  • 五、分布式事务
  • 拓展:redis事务

一、spring事务的实现方式和原理以及隔离级别

1、spring事务的实现方式

在使用Spring框架时,可以有两种使用事务的方式,一种是编程式的,一种是申明式的。

【1】- @Transactional注解就是申明式的。
比如我们可以通过在某个方法上增加@Transactional注解,就可以开启事务,这个方法中所有的sql都会在一个事务中执行,统一成功或失败。
在一个方法上加了@Transactional注解后,Spring会基于这个类生成一个代理对象,会将这个代理对象作为bean,当在使用这个代理对象的方法时,如果这个方法上存在@Transactional注解,那么代理逻辑会先把事务的自动提交设置为false,然后再去执行原本的业务逻辑方法,如果执行业务逻辑方法没有出现异常,那么代理逻辑中就会将事务进行提交,如果执行业务逻辑方法出现了异常,那么则会将事务进行回滚。

PS:如果关于动态代理不懂的可以去复习一遍动态代理的源码就清楚了

public static Object newProxyInstance(ClassLoader loader, //用哪个类加载器去加载代理对象
                                          Class<?>[] interfaces, //动态代理类需要实现的接口
                                          InvocationHandler h) //动态代理方法在执行时,会调用h里面的invoke方法去执行
        throws IllegalArgumentException

当然,针对哪些异常回滚事务是可以配置的,可以利用@Transactional注解中的rollbackFor属性进行配置,默认情况下会对RuntimeException和Error进行回滚。
PS:这里可以结合编程使用:
方式一:

@Transactional(rollbackFor = Exception.class)
public R function() {
    try {
        // 业务代码
        ......
    } catch (Exception e) {
        // 手动回滚事务
        TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
        return R.fail();
    }
    return R.suc();
}

方式二:

@Transactional(rollbackFor = Exception.class)
public R function() {
    try {
        // 业务代码
        ......
       // 设置事务回滚点
        Object savePoint = TransactionAspectSupport.currentTransactionStatus().createSavepoint();
       // 业务代码
        ......
    } catch (Exception e) {
       // 手动回滚到savePoint事务点
        TransactionAspectSupport.currentTransactionStatus().rollbackToSavepoint(savePoint);
        return R.fail();
    }
    return R.suc();
}

当然,自己设定回滚的条件也可以,即如果满足什么条件,就手动回滚事务。

【2】- 编程式可以手动创建、回滚和提交事务。
例如:
1.注入DataSourceTransactionManager

@Autowired
private DataSourceTransactionManager dataSourceTransactionManager;

2.手动开启事务

DefaultTransactionDefinition defaultTransactionDefinition = new DefaultTransactionDefinition();
defaultTransactionDefinition.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW);
TransactionStatus transaction = dataSourceTransactionManager.getTransaction(defaultTransactionDefinition);

3.try catch业务代码

try{
    // 业务代码
   ......
    // 手动提交事务
    dataSourceTransactionManager.commit(transaction);
} catch {
    // 手动回滚事务
    dataSourceTransactionManager.rollback(transaction);
}

PS:如果A方法调用了B方法,想在A方法中开启事务,B方法中提交事务,那么可以将TransactionStatus transaction作为参数传递给B方法。

2、spring事务隔离级别

spring事务隔离级别就是数据库的隔离级别:外加一个默认级别

  • read uncommitted(未提交读)
  • read committed(提交读、不可重复读)
  • repeatable read(可重复读)
  • serializable(可串行化)

相关知识:
【数据库】数据库中事务的隔离级别(读未提交、读已提交、重复读、可串行化)
【数据库】幻读与不可重复读

数据库的配置隔离级别是Read Commited,而Spring配置的隔离级别是Repeatable Read,请问这时隔离 级别是以哪一个为准?
以Spring配置的为准,如果spring设置的隔离级别数据库不支持,效果取决于数据库。

二、spring事务传播机制

多个事务方法相互调用时,事务如何在这些方法间传播?

方法A是一个事务的方法,方法A执行过程中调用了方法B,那么方法B有无事务以及方法B对事务的要求不同都会对方法A的事务具体执行造成影响,同时方法A的事务对方法B的事务执行也有影响,这种影响具体是什么就由两个方法所定义的事务传播类型所决定。

REQUIRED(Spring默认的事务传播类型):如果(B)当前没有事务,则自己新建一个事务,如果当前存在事务,则加入(A)这个事务。

其它作为了解吧:
SUPPORTS:当前存在事务,则加入当前事务,如果当前没有事务,就以非事务方法执行。
MANDATORY:当前存在事务,则加入当前事务,如果当前事务不存在,则抛出异常。
REQUIRES_NEW:创建一个新事务,如果存在当前事务,则挂起该事务。
NOT_SUPPORTED:以非事务方式执行,如果当前存在事务,则挂起当前事务
NEVER:不使用事务,如果当前事务存在,则抛出异常
NESTED:如果当前事务存在,则在嵌套事务中执行,否则REQUIRED的操作一样(开启一个事务)

三、spring事务什么时候会失效

spring事务的原理是AOP,进行了切面增强,那么失效的根本原因是这个AOP不起作用了!常见情况有如下几种:

  1. 类内部调用:A 类的 a1 方法没有标注 @Transactional,a2 方法标注 @Transactional,在 a1 里面调用 a2。(ps:如果是A 类的 a1 方法标注 @Transactional,a2 方法没有标注@Transactional,在 a1 里面调用 a2,则生效)

打个比方:UserServiceUserServiceImplUserServiceImpl 去实现了 UserService 接口中的某个方法,如果在这个方法上加上 @Transactional 注解则会基于这个类(UserServiceImpl)生成一个代理对象,而生成得到代理对象的源码中(涉及动态代理):

public static Object newProxyInstance(ClassLoader loader, //用哪个类加载器去加载代理对象
                                          Class<?>[] interfaces, //动态代理类需要实现的接口
                                          InvocationHandler h) //动态代理方法在执行时,会调用h里面的invoke方法去执行
        throws IllegalArgumentException
//生成得到代理类
public Object getProxy(){
	//注意这里的getInterfaces()
	return Proxy.newProxyInstance(this.getClass().getClassLoader(),
               UserServiceImpl.getClass().getInterfaces(), 继承InvocationHandler接口的类对象);
}

通过 getInterfaces() 拿到 UserService 生成动态代理类。

由此可得知,当发生类内部调用时,如果是用 this.updateUserAge() 方式,即使 updateUserName() 实现了 UserService 并加了 @Transactional 注解,事务也是不会生效的,因为这里的 this 指的是 UserServiceImpl,而事务是拿到 UserServiceImpl 生成的代理对象(UserService),用他来做的事务处理,而 UserServiceImpl 不是代理对象。

public interface UserService {
	boolean updateUser();
	boolean updateUserName();
}
@Service
public class UserServiceImpl implements UserService {

	@Override
	public boolean updateUser() {
		......
		//这里的this指的是UserServiceImpl,这种调用方式下面的两个事务均不会生效
		this.updateUserName();
		this.updateUserAge();
		......
	}
	
	@Override
	@Transactional
	public boolean updateUserName() {
		......
	}
	
	@Transactional
	public boolean updateUserAge() {
		......
	}
}

解决方法很简单,让那个this变成代理类即可!

//通过api获取到代理对象
UserService proxy = (UserService) AopContext.currentProxy();
proxy.updateUserName();

需要导包和在启动类上加注解才能这样显式地获取

ps:如果对于这点不理解的可以看看事务的原理,或者结合这个视频进行理解https://www.bilibili.com/video/BV1cr4y1671t?p=54,如果还是不理解就可以看看动态代理,关于动态代理不懂的可以去复习一遍动态代理的源码就清楚了

  1. 方法不是public的:@Transactional 只能用于 public 的方法上,否则事务不会失效,如果要用在非 public 方法上,可 以开启 AspectJ 代理模式。
  2. 异常不匹配:@Transactional 未设置 rollbackFor 属性(抛出的异常没有被定义,默认为RuntimeException),而方法返回的是其它 Exception 等异常。
  3. 多线程情况:父线程调用子线程,父(子)线程抛出异常,子(父)线程不抛出异常:父(子)线程抛出线程,事务回滚,因为子(父)线程是独立存在,和父(子)线程不在同一个事务中,所以子(父)线程的修改并不会被回滚
  4. 数据库不支持事务。
  5. 没有被spring管理。

参考:https://www.jb51.net/article/256279.htm

四、案例&注意点&大坑

如果一个方法使用@Transactional开启了事务,方法的业务中修改了A表,接着方法又调用异步服务修改A表,则会发生死锁问题。
原因:事务先给A表上了表锁,紧接着执行的异步方法是新开的线程,无法加入到主线程的事务中,所以需要释放了A表的锁才能够修改A表,而因为整个方法未执行完,事务未提交,所以就释放不了锁,最后造成死锁问题。
解决方法:去掉@Transactional,使用编程方法,手动添加、回滚和提交事务(在开启异步服务前就将事务提交)。
这个问题涉及到事务的原理,可以回看前言或前面的内容。

如果业务中修改了A表,然后调用其它异步服务,其它异步服务修改了B表,若这个异步服务出现异常,A表仍然会被修改,不会受其它异步服务影响。若不是异步服务,是同步的话,则受事务影响,即其它服务出现异常,AB表都不会被修改。(以上说的服务都是属于同一个微服务中,若是涉及到远程调用其它微服务,则属于分布式事务,以上讨论的是本地事务。)

五、分布式事务

待更新

拓展:redis事务

redis中事务的概念就是一次执行多条命令;并带有以下三个保证

  • 批量操作在发送 EXEC 命令前被放入队列缓存。
  • 收到 EXEC 命令后进入事务执行,事务中任意命令执行失败,其余的命令依然被执行
  • 在事务执行过程,其他客户端提交的命令请求不会插入到事务执行命令序列中

一个redis事务会经过三个步骤:开始事务 - 》命令入队 - 》执行事务

注意:

  • redis是不支持事务回滚的操作,看上面加粗的文字;
  • 单个redis命令是原子性的,但是多个redis事务是多个命令一起执行的时候是不具备原子性的;

你可能感兴趣的:(数据库,后端(泛),自用复习,spring,数据库,java)