1 事务概念
事务是数据库操作最基本单元,逻辑上的一组操作,要么都成功,要么都失败。
典型场景:银行转账
(1) lucy 转账100元给 mary
(2) lucy 少100,mary 多100
事务特性ACID
(1) 原子性:一组逻辑操作,不可分割,要么都成功,一个失败即全部失败。
(2) 一致性:操作之前和操作之后总量不变。转账前后总量依然200。
(3) 隔离性:多个事务操作时,不会相互影响。
(4) 持久性:事务提交之后,数据库就发生改变。
2 场景引入
环境搭建
创建数据库表
DROP TABLE IF EXISTS `t_account`;
CREATE TABLE `t_account` (
`id` varchar(20) CHARACTER SET latin1 COLLATE latin1_swedish_ci NOT NULL,
`username` varchar(30) CHARACTER SET latin1 COLLATE latin1_swedish_ci NULL DEFAULT NULL,
`money` int(255) NULL DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = latin1 COLLATE = latin1_swedish_ci ROW_FORMAT = Dynamic;
INSERT INTO `t_account` VALUES ('1', 'Lucy', 1000);
INSERT INTO `t_account` VALUES ('2', 'Mary', 1000);
创建实体类
public class Account {
private String id;
private String username;
private Integer money;
public Account() {
}
public Account(String id, String username, Integer money) {
this.id = id;
this.username = username;
this.money = money;
}
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public Integer getMoney() {
return money;
}
public void setMoney(Integer money) {
this.money = money;
}
@Override
public String toString() {
return "Account{" +
"id='" + id + '\'' +
", username='" + username + '\'' +
", money=" + money +
'}';
}
}
创建service类、dao类,并注入对象
@Service
public class AccountService {
@Autowired
AccountDao accountDao;
}
public interface AccountDao {
}
@Repository
public class AccountDaoImpl implements AccountDao {
@Autowired
JdbcTemplate jdbcTemplate;
}
创建外部配置文件
jdbc.properties
jdbc.url=jdbc:mysql://localhost:3306/user_db?characterEncoding=utf-8
jdbc.username=root
jdbc.password=Ylj123..
jdbc.driver=com.mysql.cj.jdbc.Driver
创建spring配置文件
<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/context http://www.springframework.org/schema/context/spring-context.xsd">
<context:property-placeholder location="classpath:jdbc.properties" file-encoding="utf-8"/>
<context:component-scan base-package="com.wit"/>
<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource" destroy-method="close">
<property name="url" value="${jdbc.url}"/>
<property name="username" value="${jdbc.username}"/>
<property name="password" value="${jdbc.password}"/>
<property name="driverClassName" value="${jdbc.driver}"/>
</bean>
<!-- 配置jdbcTemplate对象,注入DataSource -->
<bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
<property name="dataSource" ref="dataSource"/>
</bean>
</beans>
代码编写
@Service
public class AccountService {
@Autowired
AccountDao accountDao;
public void accountMoney(){
// Lucy 转出100
accountDao.reduceMoney();
// Mary 转入100
accountDao.addMoney();
}
}
public interface AccountDao {
void addMoney();
void reduceMoney();
}
@Repository
public class AccountDaoImpl implements AccountDao {
@Autowired
JdbcTemplate jdbcTemplate;
// Mary转入100
public void addMoney() {
String sql = "update t_account set money=money+? where username=?";
jdbcTemplate.update(sql,100,"Mary");
}
// Lucy 转出100
public void reduceMoney() {
String sql = "update t_account set money=money-? where username=?";
jdbcTemplate.update(sql,100,"Lucy");
}
}
测试
@Test
public void testAccountMoney(){
ApplicationContext context = new ClassPathXmlApplicationContext("bean.xml");
AccountService accountService = context.getBean("accountService", AccountService.class);
accountService.accountMoney();
}
可能出现的问题
假如步骤一执行过后,出现异常。这时Lucy账户中已经扣掉100,然而由于步骤1之后的异常,程序无法执行到步骤2,也就是此时Mary账户并未增加100。
模拟异常
@Service
public class AccountService {
@Autowired
AccountDao accountDao;
public void accountMoney(){
// Lucy 转出100
accountDao.reduceMoney();
// 通过这一步模拟异常产生,使程序无法执行步骤2
int i = 10 / 0;
// Mary 转入100
accountDao.addMoney();
}
}
测试:结果显示,Lucy 账户少100,然而Mary账户并未增加100。
3 Spring事务
3.1 事务简介
(1) 事务一般添加在JAVAEE三层结构的service层。
(2) Spring事务操作分为两种:编程式事务管理和声明式事务管理。
编程式事务管理:步骤需要一步一步代码实现,使用起来很麻烦,一般不用。
public void accountMoney(){
try{
// 1. 开启事务
// 2. 业务操作
accountDao.reduceMoney();
int i = 10 / 0;
accountDao.addMoney();
// 3. 没有错误,正常提交
}catch (Exception e){
// 4. 出现异常,事务回滚
}
}
声明式事务管理:更加方便,非常常用,底层使用到了AOP原理。包含两种:基于注解方式、 基于xml配置文件方式。
事务管理API
提供了一个接口
PlatformTransactionManager
,代表了事务管理器,这个接口针对不同的框架提供了不同的实现类。
3.2 注解声明式事务管理
(1) 在spring配置文件配置事务管理器
<!-- 配置事务管理器 -->
<!-- 配置针对当前框架的PlatformTransactionManager实现类 -->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"/>
</bean>
(2) 开启事务注解
引入
tx
名称空间
<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/context"
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.xsd
http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd">
开启事务注解
<!-- 开启事务注解,指明当前使用的事务管理器 -->
<tx:annotation-driven transaction-manager="transactionManager"/>
在service类上面或者方法上面添加事务注解
@Transactional
(1) @Transactional
加在类上面,表示当前类所有方法都加了事务。
(2) @Transactional
加载方法上面,只为当前方法加了事务。
测试
未发生异常时:Lucy转出100,Mary转入100。
发生异常时:Lucy和Mary都没有变化。
3.3 声明式事务管理参数配置
在 @Transactional 这个注解中可以配置事务相关的参数。较为重要的:
propagation
、isolation
、timeout
、readOnly
、rollbackFor
、noRollbackFor
。
propagation:事务传播行为
多个事务进行操作时,事务如何处理。比如下图:有事务的add调用没事务的update、或者没事务的update调用有事务的add、或者两个有事务的方法互相调用。事务如何传播。传播行为一共有七种:REQUIRED(默认行为)、REQUIRED_NEW、SUPPORTS、NOT_SUPPORTED、MANDATORY、NEVER、NESTED。
![image-20210312164108544](https://gitee.com/lovely-dog/typora-image/raw/master/img/image-20210312164108544.png)
![image-20210312164602665](https://gitee.com/lovely-dog/typora-image/raw/master/img/image-20210312164602665.png)
ioslation:事务隔离级别。
事务具有隔离性,多个事务操作之间不会产生影响。不考虑隔离性会出现很多问题:脏读、不可重复度、幻读
(1) 引发的问题
- 脏读:脏读就是指当一个事务正在访问数据,并且对数据进行了修改,而这种修改还没有提交到数据库中,这时,另外一个事务也访问 这个数据,然后使用了这个数据。
- 不可重复度:是指在一个事务内,多次读同一数据。在这个事务还没有结束时,另外一个事务也访问该同一数据。那么,在第一个事务中的两 次读数据之间,由于第二个事务的修改,那么第一个事务两次读到的的数据可能是不一样的。这样就发生了在一个事务内两次读到的数据是不一样的,因此称为是不 可重复读。
- 幻读:是指当事务不是独立执行时发生的一种现象,例如第一个事务对一个表中的数据进行了修改,这种修改涉及到表中的全部数据行。 同时,第二个事务也修改这个表中的数据,这种修改是向表中插入一行新数据。那么,以后就会发生操作第一个事务的用户发现表中还有没有修改的数据行,就好象 发生了幻觉一样。
(2) 设置隔离级别。MySQL默认是可重复读级别。
timeout:超时时间
事务需要在一定时间内进行提交,不提交则回滚。这个时间称为超时时间,默认是-1,不超时,单位是秒。
readOnly:是否只读
默认值是false,表示可读可写,设置成true之后,只能读,不能写。
rollbackFor:回滚
设置出现了哪些异常,进行回滚。
noRollbackFor:不回滚
出现了什么异常,不进行回滚。
3.4 XML配置声明式事务管理
(1) 配置事务管理器
<!-- 开启事务注解,指明当前使用的事务管理器 -->
<tx:annotation-driven transaction-manager="transactionManager"/>
(2) 配置通知
<!-- 配置通知 -->
<tx:advice id="txadvice">
<!-- 配置事务参数 -->
<tx:attributes>
<!-- 指定符合规则的方法上面添加事务
accountMoney:表示在accountMoney上面添加事务
account*:表示以account开头的方法上面添加事务
-->
<tx:method name="accountMoney" propagation="REQUIRED"/>
</tx:attributes>
</tx:advice>
(3) 配置切入点和切面
<aop:config>
<!-- 配置切入点 -->
<aop:pointcut id="pt" expression="execution(* com.wit.service.AccountService.*(..))"/>
<!-- 配置切面 -->
<aop:advisor advice-ref="txadvice" pointcut-ref="pt"/>
</aop:config>
(4) 删除 @Transactional
注解,测试。
@Test
public void testAccountMoney(){
ApplicationContext context = new ClassPathXmlApplicationContext("bean.xml");
AccountService accountService = context.getBean("accountService", AccountService.class);
accountService.accountMoney();
}
3.5 完全注解开发
创建配置类,取代配置文件。在 AccountService
上加上 @Transactional
注解。
@Configuration // 配置类
@ComponentScan(basePackages = "com.wit") // 扫描包
@EnableTransactionManagement // 开启事务,相当于 annotation-driven
public class TxConfig {
private Properties properties;
// 非静态代码块,每次创建对象时执行一次。也可修改为静态代码块,只在第一次创建对象时执行一次。
{
// 读取 jdbc.properties 中的属性
properties = new Properties();
InputStream in = TxConfig.class.getClassLoader().getResourceAsStream("jdbc.properties");
try {
properties.load(in);
} catch (IOException e) {
e.printStackTrace();
}
}
// 创建数据库连接池,将放回对象加入IOC容器
@Bean // 相当于配置数据库连接池 bean
public DruidDataSource getDruidDataSource(){
DruidDataSource dataSource = new DruidDataSource();
dataSource.setDriverClassName(properties.getProperty("jdbc.driver"));
dataSource.setUrl(properties.getProperty("jdbc.url"));
dataSource.setUsername(properties.getProperty("jdbc.username"));
dataSource.setPassword(properties.getProperty("jdbc.password"));
return dataSource;
}
@Bean // 参数这里,数据库连接已经创建并加入到IOC容器中。这里根据 DataSource 类型注入。
public JdbcTemplate getJdbcTemplate(DataSource dataSource){
JdbcTemplate template = new JdbcTemplate();
template.setDataSource(dataSource);
return template;
}
@Bean // 创建事务管理器,加入到IOC容器中。
public DataSourceTransactionManager getTransactionManager(DataSource dataSource){
DataSourceTransactionManager transactionManager = new DataSourceTransactionManager();
transactionManager.setDataSource(dataSource);
return transactionManager;
}
}
对比 bean.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:aop="http://www.springframework.org/schema/aop"
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.xsd
http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd
http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd">
<!-- 读取jdbc.properties,相当于TxConfig中非静态代码块中的内容 -->
<context:property-placeholder location="classpath:jdbc.properties" file-encoding="utf-8"/>
<!-- 扫描包,相当于TxConfig上面的@ComponentScan(basePackages = "com.wit")注解 -->
<context:component-scan base-package="com.wit"/>
<!-- 配置dataSource,相当于TxConfig中的getDruidDataSource方法 -->
<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource" destroy-method="close">
<property name="url" value="${jdbc.url}"/>
<property name="username" value="${jdbc.username}"/>
<property name="password" value="${jdbc.password}"/>
<property name="driverClassName" value="${jdbc.driver}"/>
</bean>
<!-- 配置jdbcTemplate,相当于TxConfig中的getJdbcTemplate方法 -->
<bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
<property name="dataSource" ref="dataSource"/>
</bean>
<!-- 配置事务管理器,相当于TxConfig中的getTransactionManager方法 -->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"/>
</bean>
<!-- 事务管理器,相当于TxConfig上面的@EnableTransactionManagement注解 -->
<tx:annotation-driven transaction-manager="transactionManager"/>
<!-- 事务管理,相当于在AccountService上边加入@Transactional注解 -->
<tx:advice id="txadvice">
<tx:attributes>
<tx:method name="accountMoney" propagation="REQUIRED"/>
</tx:attributes>
</tx:advice>
<aop:config>
<aop:pointcut id="pt" expression="execution(* com.wit.service.AccountService.*(..))"/>
<aop:advisor advice-ref="txadvice" pointcut-ref="pt"/>
</aop:config>
</beans>
测试
@Test
public void testAccountMoneyAnno(){
ApplicationContext context = new AnnotationConfigApplicationContext(TxConfig.class);
AccountService accountService = context.getBean("accountService", AccountService.class);
accountService.accountMoney();
}