什么是事务
通俗来讲事务就是在程序运行的时候把我们的sql一起打包,然后一起提交,它要么失败,要么成功
Spring的事务管理分类
1.编程式事务管理
编程式事务管理是通过编写代码来实现的,其中包括定义事务开始,正常事务时的提交和异常事务的回滚
2.声明式事务
声明式事务也可以通过在我们要执行的方法前加@Transactional来解决
@Transactional
public void myTransactionalMethod() {
// 事务操作
}
声明式事务是建立在AOP(控制反转上的),它的本质是对我们执行的方法(如增上改)的前后进行一个拦截,在执行方法之前开启一个事务,如果事务正常就进行提交,如果异常就进行事务的回滚
Spring中事务的隔离级别
1.default
default属于默认的隔离级别,一般式数据库的默认设置
2.read_uncommitted
read_uncommitted指读取了事务但是没有提交,它允许事务读取其他没有提交的事务所做的修改,它的隔离性最低,它会导致脏读,不可重复度,和幻读等问题
3.read_commtted
read_commtted表示读了并提交了,它可以保证一个事务不会读取到另外一个事务中没有提交的数据中去,它可以避免脏读问题,但不可重复读和幻读可能还会出现
4.repeatable_read
repeatable_read表示可以重复读,它可以保证同一个事务在进行多次读取时,读取的结果始终一致,可以避免脏读和不可重复读,但幻读还是会出现
5.serializable
serializable表示将事务串行化执行,完全保证事务的隔离性。可以避免脏读、不可重复读和幻读问题,但是会降低并发性能能,一般不会使用
什么是脏读
在一个事务中,读取到的数据是另一个事务的中间结果,这个中间结果可能最终并不会被提交(一个事务读取了另一个事务未提交的数据)
什么是幻读
当一个事务在读取一个范围内的数据时,另一个并发事务新增或删除了一些符合该范围条件的数据,导致第一个事务在后续读取时,发现有了额外的未预期的数据或者缺少了之前看到的数据。
案例解析
假设有两个并发事务正在操作一个学生表,事务A正在查询所有分数大于80分的学生记录,而事务B在这个时候插入了一个新的分数大于80分的学生记录。如果事务A继续查询,就会发现有了一个之前不存在的学生记录,就像"幻觉"一样出现了新的数据。
这是因为在默认的隔离级别下,数据库在事务中的查询操作仅仅锁住了查询的记录行,而没有锁住范围。因此,在事务A读取完初始数据后,事务B新增了数据,导致事务A再次读取时,出现了额外的数据
什么是不可重复读
不可重复读指的是在一个事务内,最开始读到的数据和事务结束前的任意时刻读到的同一批数据出现不一致的情况。
案例讲解:(转账案例)
首先我们先定义一个转账的方法,比如说A同志要转账给B同志500元,转账时进行一个事务提交时出现,A同志已经转出了500元,但B同志却没有收到500元,按正常的逻辑来讲A同志转出500元后,B同志的账户中应该在原有的余额基础上增加500元,这时候B同志却没有收到,如果我们开启了事务管理的话,在转账中,两者之间有一者出现事务异常的话,我们的事务管理就会对我们执行的方法进行一个事务回滚
代码展示
项目总体结构
数据库表设计
pojo实体类
pojo实体类 @Data注解可以自动帮我们生成setter getter方法 只要导入lombok的依赖即可
===========================================================================================
@Data
public class Account {
private Integer id;//转账人的id编号
private String name;//转账人姓名
private Integer money;//转账的金额
}
mapper接口包
定义一个AccountMapper接口
mapper接口 分别定义一个转钱方 和收取方的接口
===========================================================================================
public interface AccountMapper {
/**
* 加钱方
* @param name
* @param money
* @return
*/
Integer UpdateIn(@Param("name") String name,@Param("money") int money);
/**
* 转出方
* @param name
* @param money
* @return
*/
Integer UpdateOut(@Param("name") String name,@Param("money") int money);
}
service业务逻辑层
@Transactional可以用来声明事务
==========================================================================================
public interface AccountService {
/**
* 转账操作
* @param out 转出
* @param in 收取
* @param money 金额
*/
@Transactional
public void updateAccount(String out,String in,int money);
}
service接口的实现类 (在接口实现类中调用收取和转入的方法)
解析: @Autowired 表示自动装配 相当于我们在spring的配置文件中(applicationContext.xml)注入一个bean(依赖)
=========================================================================================
@Service("Account")
public class AccountServiceImpl implements AccountService{
@Autowired
private AccountMapper accountMapper;
@Override
public void updateAccount(String out, String in, int money) {
accountMapper.UpdateOut(out,money);//转出
accountMapper.UpdateIn(in,money);//收取
}
}
resource下的mapper映射文件
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"https://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="mapper.AccountMapper">
<!--收取方-->
<update id="UpdateIn">
UPDATE account set money = money + #{money} where name =#{name}
</update>
<!--转出方-->
<update id="UpdateOut">
UPDATE account set money = money - #{money} where name =#{name}
</update>
</mapper>
applicationContext.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:aop="http://www.springframework.org/schema/aop"
xmlns:tx="http://www.springframework.org/schema/tx"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:mvc="http://www.springframework.org/schema/mvc"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd
http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/cache http://www.springframework.org/schema/cache/spring-cache.xsd http://www.springframework.org/schema/mvc https://www.springframework.org/schema/mvc/spring-mvc.xsd">
<!-- 1加载db.properties -->
<context:property-placeholder location="classpath:jdbc.properties"/>
<!-- 2创建数据源 JNDI获取数据源(使用dbcp连接池) org.apache.commons.dbcp.BasicDataSource-->
<bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource">
<property name="driverClassName" value="${jdbc.driver}"/>
<property name="url" value="${jdbc.url}"/>
<property name="username" value="${jdbc.username}"/>
<property name="password" value="${jdbc.password}"/>
</bean>
<!-- 3创建SqlSessionFactory -->
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
<!-- 加载数据源 -->
<property name="dataSource" ref="dataSource"/>
<property name="configLocation" value="classpath:mybatis-config.xml"/>
<!-- 设置别名 -->
<!-- <property name="typeAliasesPackage" value="com.lzl.model"/>-->
</bean>
<!-- 4创建Mapper扫描器 -->
<bean id="mapperScannerConfigurer" class="org.mybatis.spring.mapper.MapperScannerConfigurer">
<property name="basePackage" value="mapper"/>
<property name="sqlSessionFactoryBeanName" value="sqlSessionFactory"/>
</bean>
<!--注解实现依赖注入 扫描包里面的注解-->
<context:component-scan base-package="mapper,service"/>
<!-- 4.1 配置事务管理器 -->
<bean id="txManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"></property>
</bean>
<!-- 编写通知:对事务进行增强(通知),需要编写对切点和具体执行事务细节
属性:id:唯一标识
transaction-manager:指定事务管理器 id,默认值就是 transactionManager-->
<tx:advice id="advice" transaction-manager="txManager">
<tx:attributes>
<!-- 事务配置
属性:name:对哪些方法起作用,例如:insert* 表示所有以 insert 开头的方法名称。 一般只需要对增、删、改方法添加事务
rollback-for:指定需要进行事务回滚的异常类,默认是 uncheck 异常 其它属性一般默认即可 -->
<tx:method name="insert*" rollback-for="java.lang.Exception"/>
<tx:method name="delete*" rollback-for="java.lang.Exception"/>
<tx:method name="update*" rollback-for="java.lang.Exception"/>
</tx:attributes>
</tx:advice>
<!-- 编写 aop,对目标生成代理,进行事务的通知 -->
<aop:config>
<!-- 配置切点表达式 -->
<aop:pointcut id="txPointcut" expression="execution (* service.AccountServiceImpl.*ServiceImpl.*(..))"/>
<!-- 将切点和事务的通知整合 -->
<aop:advisor advice-ref="advice" pointcut-ref="txPointcut"/>
</aop:config>
<!-- 开启事务的注解支持
transaction-manager 属性:指定事务管理器 id,默认值就是 transactionManager
-->
<tx:annotation-driven transaction-manager="txManager"/>
<!-- 自动扫描,将类自动注册为bean,@Controller注解在spring mvc中扫描 -->
<!--<context:component-scan base-package="controller">
<context:exclude-filter type="annotation" expression="org.springframework.stereotype.Controller"/>
</context:component-scan>-->
</beans>
注意事务通知一定要导入事务通知的相关属性
jdbc.properties连接数据库的配置
jdbc.driver=com.mysql.cj.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/ketai?serverTimezone=GMT-8&useUnicode=true&character=utf-8
jdbc.username=root
jdbc.password=root
log4j.properties 日志文件(可有可无)
### 设置###
log4j.rootLogger = debug,stdout,logfile,error
### 输出信息到控制抬 ###
log4j.appender.stdout = org.apache.log4j.ConsoleAppender
log4j.appender.stdout.Target = System.out
log4j.appender.stdout.layout = org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern = [%-5p] %d{yyyy-MM-dd HH:mm:ss,SSS} method:%l%n%m%n
### 输出DEBUG 级别以上的日志到=log.log ###
log4j.appender.logfile = org.apache.log4j.DailyRollingFileAppender
### 这里不写路径就是在当前目录下创建日志文件 ###
log4j.appender.logfile.File = logs/log.log
log4j.appender.logfile.Append = true
log4j.appender.logfile.Threshold = DEBUG
log4j.appender.logfile.layout = org.apache.log4j.PatternLayout
log4j.appender.logfile.layout.ConversionPattern = %-d{yyyy-MM-dd HH:mm:ss} [ %t:%r ] - [ %p ] %m%n
###单独保存ERROR级别以上的异常到=error.log ###
log4j.appender.error = org.apache.log4j.DailyRollingFileAppender
### 这里不写路径就是在当前目录下创建日志文件 ###
log4j.appender.error.File =error.log
log4j.appender.error.Append = true
log4j.appender.error.Threshold = ERROR
log4j.appender.error.layout = org.apache.log4j.PatternLayout
log4j.appender.error.layout.ConversionPattern = %-d{yyyy-MM-dd HH:mm:ss} [ %t:%r ] - [ %p ] %m%n
mybatis-config.xml sql映射文件
测试类
public class TestAccount {
/**
* 模拟转账
*/
@Test
public void accountTest(){
ApplicationContext context=new ClassPathXmlApplicationContext("ApplicationContext.xml");
AccountService service=(AccountService) context.getBean("Account");
service.updateAccount("nancy","李华",10);
}
}
没有测试前的数据
测试成功后(转账成功)
下面我们制造一个异常,遇到异常我们事务会自动帮我们进行一个回滚
结果
数据库数据没有被改变 因为它自动帮我们进行了事务的回滚