Spring4 事务管理

本章是Spring4 教程中的最后一章,也是非常重要的一章。如果说学习IOC是知识的入门,那学习事务管理就是知识的提升。本章篇幅可能有一丢丢长,也有一丢丢难,需要读者细细品味。主要从三个方面开始:事务简介,基于注解的事务管理 和基于xml的事务管理。


准备环境

mysql文件,两张表:一个用户表,字段有帐号和余额。一个商品表,字段有sku,售价和库存。

DROP TABLE IF EXISTS `user`;
CREATE TABLE `user` (
`id` bigint(20) NOT NULL,
`account` varchar(255) NOT NULL,
`balance` float DEFAULT NULL COMMENT '用户余额',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8; -- ----------------------------
-- Records of user
-- ----------------------------
INSERT INTO user VALUES ('1', 'itdragon', '100');
DROP TABLE IF EXISTS `product`;
CREATE TABLE `product` (
`id` bigint(20) NOT NULL,
`sku` varchar(255) NOT NULL COMMENT '商品的唯一标识',
`price` float NOT NULL COMMENT '商品价格',
`stock` int(11) NOT NULL COMMENT '商品库存',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8; -- ----------------------------
-- Records of product
-- ----------------------------
INSERT INTO product VALUES ('1', 'java', '40', '10');
INSERT INTO product VALUES ('2', 'spring', '50', '10');

事务简介

工作中应该经常听到:"这是一个事务,你要保证它数据的一致性,在这里加个注解吧!"。于是我们就稀里糊涂地用,好像也没出什么问题。

因为加上注解,说明该方法支持事务的处理。事务就是一系列的动作,这一系列的动作要么都成功,要么都失败。所以你才会觉得没出什么问题。管理事务是应用程序开发必不可少的技术,用来确保数据的完整性和一致性,特别是和钱有关系的事务。

事务有四个关键属性:

** 原子性 **:一系列的动作,要么都成功,要么都失败。

** 一致性 **:数据和事务状态要保持一致。

** 隔离性 **:为了防止数据被破坏,每个事务之间都存在隔离性。

** 持久性 **:一旦事务完成, 无论发生什么系统错误, 它的结果都不应该受到影响。

我们用例子更好地说明事务:

A给B转账,A出账500元,B因为某种原因没有成功进账。若A出账的500元不回滚到A账户余额中,就会出现数据的不完整性和不一致性的问题。

本章模拟用户购买商品的场景。商场的下单逻辑是:先发货后设置用户余额。如果用户余额充足,商品库存充足的情况,是没有什么问题的。但若余额不足却购买商品,库存减少了,扣除用户余额时会因为余额不足而抛出异常,到最后用户余额并没有减少,商品库存却减少了,显然是不合理的。现在我们用Spring的事务管理来解决这种问题。


基于注解的事务管理

核心文件 applicationContext.xml。既然用到注解,就需要配置自动扫描包context:component-scan,还需要配置JdbcTempalte。最后要配置事务管理器和启动事务注解 tx:annotation-driven

JDBC配置的事务管理器的class指定路径是DataSourceTransactionManager,

Hibernate配置的事务管理器的class指定路径是HibernateTransactionMannger 。两个的用法都是一样,只是配置事务管理时class指定的路径不同罢了。这是因为 Spring 在不同的事务管理上定义了一个抽象层。我们无需了解底层的API,就可以使用Spring的事务管理。

<?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: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/tx http://www.springframework.org/schema/tx/spring-tx-4.0.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.0.xsd"> <context:component-scan base-package="com.itdragon.spring"></context:component-scan> <!-- 导入资源文件 -->
<context:property-placeholder location="classpath:db.properties"/> <!-- 配置 C3P0 数据源 -->
<bean id="dataSource"
class="com.mchange.v2.c3p0.ComboPooledDataSource">
<property name="user" value="${jdbc.user}"></property>
<property name="password" value="${jdbc.password}"></property>
<property name="jdbcUrl" value="${jdbc.jdbcUrl}"></property>
<property name="driverClass" value="${jdbc.driverClass}"></property> <property name="initialPoolSize" value="${jdbc.initPoolSize}"></property>
<property name="maxPoolSize" value="${jdbc.maxPoolSize}"></property>
</bean> <!-- 配置 Spirng 的 JdbcTemplate -->
<bean id="jdbcTemplate"
class="org.springframework.jdbc.core.JdbcTemplate">
<property name="dataSource" ref="dataSource"></property>
</bean> <!-- 配置 NamedParameterJdbcTemplate, 该对象可以使用具名参数, 其没有无参数的构造器, 所以必须为其构造器指定参数 -->
<bean id="namedParameterJdbcTemplate"
class="org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate">
<constructor-arg ref="dataSource"></constructor-arg>
</bean> <!-- 配置事务管理器 -->
<bean id="transactionManager"
class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"></property>
</bean> <!-- 启用事务注解 如果配置的事务管理器的id就是transactionManager , 这里是可以省略transaction-manager -->
<tx:annotation-driven transaction-manager="transactionManager"/> </beans>

接下来是事务的业务代码,所有类都放在了一个目录下,没别的原因,就是因为懒。

核心是消费事务类 PurchaseService。介绍事务注解@Transactional的语法。

其次是批量消费事务类BatchPurchaseService。用于配合PurchaseService测试事务的传播性。

然后是事务测试类TransactionTest。主要负责测试和详细解释事务语法。

最后是自定义异常类。是为了测试事务的回滚属性。

PurchaseService(重点,注解语法),测试时,将注解逐一放开。

import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Isolation;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional; @Service
public class PurchaseService { @Autowired
private ShopDao shopDao; /**
* 模拟用户购买商品,测事务回滚
* 最基本用法,直接在方法或者类上使用注解@Transactional。值得注意的是:只能在公共方法上使用
* 对应的测试方法是 basicTransaction()
*/
@Transactional
/**
* 事务的传播 propagation=Propagation.REQUIRED
* 常用的有两种 REQUIRED,REQUIRES_NEW
* 对应的测试方法是 propagationTransaction()
*/
// @Transactional(propagation=Propagation.REQUIRED)
/**
* 事务的隔离性
* 将事务隔离起来,减少在高并发的场景下发生 脏读,幻读和不可重复读的问题
* 默认值是READ_COMMITTED 只能避免脏读的情况。
* 不好演示,没有对应的测试方法。
*/
// @Transactional(isolation=Isolation.READ_COMMITTED)
/**
* 回滚事务属性
* 默认情况下声明式事务对所有的运行时异常进行回滚,也可以指定某些异常回滚和某些异常不回滚。(意义不大)
* noRollbackFor 指定异常不回滚
* rollbackFor 指定异常回滚
*/
// @Transactional(noRollbackFor={UserException.class, ProductException.class})
/**
* 超时和只读属性
* 超时:在指定时间内没有完成事务则回滚。可以减少资源占用。参数单位是秒
* 如果超时,则提示错误信息:
* org.springframework.transaction.TransactionTimedOutException: Transaction timed out
* 只读属性:指定事务是否为只读. 若事务只读数据则有利于数据库引擎优化事务。
* 因为该事务有修改数据的操作,若设置只读true,则提示错误信息
* nested exception is java.sql.SQLException: Connection is read-only. Queries leading to data modification are not allowed
* 对应的测试方法是 basicTransaction()
*/
// @Transactional(timeout=5, readOnly=false)
public void purchase(String account, String sku) {
//1. 获取书的单价
float price = shopDao.getBookPriceBySku(sku); //2. 更新数的库存
shopDao.updateBookStock(sku); //3. 更新用户余额
shopDao.updateUserBalance(account, price);
// 测试超时用的
/*try {
Thread.sleep(6000);
} catch (InterruptedException e) {
}*/
} }

批量消费事务类BatchPurchaseService

import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; @Service
public class BatchPurchaseService { @Autowired
private PurchaseService purchaseService; // 批量采购书籍,事务里面有事务
@Transactional
public void batchPurchase(String username, List<String> skus) {
for (String sku : skus) {
purchaseService.purchase(username, sku);
}
} }

事务测试类TransactionTest(重点,知识点说明

import java.util.Arrays;
import org.junit.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext; public class TransactionTest { private ApplicationContext ctx = null;
private PurchaseService purchaseService = null;
private BatchPurchaseService batchPurchaseService = null; {
ctx = new ClassPathXmlApplicationContext("applicationContext.xml");
purchaseService = (PurchaseService) ctx.getBean("purchaseService");
batchPurchaseService = (BatchPurchaseService) ctx.getBean("batchPurchaseService");
} /**
* 用户买一本书
* 基本用法-事务回滚
* 把@Transactional 注释。假设当前用户余额只有10元。单元测试后,用户余额没有变,spring的库存却减少了。赚了!!!
* 把@Transactional 注释打开。假设当前用户余额只有10元。单元测试后,用户余额没有变,spring的库存也没有减少。这就是回滚。
* 回滚:按照业务逻辑,先更新库存,再更新余额。现在是库存更新成功了,但在余额逻辑抛出异常。最后数据库的值都没有变。也就是库存回滚了。
*/
@Test
public void basicTransaction() {
System.out.println("^^^^^^^^^^^^^^^^^@Transactional 最基本的使用方法");
purchaseService.purchase("itdragon", "spring");
} /**
* 用户买多本书
* 事务的传播性 -大事务中,有小事务,小事务的表现形式
* 用@Transactional, 当前用户余额50,是可以买一本书的。运行结束后,数据库中用户余额并没有减少,两本书的库存也都没有减少。
* 用@Transactional(propagation=Propagation.REQUIRED), 运行结果是一样的。
* 把REQUIRED 换成 REQUIRES_NEW 再运行 结果还是一样。。。。。
* 为什么呢???? 因为我弄错了!!!!!
* 既然是事务的传播性,那当然是一个事务传播给另一个事务。
* 需要新增一个事务类批量购买 batchPurchase事务, 包含了purchase事务。
* 把 REQUIRED 换成 REQUIRES_NEW 运行的结果是:用户余额减少了,第一本书的库存也减少了。
* REQUIRED:如果有事务在运行,当前的方法就在这个事务内运行。否则,就启动一个新的事务,并在自己的事务内运行。大事务回滚了,小事务跟着一起回滚。
* REQUIRES_NEW:当前的方法必须启动新事务,并在自己的事务内运行。如果有事务在运行,应该将它挂起。大事务虽然回滚了,但是小事务已经结束了。
*/
@Test
public void propagationTransaction() {
System.out.println("^^^^^^^^^^^^^^^^^@Transactional(propagation) 事务的传播性");
batchPurchaseService.batchPurchase("itdragon", Arrays.asList("java", "spring"));
} /**
* 测试异常不回滚,故意超买(不常用)
* 当前用户余额10元,买了一本价值40元的java书。运行结束后,余额没有少,java书的库存减少了(赚了!)。因为设置指定异常不回滚!
* 指定异常回滚就不测了。
*/
@Test
public void noRollbackForTransaction() {
System.out.println("^^^^^^^^^^^^^^^^^@Transactional(noRollbackFor) 设置回滚事务属性");
purchaseService.purchase("itdragon", "java");
}
}

业务处理接口以及接口实现类

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Repository; @Repository("shopDao")
public class ShopDaoImpl implements ShopDao { @Autowired
private JdbcTemplate jdbcTemplate; @Override
public float getBookPriceBySku(String sku) {
String sql = "SELECT price FROM product WHERE sku = ?";
/**
* 第二个参数要用封装数据类型,如果用float.class,会提示 Type mismatch affecting row number 0 and column type 'FLOAT':
* Value [40.0] is of type [java.lang.Float] and cannot be converted to required type [float] 错误
*/
return jdbcTemplate.queryForObject(sql, Float.class, sku);
} @Override
public void updateBookStock(String sku) {
// step1 防超卖,购买前先检查库存。若不够, 则抛出异常
String sql = "SELECT stock FROM product WHERE sku = ?";
int stock = jdbcTemplate.queryForObject(sql, Integer.class, sku);
System.out.println("^^^^^^^^^^^^^^^^^商品( " + sku + " )可用库存 : " + stock);
if(stock == 0){
throw new ProductException("库存不足!再看看其他产品吧!");
}
// step2 更新库存
jdbcTemplate.update("UPDATE product SET stock = stock -1 WHERE sku = ?", sku);
} @Override
public void updateUserBalance(String account, float price) {
// step1 下单前验证余额是否足够, 若不足则抛出异常
String sql = "SELECT balance FROM user WHERE account = ?";
float balance = jdbcTemplate.queryForObject(sql, Float.class, account);
System.out.println("^^^^^^^^^^^^^^^^^您当前余额 : " + balance + ", 当前商品价格 : " + price);
if(balance < price){
throw new UserException("您的余额不足!不支持购买!");
}
// step2 更新用户余额
jdbcTemplate.update("UPDATE user SET balance = balance - ? WHERE account = ?", price, account);
// step3 查看用于余额
System.out.println("^^^^^^^^^^^^^^^^^您当前余额 : " + jdbcTemplate.queryForObject(sql, Float.class, account));
} }

最后两个自定义的异常类

public class UserException extends RuntimeException{  

    private static final long serialVersionUID = 1L;  

    public UserException() {
super();
} public UserException(String message) {
super(message);
} }
public class ProductException extends RuntimeException{  

    private static final long serialVersionUID = 1L;  

    public ProductException() {
super();
} public ProductException(String message) {
super(message);
} }

当用户余额10元不够买售价为50的书,书的库存充足的情况。测试basicTransaction()方法打印的结果:用户余额不减少,库存也不减少

当用户余额50元准备购买两本总价为90的书,但余额只够买一本书,书的库存充足的情况,测试propagationTransaction()方法打印的结果:若用 REQUIRES_NEW则两本中可以买一本;若用REQUIRED则一本都买不了。(事务的传播性有7种,这里主要介绍常用的REQUIRED和REQUIRES_NEW)

当用户余额10元不够买售价40元的书,书的库存充足的情况。测试noRollbackForTransaction()方法打印的结果:用户余额没有减少,但商品库存减少了,说明事务没有回滚。

细细品味后,其实也很简单。事务就是为了保证数据的一致性。出了问题就把之前修改过的数据回滚。


基于xml的事务管理

如果你理解了基于注解的事务管理,那基于xml的事务管理就简单多了。由于篇幅已经太长了,这里我长话短说。

首先把上面的java类中的所有IOC注解,@Transactional注解和@Autowired去掉。被@Autowired修饰的属性,还需要另外生成setter方法。

然后配置applicationContext.xml文件。将启动事务注解的代码删掉。将之前用自动扫描包的IOC注解和@Autowired注解的代码都配置bean(IOC知识),然后 配置事务属性,最后 配置事务切入点(AOP知识),这是系列博客,不懂的可以看前面几章。

<?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:tx="http://www.springframework.org/schema/tx"
xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-4.0.xsd
http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-4.0.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.0.xsd"> <!-- 导入资源文件 -->
<context:property-placeholder location="classpath:db.properties"/> <!-- 配置 C3P0 数据源 -->
<bean id="dataSource"
class="com.mchange.v2.c3p0.ComboPooledDataSource">
<property name="user" value="${jdbc.user}"></property>
<property name="password" value="${jdbc.password}"></property>
<property name="jdbcUrl" value="${jdbc.jdbcUrl}"></property>
<property name="driverClass" value="${jdbc.driverClass}"></property> <property name="initialPoolSize" value="${jdbc.initPoolSize}"></property>
<property name="maxPoolSize" value="${jdbc.maxPoolSize}"></property>
</bean> <!-- 配置 Spirng 的 JdbcTemplate -->
<bean id="jdbcTemplate"
class="org.springframework.jdbc.core.JdbcTemplate">
<property name="dataSource" ref="dataSource"></property>
</bean> <!-- 配置 NamedParameterJdbcTemplate, 该对象可以使用具名参数, 其没有无参数的构造器, 所以必须为其构造器指定参数 -->
<bean id="namedParameterJdbcTemplate"
class="org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate">
<constructor-arg ref="dataSource"></constructor-arg>
</bean> <bean id="shopDao" class="com.itdragon.spring.my.transactionxml.ShopDaoImpl">
<property name="jdbcTemplate" ref="jdbcTemplate"></property>
</bean>
<bean id="purchaseService" class="com.itdragon.spring.my.transactionxml.PurchaseService">
<property name="shopDao" ref="shopDao"></property>
</bean>
<bean id="batchPurchaseService" class="com.itdragon.spring.my.transactionxml.BatchPurchaseService">
<property name="purchaseService" ref="purchaseService"></property>
</bean> <!-- 配置事务管理器 -->
<bean id="transactionManager"
class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"></property>
</bean> <!-- 配置事务属性 -->
<tx:advice id="txAdvice" transaction-manager="transactionManager">
<tx:attributes>
<!-- 根据方法名指定事务的属性 -->
<tx:method name="purchase"
propagation="REQUIRES_NEW"
timeout="3"
read-only="false"/>
<tx:method name="batchPurchase"/>
</tx:attributes>
</tx:advice> <!-- 配置事务切入点 -->
<aop:config>
<aop:pointcut expression="execution(* com.itdragon.spring.my.transactionxml.PurchaseService.purchase(..))"
id="pointCut"/>
<aop:advisor advice-ref="txAdvice" pointcut-ref="pointCut"/>
</aop:config>
<aop:config>
<aop:pointcut expression="execution(* com.itdragon.spring.my.transactionxml.BatchPurchaseService.batchPurchase(..))"
id="batchPointCut"/>
<aop:advisor advice-ref="txAdvice" pointcut-ref="batchPointCut"/>
</aop:config>
</beans>

代码亲测可用。有什么错误地方可以指出。

到这里Spring4 的教程也就结束了。感谢您的观看!!!

Spring4 事务管理的更多相关文章

  1. Spring4笔记9--Spring的事务管理(AOP应用的例子)

    Spring的事务管理: 事务原本是数据库中的概念,在 Dao 层.但一般情况下,需要将事务提升到业务层,即 Service 层.这样做是为了能够使用事务的特性来管理具体的业务.   在 Spring ...

  2. Hibernate与Spring的事务管理

    什么是事务 这个问题比较大,按照我的理解就是,一个事务内的n个操作,要么全部完成,一旦有一个操作有问题,那么所有的操作都全部回滚. Jdbc的事务 首先,大家已经知道了,事务说白了就是一个词----统 ...

  3. Spring 事务管理案例

    事务管理简介   Spring 事务管理有两种方式:一种是编程式事务管理,即通过编写代码实现事物管理,包括定义事务的开始,程序正常执行后的事物提交,异常时进行的事务回滚.另一种是基于AOP技术实现的声 ...

  4. Spring事务管理配置以及异常处理

    Spring事务管理配置: <?xml version="1.0" encoding="UTF-8"?> <beans xmlns=" ...

  5. Spring Boot -- Spring Boot之@Async异步调用、Mybatis、事务管理等

    这一节将在上一节的基础上,继续深入学习Spring Boot相关知识,其中主要包括@Async异步调用,@Value自定义参数.Mybatis.事务管理等. 本节所使用的代码是在上一节项目代码中,继续 ...

  6. Spring Framework之事务管理

    目录 问题 数据库事务 事务的定义 事务的目的 事务的特性 事务隔离级别 数据并发问题 事务隔离级别对数据并发问题的作用 快照读 Spring事务管理 事务管理接口 TransactionDefini ...

  7. Spring—SSJ集成&声明式事务管理

    1.   课程介绍 1.  SSJ集成;(掌握) 2.  声明式事务管理;(掌握) 什么是三大框架 2.1.  ssh Struts/Struts2 Spring Hibernate 2.2.  ss ...

  8. Spring基于AOP的事务管理

                                  Spring基于AOP的事务管理 事务 事务是一系列动作,这一系列动作综合在一起组成一个完整的工作单元,如果有任何一个动作执行失败,那么事务 ...

  9. spring声明式事务管理总结

    事务配置 首先在/WEB-INF/applicationContext.xml添加以下内容: <!-- 配置事务管理器 --> <bean id="transactionM ...

随机推荐

  1. oracle状态

    Oracle_四种状态 oracle四种状态 1.shutdown(完全关闭) 2.nomount(未加载) 3.mount(已加载) 4.open(完全打开) Shutdown状态 Shutdown ...

  2. 初识SQL Server2017 图数据库(一)

    背景: 图数据库对于表现和遍历复杂的实体之间关系是很有效果的.而这些在传统的关系型数据库中尤其是对于报表而言很难实现.如果把传统关系型数据库比做火车的话,那么到现在大数据时代,图数据库可比做高铁.它已 ...

  3. PHP windowns安装扩展包

    1.  php_msgpack.dll php.ini 添加  extension=php_msgpack.dll 下载dll: http://pecl.php.net/package/msgpack ...

  4. win10 uwp 通知列表

    经常看到小伙伴问,问已经绑定列表,在进行修改时,不会通知界面添加或删除.这时问题就在,一般使用的列表不会在添加时通知界面,因为他们没有通知. 本文:知道什么是通知的列表,如何去写一个通知列表 在 C# ...

  5. centos安装SWFtools服务(pdf2swf)

    第一步:下载swftools-0.9.2.tar.gz 第二步:swftools tar -xzvf swftools-0.9.2.tar.gz cd swftools-0.9.2 ./configu ...

  6. ThreadPoolExecutor系列<一、ThreadPoolExecutor 机制>

    本文系作者原创,转载请注明出处:http://www.cnblogs.com/further-further-further/p/7681529.html 解决问题: 1. 处理大量异步任务时能减少每 ...

  7. Fedora 19 搭建Qt环境

    1.搭建桌面环境fedora的源里包含的需要的套件包,用下面命令安装sudo yum intall qt qt-devel qt-x11 qt-doc qt-demos qt-examples qt- ...

  8. LeetCode 461. Hamming Distance (汉明距离)

    The Hamming distance between two integers is the number of positions at which the corresponding bits ...

  9. LeetCode 448. Find All Numbers Disappeared in an Array (在数组中找到没有出现的数字)

    Given an array of integers where 1 ≤ a[i] ≤ n (n = size of array), some elements appear twice and ot ...

  10. N厂水鬼烂大街?那来看ZF厂V4帝舵小红花

    自从帝舵小红花推上市面之后,各大工厂都在推出新版本,但做得最成熟的还是ZF厂,帝舵这个品牌是非常低调的,很少有人关注,但是ZF厂在这款腕表也是下了不少功夫,曾经帝舵小红花和N厂水鬼并列为最顶级的表畅销 ...