基于XML的声明式事务管
1. 什么是事务
首先说一下什么是事务。
事务(Transaction)指一个操作,由多个步骤组成,要么全部成功,要么全部失败。
比如我们常用的转账功能,假设A账户向B账号转账,那么涉及两个操作:
(1)从A账户扣钱;
(2)往B账户加入等量的钱。
因为是独立的两个操作,所以可能有一个成功,一个失败的情况。但是因为在这种场景下,必须要保证事务,即要么同时成功,要么同时失败(一个失败需要回滚),不能存在从 A 账户扣钱成功,往 B 账户加入等量钱失败这种情况。
2. 生活中处处可见事务
事务不止存在于数据库中,生活中处处存在事务,只要是涉及多个步骤来完成一件事情时,就涉及到事务。
比如彩礼三金和结婚是一个事务,南方给了价值几十万的彩礼和三金,女方会答应如期将女儿嫁出。如果女方毁约,一般会如数退还彩礼三金。如果遇到蛮横无理的女方,那么就破坏了事务,男方会采取法律或特殊手段要回彩礼三金,强制达到事务。
再如菜市场买东西,一手交钱一手交货;购买机票到最后完成乘机或退还机票(2021年春节因疫情尚未结束倡导就地过年就出现大面积免手续费退还机票的事情);网购下单到满意确收货或不满意退款退货等等。
3.数据库事务
因为数据库操作在日常开发中较为常见,所以在计算机术语中,我们说的事务一般指数据库事务。
数据库事务应该具有4个属性:原子性、一致性、隔离性、持久性。这四个属性通常称为 ACID 特性。
原子性(Atomicity):事务作为一个整体被执行,包含在其中的对数据库的操作要么全部被执行,要么都不执行。
一致性(Consistency):事务应确保数据库的状态从一个一致状态转变为另一个一致状态。一致状态的含义是数据库中的数据应满足完整性约束。
隔离性(Isolation):多个事务并发执行时,一个事务的执行不应影响其他事务的执行。
持久性(Durability):一个事务一旦提交,他对数据库的修改应该永久保存在数据库中。
我们还是用上面“A账户向B账号汇钱”的例子来说明如何通过数据库事务保证数据的正确性。
熟悉关系型数据库事务的都知道从帐号A到帐号B需要6个操作:
(1)从A账号中把余额读出来(500)。
(2)对A账号做减法操作(500-100)。
(3)把结果写回A账号中(400)。
(4)从B账号中把余额读出来(500)。
(5)对B账号做加法操作(500+100)。
(6)把结果写回B账号中(600)。
原子性:
保证1-6所有过程要么都执行,要么都不执行。一旦在执行某一步骤的过程中发生问题,就需要执行回滚操作。 假如执行到第五步的时候,B账户突然不可用(比如被注销),那么之前的所有操作都应该回滚到执行事务之前的状态。
一致性:
在转账之前,A和B的账户中共有500+500=1000元钱。在转账之后,A和B的账户中共有400+600=1000元。也就是说,数据的状态在执行该事务操作之后从一个状态改变到了另外一个状态,两个状态数据总额时一致的,不能凭空变多或变少。
隔离性:
在 A 向 B 转账的整个过程中,只要事务还没有提交(commit),查询 A 账户和 B 账户的时候,两个账户里面的钱的数量都不会有变化。如果在 A 给 B 转账的同时,有另外一个事务执行了 C 给 B 转账的操作,那么当两个事务都结束的时候,B 账户里面的钱应该是 A 转给 B 的钱加上 C 转给 B 的钱再加上自己原有的钱。
持久性:
一旦转账成功(事务提交),两个账户的里面的钱就会真的发生变化(会把数据写入数据库做持久化保存)。
事务是个好多西,因为它符合我们的预期。但是很多场景下,很难保证事务,或者说保证事务需要付出很大的成本。此时就需要我们来权衡利弊,整出一个低成本又符合实际应用场景的设计方案。
4.数据库事务的使用
对于单条SQL语句,数据库系统自动将其作为一个事务执行,这种事务被称为隐式事务。
要手动把多条SQL语句作为一个事务执行,使用BEGIN开启一个事务,使用COMMIT提交一个事务,这种事务被称为显式事务,例如,把上述的转账操作作为一个显式事务:
BEGIN; UPDATE accounts SET balance = balance - 100 WHERE id = 1; UPDATE accounts SET balance = balance + 100 WHERE id = 2; COMMIT;
很显然多条SQL语句要想作为一个事务执行,就必须使用显式事务。
COMMIT是指提交事务,即试图把事务内的所有SQL所做的修改永久保存。如果COMMIT语句执行失败了,整个事务也会失败。
有些时候,我们希望主动让事务失败,这时,可以用ROLLBACK回滚事务,整个事务会失败:
BEGIN; UPDATE accounts SET balance = balance - 100 WHERE id = 1; UPDATE accounts SET balance = balance + 100 WHERE id = 2; ROLLBACK;
5.基于XML的声明式事务控制
Spring 的声明式事务顾名思义就是采用声明的方式来处理事务。这里所说的声明,就是指在配置文件中声明,用在 Spring 配置文件中声明式的处理事务来代替代码式的处理事务。
声明式事务控制明确事项:
- 谁是切点?
- 谁是通知?
- 配置切面?
6. 纯XML声明式事务实现
(1)创建数据库
use myTest; -- ---------------------------- -- Table structure for user -- ---------------------------- DROP TABLE IF EXISTS `account`; CREATE TABLE `account` ( `id` int(4) unsigned NOT NULL AUTO_INCREMENT COMMENT '编号', `name` VARCHAR(30) NOT NULL DEFAULT '匿名' COMMENT '姓名', `money` DOUBLE NOT NULL DEFAULT '0.0' COMMENT '余额', PRIMARY KEY (`id`) USING BTREE ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='账户信息表';
INSERT INTO account(name, money) VALUES("张三", 400.0);
INSERT INTO account(name, money) VALUES("李四", 1100.0);
(2)导入pom坐标
<properties> <spring.vision>4.0.2.RELEASE</spring.vision> <java.version>8</java.version> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <maven.compiler.source>1.8</maven.compiler.source> <maven.compiler.target>1.8</maven.compiler.target> </properties> <dependencies> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-context</artifactId> <version>${spring.vision}</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-webmvc</artifactId> <version>${spring.vision}</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-aop</artifactId> <version>4.2.0.RELEASE</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-aspects</artifactId> <version>4.2.0.RELEASE</version> </dependency> <dependency> <groupId>javax.servlet</groupId> <artifactId>jstl</artifactId> <version>1.2</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-context</artifactId> <version>5.2.6.RELEASE</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-jdbc</artifactId> <version>5.2.7.RELEASE</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-tx</artifactId> <version>5.2.7.RELEASE</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>5.1.46</version> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid</artifactId> <version>1.1.10</version> </dependency> <dependency> <groupId>org.aspectj</groupId> <artifactId>aspectjweaver</artifactId> <version>1.8.9</version> </dependency> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.12</version> <scope>compile</scope> </dependency> </dependencies>
(3)xml 配置文件(context.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:p="http://www.springframework.org/schema/p" xmlns:util="http://www.springframework.org/schema/util" xmlns:context="http://www.springframework.org/schema/context" xmlns:mvc="http://www.springframework.org/schema/mvc" xmlns:tx="http://www.springframework.org/schema/tx" xmlns:aop="http://www.springframework.org/schema/aop" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/mvc https://www.springframework.org/schema/mvc/spring-mvc.xsd http://www.springframework.org/schema/tx https://www.springframework.org/schema/tx/spring-tx.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd"> <!--开启组件扫描--> <context:component-scan base-package="cn.gqx" /> <!--引入外部的属性文件--> <context:property-placeholder location="classpath:jdbc.properties"/> <!--配置连接池--> <bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource"> <property name="driverClassName" value="${jdbc.driverClass}"></property> <property name="url" value="${jdbc.url}" ></property> <property name="username" value="${jdbc.username}" ></property> <property name ="password" value="${jdbc.password}" ></property> </bean> <!--创建jdbcTemplate对象--> <bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate"> <!--注入DataSource--> <property name="dataSource" ref="dataSource"></property> </bean> <!--配置事务增强--> <!--平台事务管理器DataSourceTransactionManager--> <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"> <!--配置数据源--> <property name="dataSource" ref="dataSource"/> </bean> <!--事务增强配置--> <tx:advice id="txAdvice" transaction-manager="transactionManager"> <tx:attributes> <!--切点方法的事务参数的配置--> <!-- name:切点方法名称 isolation:事务的隔离级别 propogation:事务的传播行为 timeout:超时时间 read-only:是否只读 --> <tx:method name="*"/> </tx:attributes> </tx:advice> <!--事务AOP织入--> <aop:config> <!--配置切面=切点+通知--> <!--抽取切点--> <aop:pointcut id="pointcut1" expression="execution(* cn.gqx.service.impl.*.*(..))"/> <!--通知--> <aop:advisor advice-ref="txAdvice" pointcut-ref="pointcut1"/> </aop:config> </beans>
事务配置<tx:advice>通知标签(增强)
属性id:自定义唯一表示
transaction-manager属性:事务管理类,配置事务管理类的id属性值
事务属性配置<tx:attributes>子标签
<tx:method>事务方法标签
- 属性name:方法名
- 属性read-only:是否只读事务,查询都是只读,其他是非只读
- 属性propagation:用于指定事务的传播行为。默认值是
REQUIRED
,表示一定会有事务,增删改的选择。查询方法可以选择SUPPORTS
.-
事务的传播行为包含7种:(required / supports / mandatory / requires_new / not supported / never / nested)
PROPAGATION_REQUIRED:如果当前没有事务,就创建一个新事务,如果当前存在事务,就加入该事务,这是最常见的选择,也是Spring默认的事务传播行为。(required需要,没有新建,有加入)
PROPAGATION_SUPPORTS:支持当前事务,如果当前存在事务,就加入该事务,如果当前不存在事务,就以非事务执行。(supports支持,有则加入,没有就不管了,非事务运行)
PROPAGATION_MANDATORY:支持当前事务,如果当前存在事务,就加入该事务,如果当前不存在事务,就抛出异常。(mandatory强制性,有则加入,没有异常)
PROPAGATION_REQUIRES_NEW:创建新事务,无论当前存不存在事务,都创建新事务。(requires_new需要新的,不管有没有,直接创建新事务)
PROPAGATION_NOT_SUPPORTED:以非事务方式执行操作,如果当前存在事务,就把当前事务挂起。(not supported不支持事务,存在就挂起)
PROPAGATION_NEVER:以非事务方式执行,如果当前存在事务,则抛出异常。(never不支持事务,存在就异常)
PROPAGATION_NESTED:如果当前存在事务,则在嵌套事务内执行。如果当前没有事务,则按REQUIRED属性执行。(nested存在就在嵌套的执行,没有就找是否存在外面的事务,有则加入,没有则新建)
-
- 属性isolation:事务隔离级别,默认配置DEFAULT,默认值是
DEFAULT
,表示使用数据库的默认隔离级别 - 属性timeout:用于指定事务的超时时间,默认值是
-1
,表示永不超时。如果指定了数值,以秒为单位。 - 属性no-rollback-for:用于指定一个异常,当产生该异常时,事务回滚;产生其他异常时,事务不会滚;没有默认值(设置值),任何异常都会回滚。
- 属性rollback-for:用于指定一个异常,当产生该异常时,事务不会滚;产生其他异常时事务回滚;没有默认值(设置值),任何异常都会回滚
以上回滚属性不配置,遇到异常就回滚
- aop切面配置
<aop:config>
标签<aop:advisor>
子标签- 属性advice-ref:引用通知,配置tx:advice标签的属性值
- 属性pointcut:切点配置
(4) 数据库配置信息
jdbc.driverClass=com.mysql.jdbc.Driver jdbc.url=jdbc:mysql://127.0.0.1:3306/myTest?characterEncoding=utf8&useUnicode=true&useSSL=false jdbc.username=root jdbc.password=123456
(5)代码
Account
package cn.gqx.bean; public class Account { private Integer id; private String name; private Double money; public Account(Integer id, String name, Double money) { this.id = id; this.name = name; this.money = money; } public Integer getId() { return id; } public void setId(Integer id) { this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name; } public Double getMoney() { return money; } public void setMoney(Double money) { this.money = money; } }
DAO的接口层与实现层
public interface AccountDao { int update(Account account, Double n); }
实现层
@Repository public class AccountDaoImpl implements AccountDao { @Autowired private JdbcTemplate jdbcTemplate; @Override public int update(Account account, Double n) { String sql = "update account set money = money+? where id=?"; int result = jdbcTemplate.update(sql, n, account.getId()); System.out.println(result); return result; } }
开启注解
<!-- 开启事务注解驱动 --> <tx:annotation-driven transaction-manager="transactionManager" />