• spring的事务操作(重点)


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

  • 相关阅读:
    042.hiveLEFT SEMI JOIN 、 left anti join、inner join、full join
    032.hive rollup 、 with cube 、 grouping sets
    023.linuxshell抽取文本中某几行插入到另一个文
    041.mysql查询mysql元数据来格式化datax同步脚本,查询语句、拼接的json语句dataxmysql到hive
    33.hivecollect_set组合数组(数组内去重) 、array_contains 判断数组内是否又某个值返回布尔类型、concat_ws
    vue vant组件库 card组件 修改 thumb属性的图片 参数后不及时刷新解决
    idea 警告 The IDE is running low on memory and this might affect performance. Please consider increasing available heap. 解决
    尺子控件WinForm控件开发系列
    自定义形状按钮WinForm控件开发系列
    code ERESOLVE, ERESOLVE could not resolve
  • 原文地址:https://www.cnblogs.com/yunjiandubu/p/10165404.html
Copyright © 2020-2023  润新知