一、什么是事务
事务是一组数据库操作,这些操作要么全部成功,要么全部失败并回滚。
在一个业务流程中,通常需要多条DML(insert、delete、update)语句共同联合才能完成。这些语句必须同时成功或者同时失败。
二、事务的四大特性
- 原子性:事务是最小不可分割单元,全部成功,或者全部回滚。
- 一致性:事务前后数据状态合法(转账总金额不变)。
- 隔离性:多个事务并发执行,互相隔离。
持久性:事务提交成功,数据库永久落库,宕机也不丢失。
三、事务的四个处理过程
- 开启事务
- 执行核心业务代码
- 提交事务(核心业务过程中没有出现异常才能提交事务)
- 回滚事务(核心业务处理过程中出现异常就回滚事务)
四、事务并发带来的问题
- 脏读:读到别的事务未提交的数据,例如:
- 不可重复读:同一个事务内,两次读同一个数据,中间被别的事务修改提交,两次结果不一样。
- 幻读:同一个事务,两次查询,别的事务新增/删除数据,行数变了。例如:
五、事务的隔离级别(解决并发问题)
六、事务属性
Propagation propagation() default Propagation.REQUIRED;--用来设置事务的传播特性
Isolation isolation() default Isolation.DEFAULT;--设置事务的隔离级别
int timeout() default -1;--设置事务的超时时间
boolean readOnly() default false;--设置只读事务
Class<? extends Throwable>[] rollbackFor() default {};--指定哪些异常时,回滚
Class<? extends Throwable>[] noRollbackFor() default {};--指定某个异常时不回滚
6.1 事务传播行为
什么是事务的传播行为?
在某个类中,有两个方法,a方法和b方法,连个方法上都有事务,在a方法中调用b方法,事务是如何传递的?是合并到一个事务?还是开启一个新的事务?这就是事务的传播行为。
REQUIRED(默认)有事务,就加入当前事务;没有,新建事务。共用同一个事务,一起提交、一起回滚。👉 绝大多数业务使用
SUPPORTS有事务就加入;没有事务,就非事务运行。适合查询、只读方法。
MANDATORY必须在已有事务里执行;外面没有事务 → 直接抛异常。强制上层开启事务。
REQUIRES_NEW一定新建独立事务;外层如果有事务,先把外层事务挂起。两个事务完全独立,互不影响。、
NOT_SUPPORTED以非事务方式执行;外层有事务,就把外层事务挂起。不参与事务。
NEVER必须无事务运行;外层存在事务 → 抛出异常。禁止在事务内调用。
NESTED 嵌套事务外层有事务:开启嵌套子事务,基于数据库savepoint保存点; 内层子事务回滚,只回滚到保存点,不影响外层; 外层事务回滚,嵌套子事务跟着一起回滚。 外层没有事务:行为等价于 REQUIRED。
在代码中设置事务的传播行为,代码如下:
@Transactional(propagation = Propagation.REQUIRED)七、Spirng实现事务的两种方式
- 编程式事务(这里不讲,用的少)
- 声明式事务(重点)
- 基于注解的方式(重点)
- 基于XML的方式(目前不做讲解)
基于注解的方式,测试传播行为
代码如下:
目录结构如下:
两个类MyService1和MyService2类:
@Service public class MyService1 { @Autowired private AccountMapper accountMapper; @Autowired private MyService2 myService2; @Transactional(propagation = Propagation.REQUIRED) public void save(Account account) { accountMapper.insert(account); try { Account act = new Account(); act.setActNo("act-004"); act.setBalance(new BigDecimal(200)); myService2.save(act); } catch (Exception e) { } } }@Service public class MyService2 { @Autowired private AccountMapper accountMapper; @Transactional(propagation = Propagation.REQUIRED) public void save(Account account) { accountMapper.insert(account); //模拟异常 // System.out.println(10/0); } }SpringMybatisConfig代码如下
@Configuration //组件扫描 @ComponentScan("com.cjs.bank") @EnableAspectJAutoProxy(proxyTargetClass = true) //引入外部属性配置文件 @PropertySource("classpath:application.properties") //Mapper扫描器 @MapperScan(basePackages = {"com.cjs.bank.mapper"}) //开启事务注解支持 @EnableTransactionManagement public class SpringMybatisConfig { //配置数据源 @Bean public DataSource dataSource(@Value("${mybatis.data.source.driver}") String driverClassName, @Value("${mybatis.data.source.url}") String url, @Value("${mybatis.data.source.user}") String name, @Value("${mybatis.data.source.password}") String password ) { HikariDataSource dataSource = new HikariDataSource(); dataSource.setDriverClassName(driverClassName); dataSource.setJdbcUrl(url); dataSource.setUsername(name); dataSource.setPassword(password); return dataSource; } //注意:方法参数dataSource Spring IoC容器会自动注入 //配置SqlSessinFactioryBean @Bean public SqlSessionFactoryBean sqlSessionFactoryBean(@Value("${mybatis.config.location}") String configLocatin,DataSource dataSource, @Value("${mybatis.type.aliases.package}") String typeAliasesPackage ) { SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean(); // Resource resource = new ClassPathResource(configLocatin); sqlSessionFactoryBean.setConfigLocation(new ClassPathResource(configLocatin)); sqlSessionFactoryBean.setDataSource(dataSource); sqlSessionFactoryBean.setTypeAliasesPackage(typeAliasesPackage); //指定Mapper XML文件位置 try { sqlSessionFactoryBean.setMapperLocations( new PathMatchingResourcePatternResolver().getResources("classpath*:com/cjs/bank/mapper/*.xml")); } catch (java.io.IOException e) { throw new RuntimeException("加载Mapper XML失败", e); } return sqlSessionFactoryBean; //返回IoC容器管理 } //配置事务管理器,还需要开启事务注解,这样@Transactional才可以使用,因此还需要配置一个开关 @Bean public DataSourceTransactionManager transactinManage(DataSource dataSource) { DataSourceTransactionManager dataSourceTransactionManager = new DataSourceTransactionManager(); dataSourceTransactionManager.setDataSource(dataSource); return dataSourceTransactionManager; } }Account代码如下:
@Data public class Account { private Long id; private String actNo; private BigDecimal balance; }AccountMapper代码如下:
public interface AccountMapper { /** * 根据账号查询账号信息 * @param actNo 账号 * @return 账号信息 */ Account selectByActNo(String actNo); /** * 根据账号的余额 * @param account 账号对象 * @return 1:表示成功,其他值表示失败 */ int updateBelanceByActNo(Account account); /** * 保存账号信息 * @param account * @return */ int insert(Account account); }AccountMapper.XML关键代码如下:
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="com.cjs.bank.mapper.AccountMapper"> <insert id="insert"> insert into t_act(act_no,balance) values(#{actNo},#{balance}) </insert> </mapper>单元测试代码如下:
@Test public void test2(){ ApplicationContext act = new AnnotationConfigApplicationContext(SpringMybatisConfig.class); MyService1 myService1 = act.getBean(MyService1.class); Account account = new Account(); account.setActNo("act-003"); account.setBalance(new BigDecimal(200)); myService1.save(account); }数据库图片:
程序分析:
以上代码主要是测试事务的传播行为,有的代码多余,是其他公共,不影响此处的操作。数据库图片为结构图。
操作步骤:修改在MyService1和MyService2中的关键代码
@Transactional(propagation = Propagation.REQUIRED)
会有不同的结果。