事务管理
1.什么是事务呢?
事务指的是:在一组业务逻辑中,要么全部成功,要么全部失败。
2. 事务的特性又有那些呢?【ACID】
原子性【Atomicity】:在一组事务操作中,要么全都成功,要么全都失败,互相不可分割
一致性【Consistency】: 在事务发生的前后数据的完整性必须保存一致。
隔离性【Isolation】:事务被多个用户并发访问时,多个用户之间的数据要互相隔离
持久性【Durability】:事务一旦提交,对数据的修改将是持久的。
3. 并发访问会出现那些问题?【三种隔离问题】
------ 如果不考虑事务的隔离性,事务会出现并发访问问题
3.1 脏读
---- 含义: 一个事务读到了另一个事务没有提交是事务
详解:举例转账功能 --- 事务A、B各有10元
事务A说 : 给事务B转账2元 ----- 8 元
事务B说:好的我已经收到了 ----- 12元
-------------【可在A、B事务并发访问时,出现了脏读问题,事务B读到了事务A没有提交的事务】
结果:事务A和事务B的金额还是没有进行转账前的各自10元
-------------【脏读:读到了没有提交的事务,事务进行了回滚操作,恢复了原来的数据信息】
图集详解
3.2 不可重复读
----含义:一个事务读到了另一个事务已经提交的事务【update】,造成在一个事务中多次查询结果不一致
详解:
事务A进行查账100元,事务B从账号取出20元
事务A: 第一次查询到100快,并没有提交事务commit
事务B:从账户上取出20元,并对账户余额进行update修改
----------- 事务A第二次查询,查到了事务B已经提交的数据80元
----------- 两次查询结果不一样,发生并发访问问题,不可重复读
图集详解
3.3 虚读/幻读
--- 含义:一个事务读到了另一个事务已经提交的事务【insert】,在事务中多次查询结果不一致。(数据量不同)
详解
事务A进行数据查询功能,事务B进行insert添加功能
事务A: 第一次查询到3条,并没有提交事务commit
事务B:添加了一条数据
---------- 事务A第二次查询查询到了4条数据,查到了事务B已经提交的数据
---------- 两次查询结果不一样,发生并发访问问题,不可重复读
图集详解
4 那不可重复读【update】和幻读【insert】又有什么区别呢?
不可重复读是基于update修改发生的并发访问问题
幻读是基于insert添加,发生的并发访问问题
5 隔离级别有那些?
------ 为了解决并发访问问题,数据库规范规定了4种隔离级别,分别用于描述两个事务并发的所有情况
1)读未提交【read uncommitted】:一个事务读到了另一个事务没有提交的数据
存放:3个问题【脏读、不可重复读、虚读】
解决:0个问题
------- 效率最高,引发所有读问题,基本不设置
2)读已提交【read committed】: 一个事务读到了另一个事务已经提交的数据
存放:2个问题【不可重复读、虚读】
解决:1个问题【脏读】
------- 如果要效率,那么选择这个 read committed 读已提交
3)可重复读【repeatable read】: 在一个事务中读到的数据信息始终保持一致,无论另一个事务是否提交
存放:1个问题【虚读】
解决:2个问题【脏读、不可重复读】
------- 如果要安全,那么选择这个 repeatable read 可重复读
------ 还剩一个问题未解决,但是虚读的问题可以通过程序来规避
事务刚开启时,可以count(*)
事务要关闭时,可以count(*)
对比:如果两次数据一致,说明没有虚读
4)串行化【serializable】: 同时只能执行一个事务,相当事务中的单线程
存放:0个问题
解决:3个问题【脏读、不可重复读、虚读】
------- 没有效率,但安全性最高,基本不设置
5.1 事务性能和安全性的比较
性能:读未提交 > 读已提交 > 可重复读 > 串行化
安全:串行化 > 可重复读 > 读已提交 > 读未提交
5.2 常见数据库的默认隔离级别
MySql : 可重复读:安全,本身做的优化比较好
Oracle : 读已提交:效率高
6.事务注解关联Spring
相关注解
@EnableTransactionManagement //书写在SpringConfiguration配置类中
@Transactional //开启事务 书写在service接口实现类中
7.转账案例,事务应用
7.1图集导航
7.2 代码数据【可参考】
7.2.1 config 配置类
MyBatisConfiguration
package com.czxy.demo17_accountSW.config; import com.github.pagehelper.PageHelper; import org.apache.ibatis.plugin.Interceptor; import org.apache.ibatis.session.SqlSessionFactory; import org.mybatis.spring.SqlSessionFactoryBean; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import tk.mybatis.spring.mapper.MapperScannerConfigurer; import javax.sql.DataSource; import java.util.Properties; @Configuration public class MyBatisConfiguration { @Bean public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception { //1 创建工厂 // 1.通过工厂bean创建对象,最后需要调用 getObject()获得具体的对象 SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean(); //2 设置数据-- SqlMapConfig.xml 配置信息 // 1.1 设置数据源 factoryBean.setDataSource(dataSource); // 1.2 设置别名包扫描 factoryBean.setTypeAliasesPackage("com.czxy.demo17_accountSW.doamin"); // 1.3 全局配置:驼峰映射 org.apache.ibatis.session.Configuration config = new org.apache.ibatis.session.Configuration(); config.setMapUnderscoreToCamelCase(true); factoryBean.setConfiguration(config); // 2 插件配置 // 2.1 分页插件 PageHelper pageHelper = new PageHelper(); Properties pageProps = new Properties(); pageProps.setProperty("dialect", "mysql"); pageProps.setProperty("rowBoundsWithCount", "true"); pageHelper.setProperties(pageProps); factoryBean.setPlugins(new Interceptor[] { pageHelper }); // 返回SqlSessionFactory return factoryBean.getObject(); } /** * 扫描Dao的包,查找各种XxxMapper接口,创建好UserMapper等对象存入到IOC的容器中 * @return */ @Bean public MapperScannerConfigurer mapperScanner() { MapperScannerConfigurer configurer = new MapperScannerConfigurer(); configurer.setBasePackage("com.czxy.demo17_accountSW.mapper"); return configurer; } }
SpringConfiguration
package com.czxy.demo17_accountSW.config; import com.alibaba.druid.pool.DruidDataSource; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.PropertySource; import org.springframework.jdbc.datasource.DataSourceTransactionManager; import org.springframework.transaction.annotation.EnableTransactionManagement; import javax.sql.DataSource; @Configuration @PropertySource("classpath:db2.properties") @ComponentScan(basePackages = "com.czxy.demo17_accountSW") @EnableTransactionManagement public class SpringConfiguration { @Value("${jdbc.driver}") private String driver; @Value("${jdbc.url}") private String url ; @Value("${jdbc.username}") private String username; @Value("${jdbc.password}") private String password; @Bean public DataSource dataSource(){ DruidDataSource druidDataSource = new DruidDataSource(); druidDataSource.setDriverClassName(driver); druidDataSource.setUrl(url); druidDataSource.setUsername(username); druidDataSource.setPassword(password); return druidDataSource; } //事务管理器【管理事务,事务出现异常会进行回滚事务】 @Bean public DataSourceTransactionManager dataSourceTransactionManager(DataSource dataSource) { return new DataSourceTransactionManager(dataSource); } }
7.2.2 daomain
- Account
@Table(name = "account") public class Account { @Id private Integer id; private String name; private Float money; //注:构造和set/get方法省略 }
7.2.3 mapper
- AccountMapper
public interface AccountMapper extends Mapper<Account> { }
7.2.4 service
- AccountService接口
public interface AccountService { /** * 转账 * @param outId 汇款 * @param inId 收款 * @param money 金额 */ public void change(Integer outId,Integer inId,Float money); }
- AccountServiceImpl接口实现类
package com.czxy.demo17_accountSW.service.impl; import com.czxy.demo17_accountSW.doamin.Account; import com.czxy.demo17_accountSW.mapper.AccountMapper; import com.czxy.demo17_accountSW.service.AccountService; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import javax.annotation.Resource; @Service @Transactional //开启事务 public class AccountServiceImpl implements AccountService { @Resource private AccountMapper accountMapper; //转账 ---》 汇款、收款、金额 @Override /** * 转账 * @param outId 汇款 * @param inId 收款 * @param money 金额 */ public void change(Integer outId, Integer inId, Float money) { //(id查询、修改金额、更新) //根据汇款人id进行汇款,返回一个汇款人的对象信息 Account outAccount = accountMapper.selectByPrimaryKey(outId); //进行设置汇款人的剩余金额 = 汇款钱的金额 - 汇款的金额 outAccount.setMoney(outAccount.getMoney() - money); //进行更新设置剩余金额,根据id进行修改保存 accountMapper.updateByPrimaryKey(outAccount); //模拟错误异常,出错了, // 但是汇款已经完成,数据已经修改,但收款人这边由于报错没有收到汇款,那怎么办呢? // 不用担心事务注解@Transactional 就是为了防止该类事情发生而存在的。 // 开启事务,当报异常了,停止了业务的进行,那么事务就会执行回滚事务,恢复数据 int i = 1/0 ; //收款人 + 钱 //根据收款人id,获取收款人对象 Account inAccount = accountMapper.selectByPrimaryKey(inId); //对其收款人金额进行累加 = 原本金额+汇款金额 inAccount.setMoney(inAccount.getMoney() +money); //根据收款人id进行更新保存金额 accountMapper.updateByPrimaryKey(inAccount); } }
7.2.5 properties 文本
jdbc.driver=com.mysql.cj.jdbc.Driver jdbc.url=jdbc:mysql://localhost:3306/day12_02_affair jdbc.username=root jdbc.password=1234
7.2.6测试类
package com.czxy.demo17_accountSW; import com.czxy.demo17_accountSW.config.MyBatisConfiguration; import com.czxy.demo17_accountSW.config.SpringConfiguration; import com.czxy.demo17_accountSW.service.AccountService; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit4.SpringRunner; import javax.annotation.Resource; @RunWith(SpringRunner.class) @ContextConfiguration(classes = {SpringConfiguration.class, MyBatisConfiguration.class}) public class TestTx { @Resource private AccountService accountService; @Test public void testDemo(){ accountService.change(1,2,300f); } }
- 测试结果
- 测试前
- 测试后
8 .当测试时发生异常报错了怎么呢?在我已经汇款之后就出现系统异常,导致收款方没有收到款,怎么办呢?
//模拟错误异常,出错了, // 但是汇款已经完成,数据已经修改,但收款人这边由于报错没有收到汇款,那怎么办呢? // 不用担心事务注解@Transactional 就是为了防止该类事情发生而存在的。 // 开启事务,当报异常了,停止了业务的进行,那么事务就会执行回滚事务,恢复数据 //------ 这里已除0异常为例 int i = 1/0 ;
9.图集总结