事务
事务全称叫数据库事务,是数据库并发控制时的基本单位,它是一个操作集合,这些操作要么不执行,要么都执行,不可分割。例如我们的转账这个业务,就需要进行数据库事务的处理。
转账中至少会涉及到两条 sql 语句:
1
2
|
update acoount set balance = balance - money where id = 'a' ; update acoount set balance = balance + money where id = 'b' |
上面这两条 sql 就可以要看成是一个事务,必须都执行,或都不执行。如何保证呢,一般这样表示:
1
2
3
4
5
6
7
8
9
10
11
12
|
# 开启事务 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 时代我们需要这样手动的处理事务。
1
2
3
4
5
6
7
8
|
// 获取连接 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
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
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 已经为我们封装好了处理事务的通知,我们只需要配置一下即可。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
|
<!-- 导入 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> |
上面的配置文件中,在配置通知时,我们具体到不同的方法会有不同的配置,在项目应用时,会使用通配符来进行配置。下面介绍一下使用注解来处理事务,看起来会比较简单。
步骤和上面有重复的部分,需要导包导入约束,接下来就是配置一下使用注解管理事务的开关
使用注解配置
1
2
|
<!-- 打开注解配置 aop 事务 --> <tx:annotation-driven/> |
下面是使用注解为 service 中的方法配置事务处理的属性,当然,每一个方法都写会比较麻烦,也可以在类上面使用注解,若是某个方法的处理规则不一致就单独使用注解配置一下。
1
2
3
|
@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 标签是最基本的标签,主要用来配置各种对象。
1
2
3
4
5
6
7
8
9
10
11
12
13
|
<!-- 属性介绍: 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> |
对应的注解有以下几个,但是想要使用注解之前要首先配置一下……
1
2
|
<!-- 打开注解配置,扫描包及其子包 --> <context:component-scan base- package = "yu" ></context:component-scan> |
使用注解的时候,我们可以使用 @component 来表示将这个对象交由 spring 管理,@scope 来指定对象的作用域。之后便可以使用 @resource 来获取对象。
在注册对象的时候我们可以使用 @component ,但是若是每一个对象都是用这个注解,不能很好的分辨出对象属于哪一层,所以 spring 又提供了 @controller @service @repository 来分别表示控制器层,service 层和 dao 层的对象,功能和 @component 是一模一样的。
同样的在为对象赋值的时候,我们可以使用注解 @autowired 来自动获取容器中的对象,可是若是有重名的情况就需要另外一个注解 @qualifier 来具体指定叫什么名字,这样就有点麻烦了,我们一般都是直接使用 @resource 来指定对象。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
@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 种通知齐上阵的现象。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
<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 的开关。
1
|
<aop:aspectj-autoproxy></aop:aspectj-autoproxy> |
之后就是在通知类中进行配置即可。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
@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 主要是和全局有关的配置
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
<!-- 打开注解配置对象,扫描包及其子包 --> <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 对象。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
<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> |
使用注解配置时还是需要打开注解配置的开关
1
2
|
<!-- 打开注解配置 aop 事务 --> <tx:annotation-driven/> |
在具体的业务方法上或是类上使用注解 @transactional 来配置事务处理的方式。
1
2
3
|
@transactional (isolation=isolation.repeatable_read,propagation = propagation.required,readonly= false ) @override public void transfer(integer from, integer to, double money) {...} |
最后有一个完美的意外,那就是 import 标签。用于导入其它的配置模块到主配置文件中。
1
2
|
<!-- 导入其它的 spring 配置模块 --> < import resource= "yu/transation/applicationcontext.xml" /> |
总结
以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,如果有疑问大家可以留言交流,谢谢大家对服务器之家的支持。
原文链接:https://www.cnblogs.com/YJK923/p/10187081.html