4

【Spring系列】- Spring事务底层原理 - 怒放吧德德

 1 year ago
source link: https://www.cnblogs.com/lyd-code/p/16909632.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

Spring事务底层原理

😄生命不息,写作不止
🔥 继续踏上学习之路,学之分享笔记
👊 总有一天我也能像各位大佬一样
🏆 一个有梦有戏的人 @怒放吧德德
🌝分享学习心得,欢迎指正,大家一起学习成长!

事务.jpg

前言

昨天学习了bean生命周期底层原理,今天就来接着简单学习spring事务的底层理解。

实验准备

1|0配置文件

首先在配置文件中配置jdbcTemplate和事务管理器,并且需要开启事务的注解@EnableTransactionManagement以及@Configuration注解



@ComponentScan("com.lyd") @EnableTransactionManagement @Configuration public class ApplicationConfig { @Bean public JdbcTemplate jdbcTemplate() { return new JdbcTemplate(dataSource()); } @Bean public PlatformTransactionManager transactionManager() { DataSourceTransactionManager transactionManager = new DataSourceTransactionManager(); transactionManager.setDataSource(dataSource()); return transactionManager; } @Bean public DataSource dataSource() { DriverManagerDataSource dataSource = new DriverManagerDataSource(); dataSource.setUrl("jdbc:mysql://127.0.0.1:3306/eladmin?serverTimezone=Asia/Shanghai&characterEncoding=utf8&useSSL=false&useOldAliasMetadataBehavior=true"); dataSource.setUsername("root"); dataSource.setPassword("12356"); return dataSource; } }

1|0准备数据表

本次实验使用学生表,就简单几个字段。

image.png

Spring事务的底层原理

       我们在需要加上事务的方法上添加@Transactional注解,然后在此方法中使用jdbcTemplate去执行SQL语句,再来执行此方法,观察可以看到,事务真的回滚了。



@Component public class UserService { @Autowired private JdbcTemplate jdbcTemplate; @Transactional public void test(){ jdbcTemplate.execute("insert into student values (1, 'lyd', 18, '20183033210')"); throw new NullPointerException(); } }

1|0原理

       首先spring会调用代理对象,对于事务,代理对象会通过执行事务的切面逻辑。在这个切面逻辑,Spring会去判断是否含有@Transactional事务注解,如果有才会去开启事务。spring的事务管理器会新建一个数据库连接conn,紧接着会把conn.autocommit 设置为 false ,autocommit(自动提交),每次执行完SQL后就会立马提交,因此这里需要设置为false。spring默认是开启了自动提交,当SQL执行结束之后就会提交,当遇到异常的时候,由于前面的事务都已经提升,因此就没法回滚了,所以需要把自动提交给关闭了。最后在通过第一次创建的对象(Spring个生命周期中通过构造方法创造的对象)去执行test方法。接着会去执行SQL语句,在此SQL执行完之后是不会进行提交的,在执行SQL语句之前,jdbcTemplate会去拿到事务管理器创建的这个数据库连接conn。当执行完test方法后,Spring事务会去判断是否有异常,没有异常就会提交事务(conn.commit()),否者就会事务回滚(conn.rollback());

image.png

1|0Spring事务失效

       接下来实现一个案例,在test方法中调用本类的另一个方法Add,两个方法都是执行了插入的SQL语句。两个方法都加上了@Transactional注解,只是在第二个方法中的注解标上一个策略:propagation = Propagation.NEVER,这个的意思是:总是非事务地执行,如果存在一个活动事务,则抛出异常。按道理来说以下代码执行将会出现异常,并且会回滚事务。



@Component public class UserService { @Autowired private JdbcTemplate jdbcTemplate; @Transactional public void test(){ jdbcTemplate.execute("insert into student values (1, 'lyd', 18, '20183033210')"); doAdd(); } @Transactional(propagation = Propagation.NEVER) public void doAdd() { jdbcTemplate.execute("insert into student values (2, 'lyy', 18, '20183033211')"); } }

可是,最后的结果却不是预期结果。

image.png

1|0失效原理

       可见最后还是将两条数据插入了,显然这个事务是不回滚的,那么这是为什么呢?从上面说事务的底层原理就可以知道,当spring创建了代理对象,在代理对象内部的test方法中的切面逻辑,会去创建数据库连接等等,最后由普通对象(UserService.class通过构造方法去创建的对象)去执行test,也就是相当于是使用了普通对象去执行doAdd方法,普通对象就只是构造方法实例化的一个对象,执行doAdd并不会去检测这个@Transactional注解,因此这个事务就不会被执行到,也就不会回滚。然而spring应该是在执行代理类的test方法时候回去判断@Transactional注解,会有额外的逻辑去判断事务,也就是doAdd应该也要由代理对象去执行。

1|0解决方案

那么有办法解决吗?办法肯定过有,接下来介绍一种解决方法

1|0方案一

将add方法抽到另一个bean类里面



@Component public class UserBaseService { @Autowired private JdbcTemplate jdbcTemplate;

@Transactional(propagation = Propagation.NEVER) public void doAdd() { jdbcTemplate.execute("insert into student values (2, 'lyy', 18, '20183033211')"); } }

再来通过bean对象来执行这个doAdd方法



@Component public class UserService { private JdbcTemplate jdbcTemplate; private UserBaseService userBaseService; jdbcTemplate.execute("insert into student values (1, 'lyd', 18, '20183033210')"); userBaseService.doAdd(); } }

然后把数据表清空,在此执行

image.png

这回就报错了,而且也是我们所期望的错误,在看一下数据表,发现数据没有保存进去。

1|0方案二

       那如果我们还是想要在本类中去执行这个doAdd方法呢?其实也是可以,就是我们通过自己调用自己的方式,在本类中引用本类的bean对象,此时他就是一个代理对象,这样事务的策略也就能够实现了。



@Autowired private UserService userService;

@Transactional public void test(){ jdbcTemplate.execute("insert into student values (1, 'lyd', 18, '20183033210')"); throw new NullPointerException(); userService.doAdd(); }

也能得到我们预期的实现效果,数据库中也没有相关数据。

image.png

@Configuration底层原理

在上面我们提到了jdbcTemplate获得数据库连接,那么这个又是如何得到呢?我们来看一下一开始的配置。



@Bean public JdbcTemplate jdbcTemplate() { return new JdbcTemplate(dataSource()); }

@Bean public PlatformTransactionManager transactionManager() { DataSourceTransactionManager transactionManager = new DataSourceTransactionManager(); transactionManager.setDataSource(dataSource()); return transactionManager; }

@Bean public DataSource dataSource() { DriverManagerDataSource dataSource = new DriverManagerDataSource(); dataSource.setUrl("jdbc:mysql://127.0.0.1:3306/eladmin?serverTimezone=Asia/Shanghai&characterEncoding=utf8&useSSL=false&useOldAliasMetadataBehavior=true"); dataSource.setUsername("root"); dataSource.setPassword("12356"); return dataSource; }

1|0贯穿逻辑

       在创建 jdbcTemplate 的bean对象的时候,会去调用 dataSource ,在创建事务管理PlatformTransactionManager也会去调用一次dataSource方法,在这个方法中会创建新的dataSource,那么,所获得的事务不就不一样了吗,显然这是不行的。那么在spring中,他是如何实现单例的呢?就是通过 @Configuration 注解来实现。
       其实spring会通过ThreadLocal(线程变量,是一个以ThreadLocal对象为键、任意对象为值的存储结构),在spring中ThreadLocal<Map<DataSource, conn>>是根据 DataSource来存储连接conn,如果没有 @Configuration注解来实现,两次使用的DataSource就是不同的。 jdbcTemplate在获取 DataSource 对象的时候,会去ThreadLocal的map根据jdbcTemplate自己的DataSource去找连接,然而这时候DataSource对象不同,他就找不到,就会自己从新生成连接,当执行完SQL语句之后,就会去提交,这时候接下来再抛异常已经没用了。

1|0@Configuration原理

       然而,@Configuration能够实现单例DataSource对象呢?这就是因为@Configuration也是采用了动态代理会创建ApplicationConfig的代理对象。spring会创建ApplicationConfig的代理对象,这个代理对象会去调用jdbcTemplate方法,而代理对象会执行super.jdbcTemplate(),在这个方法中需要执行dataSource()方法,他会首先去容器中找是否有dataSource的bean对象,如果有直接返回,没有就会创建,创建之后会将单例进行保存。接着transactionManager方法也是由代理对象去执行的,在他需要dataSource对象的时候,也是现去容器查找,这就能够实现他们两个的dataSource是一样的了。这样事务拿到的数据库连接就是相同了,如果是在使用dataSource这个bean对象的时候,使用的beanName是不同,那么最后得到的连接也就不同。

👍创作不易,如有错误请指正,感谢观看!记得点赞哦!👍

__EOF__


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK