Spring事务(四)

事务管理概述

  • 事务概述
  1. 在应用开发领域, 为了保证数据的完整性和一致性, 必须引入数据库事务.
  2. 事务就是一组由于逻辑上紧密关联而合并成一个整体的多个数据库操作. 这些操作要么都执行, 要么都不执行.
  3. 事务的四个关键属性(ACID)
    • 原子性: 不可再分, 事务中的所有操作要么都执行, 要么都不执行.
    • 一致性: 事务中不管涉及多少操作, 必须保证事务执行前数据正确, 事务执行之后数据正确.
    • 隔离性: 事务往往是并发的. 因此每个事务都应与其他事务隔离开, 防止数据损坏.(并发执行不会互相干扰)
    • 持久性: 事务执行完后, 对数据的修改永久保存. 通常, 事务对数据的修改应被写入到硬盘.
  • Spring事务管理
  1. 编程式事务管理
    • 使用原生的JDBC进行事务管理
      • 获取数据库连接Connection对象.
      • 取消事务的自动提交
      • 执行操作
      • 正常完成操作时手动提交事务.
      • 执行失败时回滚事务
      • 关闭相应资源
    • 这是所有事物管理的基石, 也是最典型的编程式事务管理(将事务管理代码嵌入到业务方法中来控制事务的提交回滚).
    • 相对于核心业务, 事务管理代码不那么核心, 所以这样会造成大程度的代码冗余.
  2. 声明式事务管理
    • 大多数情况下声明式事务比编程式事务管理更好: 它将事务管理代码从业务方法中分离出来, 以声明的方式来实现事务管理.
    • 事务管理代码的固定模式作为一种横切关注点, 可以通过AOP方法模块化,进而借助SpringAOP框架实现声明式事务管理.
    • Spring在不同的事务管理API之上定义了一个抽象层, 通过配置的方式使其生效, 从而让应用程序开发人员不必了解事务管理API的底层实现细节, 就可以使用Spring的事务管理机制.
    • Spring既支持编程式事务管理, 也支持声明式的事务管理.
  3. Spring提供的事务管理器
    • 开发人员通过配置事务管理器来进行事务管理, 而不需要知道其底层实现.
    • 无论使用Spring的哪种事务管理策略, 都必须有事务管理器.
  4. 事务管理器的主要实现
    • DataSourceTransactionManager: 在应用程序中只需要处理一个数据源,而且通过 JDBC 存取.
    • JtaTransactionManager: 在 JavaEE 应用服务器上用JTA进行事务管理
    • HibernateTransactionManager: 用 Hibernate 框架存取数据库

注解配置事务

  • 购书案例
  1. 结构
  2. BookDAOImpl
    @Repository
    public class BookDAOImpl implements BookDAO {
    
        @Autowired
        private JdbcTemplate jdbcTemplate;
    
        @Override
        public Integer selectPrice(String bid) {
            Integer price = jdbcTemplate.queryForObject("select price from book where bid = ?", new Object[]{bid}, Integer.class);
            return price;
        }
    
        @Override
        public void updateSt(String bid) {
            //先获取该书库存
            Integer st = jdbcTemplate.queryForObject("select st from stock where sid = ?", new Object[]{bid}, Integer.class);
            if(st <= 0) {
                throw new RuntimeException();
            } else {
                st--;
                jdbcTemplate.update("update stock set st = ? where sid = ?", st, bid);
            }
        }
    
        @Override
        public void updateBalance(String uid, Integer price) {
            Integer balance = jdbcTemplate.queryForObject("select balance from money where bid = ?", new Object[]{uid}, Integer.class);
            if(balance < price) {
                throw new RuntimeException();
            } else {
                jdbcTemplate.update("update money set balance = balance - ? where uid = ?", price, uid);
            }
    
        }
    }
  3. BookServiceImpl
    @Service
    public class BookServiceImpl implements BookService {
    
        @Autowired
        private BookDAO dao;
    
        @Override
        public void buyBook(String bid, String uid) {
            Integer price = dao.selectPrice(bid);
            dao.updateSt(bid);
            dao.updateBalance(uid, price);
        }
    }
  4. BookController
    @Controller
    public class BookController {
    
        @Autowired
        private BookService service;
    
        public void buyBook() {
            service.buyBook("1", "1001");
        }
    }
  5. Test
    public static void main(String[] args) {
            ConfigurableApplicationContext cac = new ClassPathXmlApplicationContext("book.xml");
    
            BookController controller = cac.getBean("bookController", BookController.class);
            controller.buyBook();
    
            cac.close();
        }
  6. 问题
    • 当余额不足时, 再buyBook, 库存会减少, 余额不会减少.
    • 所以我们需要在Service层对buyBook进行事务管理.
  • 注解配置
  1. 配置文件
    • 配置事务管理器(使用jdbc)
      • 必须链接数据源
        <context:property-placeholder location="classpath:conf/db.properties" />
            <bean id="data" class="com.alibaba.druid.pool.DruidDataSource">
                <property name="driverClassName" value="${jdbc.driver}"></property>
                <property name="url" value="${jdbc.url}"></property>
                <property name="username" value="${jdbc.username}"></property>
                <property name="password" value="${jdbc.password}"></property>
            </bean>
        
            <bean id="dataSourceTransactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
                <!-- 事务管理器依赖于数据源所产生的链接对象 -->
                <property name="dataSource" ref="data"></property>
            </bean>
    • 开启注解驱动
      • 指定事务管理器: transaction-manager属性
      • 若把事务管理器id设位transactionManager, 就不用写该属性了.
        <tx:annotation-driven transaction-manager="dataSourceTransactionManager" />
  2. 使用注解进行配置
    • 开启注解驱动: @EnableTransactionManagement
    • //开启基于注解的事务管理功能
      @EnableTransactionManagement
      @ComponentScan("test")
      @Configuration
      public class TxConfig {
      
          //数据源
          @Bean
          public DataSource dataSource() {
      
              DruidDataSource dataSource = new DruidDataSource();
              dataSource.setUsername("root");
              dataSource.setPassword("root");
              dataSource.setDriverClassName("com.mysql.jdbc.Driver");
              dataSource.setUrl("jdbc:mysql://localhost:3306/test");
              
              return dataSource;
          }
          
          //注册事务管理器在容器中(链接数据源)
          @Bean
          public PlatformTransactionManager transactionManager() throws Exception{
              return new DataSourceTransactionManager(dataSource());
          }
      }
  3. 需要在进行事务控制的方法上加@Transactional注解
        @Override
        @Transactional
        public void buyBook(String bid, String uid) {
            Integer price = dao.selectPrice(bid);
            dao.updateSt(bid);
            dao.updateBalance(uid, price);
        }
    • @Transactional: 对方法中所有的操作作为一个事务进行管理.
      • 在方法上使用, 只对方法有效果.
      • 在类上使用, 对类中所有方法都有效果.
    • Transactional中可以设置的属性
      • propagation
      • isolaction
      • timeout
      • readOnly
      • rollbackFor
      • rollbackForClassName
      • noRollbackFor
      • noRollbackForClassName
  • 事务的传播行为(propagation)
  1. 简介
    • A方法和B方法都有事务, 当A在调用B的时候, 会将A中的事务传播给B方法, 而B方法对于事务的处理方式就是事务的传播方式.
  2. 常用值
    • propagation=Propagation.REQUIRED
      • 必须使用调用者的事务
    • propagation=Propagation.REQUIRES_NEW
      • 将调用者的事务挂起, 不使用调用者的事务, 使用自身事务.
    • 默认值时REQUIRED
  3. Spring的七种传播行为
  • 事务的隔离级别(isolaction)
  1. 事务的隔离级别: 在并发的情况下, 操作数据的一种规定.
  2. 四种隔离级别
    • 读未提交: isolation=Isolation.READ_UNCOMMITTED
      • 读取数据时, 可以读到别的请求对它进行的未提交的操作.
    • 读已提交: isolation=Isolation.READ_COMMITTED
      • 读取数据时, 可以读到别的请求对它进行的已提交的操作.
    • 可重复读: isolation=Isolation.REPEATABLE_READ 
      •  读取已存在的数据时, 别人不能对这些数据做出任何修改
    • 串行化: isolation=Isolation.SERIALIZABLE 
      • 相当于单线程
    • 默认隔离级别和数据库一致
  3. 解决并发问题的能力
  4. 各数据库对事物隔离级别的支持程度
  • 超时(timeout)和只读(readOnly)
  1. 由于事务可以在行和表上获得锁, 因此长事务会占用资源, 并对整体性能产生影响.
  2. 超时: 在事务强制回滚前最多可以执行(等待)的时间.
    • timeout = 3
      • 如果当前请求3秒内没有完成功能的话, 强制回滚. 
    • 这样可以防止长期允许的事务占用资源.
  3. 只读: 指定当前事务中的一系列操作是否为只读.
    • readOnly = true
    • 设置为只读后, Spring会通知Mysql该事务全是读的操作, 可以不用加锁. 故这样可以帮助数据库引擎优化事务. 
    • 注意: 设置完该属性后, 不管事务中有没有写的操作, 锁都会消失, 所以设置readOnly=true的事务中必须保证全是读操作, 否则会出现严重错误.
  • 触发事务回滚的异常
  1. 默认情况
    • 捕获到RuntimeException或Error时回滚, 而捕获到编译时异常不会滚.
  2. rollbackFor
    • 指定遇到时必须进行回滚的异常类型, 可以为多个.
    • rollbackFor在{ }内填类的class对象.
    • rollbackForClassName在{ }内填全类名.
  3. noRollbackFor
    • 指定遇到时不回滚的异常类型, 可以为多个.
    • noRollbackFor在{ }内填类的class对象.
    • noRollbackForClassName在{ }内填全类名. 
       @Transactional(propagation = Propagation.REQUIRED, isolation = Isolation.REPEATABLE_READ, 
      timeout = 3, readOnly = true, noRollbackFor = {NullPointerException.class, ArithmeticException.class})
posted @ 2020-06-02 10:38  yellowstreak  阅读(137)  评论(0编辑  收藏  举报