SSM框架(3):配置Spring的事务管理器,实现事务控制
一、相关概念
1、不可重复读 和 幻读 的区别
很多人容易搞混不可重复读和幻读,确实这两者有些相似。但不可重复读重点在于update和delete,而幻读的重点在于insert。
如果使用锁机制来实现这两种隔离级别,在可重复读中,该sql第一次读取到数据后,就将这些数据加锁,其它事务无法修改这些数据,就可以实现可重复 读了。但这种方法却无法锁住insert的数据,所以当事务A先前读取了数据,或者修改了全部数据,事务B还是可以insert数据提交,这时事务A就会 发现莫名其妙多了一条之前没有的数据,这就是幻读,不能通过行锁来避免。需要Serializable隔离级别 ,读用读锁,写用写锁,读锁和写锁互斥,这么做可以有效的避免幻读、不可重复读、脏读等问题,但会极大的降低数据库的并发能力。
所以说不可重复读和幻读最大的区别,就在于如何通过锁机制来解决他们产生的问题。
上文说的,是使用悲观锁机制来处理这两种问题,但是MySQL、ORACLE、PostgreSQL等成熟的数据库,出于性能考虑,都是使用了以乐观锁为理论基础的MVCC(多版本并发控制)来避免这两种问题。
当然, 从总的结果来看, 似乎两者都表现为两次读取的结果不一致。但如果你从控制的角度来看, 两者的区别就比较大。
对于前者, 只需要锁住满足条件的记录
对于后者, 要锁住满足条件及其相近的记录
2、什么是:悲观锁和乐观锁?
- 悲观锁:正如其名,它指的是对数据被外界(包括本系统当前的其他事务,以及来自外部系统的事务处理)修改持保守态度,因此,在整个数据处理过程中,将数据处 于锁定状态。悲观锁的实现,往往依靠数据库提供的锁机制(也只有数据库层提供的锁机制才能真正保证数据访问的排他性,否则,即使在本系统中实现了加锁机 制,也无法保证外部系统不会修改数据)。在悲观锁的情况下,为了保证事务的隔离性,就需要一致性锁定读。读取数据时给加锁,其它事务无法修改这些数据。修改删除数据时也要加锁,其它事务无法读取这些数据。
- 乐观锁:相对悲观锁而言,乐观锁机制采取了更加宽松的加锁机制。悲观锁大多数情况下依靠数据库的锁机制实现,以保证操作最大程度的独占性。但随之而来的就是数据库性能的大量开销,特别是对长事务而言,这样的开销往往无法承受。而乐观锁机制在一定程度上解决了这个问题。乐观锁,大多是基于数据版本( Version )记录机制实现。何谓数据版本?即为数据增加一个版本标识,在基于数据库表的版本解决方案中,一般是通过为数据库表增加一个 “version” 字段来实现。读取出数据时,将此版本号一同读出,之后更新时,对此版本号加一。此时,将提交数据的版本数据与数据库表对应记录的当前版本信息进行比对,如 果提交的数据版本号大于数据库表当前版本号,则予以更新,否则认为是过期数据。
- 要说明的是,MVCC的实现没有固定的规范,每个数据库都会有不同的实现方式,这里讨论的是InnoDB的MVCC。
以上内容转载自:不可重复读和幻读的区别
3、什么是事务?事务有什么要求或特征?
大家所了解的事务Transaction,它是一些列严密操作动作,要么都操作完成,要么都回滚撤销。Spring事务管理基于底层数据库本身的事务处理机制。数据库事务的基础,是掌握Spring事务管理的基础。这篇总结下Spring事务。
事务具备ACID四种特性,ACID是Atomic(原子性)、Consistency(一致性)、Isolation(隔离性)和Durability(持久性)的英文缩写。
(1)原子性(Atomicity)
事务最基本的操作单元,要么全部成功,要么全部失败,不会结束在中间某个环节。事务在执行过程中发生错误,会被回滚到事务开始前的状态,就像这个事务从来没有执行过一样。
(2)一致性(Consistency)
事务的一致性指的是在一个事务执行之前和执行之后数据库都必须处于一致性状态。如果事务成功地完成,那么系统中所有变化将正确地应用,系统处于有效状态。如果在事务中出现错误,那么系统中的所有变化将自动地回滚,系统返回到原始状态。
(3)隔离性(Isolation)
指的是在并发环境中,当不同的事务同时操纵相同的数据时,每个事务都有各自的完整数据空间。由并发事务所做的修改必须与任何其他并发事务所做的修改隔离。事务查看数据更新时,数据所处的状态要么是另一事务修改它之前的状态,要么是另一事务修改它之后的状态,事务不会查看到中间状态的数据。
(4)持久性(Durability)
指的是只要事务成功结束,它对数据库所做的更新就必须永久保存下来。即使发生系统崩溃,重新启动数据库系统后,数据库还能恢复到事务成功结束时的状态。
4、事务的传播特性
事务传播行为就是多个事务方法调用时,如何定义方法间事务的传播。Spring定义了7中传播行为:
(1)propagation_requierd:如果当前没有事务,就新建一个事务,如果已存在一个事务中,加入到这个事务中,这是Spring默认的选择。
(2)propagation_supports:支持当前事务,如果没有当前事务,就以非事务方法执行。
(3)propagation_mandatory:使用当前事务,如果没有当前事务,就抛出异常。
(4)propagation_required_new:新建事务,如果当前存在事务,把当前事务挂起。
(5)propagation_not_supported:以非事务方式执行操作,如果当前存在事务,就把当前事务挂起。
(6)propagation_never:以非事务方式执行操作,如果当前事务存在则抛出异常。
(7)propagation_nested:如果当前存在事务,则在嵌套事务内执行。如果当前没有事务,则执行与propagation_required类似的操作。
5、事务的隔离级别
(1)read uncommited:是最低的事务隔离级别,它允许另外一个事务可以看到这个事务未提交的数据。
(2)read commited:保证一个事物提交后才能被另外一个事务读取。另外一个事务不能读取该事物未提交的数据。
(3)repeatable read:这种事务隔离级别可以防止脏读,不可重复读。但是可能会出现幻象读。它除了保证一个事务不能被另外一个事务读取未提交的数据之外还避免了以下情况产生(不可重复读)。
(4)serializable:这是花费最高代价但最可靠的事务隔离级别。事务被处理为顺序执行。除了防止脏读,不可重复读之外,还避免了幻象读
(5)脏读、不可重复读、幻象读概念说明:
a.脏读:指当一个事务正字访问数据,并且对数据进行了修改,而这种数据还没有提交到数据库中,这时,另外一个事务也访问这个数据,然后使用了这个数据。因为这个数据还没有提交那么另外一个事务读取到的这个数据我们称之为脏数据。依据脏数据所做的操作肯能是不正确的。
b.不可重复读:指在一个事务内,多次读同一数据。在这个事务还没有执行结束,另外一个事务也访问该同一数据,那么在第一个事务中的两次读取数据之间,由于第二个事务的修改第一个事务两次读到的数据可能是不一样的,这样就发生了在一个事物内两次连续读到的数据是不一样的,这种情况被称为是不可重复读。
c.幻象读:一个事务先后读取一个范围的记录,但两次读取的纪录数不同,我们称之为幻象读(两次执行同一条 select 语句会出现不同的结果,第二次读会增加一数据行,并没有说这两次执行是在同一个事务中)
六、事务几种实现方式
(1)编程式事务管理对基于 POJO 的应用来说是唯一选择。我们需要在代码中调用beginTransaction()、commit()、rollback()等事务管理相关的方法,这就是编程式事务管理。目前,编程式事务用的相对比较少;
(2)基于 TransactionProxyFactoryBean的声明式事务管理
(3)基于 @Transactional 的声明式事务管理
(4)基于Aspectj AOP配置事务
以上内容转载自:Spring事务管理之几种方式实现事务
七、为什么事务操作注解,要添加在Service实现类上,而不是在service接口 或 Controller中呢?
使用注解的方式进行事务控制时,将@Transactional注解写在实现类的方法或类上!不建议写在接口类中!
Spring团队的建议是你在具体的类(或类的方法)上使用 @Transactional 注解,而不要使用在类所要实现的任何接口上。你当然可以在接口上使用 @Transactional 注解,但是这将只能当你设置了基于接口的代理时它才生效。因为注解是不能继承的,这就意味着如果你正在使用基于类的代理时,那么事务的设置将不能被基于类的代理所识别,而且对象也将不会被事务代理所包装(将被确认为严重的)。因此,请接受Spring团队的建议并且在具体的类上使用 @Transactional 注解。
在以上解决方法中,若将@Transactional 注解写在接口上,则无法实现事务。所以请将 @Transactional 注解写在实现类中!
二、实践中遇到的问题 及 总结
1、@Transactional注解事务无效的几种可能性。
(1)Springs事务控制器,默认情况下监听的是运行时异常及其子类异常。如果异常不是运行时异常,可以在配置事务时,显示地设置事务监听的异常类型(rollbackOn = Exception.class)
(2)是不是配置文件的没配置好,比如Bean,比如mapper.xml:
<!-- 配置事物管理类 -->
<bean id="transactionManager"
class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dynamicDataSource" />
</bean>
<!-- 开启注解式事物扫描 -->
<tx:annotation-driven transaction-manager="transactionManager" />
(3)还有一种情况,因为我以前写的都是对一个数据库的操作,现在是一个数据源多个数据库操作,当时有点怀疑我是不是少配置了什么,导致多数据库时无法启动事务,于是便还原到一个数据库,结果事务还是无效,排除此问题
经过一番折腾,在网上找到一篇文章,说原因是applicationContext.xml的父容器先于Servlet的子容器生效,将Service提前加载了。
于是验证了一下,首先去掉Service实现类的@Service注解,在spring.xml(也就是applicationContext.xml,我起名是spring.xml),配置该类的Bean:
<bean id="sysUserServiceImp" class="cn.kx59.user.service.imp.SysUserServiceImp"></bean>
结果运行之后:事务起作用了。
原因如下: Spring容器优先加载由ServletContextListener(对应applicationContext.xml,我这里是spring.xml)产生的父容器,而SpringMVC(对应spring-mvc.xml)产生的是子容器。 子容器Controller进行扫描装配时装配的@Service注解的实例是没有经过事务加强处理,即没有事务处理能力的Service,而父容器进行初始化的Service是保证事务的增强处理能力的。如果不在子容器中将Service exclude掉,此时得到的将是原样的无事务处理能力的Service。 所以我们要在扫描的时候在子容器中将Service exclude掉就好了。
也就是在spring-mvc.xml中进行如下修改: <!--扫描Controller--> <context:component-scan base-package="cn.kx59"> <context:include-filter type="annotation" expression="org.springframework.stereotype.Controller" /> <!--下面这个是防止事务没起作用,spring.xml的父容器先于Servlet的子容器生效,将Service提前加载了。这里不用再进行加载装配--> <context:exclude-filter type="annotation" expression="org.springframework.stereotype.Service" /> </context:component-scan>
以上内容转载自:@Transaction注解后,事务未能成功
三、项目实践
业务描述:
用户Newbie有一张银行卡 和 一份股票,数据分别存储在银行账户表 和 股票记录表中。当用户消费minusMoney元去购买股票时,银行账户表中的余额减去minusMoney,而股票记录表中的股票数增加minusMoney,将两张表的修改动作绑定为事务进行控制。
测试时,首先让银行账户表减去相应金额,并执行成功;然后,方法主动抛出异常,使得后续的修改股票记录表的操作失败,进行事务操作测试。
步骤一:(1)在数据库中增加银行账户表 和 股票记录表;(2)在项目中增加对应的文件:pojo、beanMapper、beanMapper.xml。
在项目中增加MyBatis-Generator的maven依赖,可以自动生成projo、mapper、mapper.xml三种文件。方式请参考以下链接:
利用MyBatis-Generator自动生成实体类、Dao层和Mapper.xml的两种方法总结
# 用户银行账户表 drop table if exists user_account; create table user_account( account_id varchar(25) not null comment '账户ID', user_id varchar(25) not null comment '用户ID', balance int default 0 comment '账户余额', remark varchar(50) default '主库master' comment '信息备注', primary key (account_id) ); insert into user_account (account_id,user_id,balance) values ('6222-0001','newbie',100); # 用户股票记录表 drop table if exists user_stock; create table user_stock( stock_id varchar (25) not null comment '股票ID', user_id varchar (25) not null comment '用户ID', stock_name varchar (32) comment '股票名称', count_num int default 0 comment '拥有股的数量', remark varchar(50) default '主库master' comment '信息备注', primary key (stock_id) ); insert into user_stock (stock_id,user_id,stock_name,count_num) values ('AB-01','newbie','AB股','0');
步骤二:配置Spring提供的事务管理器DataSourceTransactionManager,并将数据源作为参数传入事务管理器。实现事务控制的方式有两种,实际项目开发中任选其一即可。
方式一:开启事务注解扫描功能,进行事务控制;
方式二:使用AOP切面方式,植入事务控制;
<!-- 配置事务管理器 --> <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"> <property name="dataSource" ref="dynamicDataSource"/> </bean>
<!--方式一:开启事务注解扫描功能,进行事务管理 --> <tx:annotation-driven transaction-manager="transactionManager"/>
<!-- 方式二:使用AOP切面方式,植入事务控制 --> <!-- 使用aop方式,将事务植入到service方法上,实现事务操作 --> <tx:advice id="txAdvice" transaction-manager="transactionManager"> <tx:attributes> <!-- 为连接点指定事务属性 : 切入的方法、事务传播行为、监听的异常类型 --> <tx:method name="aopAdvice*" propagation="REQUIRED" rollback-for="Exception"/> </tx:attributes> </tx:advice> <!-- aop切入点设置 --> <aop:config> <aop:pointcut id="pointcutService" expression="execution(* *..service.*.*(..))"/> <aop:advisor advice-ref="txAdvice" pointcut-ref="pointcutService"/> </aop:config>
步骤三:编写Service层业务逻辑,并在Service层中,实现事务的控制(增加事务注解、或植入aop切面)
package com.newbie.service.impl; import com.newbie.dao.UserAccountMapper; import com.newbie.dao.UserStockMapper; import com.newbie.domain.UserAccount; import com.newbie.domain.UserStock; import com.newbie.service.ITransactionService; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Propagation; import javax.annotation.Resource; import javax.transaction.Transactional; /** * 单数据库中的事务操作测试 */ @Service public class TransactionService implements ITransactionService { @Resource private UserAccountMapper userAccountMapper; @Resource private UserStockMapper userStockMapper; /** * 没有增加事务控制 */ public void notTransactionCommit() throws Exception{ updateDBData(); } /** * 使用注解的方式,增加事务控制 */ @Transactional(rollbackOn = Exception.class) public void annotationCommit() throws Exception{ updateDBData(); } /** * 基于AOP切面的方式,增加事务控制 */ public void aopAdviceCommit() throws Exception{ updateDBData(); } /** * 查询用户的账户余额 和 股票票数 * @return */ public String queryData(){ String accountId = "6222-0001"; //账户Id String stockId = "AB-01"; //股票ID //查询用户的账户余额 和 股票票数 UserAccount account = userAccountMapper.selectByPrimaryKey(accountId); UserStock stock = userStockMapper.selectByPrimaryKey(stockId); String message = "<p>查询成功<p>"; message += "<p>账户Id : "+account.getAccountId()+" , 余额 : "+account.getBalance()+"</p>"; message += "<p>股票Id:"+stock.getStockId()+" , 股票数 :"+stock.getCountNum()+"</p>"; System.out.println("================== service : message = "+message); return message; } /** * 重置数据:将账户余额 和 股票票数 重置到原始状态 */ public void updateResetData(){ UserAccount account = new UserAccount(); account.setAccountId("6222-0001"); account.setBalance(100); UserStock stock = new UserStock(); stock.setStockId("AB-01"); stock.setCountNum(0); userAccountMapper.updateByPrimaryKeySelective(account); userStockMapper.updateByPrimaryKeySelective(stock); } /** * 修改数据 * 第一步:查询消费前,用户的账户余额 和 股票票数 * 第二步:修改账户余额,余额减去 20元 * 第三步:修改股票票数,票数增加 20股 */ public void updateDBData() throws Exception{ String accountId = "6222-0001"; //账户Id String stockId = "AB-01"; //股票ID int minusMoney = 20; //消费金额 //查询消费前,用户的账户余额 和 股票票数 UserAccount account = userAccountMapper.selectByPrimaryKey(accountId); UserStock stock = userStockMapper.selectByPrimaryKey(stockId); //修改账户余额 和 股票票数 account.setBalance(account.getBalance() - minusMoney); stock.setCountNum(stock.getCountNum() + minusMoney); //执行数据库操作,完成修改 userAccountMapper.updateByPrimaryKey(account); if(true){ throw new Exception("出现了异常,程序中断了,应该将之前的数据回退"); // Spring事务控制,默认情况下监听的是运行时异常及其子类异常 // 不过可以在配置事务时,修改事务监听的异常类(rollbackOn = Exception.class) //int rs = 1/0; } userStockMapper.updateByPrimaryKey(stock); } }
步骤四:编写Controller
package com.newbie.controller; import com.newbie.domain.User; import com.newbie.service.ITransactionService; import com.newbie.service.impl.TransactionService; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.RequestMapping; import javax.annotation.Resource; import javax.transaction.Transactional; import java.util.List; /** * 测试:单数据库中的事务操作 */ @Controller public class TransactionController { @Resource ITransactionService transactionService; /** * 修改数据:没有增加事务控制 */ @RequestMapping("/notTransactionCommit") public String notTransactionCommit(Model model) { //执行数据库操作 try { transactionService.notTransactionCommit(); } catch (Exception e) { e.printStackTrace(); } //设置向客户端返回的model数据 和 逻辑视图名称 return this.setModel(model); } /** * 修改数据:使用注解的方式,增加事务控制 */ @RequestMapping("/annotationCommit") public String annotationCommit(Model model){ try { transactionService.annotationCommit(); } catch (Exception e) { e.printStackTrace(); } //设置向客户端返回的model数据 和 逻辑视图名称 return this.setModel(model); } /** * 修改数据:基于AOP切面的方式,增加事务控制 */ @RequestMapping("/aopAdviceCommit") public String aopAdviceCommit(Model model) { try { transactionService.aopAdviceCommit(); } catch (Exception e) { e.printStackTrace(); } //设置向客户端返回的model数据 和 逻辑视图名称 return this.setModel(model); } /** * 重置数据:将账户余额 和 股票票数 重置到原始状态 * @param model * @return */ @RequestMapping("/resetData") public String resetData(Model model){ transactionService.updateResetData(); model.addAttribute("message","数据重置完成"); return "showInfo"; } /** * 设置向客户端返回的model数据 和 逻辑视图名称 */ public String setModel(Model model) { String message = "操作失败"; message = transactionService.queryData(); model.addAttribute("message", message); return "showInfo"; } }
五、编写前端页面,请求操作数据库
//index.jsp 请求页面
<%@ page contentType="text/html;charset=UTF-8" language="java" %> <html> <head> <title>测试多数据源模式</title> </head> <body> <h2>练习二:单数据库中事务操作</h2> <h3>方式一:使用注解方式:@</h3> <a href="notTransactionCommit">没有增加事务控制</a><br/><br/> <a href="annotationCommit">事务控制:使用注解的方式</a><br/><br/> <a href="aopAdviceCommit"> 事务控制:基于AOP切面的方式</a><br/><br/> <a href="resetData"> 重置数据:账户余额=100 和 股票票数=0</a><br/><br/> </body> </html>
//showInfo.jsp 结果显示页面 <%@ page contentType="text/html;charset=UTF-8" language="java" %> <html> <head> <title>显示结果信息</title> </head> <body> 处理信息:${message}<br/> 处理结果:<br/> 用户ID:${user.id} <br/> 用户名:${user.username}<br/> 职 级:${user.title}<br/> 数据源:${user.remark}<br/> </body> </html>
六、查看结果
1、index.jsp 请求页面效果
2、事务控制时,事务执行过程中发生异常,数据自动回退,所以结果没有变化
3、没有进行事务控制时,数据直接提交了,没有回退,结果账户余额减少了,而股票数却没有增加。