基于注解的声明式事务

基于注解的声明式事务

模拟场景:用户买书

      具体操作:先把价格查出来,在更新图书的库存,最后更新用户的余额,

                        也有可能库存不够的情况,或者余额不足,通过余额不够的情况测试声明式事务。

                        如果库存或余额不够的话,数据库层面不会报错,所以不符合业务逻辑,

                       所以得库存或者余额不够就不能让买,这就符合了。提交或者回滚的时候有没有异常的。

                       大部分情况都是因为业务逻辑问题需要进行回滚,也不会出现异常,

      从两个方面解决:

                ①从数据库层面解决 :为当前字段设置关键字unsigned无符号,就是没有负号的意思

                                                     所以就可以从sql层面抛出异常。所以就可以以业务逻辑问题生异常,

                                                    最终实现一个回滚和提交的效果。

               ②java代码层面解决:可以在更新库存的时候判断库存够不够,如果够直接操作更新库存,

                                                   如果不够,手动抛出运行时异常。

                                   所以事务就是来判断核心操作有没有异常,如果有异常就回滚,没有异常就提交。

                            如果是业务逻辑方面问题,需要从数据库或者java代码来给设置一个异常。

 

        无事务功能实现

           ①创建表

CREATE TABLE `t_book` (
`book_id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键',
`book_name` varchar(20) DEFAULT NULL COMMENT '图书名称',
`price` int(11) DEFAULT NULL COMMENT '价格',
`stock` int(10) unsigned DEFAULT NULL COMMENT '库存(无符号)',
PRIMARY KEY (`book_id`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8;
insert into `t_book`(`book_id`,`book_name`,`price`,`stock`) values (1,'斗破苍
穹',80,100),(2,'斗罗大陆',50,100);
CREATE TABLE `t_user` (
`user_id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键',
`username` varchar(20) DEFAULT NULL COMMENT '用户名',
`balance` int(10) unsigned DEFAULT NULL COMMENT '余额(无符号)',
PRIMARY KEY (`user_id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8;
insert into `t_user`(`user_id`,`username`,`balance`) values (1,'admin',50);

    ②创建实体类

    ③创建组件

         Controller层

@Controller
public class BookController {
//controller需要用到BookSdervice对象
   //自动装配
@Autowired
   private BookService bookService;
//买书的时候,需要知道谁买书,也要知道买的哪一本书,所以要把userId和bookId传过来
public void buyBook(Integer userId,Integer bookId){
   //调用bookService,把userId和bookId传进来
   bookService.buyBook(userId,bookId);
}
}

         service接口

public interface BookService {
   /**
    * 买书操作
    * @param userId
    * @param bookId
    */
   void buyBook(Integer userId, Integer bookId);
}

          实现类

@Service
public class BookServiceImpl implements BookService {
   /**
    * service中访问数据库
    * @param userId
    * @param bookId
    */
   @Autowired
   //创建持久层对象
   private BookDao bookDao;
   @Override
   public void buyBook(Integer userId, Integer bookId) {
       //1.查询图书价格
       Integer price= bookDao.getPriceByBookId(bookId);
       //2.更新图书的库存
       bookDao.updateStock(bookId);
       //3.更新用户的余额
       //条件:更新的是哪个用户余额,userId,price传进来
       bookDao.updateBalance(userId,price);
  }
}

           BookDao接口

/**
* 默认情况下,一个sql语句独占一个事务,且自动提交
* 所以没有设置事务的情况下,三个sql语句独占一个事务且自动提交的。
* 在mysql中,一个sql独占一个事务且自动提交
* 所以得关闭事务提交,如果不关闭每个sql语句独占一个事务
* 所以更新图书的库存成功,更新用户余额失败抛出sql异常
*/
public interface BookDao {
   //1.根据图书id查询图书的价格
   Integer getPriceByBookId(Integer bookId);
   //2.更新图书的库存
   void updateStock(Integer bookId);
  //3.更新用户的余额
   void updateBalance(Integer userId, Integer price);
}

           实现类

@Repository
public class BookDaoImpl implements BookDao {
//创建一个jdbctemplate
   @Autowired
   private JdbcTemplate jdbcTemplate;
   @Override
   public Integer getPriceByBookId(Integer bookId) {
       //查询单行单列的数据
       String sql="select price from t_book where book_id =?";
       //sql语句,查询的数据转换的类型,占位符所附的值
       return jdbcTemplate.queryForObject(sql,Integer.class,bookId);
  }
   @Override
   public void updateStock(Integer bookId) {
       String sql="update t_book set stock = stock-1 where book_id=?";
       jdbcTemplate.update(sql,bookId);
  }
   @Override
   public void updateBalance(Integer userId, Integer price) {
       //传输过来的价格多少,余额就减多少
       String sql="update t_user set balance = balance - ? where user_id = ?";
       jdbcTemplate.update(sql,price,userId);
  }
}

          ④测试类

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("classpath:tx-annotation.xml")
public class TxByAnnotationTest {
   @Autowired
   private BookController bookController;
   @Test
   public void testBuyBook(){
       //用户id为1的用户买图书id为1的图书
       bookController.buyBook(1,1);
  }
}

      ⑤模拟场景

用户购买图书,先查询图书的价格,再更新图书的库存和用户的余额

假设用户id为1的用户,购买id为1的图书

用户余额为50,而图书价格为80

购买图书之后,用户的余额为-30,数据库中余额字段设置了无符号,因此无法将-30插入到余额字段

此时执行sql语句会抛出SQLException

      ⑥观察结果

因为没有添加事务,图书的库存更新了,但是用户的余额没有更新

显然这样的结果是错误的,购买图书是一个完整的功能,更新库存和更新余额要么都成功要么都失败

 

    实现加入事务功能

                 @Transactional

                    不需要手动创建事务的切面,也不需要写通知的,

               因为在spring中,提供了有事务管理切面和通知,叫做事务管理器。

                       三层架构中,是在service设置事务管理。

          ①基于注解的声明式事务的配置

 <context:property-placeholder location="classpath:jdbc.properties"></context:property-placeholder>
   <!-- 配置数据源 -->
   <bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource">
        ...
   </bean>
   <bean class="org.springframework.jdbc.core.JdbcTemplate">
       <property name="dataSource" ref="dataSource"></property>
   </bean>
<!-- ① 配置事务管理器(切面) -->
<!--
 DataSourceTransactionManager: 前面单词处理事务必须有数据源,数据源管理连接的
   事务相关代码都是通过连接对象设置的,所以事务管理必须依赖数据源的
   -->
   <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
       <property name="dataSource" ref="dataSource"></property>
   </bean>
<!--
  ② 开启事务的注解驱动
      注意:导入的名称空间需要 tx 结尾的那个。
  作用是将当前事务管理器把切面里面的通知作用到当前连接点上
  @Transactional注解:所标识的方法或类中所有的方法使用事务进行管理
  就是把通知作用于连接点上的,注解加到哪个方法上就是连接点,
  或者加到类上如果加载类上这个类中所有方法都是连接点
  -->
<!--   颜色变灰色用的就是默认值-->
   <tx:annotation-driven transaction-manager="transactionManager"/>

          ② (添加事务注解)事务管理的方法上添加@Transactional

@Service
public class BookServiceImpl implements BookService {
@Autowired
private BookDao bookDao;
@Override
/**
* 声明式事务的配置步骤:
* 1.在spring的配置文件中配置事务管理器
* 2.开启事务的注解驱动
* 在需要被事务管理的方法上,添加@Transactional注解,该方法就会被事务管理
* @Transactional注解:标识的位置:①方法 ②类 :类中所有方法都被事务管理
* 实现的效果:要么都成功,要么都失败
*/
@Transactional
public void buyBook(Integer userId, Integer bookId) {
....
}
}

      ③观察结果

              由于使用了Spring的声明式事务,更新库存和更新余额都没有执行。

 

     事务属性

         1 . 只读

              ①介绍

                  对一个查询操作来说,如果我们把它设置成只读,就能够明确告诉数据库,

                   这个操作不涉及写操作。这样数据库就能够针对查询操作来进行优化。

              ②使用方式

   @Transactional(readOnly = true)

             ③注意

                   对增删改操作设置只读会抛出下面异常:

                  Caused by: java.sql.SQLException: Connection is read-only. Queries leading to data modification

                               are not allowed

        2 . 超时

              ①介绍

                 事务在执行过程中,有可能因为遇到某些问题,导致程序卡住,

                  从而长时间占用数据库资源。而长时间占用资源,

                 大概率是因为程序运行出现了问题(可能是Java程序或MySQL数据库或网络连接等等)。

             此时这个很可能出问题的程序应该被回滚,撤销它已做的操作,事务结束,把资源让出来,

                让其他正常程序可以执行。

  @Transactional(
//超时时间是几秒,如果3秒内没执行完,当前事务强制回滚,并抛出异常
timeout = 3
)
public void buyBook(Integer userId, Integer bookId) {
//程序休眠
//SECONDS选的单位是谁:例如下面就是5秒
try {
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
...

}

         ②观察结果

              执行过程中抛出异常:

                      org.springframework.transaction.TransactionTimedOutException: Transaction timed out:

                     deadline was Fri Jun 04 16:25:39 CST 2022

      3 . 回滚策略

               ①介绍

                         声明式事务默认只针对运行时异常回滚,编译时异常不回滚。

                         默认的回滚策略就是任何的运行时异常都会造成回滚。

                         可以通过@Transactional中相关属性设置回滚策略

                      rollbackFor属性:需要设置一个Class类型的对象

                      rollbackForClassName属性:需要设置一个字符串类型的全类名

                      noRollbackFor属性:需要设置一个Class类型的对象

                      rollbackFor属性:需要设置一个字符串类型的全类名

    @Transactional(
//noRollbackFor = {ArithmeticException.class}
// noRollbackForClassName = "java.lang.ArithmeticException"
//设置的是会造成回滚的异常,一般不设置,因为默认情况所以的运行时异常都会造成回滚
)

           ②观察结果

                    虽然购买图书功能中出现了数学运算异常(ArithmeticException),

                     但是我们设置的回滚策略是,当出现ArithmeticException不发生回滚,

                     因此购买图书的操作正常执行。

       4 . 事务隔离级别

                       数据库系统必须具有隔离并发运行各个事务的能力,使它们不会相互影响,避免各种并发问题。

                      一个事务与其他事务隔离的程度称为隔离级别。SQL标准中规定了多种事务隔离级别,

                      不同隔离级别对应不同的干扰程度,隔离级别越高,数据一致性就越好,但并发性越弱。

              隔离级别一共有四种:

                  ①读未提交:READ UNCOMMITTED

                                        允许Transaction01读取Transaction02未提交的修改。

                 ②读已提交:READ COMMITTED、

                                       要求Transaction01只能读取Transaction02已提交的修改。

                 ③可重复读:REPEATABLE READ

                                       确保Transaction01可以多次从一个字段中读取到相同的值,

                                       即Transaction01执行期间禁止其它事务对这个字段进行更新。

                 ④串行化:SERIALIZABLE确保Transaction01可以多次从一个表中读取到相同的行,

                                    在Transaction01执行期间,禁止其它事务对这个表进行添加、更新、删除操作。

                                    可以避免任何并发问题,但性能十分低下。

         各个隔离级别解决并发问题的能力见下表:

 

    @Transactional(
isolation = Isolation.DEFAULT //默认隔离级别
)

 

     5 . 事务传播行为

               ①介绍

                      当事务方法被另一个事务方法调用时,必须指定事务应该如何传播。

          例如:方法可能继续在现有事务中运行,也可能开启一个新事务,并在自己的事务中运行。

      例如:

 

                 现在有个结账的方法checkout,结账的方法要买两本书,每次买书的操作都是一个buybook的方法,

                 那checkout方法执行两次buybook方法,所以当前checkout方法就可以加上事务的注解,

                 那buybook方法本身也有事务的方法,那buybook到底用谁的,如果用结账的事务那么整个结账的事务算是一个事务,

                 所以买了这两本书中,第一本书买成功,第二本失败,只要有一本买不成功,那么当前结账的操作都要进行回滚,

                 所以只要有一本买不了,那都买不了,因为回滚的都是整个结账的操作。如果现在使用的是买书的操作,

                 哪一本书买不成功,只是在当前买书操作中进行回滚,那第一本书成功了,第二本书不成功只在当前买书的操作中回滚,

                 对买成功的书没有影响。

        默认情况下用的是结账的事务。

 

        创建接口CheckoutService:

public interface CheckoutService {
/**
* 结账
*/
void checkout(Integer userId, Integer[] bookIds);
}

       创建实现类CheckoutServiceImpl:

@Service
public class CheckoutServiceIml implements CheckoutService {
@Autowired
private BookService bookService;
@Override
@Transactional
//一次购买多本图书
public void checkout(Integer userId, Integer[] bookIds) {
for (Integer bookId : bookIds) {
bookService.buyBook(userId,bookId);
}
}
}

       在BookServiceImpl添加买书的操作事务

  @Transactional(
//传播行为
//REQUIRES_NEW:使用自己的事务
propagation = Propagation.REQUIRES_NEW
)

       在BookController中添加方法:

@Autowired
private CheckoutService checkoutService;
//买多本书
public void checkout(Integer userId,Integer[] bookIds){
checkoutService.checkout(userId,bookIds);
}
   @Transactional(propagation = Propagation.REQUIRES_NEW):
                                         表示不管当前线程上是否有已经开启的事务,都要开启新事务。
                                   同样的场景,每次购买图书都是在buyBook()的事务中执行,
                       因此第一本图书购买成功,事务结束,
                      第二本图书购买失败,只在第二次的buyBook()中回滚,购买第一本图书不受影响,即能买几本就买几本。
 
 
posted @ 2022-11-26 20:09  zjw_rp  阅读(191)  评论(0)    收藏  举报