如果一个业务操作中多次访问了数据库,必须保证每条SQL语句都执行成功。如果其中有一条执行失败,所有已经执行过的代码必须回滚。回到没有执行前的状态。称为事务。要么所有的SQL语句全部执行成功,要么全部失败。
事务特性 | 含义 |
---|---|
原子性(Atomicity) | 事务是工作的最小单元,整个工作单元要么全部执行成功,要么全部执行失败 |
一致性(Consistency) | 事务执行前与执行后,数据库中数据应该保持相同的状态。如:转账前总金额与转账后总金额相同。 |
隔离性(Isolation) | 事务与事务之间不能互相影响,必须保持隔离性。 |
持久性(Durability) | 如果事务执行成功,对数据库的操作是持久的。 |
并发访问下事务产生的问题:
当同时有多个用户在访问同一张表中的记录,每个用户在访问的时候都是一个单独的事务。
事务在操作时的理想状态是:事务之间不应该相互影响,实际应用的时候会引发下面三种问题。应该尽量避免这些问题的发生。通过数据库本身的功能去避免,设置不同的隔离级别。
级别 | 名字 | 隔离级别 | 脏读 | 不可重复读 | 幻读 | 数据库默认隔离级别 |
---|---|---|---|---|---|---|
1 | 读未提交 | read uncommitted | 是 | 是 | 是 | |
2 | 读已提交 | read committed | 否 | 是 | 是 | Oracle和SQL Server |
3 | 可重复读 | repeatable read | 否 | 否 | 是 | MySQL |
4 | 串行化 | serializable | 否 | 否 | 否 |
1)Read uncommitted
(读未提交): 简称RU隔离级别,所有事务中的并发访问问题都会发生,读取的是其他事务没有提交的数据
2)Read committed
(读已提交):简称RC隔离级别,会引发不可重复读和幻读的问题,读取的永远是其他事务提交的数据
3)Repeatable read
(可重复读):简称RR隔离级别,有可能会引发幻读的问题,一次事务读取到的同一行数据,永远是一样
4)Serializable
(串行化): 可以避免所有事务产生的并发访问的问题 效率及其低下
drop table if exists account;
-- 创建数据表
CREATE TABLE account (
id INT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(10),
money DOUBLE
);
-- 添加数据
INSERT INTO account (name, money) VALUES ('a', 1000), ('b', 1000);
select @@tx_isolation;
set global transaction isolation level 四种隔离;
set global transaction isolation level read uncommitted;
set global transaction isolation level read committed;
set global transaction isolation level repeatable read;
set global transaction isolation level serializable;
需要重启客户端会话
start transaction;
或者
begin;
commit;
rollback;
编程式事务简单的来说就是采用编程的方式来管理事务,编程式事务需要将事务管理的代码写入到业务方法中,相对于核心业务而言,事务管理的代码显然属于非核心业务,对核心业务代码的侵入性太大;而且如果多个模块都使用同样模式的代码进行事务管理,显然会造成较大程度的代码冗余。
我们之前使用的JDBC API来操作事务就是编程式事务;
声明式事务以声明的方式来实现事务管理,其本质是对方法前后进行拦截,然后在目标方法开始之前创建或者加入一个事务,在执行完目标方法之后根据执行情况提交或者回滚事务。
声明式事务简单的来说就是通过一些表达式声明(拦截)一堆需要被事务管理的方法,然后被声明(拦截)的方法通过AOP的方式进行事务管理(执行目标方法之前开启事务,执行目标方法之后提交或者回滚事务)
显然声明式事务管理要优于编程式事务管理:它将事务管理代码从业务方法中分离出来,这正是Spring倡导的非侵入式的开发方式。声明式事务管理使业务代码不受干扰;一个普通的对象,只要加上注解就可以获得完全的事务支持。
声明式事务唯一不足地方是,它的最细粒度只能作用到方法级别,无法做到像编程式事务那样可以作用到代码块级别。但是即便有这样的需求,也存在很多变通的方法,比如,可以将需要进行事务管理的代码块独立为方法等等。
Spring从不同的事务管理API中抽象出了一整套事务管理机制,让事务管理代码从特定的事务技术中独立出来。开发人员通过配置的方式进行事务管理,而不必了解其底层是如何实现的。
Spring所有的事务管理策略类都继承自org.springframework.transaction.PlatformTransactionManager
接口。
它为事务管理封装了一组独立于技术的方法。无论使用Spring的哪种事务管理策略(编程式或声明式),事务管理器都是必须的。
DataSourceTransactionManager
:使用Spring JDBC 或 Mybatis 等基于DataSource数据源的持久化技术时,使用 该事务管理器JpaTransactionManager
:使用JPA时采用的事务管理器JtaTransactionManager
:具有多个数据源的全局事务使用该事务管理器JdoTransactionManager
:使用JDO进行持久化时 ,使用该事务管理器HibernateTransactionManager
:使用Hibernate进行持久化时,使用该事务管理器当事务方法被另一个事务方法调用时,必须指定事务应该如何传播。
例如:方法可能继续在现有事务中运行,也可能开启一个新事务,并在自己的事务中运行。事务的传播行为可以由传播属性指定。Spring定义了7种类传播行为,在org.springframework.transaction.TransactionDefinition
类中被定义。
静态常量 | 说明 |
---|---|
PROPAGATION_REQUIRED | 如果当前没有事务,就新建一个事务,如果已经存在一个事务中,加入到这个事务中。这是最常见的选择,也是Spring的默认值。 |
PROPAGATION_SUPPORTS | 支持当前事务,如果当前没有事务,就以非事务方式执行。 |
PROPAGATION_MANDATORY | 使用当前的事务,如果当前没有事务,就抛出异常。 |
PROPAGATION_REQUIRES_NEW | 新建事务,如果当前存在事务,把当前事务挂起。 |
PROPAGATION_NOT_SUPPORTED | 以非事务方式执行操作,如果当前存在事务,就把当前事务挂起。 |
PROPAGATION_NEVER | 以非事务方式执行,如果当前存在事务,则抛出异常。 |
PROPAGATION_NESTED | 如果当前存在事务,则在嵌套事务内执行。如果当前没有事务,则执行与PROPAGATION_REQUIRED类似的操作。 |
TransactionDefinition
类中不仅定义了事务的传播行为,也定义了很多事务的隔离级别:
静态常量 | 说明 |
---|---|
ISOLATION_DEFAULT | 使用数据库默认的隔离级别,Spring默认值 |
ISOLATION_READ_UNCOMMITTED | 读未提交 |
ISOLATION_READ_COMMITTED | 读已提交 |
ISOLATION_REPEATABLE_READ | 可重复读 |
ISOLATION_SERIALIZABLE | 串行化 |
drop database if exists spring;
create database spring;
use spring;
-- 创建数据表
CREATE TABLE account (
id INT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(10),
money DOUBLE
);
-- 添加数据
INSERT INTO account (name, money) VALUES ('a', 1000), ('b', 1000);
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0modelVersion>
<groupId>com.dfbzgroupId>
<artifactId>01_SpringartifactId>
<version>1.0-SNAPSHOTversion>
<dependencies>
<dependency>
<groupId>org.springframeworkgroupId>
<artifactId>spring-contextartifactId>
<version>5.2.9.RELEASEversion>
dependency>
<dependency>
<groupId>org.springframeworkgroupId>
<artifactId>spring-jdbcartifactId>
<version>5.2.9.RELEASEversion>
dependency>
<dependency>
<groupId>org.springframeworkgroupId>
<artifactId>spring-txartifactId>
<version>5.2.9.RELEASEversion>
dependency>
<dependency>
<groupId>org.springframeworkgroupId>
<artifactId>spring-aspectsartifactId>
<version>5.2.9.RELEASEversion>
dependency>
<dependency>
<groupId>org.springframeworkgroupId>
<artifactId>spring-testartifactId>
<version>5.2.9.RELEASEversion>
dependency>
<dependency>
<groupId>mysqlgroupId>
<artifactId>mysql-connector-javaartifactId>
<version>5.1.48version>
dependency>
<dependency>
<groupId>com.alibabagroupId>
<artifactId>druidartifactId>
<version>1.2.1version>
dependency>
<dependency>
<groupId>junitgroupId>
<artifactId>junitartifactId>
<version>4.12version>
<scope>testscope>
dependency>
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
<version>1.18.18version>
dependency>
dependencies>
project>
jdbc.username=root
jdbc.password=admin
jdbc.url=jdbc:mysql:///spring
jdbc.driverClassName=com.mysql.jdbc.Driver
package com.dfbz.entity;
import lombok.Data;
/**
* @author lscl
* @version 1.0
* @intro:
*/
@Data
public class Account {
private Integer id;
private String name;
private Double money;
}
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:tx="http://www.springframework.org/schema/tx"
xmlns:context="http://www.springframework.org/schema/context"
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/tx http://www.springframework.org/schema/tx/spring-tx.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/aop https://www.springframework.org/schema/aop/spring-aop.xsd">
<context:component-scan base-package="com.dfbz" />
<context:property-placeholder location="classpath:jdbc.properties"/>
<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource">
<property name="username" value="${jdbc.username}">property>
<property name="password" value="${jdbc.password}">property>
<property name="url" value="${jdbc.url}">property>
<property name="driverClassName" value="${jdbc.driverClassName}">property>
bean>
<bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
<property name="dataSource" ref="dataSource">property>
bean>
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"/>
bean>
<tx:advice id="txAdvice" transaction-manager="transactionManager">
<tx:attributes>
<tx:method name="transfer" no-rollback-for="java.lang.ArithmeticException"/>
tx:attributes>
tx:advice>
<aop:config>
<aop:pointcut id="myPoint" expression="execution(* com.dfbz.service.AccountService.*(..))"/>
<aop:advisor advice-ref="txAdvice" pointcut-ref="myPoint">aop:advisor>
aop:config>
beans>
**默认情况下,任何的运行时异常事务都会回滚,但编译时异常都不会进行回滚;**我们可以通过一些配置来调整
no-rollback-for
:指定什么异常不回滚,可以写多个,以逗号隔开;
rollback-for
:该配置针对于编译时异常;可以写多个,以逗号隔开;
上述两个参数在指定异常时,如果指定某个异常的父类,包括这个异常的所有子类异常都会回滚;
如:no-rollback-for
指定的参数是Exception则代表Java中的任何异常都不回滚,
rollback-for
指定的参数是Exception
则Java中任何的异常都回滚;
package com.dfbz.service;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
/**
* @author lscl
* @version 1.0
* @intro:
*/
@Service
public class AccountService {
@Autowired
private JdbcTemplate jdbcTemplate;
/**
* @param str : 谁要转账
* @param target: 转账给谁
* @param flag: 模拟异常 true:出现异常 false:不出现异常
*/
public void transfer(String str, String target, Boolean flag) {
jdbcTemplate.update("update account set money=money-500 where name=?", str);
if (flag) {
// 模拟异常
int i = 1 / 0;
}
jdbcTemplate.update("update account set money=money+500 where name=?", target);
}
/**
*
* @param id
*/
public void findById(Integer id) {
Account account = jdbcTemplate.queryForObject(
"select * from account where id=?",
new BeanPropertyRowMapper<>(Account.class),
id
);
}
}
package com.dfbz.test;
import com.dfbz.service.AccountService;
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.SpringRunner;
/**
* @author lscl
* @version 1.0
* @intro:
*/
@RunWith(SpringRunner.class)
@ContextConfiguration(locations = "classpath:spring.xml")
public class Demo01 {
@Autowired
private AccountService accountService;
@Test
public void test1(){
accountService.transfer("a","b",false);
}
}
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:tx="http://www.springframework.org/schema/tx"
xmlns:context="http://www.springframework.org/schema/context"
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/tx http://www.springframework.org/schema/tx/spring-tx.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/aop https://www.springframework.org/schema/aop/spring-aop.xsd">
<context:component-scan base-package="com.dfbz" />
<context:property-placeholder location="classpath:jdbc.properties"/>
<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource">
<property name="username" value="${jdbc.username}">property>
<property name="password" value="${jdbc.password}">property>
<property name="url" value="${jdbc.url}">property>
<property name="driverClassName" value="${jdbc.driverClassName}">property>
bean>
<bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
<property name="dataSource" ref="dataSource">property>
bean>
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"/>
bean>
<tx:annotation-driven transaction-manager="transactionManager">tx:annotation-driven>
beans>
package com.dfbz.service;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Isolation;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
/**
* @author lscl
* @version 1.0
* @intro:
*/
@Service
public class AccountService {
@Autowired
private JdbcTemplate jdbcTemplate;
/**
* @param str : 谁要转账
* @param target: 转账给谁
* @param flag: 模拟异常 true:出现异常 false:不出现异常
*/
/* @Transactional(
transactionManager = "transactionManager", // 事务管理器的名称
propagation = Propagation.REQUIRED, // 传播行为
isolation = Isolation.DEFAULT, // 隔离级别
timeout = 3000, // 超时时间
readOnly = false, // 是否只读
rollbackFor = {MyException.class}, // 出现该异常回滚
noRollbackFor = {ArithmeticException.class} // 出现指定异常不回滚
) */
@Transactional // 使用注解方式管理事务
public void transfer(String str, String target, Boolean flag) {
jdbcTemplate.update("update account set money=money-500 where name=?", str);
if (flag) {
// 模拟异常
int i = 1 / 0;
}
jdbcTemplate.update("update account set money=money+500 where name=?", target);
}
}
package com.dfbz.test;
import com.dfbz.service.AccountService;
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.SpringRunner;
/**
* @author lscl
* @version 1.0
* @intro:
*/
@RunWith(SpringRunner.class)
@ContextConfiguration(locations = "classpath:spring2.xml")
public class Demo02 {
@Autowired
private AccountService accountService;
@Test
public void test1(){
accountService.transfer("a","b",false);
}
}