Spring4 事务管理

Spring4 事务管理


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


准备环境

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

[sql]  view plain  copy
  1. DROP TABLE IF EXISTS `user`;  
  2. CREATE TABLE `user` (  
  3.   `id` bigint(20) NOT NULL,  
  4.   `account` varchar(255) NOT NULL,  
  5.   `balance` float DEFAULT NULL COMMENT '用户余额',  
  6.   PRIMARY KEY (`id`)  
  7. ) ENGINE=InnoDB DEFAULT CHARSET=utf8;  
  8.   
  9. -- ----------------------------  
  10. -- Records of user  
  11. -- ----------------------------  
  12. INSERT INTO user VALUES ('1''itdragon''100');  
[sql]  view plain  copy
  1. DROP TABLE IF EXISTS `product`;  
  2. CREATE TABLE `product` (  
  3.   `id` bigint(20) NOT NULL,  
  4.   `sku` varchar(255) NOT NULL COMMENT '商品的唯一标识',  
  5.   `price` float NOT NULL COMMENT '商品价格',  
  6.   `stock` int(11) NOT NULL COMMENT '商品库存',  
  7.   PRIMARY KEY (`id`)  
  8. ) ENGINE=InnoDB DEFAULT CHARSET=utf8;  
  9.   
  10. -- ----------------------------  
  11. -- Records of product  
  12. -- ----------------------------  
  13. INSERT INTO product VALUES ('1''java''40''10');  
  14. INSERT INTO product VALUES ('2''spring''50''10');  

事务简介

什么是事务?

事务就是一系列的动作,一个独立的最小单元。这些动作要么都成功,要么都失败。这也是它的原子性特点。

事务的关键属性

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

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

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

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

什么是事务管理?

事务管理是企业级应用程序开发中必不可少的技术,  用来确保数据的完整性和一致性.

举个例子:

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

文章模拟的场景是用户购买商品。商场的逻辑是:用户下单后,先发货后设置用户余额。即先减库存后减余额。如果余额充足,库存充足的情况,是没有什么问题的。若余额不足的情况购买商品会出现,库存减少了,余额因为不足而抛出异常,但最后余额并没有扣,库存却减少了。现在我们用代码看看如何用事务管理解决该问题。


基于注解的事务管理

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

JDBC配置的事务管理器是DataSourceTransactionManager,

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

[html]  view plain  copy
  1. <?xml version="1.0" encoding="UTF-8"?>  
  2. <beans xmlns="http://www.springframework.org/schema/beans"  
  3.     xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"  
  4.     xmlns:context="http://www.springframework.org/schema/context"  
  5.     xmlns:tx="http://www.springframework.org/schema/tx"  
  6.     xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd  
  7.         http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-4.0.xsd  
  8.         http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.0.xsd">  
  9.       
  10.     <context:component-scan base-package="com.itdragon.spring"></context:component-scan>  
  11.       
  12.     <!-- 导入资源文件 -->  
  13.     <context:property-placeholder location="classpath:db.properties"/>  
  14.       
  15.     <!-- 配置 C3P0 数据源 -->  
  16.     <bean id="dataSource"  
  17.         class="com.mchange.v2.c3p0.ComboPooledDataSource">  
  18.         <property name="user" value="${jdbc.user}"></property>  
  19.         <property name="password" value="${jdbc.password}"></property>  
  20.         <property name="jdbcUrl" value="${jdbc.jdbcUrl}"></property>  
  21.         <property name="driverClass" value="${jdbc.driverClass}"></property>  
  22.   
  23.         <property name="initialPoolSize" value="${jdbc.initPoolSize}"></property>  
  24.         <property name="maxPoolSize" value="${jdbc.maxPoolSize}"></property>  
  25.     </bean>  
  26.       
  27.     <!-- 配置 Spirng 的 JdbcTemplate -->  
  28.     <bean id="jdbcTemplate"   
  29.         class="org.springframework.jdbc.core.JdbcTemplate">  
  30.         <property name="dataSource" ref="dataSource"></property>  
  31.     </bean>  
  32.       
  33.     <!-- 配置 NamedParameterJdbcTemplate, 该对象可以使用具名参数, 其没有无参数的构造器, 所以必须为其构造器指定参数 -->  
  34.     <bean id="namedParameterJdbcTemplate"  
  35.         class="org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate">  
  36.         <constructor-arg ref="dataSource"></constructor-arg>      
  37.     </bean>  
  38.       
  39.     <!-- 配置事务管理器 -->  
  40.     <bean id="transactionManager"   
  41.         class="org.springframework.jdbc.datasource.DataSourceTransactionManager">  
  42.         <property name="dataSource" ref="dataSource"></property>  
  43.     </bean>  
  44.       
  45.     <!-- 启用事务注解  如果配置的事务管理器的id就是transactionManager , 这里是可以省略transaction-manager -->  
  46.     <tx:annotation-driven transaction-manager="transactionManager"/>  
  47.       
  48. </beans>  

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


核心是消费事务类 PurchaseService。事务管理注解的用法。

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

然后是事务测试类TransactionTest。主要负责测试和解释事务的各种特性。

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

消费事务类 PurchaseService(主要知识点类),测试时,将注解逐一放开。

[java]  view plain  copy
  1. import java.util.List;  
  2. import org.springframework.beans.factory.annotation.Autowired;  
  3. import org.springframework.stereotype.Service;  
  4. import org.springframework.transaction.annotation.Isolation;  
  5. import org.springframework.transaction.annotation.Propagation;  
  6. import org.springframework.transaction.annotation.Transactional;  
  7.   
  8. @Service  
  9. public class PurchaseService {  
  10.       
  11.     @Autowired  
  12.     private ShopDao shopDao;  
  13.       
  14.     /** 
  15.      * 模拟用户购买商品,测事务回滚 
  16.      * 最基本用法,直接在方法或者类上使用注解@Transactional。值得注意的是:只能在公共方法上使用 
  17.      * 对应的测试方法是 basicTransaction() 
  18.      */  
  19.     @Transactional  
  20.     /** 
  21.      * 事务的传播 propagation=Propagation.REQUIRED 
  22.      * 常用的有两种 REQUIRED,REQUIRES_NEW 
  23.      * 对应的测试方法是  propagationTransaction() 
  24.      */  
  25. //  @Transactional(propagation=Propagation.REQUIRED)  
  26.     /** 
  27.      * 事务的隔离性 
  28.      * 将事务隔离起来,减少在高并发的场景下发生 脏读,幻读和不可重复读的问题 
  29.      * 默认值是READ_COMMITTED 只能避免脏读的情况。 
  30.      * 不好演示,没有对应的测试方法。 
  31.      */  
  32. //  @Transactional(isolation=Isolation.READ_COMMITTED)  
  33.     /** 
  34.      * 回滚事务属性 
  35.      * 默认情况下声明式事务对所有的运行时异常进行回滚,也可以指定某些异常回滚和某些异常不回滚。(意义不大) 
  36.      * noRollbackFor 指定异常不回滚 
  37.      * rollbackFor 指定异常回滚 
  38.      */  
  39. //  @Transactional(noRollbackFor={UserException.class, ProductException.class})  
  40.     /** 
  41.      * 超时和只读属性 
  42.      * 超时:在指定时间内没有完成事务则回滚。可以减少资源占用。参数单位是秒 
  43.      * 如果超时,则提示错误信息: 
  44.      * org.springframework.transaction.TransactionTimedOutException: Transaction timed out 
  45.      * 只读属性:指定事务是否为只读. 若事务只读数据则有利于数据库引擎优化事务。  
  46.      * 因为该事务有修改数据的操作,若设置只读true,则提示错误信息 
  47.      * nested exception is java.sql.SQLException: Connection is read-only. Queries leading to data modification are not allowed 
  48.      * 对应的测试方法是 basicTransaction() 
  49.      */  
  50. //  @Transactional(timeout=5, readOnly=false)  
  51.     public void purchase(String account, String sku) {  
  52.         //1. 获取书的单价  
  53.         float price = shopDao.getBookPriceBySku(sku);  
  54.           
  55.         //2. 更新数的库存  
  56.         shopDao.updateBookStock(sku);  
  57.           
  58.         //3. 更新用户余额  
  59.         shopDao.updateUserBalance(account, price);  
  60.         // 测试超时用的  
  61.         /*try { 
  62.             Thread.sleep(6000); 
  63.         } catch (InterruptedException e) { 
  64.         }*/  
  65.     }  
  66.       
  67. }  
批量消费事务类BatchPurchaseService

[html]  view plain  copy
  1. import java.util.List;  
  2. import org.springframework.beans.factory.annotation.Autowired;  
  3. import org.springframework.stereotype.Service;  
  4. import org.springframework.transaction.annotation.Transactional;  
  5.   
  6. @Service  
  7. public class BatchPurchaseService {  
  8.       
  9.     @Autowired  
  10.     private PurchaseService purchaseService;  
  11.       
  12.     // 批量采购书籍,事务里面有事务  
  13.     @Transactional  
  14.     public void batchPurchase(String username, List<String> skus) {  
  15.         for (String sku : skus) {  
  16.             purchaseService.purchase(username, sku);  
  17.         }  
  18.     }  
  19.   
  20. }  
事务测试类TransactionTest(主要的知识点说明)

[java]  view plain  copy
  1. import java.util.Arrays;  
  2. import org.junit.Test;  
  3. import org.springframework.context.ApplicationContext;  
  4. import org.springframework.context.support.ClassPathXmlApplicationContext;  
  5.   
  6. public class TransactionTest {  
  7.   
  8.     private ApplicationContext ctx = null;  
  9.     private PurchaseService purchaseService = null;  
  10.     private BatchPurchaseService batchPurchaseService = null;  
  11.       
  12.     {  
  13.         ctx = new ClassPathXmlApplicationContext("applicationContext.xml");  
  14.         purchaseService = (PurchaseService) ctx.getBean("purchaseService");  
  15.         batchPurchaseService = (BatchPurchaseService) ctx.getBean("batchPurchaseService");  
  16.     }  
  17.       
  18.     /** 
  19.      * 用户买一本书 
  20.      * 基本用法-事务回滚 
  21.      * 把@Transactional 注释。假设当前用户余额只有10元。单元测试后,用户余额没有变,spring的库存却减少了。赚了!!! 
  22.      * 把@Transactional 注释打开。假设当前用户余额只有10元。单元测试后,用户余额没有变,spring的库存也没有减少。这就是回滚。 
  23.      * 回滚:按照业务逻辑,先更新库存,再更新余额。现在是库存更新成功了,但在余额逻辑抛出异常。最后数据库的值都没有变。也就是库存回滚了。 
  24.      */  
  25.     @Test  
  26.     public void basicTransaction() {  
  27.         System.out.println("^^^^^^^^^^^^^^^^^@Transactional 最基本的使用方法");  
  28.         purchaseService.purchase("itdragon""spring");  
  29.     }  
  30.       
  31.     /** 
  32.      * 用户买多本书 
  33.      * 事务的传播性 -大事务中,有小事务,小事务的表现形式 
  34.      * 用@Transactional, 当前用户余额50,是可以买一本书的。运行结束后,数据库中用户余额并没有减少,两本书的库存也都没有减少。 
  35.      * 用@Transactional(propagation=Propagation.REQUIRED), 运行结果是一样的。 
  36.      * 把REQUIRED 换成 REQUIRES_NEW 再运行 结果还是一样。。。。。 
  37.      * 为什么呢???? 因为我弄错了!!!!! 
  38.      * 既然是事务的传播性,那当然是一个事务传播给另一个事务。 
  39.      * 需要新增一个事务类批量购买 batchPurchase事务, 包含了purchase事务。 
  40.      * 把 REQUIRED 换成 REQUIRES_NEW 运行的结果是:用户余额减少了,第一本书的库存也减少了。 
  41.      * REQUIRED:如果有事务在运行,当前的方法就在这个事务内运行。否则,就启动一个新的事务,并在自己的事务内运行。大事务回滚了,小事务跟着一起回滚。 
  42.      * REQUIRES_NEW:当前的方法必须启动新事务,并在自己的事务内运行。如果有事务在运行,应该将它挂起。大事务虽然回滚了,但是小事务已经结束了。 
  43.      */  
  44.     @Test  
  45.     public void propagationTransaction() {  
  46.         System.out.println("^^^^^^^^^^^^^^^^^@Transactional(propagation) 事务的传播性");  
  47.         batchPurchaseService.batchPurchase("itdragon", Arrays.asList("java""spring"));  
  48.     }  
  49.       
  50.     /** 
  51.      * 测试异常不回滚,故意超买(不常用) 
  52.      * 当前用户余额10元,买了一本价值40元的java书。运行结束后,余额没有少,java书的库存减少了(赚了!)。因为设置指定异常不回滚! 
  53.      * 指定异常回滚就不测了。 
  54.      */  
  55.     @Test  
  56.     public void noRollbackForTransaction() {  
  57.         System.out.println("^^^^^^^^^^^^^^^^^@Transactional(noRollbackFor) 设置回滚事务属性");  
  58.         purchaseService.purchase("itdragon""java");  
  59.     }  
  60. }  
业务处理接口以及接口实现类

[java]  view plain  copy
  1. import org.springframework.beans.factory.annotation.Autowired;  
  2. import org.springframework.jdbc.core.JdbcTemplate;  
  3. import org.springframework.stereotype.Repository;  
  4.   
  5. @Repository("shopDao")  
  6. public class ShopDaoImpl implements ShopDao {  
  7.       
  8.     @Autowired  
  9.     private JdbcTemplate jdbcTemplate;  
  10.   
  11.     @Override  
  12.     public float getBookPriceBySku(String sku) {  
  13.         String sql = "SELECT price FROM product WHERE sku = ?";  
  14.         /** 
  15.          * 第二个参数要用封装数据类型,如果用float.class,会提示 Type mismatch affecting row number 0 and column type 'FLOAT':  
  16.          * Value [40.0] is of type [java.lang.Float] and cannot be converted to required type [float] 错误 
  17.          */  
  18.         return jdbcTemplate.queryForObject(sql, Float.class, sku);  
  19.     }  
  20.   
  21.     @Override  
  22.     public void updateBookStock(String sku) {  
  23.         // step1 防超卖,购买前先检查库存。若不够, 则抛出异常  
  24.         String sql = "SELECT stock FROM product WHERE sku = ?";  
  25.         int stock = jdbcTemplate.queryForObject(sql, Integer.class, sku);  
  26.         System.out.println("^^^^^^^^^^^^^^^^^商品( " + sku + " )可用库存 : " + stock);  
  27.         if(stock == 0){  
  28.             throw new ProductException("库存不足!再看看其他产品吧!");  
  29.         }  
  30.         // step2 更新库存  
  31.         jdbcTemplate.update("UPDATE product SET stock = stock -1 WHERE sku = ?", sku);  
  32.     }  
  33.   
  34.     @Override  
  35.     public void updateUserBalance(String account, float price) {  
  36.         // step1 下单前验证余额是否足够, 若不足则抛出异常  
  37.         String sql = "SELECT balance FROM user WHERE account = ?";  
  38.         float balance = jdbcTemplate.queryForObject(sql, Float.class, account);  
  39.         System.out.println("^^^^^^^^^^^^^^^^^您当前余额 : " + balance + ", 当前商品价格 : " + price);  
  40.         if(balance < price){  
  41.             throw new UserException("您的余额不足!不支持购买!");  
  42.         }  
  43.         // step2 更新用户余额  
  44.         jdbcTemplate.update("UPDATE user SET balance = balance - ? WHERE account = ?", price, account);  
  45.         // step3 查看用于余额  
  46.         System.out.println("^^^^^^^^^^^^^^^^^您当前余额 : " + jdbcTemplate.queryForObject(sql, Float.class, account));  
  47.     }  
  48.   
  49. }  
最后两个自定义的异常类

[html]  view plain  copy
  1. public class UserException extends RuntimeException{  
  2.   
  3.     private static final long serialVersionUID = 1L;  
  4.   
  5.     public UserException() {  
  6.         super();  
  7.     }  
  8.   
  9.     public UserException(String message) {  
  10.         super(message);  
  11.     }  
  12.   
  13. }  
[java]  view plain  copy
  1. public class ProductException extends RuntimeException{  
  2.   
  3.     private static final long serialVersionUID = 1L;  
  4.   
  5.     public ProductException() {  
  6.         super();  
  7.     }  
  8.   
  9.     public ProductException(String message) {  
  10.         super(message);  
  11.     }  
  12.   
  13. }  
当用户余额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知识),这是系列博客,不懂的可以看前面几章。

[html]  view plain  copy
  1. <?xml version="1.0" encoding="UTF-8"?>  
  2. <beans xmlns="http://www.springframework.org/schema/beans"  
  3.     xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"  
  4.     xmlns:context="http://www.springframework.org/schema/context"  
  5.     xmlns:tx="http://www.springframework.org/schema/tx"  
  6.     xmlns:aop="http://www.springframework.org/schema/aop"  
  7.     xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd  
  8.         http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-4.0.xsd  
  9.         http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-4.0.xsd  
  10.         http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.0.xsd">  
  11.           
  12.     <!-- 导入资源文件 -->  
  13.     <context:property-placeholder location="classpath:db.properties"/>  
  14.       
  15.     <!-- 配置 C3P0 数据源 -->  
  16.     <bean id="dataSource"  
  17.         class="com.mchange.v2.c3p0.ComboPooledDataSource">  
  18.         <property name="user" value="${jdbc.user}"></property>  
  19.         <property name="password" value="${jdbc.password}"></property>  
  20.         <property name="jdbcUrl" value="${jdbc.jdbcUrl}"></property>  
  21.         <property name="driverClass" value="${jdbc.driverClass}"></property>  
  22.   
  23.         <property name="initialPoolSize" value="${jdbc.initPoolSize}"></property>  
  24.         <property name="maxPoolSize" value="${jdbc.maxPoolSize}"></property>  
  25.     </bean>  
  26.       
  27.     <!-- 配置 Spirng 的 JdbcTemplate -->  
  28.     <bean id="jdbcTemplate"   
  29.         class="org.springframework.jdbc.core.JdbcTemplate">  
  30.         <property name="dataSource" ref="dataSource"></property>  
  31.     </bean>  
  32.       
  33.     <!-- 配置 NamedParameterJdbcTemplate, 该对象可以使用具名参数, 其没有无参数的构造器, 所以必须为其构造器指定参数 -->  
  34.     <bean id="namedParameterJdbcTemplate"  
  35.         class="org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate">  
  36.         <constructor-arg ref="dataSource"></constructor-arg>      
  37.     </bean>  
  38.       
  39.     <bean id="shopDao" class="com.itdragon.spring.my.transactionxml.ShopDaoImpl">  
  40.         <property name="jdbcTemplate" ref="jdbcTemplate"></property>  
  41.     </bean>  
  42.     <bean id="purchaseService" class="com.itdragon.spring.my.transactionxml.PurchaseService">  
  43.         <property name="shopDao" ref="shopDao"></property>  
  44.     </bean>  
  45.     <bean id="batchPurchaseService" class="com.itdragon.spring.my.transactionxml.BatchPurchaseService">  
  46.         <property name="purchaseService" ref="purchaseService"></property>  
  47.     </bean>  
  48.       
  49.     <!-- 配置事务管理器 -->  
  50.     <bean id="transactionManager"   
  51.         class="org.springframework.jdbc.datasource.DataSourceTransactionManager">  
  52.         <property name="dataSource" ref="dataSource"></property>  
  53.     </bean>  
  54.       
  55.     <!-- 配置事务属性 -->  
  56.     <tx:advice id="txAdvice" transaction-manager="transactionManager">  
  57.         <tx:attributes>  
  58.             <!-- 根据方法名指定事务的属性 -->  
  59.             <tx:method name="purchase"   
  60.                 propagation="REQUIRES_NEW"   
  61.                 timeout="3"   
  62.                 read-only="false"/>  
  63.             <tx:method name="batchPurchase"/>  
  64.         </tx:attributes>  
  65.     </tx:advice>  
  66.       
  67.     <!-- 配置事务切入点 -->  
  68.     <aop:config>  
  69.         <aop:pointcut expression="execution(* com.itdragon.spring.my.transactionxml.PurchaseService.purchase(..))"   
  70.             id="pointCut"/>  
  71.         <aop:advisor advice-ref="txAdvice" pointcut-ref="pointCut"/>  
  72.     </aop:config>  
  73.     <aop:config>  
  74.         <aop:pointcut expression="execution(* com.itdragon.spring.my.transactionxml.BatchPurchaseService.batchPurchase(..))"   
  75.             id="batchPointCut"/>  
  76.         <aop:advisor advice-ref="txAdvice" pointcut-ref="batchPointCut"/>  
  77.     </aop:config>  
  78. </beans>  
代码亲测可用。有什么错误地方可以指出。


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

猜你喜欢

转载自blog.csdn.net/m_jack/article/details/80476129