将一组操作数据的SQL作为一个整体提交,要么都成功,要么都失败
使用的原因:
保证数据的完整性和一致性,对数据操作是要保证数据的安全
⑴ 原子性
将多个SQL作为一个整体,不可分割
⑵ 一致性
数据在操作前是正确的,操作后也是正确的
操作的结果应该和业务逻辑保持一致
⑶ 隔离性
多个事务并发操作同一个数据时,保证事务间的数据是隔离开来的,相关不会受到干扰,保证数据的安全
⑷ 持久性
事务操作数据库的结果,应该永久地保存到持久化存储器中
int TRANSACTION_READ_UNCOMMITTED = 1;
一个事务读取到了另一个事务还没有提交的数据
可能产生脏读,不可重复读,幻读问题。但是它解决了数据丢失问题
例如:
T1正在读取数据,并对数据进行操作
T2 修改了数据,但是还没有提交数据
T1 再次读取数据,可能发生脏读问题。因为T2可能会回滚事务
int TRANSACTION_READ_COMMITTED = 2;
一个事务读取到了另外一个事务已经提交过的数据
可能产生不可重复读,幻读问题。但是它解决了数据丢失和脏读问题
例如:
T1 正在读取数据,并对数据进行操作
T2 修改了数据,并提交了数据
T1 再次读取数据,发现两次读取到的数据不一致。这就产生了不可重复读问题
int TRANSACTION_REPEATABLE_READ = 4;
一个事务只能重复的读取当前事务中的操作数据,而不能读取到另外的事务中未提交和已提交的数据
可能产生幻读。但是它解决了数据丢失,脏读和不可重复读问题
例如:
T1 正在读取数据,并对数据进行操作
T2 修改了数据,并且提交了数据,但是没有提交数据
T1 再次读取数据,两次读取到的数据是一致的
int TRANSACTION_SERIALIZABLE = 8;
可以解决所有的问题,一般配合数据库锁的机制控制数据的安全【统计工作】
幻读问题:一个事务正在进行某种统计操作,其他事务又进行数据的增删改等操作,导致两次统计到的结果不一致
例如:
T1 正在统计表的记录数,得到一个结果
T2 向数据库中添加了一些数据
T3 再次统计表的记录数,得到的结果就比T1统计到的多
Tips:
SELECT * FROM ??? WHERE ??? FOR UPDATE; // 行级锁
即使用原生的JDBC API进行事务的管理
步骤:
⑴ 获取数据库连接对象【Connection】
⑵ 取消事务的自动提交
⑶ 执行SQL操作
⑷ 正常完成操作时,手动提交事务
⑸ 执行失败时,回滚事务
⑹ 关闭相关资源(释放连接等)
通过相关配置,给程序方法增加事务的操作
Spring框架提供了PlatformTransactionManager接口,用来管理底层的事务。并且提供了相关的实现类
⑴ DataSourceTransactionManager
Spring和JdbcTemplate或MyBatis框架集成时,提供的事务管理器
⑵ HibernateTransactionManager
Spring和Hibernate框架集成时,提供的事务管理器
⑴ 拷贝jar包
① 和IOC有关的:
commons-logging-1.1.3.jar
spring-beans-4.0.0.RELEASE.jar
spring-context-4.0.0.RELEASE.jar
spring-core-4.0.0.RELEASE.jar
spring-expression-4.0.0.RELEASE.jar
② 和AOP有关的:
spring-aop-4.0.0.RELEASE.jar
spring-aspects-4.0.0.RELEASE.jar
com.springsource.net.sf.cglib-2.2.0.jar
com.springsource.org.aopalliance-1.0.0.jar
com.springsource.org.aspectj.weaver-1.6.8.RELEASE.jar
③ 和数据库有关的:
spring-jdbc-4.0.0.RELEASE.jar
spring-orm-4.0.0.RELEASE.jar
spring-tx-4.0.0.RELEASE.jar
④ 数据库驱动和c3p0
mysql-connector-java-5.1.7-bin.jar
c3p0-0.9.1.2.jar
⑵ 创建c3p0的properties配置文件
⑶ 创建核心配置文件
添加context和tx名称空间
① 通过
<context:component-scan base-package="要扫描的包及其子包" />
标签,来设置自动扫描包
② 通过
<context:property-placeholder location="???.properties" />
标签,来引入外部配置文件
③ 通过bean 标签,声明c3p0即DataSource对象
④ 通过bean 标签,声明JdbcTemplate对象。注意需要给其dataSource属性赋值,引用DataSource(c3p0)对象
<bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
<property name="dataSource" ref="dataSource" />
</bean>
⑤ 通过bean标签,声明TransactionManager对象。注意需要给其dataSource属性赋值,引用DataSource(c3p0)对象
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource" />
</bean>
以DataSourceTransactionManager为例
⑥ 通过
<tx:annotation-driven transaction-manager="事务管理器" />
标签,来开启基于注解的声明式事务
⑷ 编写具体的DAO、Service,并在具体的业务方法上,加上@Transactional注解,即可给方法开启事务
【propagation属性】
方法被调用时,事务的开启方式
一共有7个传播行为,常用的有2个:
⑴ Propagation.REQUIRED
表示一个方法被调用时,如果已经存在了一个事务,则加入其中;否则,会开启一个新的事务。【默认值】
⑵ Propagation.REQUIRES_NEW
表示一个方法被调用时,不管调用的方法是否存在事务,都会开启一个新的事务
【isolation属性】
Isolation.DEFAULT
表示与数据库的默认隔离级别一致
MySQL 4(重复读)
Isolation.READ_UNCOMMITTED 未提交读
Isolation.READ_COMMITTED 已提交读
Isolation.REPEATABLE_READ 重复读
Isolation.SERIALIZABLE 序列化/不可并发
【rollbackFor属性】
当发生什么样的异常(包括其子类)时,进行回滚
对于Spring框架,默认情况下,RuntimeException类型才会回滚;对于编译时异常和Error,是不会回滚的
所以,需要修改Spring的默认回滚策略
例如:rollbackFor = Exception.class
Tips:
⑴ RuntimeException:运行时异常
例如FileNotFoundException,这种可以遇见到的异常
⑵ Exception:编译期异常/检查异常/受控异常
与rollbackFor相对的是:
【noRollbackFor】属性
遇到指定的异常类型(包括其子类),不进行回滚
【timeout】属性
timeout = 超时时间(秒)
如果一个事务的执行时间,超过了指定的时长(n秒),就被视为超时。当该事务执行完毕时,会抛出异常:
org.springframework.transaction.TransactionTimedOutException: Transaction timed out: deadline was ???
【readOnly】属性
一般查询的时候,会将该属性设置为true,表明为只读。这样数据库底层会对查询进行优化处理,提高查询的效率
注意:当一个连接对象(Connection)设置了只读属性,则其再操作增删改时,会报错:
java.sql.SQLException: Connection is read-only. Queries leading to data modification are not allowed
【示例一】
// 事务传播行为为当有事务时就加入,没有就新建一个事务;
// 默认隔离级别;
// 事务回滚策略:Exception及其子异常
// 超时:3秒超时
@Transactional(propagation = Propagation.REQUIRED,
isolation = Isolation.DEFAULT,
rollbackFor = Exception.class,
timeout = 3)
public void testTransaction() { }
【示例二】
// 查询业务操作,设置连接属性为只读,提高效率
@Transactional(readOnly = true)
public void testQuery() { }
【SQL语句】
USE test;
CREATE TABLE books(
id INT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(100) NOT NULL,
price INT NOT NULL,
stock INT NOT NULL
);
INSERT INTO books(name, price, stock)
VALUES('小王子', 34, 100),
('圣经', 48, 100),
('福尔摩斯探案集', 84, 100);
【Book类(JavaBean)】
属性:Integer id, String name, Integer price, Integer stock;提供get和set,有参无参构造,重写toString方法
【核心配置文件】
<context:component-scan base-package="com.test.tx" />
<context:property-placeholder location="classpath:/jdbc.properties" />
<bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource">
<property name="driverClass" value="${jdbc.driverClass}" />
<property name="jdbcUrl" value="${jdbc.url}" />
<property name="user" value="${jdbc.user}" />
<property name="password" value="${jdbc.password}" />
</bean>
<!-- 声明JdbcTemplate -->
<bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
<property name="dataSource" ref="dataSource" />
</bean>
<!-- 声明事务管理器 -->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource" />
</bean>
<!-- 开启基于注解的声明式事务 -->
<tx:annotation-driven transaction-manager="transactionManager" />
【BookDao接口】
package com.test.tx.dao;
import java.util.List;
import com.test.tx.bean.Book;
public interface BookDao {
int updateBookPrice(String name, Integer price);
int updateBookStock(String name);
Book getBook(String name);
List<Book> queryBooks();
}
【BookDaoImpl实现类】
package com.test.tx.dao;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.BeanPropertyRowMapper;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.stereotype.Repository;
import com.test.tx.bean.Book;
@Repository
public class BookDaoImpl implements BookDao {
@Autowired
private JdbcTemplate jdbcTemplate;
@Override
public int updateBookPrice(String name, Integer price) {
String sql = "UPDATE test.books SET price = ? WHERE name = ?";
return jdbcTemplate.update(sql, price, name);
}
@Override
public int updateBookStock(String name) {
String sql = "UPDATE test.books SET stock = stock - 1 WHERE name = ?";
return jdbcTemplate.update(sql, name);
}
@Override
public Book getBook(String name) {
String sql = "SELECT id, name, price, stock FROM test.books WHERE name = ?";
RowMapper<Book> rowMapper = new BeanPropertyRowMapper<Book>(Book.class);
return jdbcTemplate.queryForObject(sql, rowMapper, name);
}
@Override
public List<Book> queryBooks() {
String sql = "SELECT id, name, price, stock FROM test.books";
RowMapper<Book> rowMapper = new BeanPropertyRowMapper<Book>(Book.class);
return jdbcTemplate.query(sql, rowMapper);
}
}
【BookService接口】
package com.test.tx.service;
import java.util.List;
import com.test.tx.bean.Book;
public interface BookService {
int updateBookPrice(String name, Integer price) throws Exception;
int updateBookStock(String name) throws Exception;
Book getBook(String name);
List<Book> queryBooks();
}
【BookServiceImpl实现类】
package com.test.tx.service;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import com.test.tx.bean.Book;
import com.test.tx.dao.BookDao;
@Service
public class BookServiceImpl implements BookService {
@Autowired
private BookDao bookDao;
/*
* 测试超时
*
* org.springframework.transaction.TransactionTimedOutException: Transaction
* timed out: deadline was ???
*
* @Transactional(timeout = 3)
*/
// ---------------------------------------------------------------------------------
/*
* 测试更新时,设置连接为只读
*
* Caused by: java.sql.SQLException: Connection is read-only. Queries
* leading to data modification are not allowed
*
* @Transactional(readOnly = true)
*/
// ---------------------------------------------------------------------------------
// 这里设置事务的传播行为是:不管有没有事务,都会开启一个新的事务
// 是为了测试MulService的testTx方法【因为和下面的更新图书库存操作是两个事务,所以这个事务出错了,但是不会回滚。最终效果是价格变了,但库存没有变】
@Transactional(propagation = Propagation.REQUIRES_NEW)
public int updateBookPrice(String name, Integer price) throws Exception {
// 测试超时【让线程休眠】
// Thread.sleep(4000);
return bookDao.updateBookPrice(name, price);
}
/*
* 如果不设置回滚策略,则会造成执行MulService的testTx方法时,图书的价格变了,但是库存没有变,事务并没有回滚
*
* @Transactional(rollbackFor = Exception.class)
*/
// ---------------------------------------------------------------------------------
// 这里设置事务的传播行为是:不管有没有事务,都会开启一个新的事务
// 是为了测试MulService的testTx方法【因为和上面的更新图书价格操作是两个事务,所以这个事务出错了,但是不会回滚。最终效果是价格变了,但库存没有变】
@Transactional(propagation = Propagation.REQUIRES_NEW)
public int updateBookStock(String name) throws Exception {
// 这里抛出一个编译时异常【Spring默认是不回滚的】
// FileInputStream fis = new FileInputStream("a/a/a/a");
// 这里有一个算数异常
int i = 1 / 0;
return bookDao.updateBookStock(name);
}
// 设置为只读的,提高效率
@Transactional(readOnly = true)
public Book getBook(String name) {
return bookDao.getBook(name);
}
// 设置为只读的,提高效率
@Transactional(readOnly = true)
public List<Book> queryBooks() {
return bookDao.queryBooks();
}
}
【MulService类(用于调用BookServiceImpl的两个业务方法)】
package com.test.tx.service;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
public class MulService {
@Autowired
private BookService bookService;
@Transactional
public void testTx(String name, Integer price) throws Exception {
bookService.updateBookPrice(name, price);
bookService.updateBookStock(name);
}
}
【测试类】
package junit.test;
import org.junit.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import com.test.tx.bean.Book;
import com.test.tx.service.BookService;
import com.test.tx.service.MulService;
public class TestTx {
private ApplicationContext ioc = new ClassPathXmlApplicationContext("???.xml");
@Test
// 测试执行更新图书价格操作
public void test1() throws Exception {
BookService bookService = ioc.getBean(BookService.class);
bookService.updateBookPrice("小王子", 100);
}
@Test
// 测试更新图书的库存和价格【两个Dao操作】
public void test2() throws Exception {
MulService mulService = ioc.getBean(MulService.class);
String name = "小王子";
Integer price = 200;
mulService.testTx(name, price);
}
@Test
public void test3() {
BookService bookService = ioc.getBean(BookService.class);
Book book = bookService.getBook("小王子");
System.out.println(book);
}
}
<tx:advice id="通知的id" transaction-manager="事务管理器" ></tx:advice>
用于声明通知的事务标签。它有tx:attributes子标签
<tx:attributes></tx:attributes>
tx:advice的子标签,在其里面 声明开启事务的方法标签
<tx:method
name=""
isolation="DEFAULT"
no-rollback-for=""
propagation="REQUIRED"
read-only="false"
rollback-for=""
timeout="-1" />
name:需要开启事务的方法的名字
isolation:事务隔离级别
no-rollback-for:事务不回滚策略
propagation:事务传播行为
read-only:只读属性
rollback:事务回滚策略
timeout:超时【默认值为-1,即不超时】
Tips:name属性可以使用* 来代替多个字符
例如:getUser方法名,可以用get* 来代替
设置开启事务方法的Transaction的属性
<aop:config></aop:config>
在其里面声明切入点表达式和组织者
<aop:pointcut expression="切入点表达式" id="表达式的id" />
用于声明切入点表达式
<aop:advisor advice-ref="所引用的事务标签的组织者的id" pointcut-ref="所引用的切入点表达式的id" />
用于声明组织者,需要引用事务的通知的id(tx:advice)和切入点表达式(aop:pointcut)
该标签用于匹配需要开启事务的业务逻辑方法
<tx:advice id="adviceId" transaction-manager="transactionManager">
<tx:attributes>
<!-- 设置开启事务方法的Transaction的属性 -->
<tx:method name="test*" propagation="REQUIRED" />
<!-- 设置超时为3秒;出现异常就回滚的策略 -->
<tx:method name="update*" timeout="3" rollback-for="java.lang.Exception" />
<!-- 查询业务方法,设置连接为只读,提高效率 -->
<tx:method name="query*" read-only="true" />
<!-- 事务隔离级别:重复读 -->
<tx:method name="get*" read-only="true" isolation="REPEATABLE_READ" />
</tx:attributes>
</tx:advice>
<aop:config>
<!-- 声明切入点表达式 -->
<aop:pointcut expression="execution(* com.test..service..*(..))" id="pointcutId" />
<!-- 声明组织者 -->
<aop:advisor advice-ref="adviceId" pointcut-ref="pointcutId" />
</aop:config>