编程式事务:
事务功能的相关操作全部通过自己编写代码来实现:
Connection conn=.........; try{ //开启事务;关闭事务的自动提交 conn.setAutoCommit(false); //核心操作 //提交事务 conn.commit; } catch(Exception e){ //回滚事务 conn.rollback(); }finally{ //释放数据库连接 conn.close(); }
上述这种编程式的实现方式存在很多缺陷:
细节没有被屏蔽,具体操作过程中,所有细节都需要程序员自己来完成,比较繁琐 代码复用性不高:如果没有有效抽取出来,每次实现功能都需要自己编写代码,代码就没有得到复用
声明式事务:
既然事务控制的代码有规律可循的,并且代码的结构基本是确定的,因此框架就可以将固定模式的代码抽取出来,进行相关的封装
封装起来后,我们只需要在配置文件中进行简单的配置即可完成操作
这样做能够提高开发效率,消除了冗余的代码,框架会综合考虑相关领域中在实际开发环境下有可能遇到的各种问题,进行了健壮性,性能等各个方面的优化
基于注解的声明式事务:实现事务功能
数据库准备工作:
创建图书表:
<!-- comment是作为列或者行的注释的--> create table t_book(book_id int not null auto_increment comment '主键',book_name varchar(20) default null comment '图书名称',price int default null comment '价格',stock int unsigned default null comment '库存(无符号)',primary key(book_id));
插入数据:
insert into t_book(book_id,book_name,price,stock)values(1,'斗破苍穹',80,100),(2,'斗罗大陆',50
创建用户表:
create table t_user(user_id int not null auto_increment comment '主键',username varchar(20) default null comment '用户名',balance int unsigned default null comment '余额(无符号)',primary key(user_id));
插入数据:
insert into t_user(user_id,username,balance) values (1,'admin',50);
通过生活经验可知,图书购买的过程往往会出现余额不足或者库存不足的情况,它就类似于我们在进行事务处理时产生的异常,因此需要执行回滚操作,但我们早在学习mysql中就说过,引起事务回滚的原因通常并不是SQL语句出现错误,而是业务的核心逻辑出现问题,但业务逻辑并不会产生异常,那么要处理出现问题的业务,我们可以在数据库层面对其进行解决,也可以在java代码中对其进行处理,在数据库中进行的处理即为,设置库存量和余额量不能小于0,具体操作为这两个字段设置关键字unsigned,设置当前字段的值是不能为负值的,当然也可以通过在java代码层面自定义异常等,当业务逻辑出现问题时,抛出异常即可
java准备工作:
添加依赖:
<dependencies> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-orm</artifactId> <version>5.3.1</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-test</artifactId> <version>5.3.1</version> </dependency> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.13.1</version> <scope>test</scope> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.31</version> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid</artifactId> <version>1.2.16</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-jdbc</artifactId> <version>5.3.8</version> <scope>compile</scope> </dependency> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.13.1</version> <scope>compile</scope> </dependency> </dependencies>
通过创建我们之前学习的经典的三层架构模型,实现业务逻辑:
控制层:
package spring_txAnnotation.Controller; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Controller; import spring_txAnnotation.Service.ServiceBook; @Controller public class BookController { @Autowired private ServiceBook serviceBook; public void BuyBook(Integer userId,Integer BookId){ serviceBook.buyBook(userId,BookId); } }
持久层:
package spring_txAnnotation.Dao; public interface BookDao { Integer getPrice(Integer bookId);//根据图书的id查询图书的价格 void updateStock(Integer bookId);//根据图书的id修改图书的库存量 void updateBalance(Integer userId, Integer price);//对用户的余额进行修改操作 }
持久层的实现类:
package spring_txAnnotation.Dao; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.stereotype.Repository; @Repository public class BookDaoImpl implements BookDao { @Autowired private JdbcTemplate jdbcTemplate; @Override public Integer getPrice(Integer bookId) { String sql="select price from t_book where book_id=?"; 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); } }
业务层:
package spring_txAnnotation.Service; public interface ServiceBook { void buyBook(Integer userId, Integer bookId); }
业务层的实现类:
package spring_txAnnotation.Service; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import spring_txAnnotation.Dao.BookDao; @Service public class ServiceBookImpl implements ServiceBook { @Autowired private BookDao bookDao; @Override public void buyBook(Integer userId, Integer bookId) { //查询图书的价格 Integer book_price=bookDao.getPrice(bookId); //更新图书的库存 bookDao.updateStock(bookId); //更新用户的余额 bookDao.updateBalance(userId,book_price); } }
配置XML文件:
<?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" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/aop https://www.springframework.org/schema/aop/spring-aop.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd"> <context:component-scan base-package="spring_txAnnotation"></context:component-scan> <context:property-placeholder location="classpath:jdbc.properties"></context:property-placeholder> <bean class="org.springframework.jdbc.core.JdbcTemplate"> <property name="dataSource" ref="datasource"></property> </bean> <bean id="datasource" 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.name}"></property> <property name="password" value="${jdbc.password}"></property> </bean> </beans>
数据库连接的部分写在外部的jdbc.properties
文件中:
driver=com.mysql.cj.jdbc.Driver url=jdbc:mysql://localhost:3306/WJR name=root password=你的密码
测试类:
import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; import spring_txAnnotation.Controller.BookController; @RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration("classpath:TXannotation.xml") public class Test1 { @Autowired private BookController bookController; @Test public void testBuyBooks(){ bookController.BuyBook(1,1); } }
运行后,报错如下:
org.springframework.dao.DataIntegrityViolationException: PreparedStatementCallback; SQL [update t_user set balance=balance-? where user_id=?]; Data truncation: BIGINT UNSIGNED value is out of range in '(`wjr`.`t_user`.`balance` - 80)'; nested exception is com.mysql.cj.jdbc.exceptions.MysqlDataTruncation: Data truncation: BIGINT UNSIGNED value is out of range in '(`wjr`.`t_user`.`balance` - 80)'
原因如下:属性为无符号的这个字段的值超出了范围
Caused by: com.mysql.cj.jdbc.exceptions.MysqlDataTruncation: Data truncation: BIGINT UNSIGNED value is out of range in '(`wjr`.`t_user`.`balance` - 80)'
上述的SQL语句对数据实现的是,用当前余额的50元去购买价格为80的图书编号为1的图书,二者相减后的结果为负数,但因为我们将其类型设置为无符号的类型,因此就产生错误
在之前学习mysql中,我们就讲到mysql默认是有提交事务的功能,一个SQL语句对应一个事务提交,那么自动提交事务的功能会带来什么麻烦呢?
如下所示:
下述的这三个方法各自对应单独的事务,也就是说,他们事务提交成功与否只和自身有关,与其他操作并没有关系
首先我们先来查看一下数据库中的数据情况,如下所示:
当我们发生了使用不足的余额去购买图书这个操作后,java控制台报错如上述,数据库中的数据变化,如下所示:
我们发现用户的余额并没有任何的改变,但是1号图书的库存量减少了1,那么也证实了我们上述所说的,不同的操作之间是互不影响的,但这种结果,显然不符合逻辑,下面我们就通过基于注解的声明式事务来对这种情况进行处理
上面在学习声明式事务的概念时,我们提到过,声明式事务不需要我们单独的写切面和通知,只需要在配置文件中进行简单的配置即可完成操作
第一步:在XML文件中,配置事务管理器-->处理数据必须有数据源,如下所示:
由于事务管理器是一个接口,如果想将接口设置为bean,则必须通过其实现类来完成
<bean class="org.springframework.jdbc.datasource.DataSourceTransactionManager" id="transactionManager"> <!--引用数据源--> <property name="dataSource" ref="dataSource"></property> </bean>
第二步:在XML文件中,开启事务的注解驱动–>驱动并不是环绕通知,事务管理器才是,我们通过注解驱动中的transaction-manager属性将其二者关联起来 ,而属性值使用为默认值
<tx:annotation-driven transaction-manager="transactionManager"/>
注意:这里的annotation有多个,一定要使用tx的
第三步:将其注解使用在方法上
使用@Transactional注解所标识的方法或类中所有的方法使用事务进行管理,也就是说@Transactional作用于那个方法上,那个方法就是连接点,如果加到类上,则该类上的所有方法都是连接点,transaction-manager用来设置事务管理器的id,如果该id是默认值[transactionManager],可以省略不写
如下所示:将其添加至方法上
报错如下:
org.springframework.dao.DataIntegrityViolationException: PreparedStatementCallback; SQL [update t_user set balance=balance-? where user_id=?]; Data truncation: BIGINT UNSIGNED value is out of range in '(`wjr`.`t_user`.`balance` - 80)'; nested exception is com.mysql.cj.jdbc.exceptions.MysqlDataTruncation: Data truncation: BIGINT UNSIGNED value is out of range in '(`wjr`.`t_user`.`balance` - 80)'
Caused by: com.mysql.cj.jdbc.exceptions.MysqlDataTruncation: Data truncation: BIGINT UNSIGNED value is out of range in '(`wjr`.`t_user`.`balance` - 80)'
咿?不禁让人产生疑惑,怎么进行事务管理之后的报错内容和上述没有进行管理时完全相同啊,好像事务管理的作用并没有在这里体现,那么我们再去查看数据库部分,是否也和上面完全相同呢?
如下所示:
与上述未进行事务管理不同的地方在于,这里的1号图书的数量并没有减少,用户余额同样也是,因为余额不足的情况下,购买失败,因此整个事务进行了回滚,显然这种才是符合逻辑的
声明式事务的属性:只读,超时,回滚策略
事务属性:只读
对一个查询操作来说,如果我们把它设置为只读,就能明确的告诉数据库,这个操作不涉及写操作,这样数据库就能够针对查询操作来进行优化
设置方法:
@Transactional(readOnly = true)
大家注意看该属性值的默认值为false,那么也就是说,只有在事务中的操作都为查询操作时,才需要将其设置为只读
注意:只有当事务中的操作都为查询操作时,才能设置事务的只读操作,而如果事务中出现了其他的操作,我们依然将其设置为只读,就会报错
举例:当我们在包含除查询以外的其他操作的事务中,将该事务设置为只读:
报错如下所示:
事务属性:超时
事务在执行过程中,有可能因为遇到某些问题,导致程序卡住,从而长时间占用数据库资源,而长时间占用资源,大概率是因为程序运行出现了问题(可能是java程序或者mysql数据库或网络连接等等)
此时这个很可能出问题的程序应该被回滚,撤销它已做的操作,事务结束,把资源让出来,让其他正常程序可以执行
概括来说就是一句话:超时回滚,释放资源
设置方法:
//将当前的事务执行时间设置为3秒钟,如果超过三秒钟,则直接抛出异常 @Transactional(timeout = 3)
举例:
报错如下:事务超时异常