这篇文章一起来回顾复习下spring的事务操作.事务是spring的重点, 也是面试的必问知识点之一.
说来这次面试期间,也问到了我,由于平时用到的比较少,也没有关注过这一块的东西,所以回答的不是特别好,所以借这一篇文章来回顾总结一下,有需要的朋友,也可以点赞收藏一下,复习一下这方面的知识,为年后的面试做准备.
首先,了解一下什么是事务?

数据库事务(Database Transaction) ,是指作为单个逻辑工作单元执行的一系列操作,要么完全地执行,要么完全地不执行。 事务处理可以确保除非事务性单元内的所有操作都成功完成,否则不会永久更新面向数据的资源。通过将一组相关操作组合为一个要么全部成功要么全部失败的单元,可以简化错误恢复并使应用程序更加可靠。一个逻辑工作单元要成为事务,必须满足所谓的ACID(原子性、一致性、隔离性和持久性)属性。事务是数据库运行中的逻辑工作单位,由DBMS中的事务管理子系统负责事务的处理。这里简单提一下事务的四个基本属性,

A(Atomic) 原子性

事务必须是原子工作单元;对于其[数据修改]事务必须是原子工作单元;对于其数据修改,要么全都执行,要么全都不执行.

C(Consistent) 一致性

事务在完成时,必须使所有的数据都保持一致状态。在相关数据库中,所有规则都必须应用于事务的修改,以保持所有数据的完整性。

I(Insulation) 隔离性

由并发事务所作的修改必须与任何其它并发事务所作的修改隔离。事务查看数据时数据所处的状态,要么是另一并发事务修改它之前的状态,要么是另一事务修改它之后的状态,事务不会查看中间状态的数据。

D(Duration) 一致性

事务完成之后,它对于系统的影响是永久性的。该修改即使出现致命的系统故障也将一直保持。
了解了事务之后,我们为什么要使用事务呢?换句话说,用事务是为了解决什么问题呢?

首先我们来看一个业务场景:Tom在书店买书,java和Oracle,2种书,单价都是100,库存量都是10本,Tom目前身上有150元.现在Tom买1本书的钱是足够的,ok,买起来,交易结束后,对于Tom来说,买到了1本书,还剩下50元.正好要出门时接到jack的电话,原来是jack要Tom帮他捎本java,他要用来复习,那接下来的交易是否可以正常进行呢?常识来说,50元买价值100元的东西肯定是买不到的,那我们看看程序中是什么情况?

首先,需要构建三张表,余额表,商品表,和商品库存表,如下:

余额表
商品表(书)
库存表(书)

然后定义接口如下:

public interface BookShopDao {
    /**
     *   根据书名获取书的单价
     */
    public  int findBookPriceByIsbn(String isbn);

    /**
     *   更新书的库存,使书号对应的库存-1
     */
    public  void  updateBookStock(String isbn);

    /**
     * 更新用户的余额:使username的balance-price
     * @param name
     * @param price
     */
    public  void updateUserAccount(String name,int price);

}
@Repository
public class BookShopDaoImpl implements  BookShopDao {

    @Autowired
    private JdbcTemplate jdbcTemplate;


    @Override
    public int findBookPriceByIsbn(String isbn) {
        String sql = "SELECT  price FROM book WHERE isbn = ?";
        return jdbcTemplate.queryForObject(sql,Integer.class,isbn);
    }

    @Override
    public void updateBookStock(String isbn) {
        //检查书的库存是否足够,不足够则抛出异常
        String sql2 = "SELECT stock  FROM book_stock WHERE isbn = ?";
        int stock = jdbcTemplate.queryForObject(sql2, Integer.class, isbn);
        if(stock == 0){
            throw new BookStockException("库存不足");
        }
        String sql ="UPDATE book_stock SET  stock= stock-1 where isbn = ?";
        jdbcTemplate.update(sql,isbn);

    }

    @Override
    public void updateUserAccount(String name, int price) {
        //验证余额是否足够,不足则抛出异常
        String sql2 ="SELECT balance FROM account WHERE username = ?";
        int balance = jdbcTemplate.queryForObject(sql2, Integer.class, name);
        if(balance <price) {
            throw  new UserAccountException("余额不足");
        }
        String sql = "UPDATE account SET balance = balance - ? WHERE  username = ?";
        jdbcTemplate.update(sql,price,name);

    }
}

上面代码中有点要注意:库存余量是否充足,余额是否充足,需要在代码中去自己判断,mysql不会帮我们加,例如,当库存数为0时,如果仍需要减1,值会变为-1,这不是我们想要的结果.
接下里定义一个service:

public interface BookShopService {
    /**
     * 购物方法
     * @param username
     * @param isbn
     */
    public void  purchase(String username,String isbn);
}
@Service
public class BookShopServiceImpl implements BookShopService{
    @Autowired
    private BookShopDao shopDao;
    /**
     * @param username
     * @param isbn
     */
    @Override
    public void purchase(String username, String isbn) {
        //1.获取书的单价
        int price = shopDao.findBookPriceByIsbn(isbn);
        //更新书的库存
        shopDao.updateBookStock(isbn);
        //更新余额
        shopDao.updateUserAccount(username,price);
    }
}

到此,基本购买流程都已经实现,我们来写一个测试方法测试一下购买的结果是什么?

余额不足
测试结果

由图中可以看出,程序报了"余额不足"的异常,tom的余额没有减少,但是书店的库存量却减少了,这明显是违反常理的,书店不会白白把书送给tom的,怎么办呢?事务就可以帮助我们解决这个难题.
这里要先了解下事务的分类:

  • 编程式事务

将事务管理代码嵌入到业务方法中来控制事务的提交和回滚,在编程式管理事务当中,必须在每个事务操作中包含额外的事务管理代码,繁琐,不便.

  • 声明式事务

是建立在AOP之上的。其本质是对方法前后进行拦截,然后在目标方法开始之前创建或者加入一个事务,在执行完目标方法之后根据执行情况提交或者回滚事务。声明式事务最大的优点就是不需要通过编程的方式管理事务,这样就不需要在业务逻辑代码中掺杂事务管理的代码,只需通过基于@Transactional注解的方式或者配置文件中做相关的事务规则声明,便可以将事务规则应用到业务逻辑中。

采用声明式事务,基于@Transactional注解,首先看下配置文件:

<?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/context http://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd">
    <!--扫描包-->
    <context:component-scan base-package="com.springtest"></context:component-scan>
    <!--导入资源文件-->
    <context:property-placeholder location="classpath:db.properties" />
    <!--配置数据源-->
    <bean id="jdbcSource" 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.initialPoolSize}"></property>
        <property name="maxPoolSize" value="${jdbc.maxPoolSize}"></property>
    </bean>
    <!--配置spring的jdbctemplate模版-->
    <bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
        <property name="dataSource" ref="jdbcSource"></property>
    </bean>
    <!--配置事务管理器-->
    <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <property name="dataSource" ref="jdbcSource"></property>
    </bean>
    <!--启用事务注解-->
    <tx:annotation-driven transaction-manager="transactionManager" />
</beans>

接下来给方法purchase()加上注解

    @Transactional()
    @Override
    public void purchase(String username, String isbn) {
        //1.获取书的单价
        int price = shopDao.findBookPriceByIsbn(isbn);
        //更新书的库存
        shopDao.updateBookStock(isbn);
        //更新余额
        shopDao.updateUserAccount(username,price);
    }

结果如下:

测试-1
测试-2

由图观之,异常出现之后,事务发生了回滚,库存不再减少,钱也不会再减少,结果正常.
拓展问题(面试):Q1: 假如此时BookShopServiceImpl中另外一个方法调用了purchase方法,那么在另外一个方法中,事务是否起作用呢?
Q2:假如此时另外一个类中方法调用了BookShopServiceImpl类中的purchase方法,那么事务又是否起作用呢?
我们来一一验证一下,首先Q1

@Service
public class BookShopServiceImpl implements BookShopService{
    @Autowired
    private BookShopDao shopDao;

    @Transactional()
    @Override
    public void purchase(String username, String isbn) {
        //1.获取书的单价
        int price = shopDao.findBookPriceByIsbn(isbn);
        //更新书的库存
        shopDao.updateBookStock(isbn);
        //更新余额
        shopDao.updateUserAccount(username,price);
    }

    @Override
    public void purchaseAgain(String username, String isbn) {
        purchase(username,isbn);
    }
}

测试结果:
测试前,数据库数据为:

测试前
测试后
异常

结果观之,事务并没有起作用,原因是什么?
启用事务首先调用的是AOP代理对象而不是目标对象,首先执行事务切面,事务切面内部通过TransactionInterceptor环绕增强进行事务的增强,即进入目标方法之前开启事务,退出目标方法时提交/回滚事务.而类内部的自我调用将无法实施切面中的增强.,解决方案的话限于篇幅,以后再写,这里知道原因就可以了.
接下来验证Q2,首先创建一个新的接口和实现类,里面调用BookShopService 的purchase方法,观察结果

@Service
public class TestBookShopServiceImpl implements TestBookShopService {
    @Autowired
    private BookShopService shopService;
    @Override
    public void testBookPurchase(String name, String isbn) {
        shopService.purchase(name,isbn);
    }
}

测试结果1
测试结果2

观察结果,在余额不足的情况下,外部方法调用purchase方法,抛出异常时,事务回滚,库存没有减少,原因同Q1相同,但正好相反,但是走了AOP代理,所以事务起作用了.


那么如果在内部的方法purchaseAgain,和外部的方法中加入事务控制又会是怎样的情况呢?
这里直接给出结论:
purchaseAgain方法加入注解@Transactional后,调用purchase方法(无论是否添加@Transactional),事务控制起作用;外部类的testBookPurchase方法调用本类的purchase方法,事务控制也是起作用的.
由此引入spring关于事务的传播行为的介绍:spring的事务传播行为一共分为以下几种:

  1. REQUIRED(常用)
  2. REQUIRES_NEW(常用)
  3. SUPPORTS
  4. NOT_SUPPORTED
  5. NEVER
  6. NESTED
  7. MANDATORY
    在@Transactional注解中是propagation属性;

事务传播属性

分别介绍:
PROPAGATION_REQUIRED 如果存在一个事务,则支持当前事务。如果没有事务则开启一个新的事务。(是spring 的默认事务传播行为)。
PROPAGATION_REQUIRES_NEW 总是开启一个新的事务。如果一个事务已经存在,则将这个存在的事务挂起。
PROPAGATION_SUPPORTS 如果存在一个事务,支持当前事务。如果没有事务,则非事务的执行。但是对于事务同步的事务管理器,PROPAGATION_SUPPORTS与不使用事务有少许不同。
PROPAGATION_NOT_SUPPORTED 总是非事务地执行,并挂起任何存在的事务。
PROPAGATION_MANDATORY 如果已经存在一个事务,支持当前事务。如果没有一个活动的事务,则抛出异常。
PROPAGATION_NEVER 总是非事务地执行,如果存在一个活动事务,则抛出异常。
PROPAGATION_NESTED 如果一个活动的事务存在,则运行在一个嵌套的事务中. 如果没有活动事务, 则按TransactionDefinition.PROPAGATION_REQUIRED 属性执行。


事务的传播行为定义了事务的控制范围,那么事务的隔离级别定义的则是事务在数据库读写方面的控制范围.
有的时候,在程序并发的情况下,会发生以下的神奇情况:

  • 脏读:对于两个事务T1,T2,T1读取了T2更新但是还未提交的字段,之后,若T2回滚,那么T1读取的内容就是临时且无效的
  • 不可重复读:对于两个事务T1,T2, T1读取了一个字段,然后被T2更新了,之后T1再次读取,字段值变掉了.
  • 幻读:两个事务T1,T2, T1从一个表中读取了一个字段,然后T2在该表中插入了一些新的行,之后,如果T1再次读取同一个表,就会多出几行数据.
    那么以上的问题要如何来解决呢,spring给出了它的解决方案,将事务的隔离性分为以下几个等级
  • READ_UNCOMMITTED
  • READ_COMMITTED
  • REPEATABLE_READ
  • SERIALIZABLE
    在@Transactional注解中是propagation属性;

隔离级别

分别介绍:
READ_UNCOMMITTED 这是事务最低的隔离级别,它充许别外一个事务可以看到这个事务未提交的数据。 这种隔离级别会产生脏读,不可重复读和幻像读;
READ_COMMITTED 保证一个事务修改的数据提交后才能被另外一个事务读取。另外一个事务不能读取该事务未提交的数据。 这种隔离级别可以避免脏读出现,但是可能会出现不可重复读和幻像读;
REPEATABLE_READ 这种事务隔离级别可以防止脏读,不可重复读。但是可能出现幻像读;
SERIALIZABLE 这是花费最高代价但是最可靠的事务隔离级别。事务被处理为顺序执行。 除了防止脏读,不可重复读外,还避免了幻像读;
以上几种隔离界别, 在了解了其作用及其可避免的情况之后,我们在工作中视情况采用,不过一般默认情况就可以处理大多数情况了.

最后小结
这篇文章回顾了spring的事务相关的技术要点,包括什么是事务,事务的四个基本属性,为什么要使用事务,事务的分类,事务的传播种类以及事务的隔离级别.大体上涵盖了事务的相关知识,但是并没有深入到源码级别来研究事务的相关实现,有机会一定要深入源码了解实现,这样才能对知识的学习理解达到庖丁解牛的地步,对自己以后的知识积累和提升也会有很大的帮助.

posted on 2018-12-23 20:03  云间独步  阅读(1789)  评论(0编辑  收藏  举报