事务全称叫数据库事务, 是数据库并发控制时的基本单位, 它是一个操作集合, 这些操作要么不执行, 要么都执行, 不可分割. 例如我们的转账这个业务, 就需要进行数据库事务的处理.
转账中至少会涉及到两条 SQL 语句:
- update Acoount set balance = balance - money where id = 'A';
- update Acoount set balance = balance + money where id = 'B'
上面这两条 SQL 就可以要看成是一个事务, 必须都执行, 或都不执行. 如何保证呢, 一般这样表示:
- # 开启事务
- begin transaction
- update Account set balance = balance - money where id = 'A';
- update Account set balance = balance + money where id = 'B'
- # 提交事务
- commit transaction
- Exception
- # 回滚事务
- rollback transaction
事务的特性 (笔试的时候会有)
Atomic(原子性): 事务中包含的操作被看做一个逻辑单元, 这个逻辑单元中的操作要么全部成功, 要么全部失败.
Consistency(一致性): 只有合法的数据可以被写入数据库, 否则事务应该将其回滚到最初状态. 在转账的时候不会出现一当少钱了, 另一方没有增加的情况.
Isolation(隔离性): 事务允许多个用户对同一个数据进行并发访问, 而不破坏数据的正确性和完整性. 同时, 并行事务的修改必须与其他并行事务的修改相互独立.
Durability(持久性): 事务完成之后, 它对于系统的影响是永久的, 该修改即使出现系统故障也将一直保留, 真实的修改了数据库.
以上 4 个属性常被简称为 acid(酸的).
事务并发的问题
脏读: 事务二读取到事务一中已经更新但是还没有提交的数据, 这就是脏读.
不可重复读: 一个事务两次读取同一个行数据结果不同, 因为有其它的事务对数据进行了更新. 此时的数据即为不可重复读数据.
幻读: 同一事务执行两次查询, 结果不一致, 因为中间有其它的事务对数据进行更改.
如何解决这些问题呢? 数据库系统为事务设置了 4 种不同的隔离级别.
事务隔离级别
读未提交 (read uncommitted): 最低级别, 可能会导入脏读.
读已提交 (read committed): 可以避免脏读, 只能查询到已经提交的数据. 且具有良好的性能, 但是不能避免不可重复读和幻读.
可重复读 (repeatable): 解决了不可重复读, 可能会出现幻读.
串行化 (serializable): 通过加锁, 使同一时间只能执行一个事务, 不出现上述问题, 但是可能会导致大量的超时现象和锁竞争.
另外, MySQL 中默认的隔离级别是可重复读. Oracle 中默认的事务隔离级别是读已提交.
说完事务, 想想我们曾经为了处理事务而写过的那些代码. 最后在说说 Spring 中是如何处理的, 学完 Spring 再也不用担心事务操作了.
在 JDBC 时代我们需要这样手动的处理事务.
// 获取连接 conn
conn.setAutoCommit(false); 设置提交方式为手工提交
- // 业务代码
- // 减钱
- // 加钱
conn.commit(); 提交事务
// 出现异常
conn.rollback(); 回滚事务
我们说处理事务那是处理数据库事务, 所以肯定要先有数据库连接才能说事务的事, 而在 Java 中我们连接数据库无非就是 JDBC, 或是对 JDBC 进一步的封装, 比方说 Hibernate ,Mybatis 或是 Dbutils 这些框架, 所以万变不离其宗, 就是这么回事, 就是看谁封装的好罢了. 你们有兴趣可以看看它们都是如何封装的.
Spring 中的如何管理事务呢
首先, 我们知道 Spring 是一个容器, 不同的框架在处理事务时用到的对象不同, 原生的 JDBC 使用 Connection , 而 Mybatis 中使用 SqlSession 对象. 而 Spring 为了整合这些不同的框架, 定义了一个 PlatformTransactionManager 接口来统一标准, 对不同的框架又有不同的实现类.
在 Spring 中根据 DAO 层技术的不同来选择不同的事务处理的对象, 是 JDBC 时使用 DataSourceTransactionManager, 是 Hibernate 时使用 HibernateTransitionmanager 对象, 核心对象就是 Transitionmanager.
在 Spring 中管理事务会涉及到这几个属性, 事务隔离级别, 是否只读, 事务的传播行为, 说到事务的传播行为, 指的就是不同的业务方法之间相互调用时, 应该如何管理事务. Spring 中一共定义了 7 种传播行为, 无脑记住使用 required , 表示支持当前事务, 若是不存在事务, 就创建一个. 例如在 A 调用 B 的时候, 会首先使用 A 的事务, 若 A 没有事务, 则新创建一个, 不管 B 有没有事务.
下面就是要实际操作一下, 需要有具体的业务逻辑, 还是那个转账的例子. 来看看如何使用 Spring 来管理事务, 有两种常见的管理方式, 我们一种一种的说.
使用 xml 配置
1 首先是导包, Spring 涉及的包是真的多, 我有一个省事的方法, 可能用到的 jar 包一下子导入.
2 导入新的约束文件, 不然在 xml 无法使用 tx 标签.
3 准备目标对象和通知并配置.
目标对象 AccountServiceImpl
- public class AccountServiceImpl implements AccountService {
- private AccountDAO ad;
- @Transactional(isolation=Isolation.REPEATABLE_READ,propagation = Propagation.REQUIRED,readOnly=false)
- @Override
- public void transfer(Integer from, Integer to, Double money) {
- ad.decreaseMoney(from, money);
- //int i = 1/0;
- ad.increaseMoney(to, money);
- }
- public void setAd(AccountDAO ad) {
- this.ad = ad;
- }
- }
在 AOP 中通知即为增强的代码, 而在处理事务时, 要增强的代码无非就是开启事务, 提交事务和回滚事务, 所以 Spring 已经为我们封装好了处理事务的通知, 我们只需要配置一下即可.
- <!-- 导入 properties 配置文件 -->
- <context:property-placeholder location="classpath:db.properties"/>
- <!-- 配置连接数据库的核心处理对象 -->
- <bean name="transactionManager" class = "org.springframework.jdbc.datasource.DataSourceTransactionManager">
- <property name="dataSource" ref= "dataSource"></property>
- </bean>
- <!-- 配置通知 -->
- <tx:advice id="txAdvise" transaction-manager="transactionManager">
- <tx:attributes>
- <tx:method name="transfer" isolation="DEFAULT" propagation="REQUIRED" read-only="false"/>
- </tx:attributes>
- </tx:advice>
- <!-- 配置 AOP , 以达成自动处理事务的要求 -->
- <aop:config>
- <aop:pointcut expression="execution(* yu.transation.*ServiceImpl.*(..))" id="txPointcut"/>
- <aop:advisor advice-ref="txAdvise" pointcut-ref="txPointcut"/>
- </aop:config>
- <bean name="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource">
- <property name="jdbcUrl" value="${jdbc.jdbcUrl}"></property>
- <property name="driverClass" value="${jdbc.driverClass}"></property>
- <property name="user" value="${jdbc.user}"></property>
- <property name="password" value="${jdbc.password}"></property>
- </bean>
- <!-- 配置 DAO 层对象 -->
- <bean name="ad" class="yu.transation.AccountDaoImpl">
- <property name="dataSource" ref = "dataSource"></property>
- </bean>
- <!-- 配置 Service 层对象 -->
- <bean name = "accountService" class = "yu.transation.AccountServiceImpl">
- <property name="ad" ref = "ad"></property>
- </bean>
上面的配置文件中, 在配置通知时, 我们具体到不同的方法会有不同的配置, 在项目应用时, 会使用通配符来进行配置. 下面介绍一下使用注解来处理事务, 看起来会比较简单.
步骤和上面有重复的部分, 需要导包导入约束, 接下来就是配置一下使用注解管理事务的开关
使用注解配置
- <!-- 打开注解配置 AOP 事务 -->
- <tx:annotation-driven/>
下面是使用注解为 Service 中的方法配置事务处理的属性, 当然, 每一个方法都写会比较麻烦, 也可以在类上面使用注解, 若是某个方法的处理规则不一致就单独使用注解配置一下.
- @Transactional(isolation=Isolation.REPEATABLE_READ,propagation = Propagation.REQUIRED,readOnly=false)
- @Override
- public void transfer(Integer from, Integer to, Double money) {...}
回顾一下, 以上主要说了事务以及 Spring 中处理事务的方式, 而这也正是 AOP 思想在 Spring 中的应用, 我们可以看到不管是前面说的 IoC 还是 AOP 在这里都有体现.
注解的出现是为了替换配置文件, 所以我就以配置文件为主, 并说一下与之对应的注解方式.
Spring 中的配置主要在核心配置文件 applicationContext.xml 中, 由不同的标签来表示, 所以首先我们就需要导入各种约束, 常用的约束有 bean,context,aop,tx .
bean 标签是最基本的标签, 主要用来配置各种对象.
- <!--
- 属性介绍:
- id: 为对象命名, 唯一性标识, 不能重复, 不能使用特殊字符.
- name: 和 id 的作用类似, 区别在于可是使用特殊字符, 可重复, 但是不建议重复.
- class: 指定对象的全类名.
- init-method: 对象初始化之后立即执行的方法.
- destroy-method: 对象销毁之前执行的方法.
- scope: 对象的作用范围, 可以设置单例 singleton 和多例 prototype. 默认为单例
- -->
- <bean name="userService" class="yu.service.UserServiceImpl">
- <property name=""value=""></property>
- <property name=""ref=""></property>
- </bean>
对应的注解有以下几个, 但是想要使用注解之前要首先配置一下......
- <!-- 打开注解配置, 扫描包及其子包 -->
- <context:component-scan base-package="yu">
- </context:component-scan>
使用注解的时候, 我们可以使用 @Component 来表示将这个对象交由 Spring 管理,@Scope 来指定对象的作用域. 之后便可以使用 @Resource 来获取对象.
在注册对象的时候我们可以使用 @Component , 但是若是每一个对象都是用这个注解, 不能很好的分辨出对象属于哪一层, 所以 Spring 又提供了 @Controller @Service @Repository 来分别表示控制器层, Service 层和 DAO 层的对象, 功能和 @Component 是一模一样的.
同样的在为对象赋值的时候, 我们可以使用注解 @Autowired 来自动获取容器中的对象, 可是若是有重名的情况就需要另外一个注解 @Qualifier 来具体指定叫什么名字, 这样就有点麻烦了, 我们一般都是直接使用 @Resource 来指定对象.
- @Component("user")
- @Scope("prototype")
- public class User{
- private String name;
- @Value(value = "18") // 属性注入, 项目中不用.
- private Integer age;
- //@Autowired 自动装配 Car 类型变量, 同一类型
- //@Qualifier("car") 指定具体的是哪一个.
- @Resource(name = "car") // 指名道姓指定是哪个对象
- private Car car;
- ...
- @PostConstruct
- public void init(){
- System.out.println("init 方法");
- }
- @PreDestroy
- public void destroy(){
- System.out.println("destory 方法");
- }
- }
aop 相关的配置和注解
在 Spring 中我们可以自定义通知和切面, 下面只是展示了如何配置, 但是在具体的业务中应该不会出现 5 种通知齐上阵的现象.
- <aop:config>
- <!-- 配置切点 -->
- <aop:pointcut expression="execution(* yu.service.*ServiceImpl.*(..))" id="pc"/>
- <aop:aspect ref="myAdvice">
- <!-- 指定名为 before 方法作为前置通知 -->
- <aop:before method="before" pointcut-ref="pc" />
- <!-- 后置 -->
- <aop:after-returning method="afterReturning" pointcut-ref="pc" />
- <!-- 环绕通知 -->
- <aop:around method="around" pointcut-ref="pc" />
- <!-- 异常拦截通知 -->
- <aop:after-throwing method="afterException" pointcut-ref="pc"/>
- <!-- 后置 -->
- <aop:after method="after" pointcut-ref="pc"/>
- </aop:aspect>
- </aop:config>
同样的, 我们也可以使用注解来达到自定义配置的方式. 同样的套路, 想用注解配置实现 aop, 需要打开注解配置 AOP 的开关.
<aop:aspectj-autoproxy></aop:aspectj-autoproxy>
之后就是在通知类中进行配置即可.
- @Aspect
- // 通知类
- public class MyAdvice {
- // 快速配置切点表达式, 方法直接调用即可
- @Pointcut("execution(* yu.service.*ServiceImpl.*(..))")
- public void pc(){}
- // 前置通知
- @Before("MyAdvice.pc()")
- public void before(){
- System.out.println("这是前置通知!!");
- }
- // 后置通知
- @AfterReturning("MyAdvice.pc()")
- public void afterReturning(){
- System.out.println("这是后置通知 (如果出现异常不会调用)!!");
- }
context 主要是和全局有关的配置
- <!-- 打开注解配置对象, 扫描包及其子包 -->
- <context:component-scan base-package="yu"></context:component-scan>
- <!-- 导入 properties 配置文件 -->
- <context:property-placeholder location="classpath:db.properties"/>
- <bean name="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource">
- <property name="jdbcUrl" value="${jdbc.jdbcUrl}"></property>
- <property name="driverClass" value="${jdbc.driverClass}"></property>
- <property name="user" value="${jdbc.user}"></property>
- <property name="password" value="${jdbc.password}"></property>
- </bean>
tx 配置事务管理中的通知
tx 用来配置通知对象, 而这个对象是由 Spring 为我们写好了, 而事务管理依赖于数据库连接对象, 所以你能看到 transactionManager 对象依赖于 dataSource 对象.
- <bean name="transactionManager" class = "org.springframework.jdbc.datasource.DataSourceTransactionManager">
- <property name="dataSource" ref= "dataSource"></property>
- </bean>
- <tx:advice id="txAdvise" transaction-manager="transactionManager">
- <tx:attributes>
- <tx:method name="transfer" isolation="DEFAULT" propagation="REQUIRED" read-only="false"/>
- </tx:attributes>
- </tx:advice>
- <aop:config>
- <aop:pointcut expression="execution(* yu.transation.*ServiceImpl.*(..))" id="txPointcut"/>
- <aop:advisor advice-ref="txAdvise" pointcut-ref="txPointcut"/>
- </aop:config>
使用注解配置时还是需要打开注解配置的开关
- <!-- 打开注解配置 AOP 事务 -->
- <tx:annotation-driven/>
在具体的业务方法上或是类上使用注解 @Transactional 来配置事务处理的方式.
- @Transactional(isolation=Isolation.REPEATABLE_READ,propagation = Propagation.REQUIRED,readOnly=false)
- @Override
- public void transfer(Integer from, Integer to, Double money) {
- ...
- }
最后有一个完美的意外, 那就是 import 标签. 用于导入其它的配置模块到主配置文件中.
- <!-- 导入其它的 Spring 配置模块 -->
- <import resource="yu/transation/applicationContext.xml" />
来源: https://www.cnblogs.com/YJK923/p/10187081.html