文章目录
前言
事务是什么?实际上学过mysql的伙伴并不陌生:
事务必须服从ACID原则。ACID指的是原子性(atomicity)、一致性(consistency)、隔离性(isolation)和持久性(durability)。通俗理解,事务其实就是一系列指令的集合。
原子性:操作这些指令时,要么全部执行成功,要么全部不执行。只要其中一个指令执行失败,所有的指令都执行失败,数据进行回滚,回到执行指令前的数据状态。
一致性:事务的执行使数据从一个状态转换为另一个状态,但是对于整个数据的完整性保持稳定。
隔离性:在该事务执行的过程中,无论发生的任何数据的改变都应该只存在于该事务之中,
对外界不存在任何影响。只有在事务确定正确提交之后,才会显示该事务对数据的改变。 其他事务才能获取到这些改变后的数据。
持久性:当事务正确完成后,它对于数据的改变是永久性的。
其实就是绑定式的执行一个需求,例如简单的转账业务,假如小A同学要给小B同学转500元,那么里面实际上包含了两个数据库更新操作:
- A用户的账上减去500元
- B用户的账户上加500元
这两个操作应该作为一个事务,如果拆开就存在问题:例如在A转账以后服务器崩溃了,那么A账户上的钱少了,但是B账户上钱并没有增加,所以就需要用事务去绑定它们。
那么按照以前编程式思维,应该获取事务当前的状态,判断是否全部完成,然后来进行事务的提交还是回滚
但是显然这是一个非常繁琐的事情,首先是对每一个事务都进行这样的判断和操作,代码很冗余,其次是事务和对数据库操作耦合度很高,不利于后面的修改和扩展。
那么Spring就为我们提供了很好的事务控制方式:称为声明式事务控制
Spring的事务控制——基于XML
啥是声明式事务控制呢?
其实就是采用声明的方式来处理事务,这里的声明就是在配置文件中配置。
它的好处和底层原理我们放在后面去总结,先体会一下Spring基于XML的事务控制是怎么实现的吧:
首先先新建一个Maven项目或者Module
(一)实现步骤与代码
1.导入相关的Maven坐标
Maven我都很贴心的注释了每一个的作用
<dependencies>
<!--spring导入-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.0.5.RELEASE</version>
</dependency>
<!--spring事务控制-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-tx</artifactId>
<version>5.0.5.RELEASE</version>
</dependency>
<!--spring jdbc模板-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
<version>5.0.5.RELEASE</version>
</dependency>
<!--spring测试-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<version>5.0.5.RELEASE</version>
</dependency>
<!--数据库连接驱动-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.15</version>
</dependency>
<!--数据库连接池-->
<dependency>
<groupId>c3p0</groupId>
<artifactId>c3p0</artifactId>
<version>0.9.1.1</version>
</dependency>
<!--日志管理-->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>RELEASE</version>
<scope>test</scope>
</dependency>
<!--AOP操作要引入的-->
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.8.9</version>
</dependency>
</dependencies>
2.数据库连接配置文件
jdbc.properties
我的Mysql是8版本的,如果有用5的就需要修改驱动
jdbc.driver=com.mysql.cj.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/test?useUnicode = true&characterEncoding = utf-8&useSSL = false&serverTimezone = GMT
jdbc.username=root
jdbc.password=root
数据库创建一个account表:
3.转账业务代码实现
先看一下结构:controller层我是在test中直接进行测试的。
实体类:这里我把getter和settter省略了
public class Account {
private String name;
private Integer money;
@Override
public String toString() {
return "Account{" +
"name='" + name + '\'' +
", money=" + money +
'}';
}
}
Dao层: 这里用到了一个JdbcTemplate(Jdbc模板类)它可以简化我们对数据库的操作
这个接口有三个方法:查询所有,转入金额,转出金额
public class UserDaoImpl implements IUserDao {
JdbcTemplate jdbcTemplate;
public List<Account> findAll() {
return jdbcTemplate.query("select * from account",new BeanPropertyRowMapper<Account>(Account.class));
}
public void in(String inMan,Integer money){
jdbcTemplate.update("update account set money=money+? where name=?",money,inMan);
}
public void out(String outMan,Integer money){
jdbcTemplate.update("update account set money=money-? where name=?",money,outMan);
}
public void setJdbcTemplate(JdbcTemplate jdbcTemplate) {
this.jdbcTemplate = jdbcTemplate;
}
}
这里还需要一些配置:
在resources包下新建一个spring的xml:applicationContext.xml文件
在里面配置数据库连接池和jdbc模板bean的声明:
注意这里需要引入context的命名空间:
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation=(省略)
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd">
<!--加载外部的jdbc.properties文件-->
<context:property-placeholder location="jdbc.properties"/>
<!--数据源DataSource-->
<bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource">
<property name="driverClass" value="${jdbc.driver}"/>
<property name="jdbcUrl" value="${jdbc.url}"/>
<property name="user" value="${jdbc.username}"/>
<property name="password" value="${jdbc.password}"/>
</bean>
<!--JdbcTemplate-->
<bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
<property name="dataSource" ref="dataSource"></property>
</bean>
</beans>
service层:
这里有一个IUserDao对象的创建,因此也需要在配置文件中声明
public class UserServiceImpl implements IUserService {
private IUserDao userDao;
@Override
public List<Account> findAll() {
return userDao.findAll();
}
@Override
public void transfer(String outMan, String inMan, Integer money) {
userDao.out(outMan,money);
//int i=1/0;
userDao.in(inMan,money);
}
public void setUserDao(IUserDao userDao) {
this.userDao = userDao;
}
}
<bean id="userDao" class="com.cdd.dao.impl.UserDaoImpl">
<property name="jdbcTemplate" ref="jdbcTemplate"/>
</bean>
Controller层: 这一层,没有写东西,而是直接在test中测试:
同样也要在配置文件中声明一下userService对象
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("classpath:applicationContext.xml")
public class UserControllerTest {
@Autowired
private IUserService userService;
@Test
public void test01(){
userService.transfer("A","B",500);
System.out.println( userService.findAll());
}
}
<bean id="userService" class="com.cdd.service.impl.UserServiceImpl">
<property name="userDao" ref="userDao"/>
</bean>
到这里,业务模块写完了,运行后可以实现转账业务:
但是如果我们在service转账业务方法中写一个自杀式的语句:int i=1/0;
那么显然在A转账给B后报错停止运行,B进账的过程就没有执行:
@Override
public void transfer(String outMan, String inMan, Integer money) {
userDao.out(outMan,money);
int i=1/0;
userDao.in(inMan,money);
}
那么我们就需要利用事务控制将金额的进出绑定为一个事务,只要有一个不成功,事务就回滚
4.对转账业务进行事务控制
在applicationContext.xml中配置:
需要引入aop和tx的命名空间
完整的配置文件:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:aop="http://www.springframework.org/schema/aop"
xmlns:tx="http://www.springframework.org/schema/tx"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd
http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd">
<!--加载外部的jdbc.properties文件-->
<context:property-placeholder location="jdbc.properties"/>
<!--数据源DataSource-->
<bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource">
<property name="driverClass" value="${jdbc.driver}"/>
<property name="jdbcUrl" value="${jdbc.url}"/>
<property name="user" value="${jdbc.username}"/>
<property name="password" value="${jdbc.password}"/>
</bean>
<!--JdbcTemplate-->
<bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
<property name="dataSource" ref="dataSource"></property>
</bean>
<bean id="userDao" class="com.cdd.dao.impl.UserDaoImpl">
<property name="jdbcTemplate" ref="jdbcTemplate"/>
</bean>
<!--目标对象 内部方法就是切点-->
<bean id="userService" class="com.cdd.service.impl.UserServiceImpl">
<property name="userDao" ref="userDao"/>
</bean>
<!--平台事务管理器-->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"/>
</bean>
<!--通知 事务的增强-->
<tx:advice id="txAdvice" transaction-manager="transactionManager">
<tx:attributes>
<tx:method name="*"/>
</tx:attributes>
</tx:advice>
<!--事务AOP的织入-->
<aop:config>
<aop:advisor advice-ref="txAdvice" pointcut="execution(* com.cdd.service.impl.*.*(..))"/><!--advisor 配置事务增强-->
</aop:config>
</beans>
到这里可以再运行一下刚才的代码:
可见当A转账给B后,执行Int i=1/0;导致运行终止,后面B进账没有执行,那么这个事务就会回滚,A的金额不变。
做到这里,你有没有发现什么?
是不是事务只需要在Spring的配置文件中配置即可,业务代码和事务是不是完全的分开?
这真的太香了,spring真强大!!!
5.Spring基于xml的事务控制实例代码
Spring基于XML的事务控制代码我已经上传到github上,有需要的伙伴可以自己去拉取:https://github.com/IsMrChen/Practice/tree/springtest/spring
注意分支:
(二)声明式事务处理的作用和底层原理分析
首先它的作用,我们通过刚才的实现过程就可以看出来:
1.事务管理不侵入开发的组件。具体来说,业务逻辑对象就不会意识到正在事务管理之中,事实上也应该如此,因为事务管理是属于系统层面的服务,而不是业务逻辑的一部分,如果想要改变事务管理策划的话,也只需要在定义文件中重新配置即可
2.在不需要事务管理的时候,只要在设定文件上修改一下,即移去事务管理服务,无需改变代码重新编译,这样维护起来极其方便。
底层原理分析:
实际上Spring声明式事务控制底层就是AOP
我们先回顾一下什么是AOP
AOP是面向切面编程,是通过预编译方式和运行期动态代理实现程序功能的统一维护的一种技术
作用:在程序运行期间,在不修改源码的情况下对方法进行功能增强
优势:减少重复代码,提高开发效率,并且便于维护
这里按照我的理解打个比方吧:
就像王者里,英雄配置铭文一样,咱今天可以加强攻击,明天可以加强防御等等,英雄和铭文是分开的,互不影响,然后通过配置对英雄(目标对象)进行增强
那么你对这个英雄所使用的所有铭文结合起来就叫做一个切面。
通过设置(配置文件)将英雄和对应的切面组合起来(织入)然后就可以使用啦
配好铭文的英雄solo起来是不是更拉风了?哈哈哈
那么同理对一个业务做事务控制也可以看成是增强目标对象的过程,咱们的英雄(目标对象)就是转账业务,切面(那些要给方法添加什么属性的铭文的组合),织入就是给咱们的英雄安装这个切面的铭文。
如果还是不明白可以看一下AOP实现过程:
1.导入AOP相关坐标
2.创建目标接口和目标类(内部有切点)
3.将目标类和切面类的对象创建权交给spring
4.在applicationContext.xml中配置织入关系
5.测式代码
这里需要注意的是在配置文件中有一个平台事务管理器的配置:
这个与dao层连接数据库操作的实现方式有关,如果dao是jdbc,jdbc模板,mybatis等实现的那么就用这个事务管理器,如果用其他的则需要根据需要进行替换。
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"/>
</bean>
注意事务的增强中:
看完xml的事务控制再看一下注解的方式,你会发现新大陆!
【Spring深入学习】Spring的事务控制——基于注解
如果文章对你有帮助,不要忘了给我点个赞吼( ̄▽ ̄)~
欢迎关注我的微信公众号:松鼠技术站