1 前言
事务是一组由于逻辑上紧密关联而合并成一个整体的多个数据操作,这些操作要么都执行,要么都不执行。事务有以下4个特性(ACID):
- 原子性(Atomicity):操作这些指令时,要么全部执行成功,要么全部不执行。只要其中一个指令执行失败,所有的指令都执行失败,数据进行回滚,回到执行指令前的数据状态。
- 一致性(Consistency):事务的执行使数据从一个状态转换为另一个状态,但是对于整个数据的完整性保持稳定。
- 隔离性(Isolation):隔离性是当多个用户并发访问数据库时,比如操作同一张表时,数据库为每一个用户开启的事务,不能被其他事务的操作所干扰,多个并发事务之间要相互隔离。
- 持久性(Durability):当事务正确完成后,它对于数据的改变是永久性的。
需要导入的 jar 包如下,其中最后三个包是 JdbcTemplate 所需的 jar 包。
2 案例
首先在 MySQL 中创建数据库:taobao,再在此数据库中创建表:users(uid int, balance int),books(bid varchar, price int, num int),users 和 books 表中数据如下:
Dao.java
package com.transaction;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Repository;
@Repository
public class Dao {
@Autowired
private JdbcTemplate jdbcTemplate;
public Integer queryPrice(String bid) { //查询书的价格
Integer price=jdbcTemplate.queryForObject("select price from books where bid=?", new Object[] {bid},Integer.class);
return price;
}
public Integer queryNum(String bid) { //查询书的库存量
Integer num=jdbcTemplate.queryForObject("select num from books where bid=?", new Object[] {bid},Integer.class);
return num;
}
public Integer queryBalance(Integer uid) { //查询用户的余额
Integer balance=jdbcTemplate.queryForObject("select balance from users where uid=?", new Object[] {uid},Integer.class);
return balance;
}
public void updateBook(String bid,Integer buy_num) { //更新书的数量
Integer num=queryNum(bid);
if(num<buy_num) {
throw new RuntimeException();
}else {
jdbcTemplate.update("update books set num=num-? where bid=?", buy_num,bid);
}
}
public void updateUser(Integer uid,Integer price,Integer buy_num) { //更新用户的余额
Integer balance=queryBalance(uid);
if(balance<price*buy_num) {
throw new RuntimeException();
}else {
jdbcTemplate.update("update users set balance=balance-? where uid=?", price*buy_num,uid);
}
}
}
其中,@Repository 用于将 Dao 标注为持久层,并由 IOC 容器生成名为 dao 的 bean;@Autowired 用于自动注入属性。@Repository 和 @Autowired 的具体用法见通过注解配置bean。
BookService.java
package com.transaction;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
public class BookService {
@Autowired
private Dao dao;
@Transactional
public void buyBook(Integer uid,String bid,Integer buy_num) {
Integer price=dao.queryPrice(bid);
dao.updateUser(uid, price, buy_num); //用户扣钱
dao.updateBook(bid, buy_num); //书店减书
}
}
其中, @Service 用于将 BookService 标注为服务层,并由 IOC 容器生成名为 bookService 的 bean;@Transactional 用于标注 buyBook() 方法为一个事务,使其内部操作为一个整体,要么全部执行,要么全不执行(因为可能存在用户扣了钱,书店缺书的情况)。另外,@Transactional 可以加在方法上,也可以加在类上(相当于在类中所有方法上加了 @Transactional)。
transaction.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"
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-4.0.xsd
http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-4.0.xsd">
<!-- 引入属性文件 -->
<context:property-placeholder location="db.properties"/>
<!-- 创建数据源 -->
<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.username}"></property>
<property name="password" value="${jdbc.password}"></property>
</bean>
<!-- 通过数据源配置JdbcTemplate -->
<bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
<property name="dataSource" ref="dataSource"></property>
</bean>
<!-- 扫描组件 -->
<context:component-scan base-package="com.transaction"></context:component-scan>
<!-- 配置事务管理器 -->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"></property>
</bean>
<!-- 开启注解驱动,即对事务相关的注解进行扫描,解析含义并执行功能 -->
<tx:annotation-driven transaction-manager="transactionManager"/>
</beans>
Test.java
package com.transaction;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class Test {
public static void main(String[] args) {
ApplicationContext ac=new ClassPathXmlApplicationContext("transaction.xml");
BookService service=ac.getBean("bookService",BookService.class);
service.buyBook(1002, "b2", 1);
}
}
由于用户 1002 有钱(100元),但书店缺书 b2(0本),因此事务没有执行,即用户没有扣钱。如果 BookService.java 中没有注解 @Transactional,即使书店缺书,用户也会扣钱。
3 @Transactional 的属性
@Transactional 注解后可以添加 propagation、isolation、timeout、readOnly、rollbackFor 等属性。
3.1 propagation(事务的传播级别)
事务传播级别是针对嵌套事务而言的,比如:事务 A 中,嵌套了 B、C、D 3个事务。事务传播级别定义了嵌套事务的传播行为,有以下7个传播级别:
传播级别 | 描述 |
REQUIRED | 如果上下文中已经存在事务,那么就加入到事务中执行,如果当前上下文中不存在事务,则新建事务执行。 |
SUPPORTS | 如果上下文存在事务,则支持加入事务,如果没有事务,则使用非事务的方式执行。 |
MANDATORY | 上下文中必须要存在事务,否则就会抛出异常。配置该方式的传播级别是有效的控制上下文调用代码遗漏添加事务控制的保证手段。 |
REQUIRED_NEW | 每次都会新建一个事务,并且同时将上下文中的事务挂起,执行当前新建事务完成以后,上下文事务恢复再执行。 |
NOT_SUPPORTE | 上下文中存在事务,则挂起事务,执行当前逻辑,结束后恢复上下文的事务。 |
NEVER | 上下文中不能存在事务,一旦有事务,就抛出 Runtime 异常,强制停止执行。 |
NESTED | 如果上下文中存在事务,则嵌套事务执行,如果不存在事务,则新建事务。 |
传播级别属性的使用方式如下:
@Transactional(propagation=Propagation.REQUIRED)
3.2 isolation(事务的隔离级别)
在访问数据时,通常存在脏读、不可重复读、幻读等问题,其释义如下:
- 脏读:A事务读取B事务尚未提交的更改数据。如:B事务中有两次取钱操作,在两次取钱之间,A事务读取了余额,那么A事务就读取了B事务未提交的数据。
- 不可重复读:在一个事务范围内,多次查询某个数据,却得到不同的结果。如:A事务中有两次查询余额,中间B事务修改了余额并提交,那么A事务两次读取的余额数就不一样。
- 幻读:在一个事务范围内,多次查询表,得到的记录数不一样。如:A事务中有两次查询用户表中用户数,中间B事务插入了一条用户记录,那么A事务两次查询的记录数就不一样。
针对上述问题,定义了4个事务隔离级别,如下:
隔离级别 | 脏读 | 不可重复读 | 幻读 | 锁 |
读未提交 READ_UNCOMMITTED | ✔ | ✔ | ✔ | 无锁 |
读已提交 READ_COMMITTED | ✘ | ✔ | ✔ | 锁正在读取的行 |
可重复读 REPEATABLE_READ | ✘ | ✘ | ✔ | 锁所有读取的行 |
串行化 SERIALIZABLE | ✘ | ✘ | ✘ | 锁表 |
注意:还有默认隔离级别(DEFAULT),即使用数据库的默认值,MySQL(可重复读)、Oracle(读已提交)、SQL Server(读已提交)。
隔离级别属性的使用方式如下:
@Transactional(isolation=Isolation.READ_COMMITTED)
3.3 timeout
timeout 指定了事务在强制回滚前最多可执行(等待)的时间,使用方法如下:
@Transactional(timeout=5) //最多执行5秒
3.4 readOnly
readOnly 用于指定事务中的一系列操作是否为只读,若设置为只读,不管事务中是否有写操作,MySQL 都会在请求访问数据库的时候不加锁,以提高性能,但是,当事务中有写操作时,一定不要设置为只读,否则脏读、不可重复读、幻读等问题都有可能会发生。readOnly 的使用方式如下:
@Transactional(readOnly=true)
3.5 rollbackFor | rollbackForClassName | noRollbackFor | noRollbackForClassName
rollbackFor | rollbackForClassName | noRollbackFor | noRollbackForClassName 用于指定事务在遇到哪些异常时会回滚(或不会滚),使用方法如下:
//事务执行过程中,若出现空指针异常或算术异常,就会回滚,其他异常不回滚
@Transactional(rollbackFor={NullPointerException.class,ArithmeticException.class})