1.事务
事务(Transaction)是数据库管理系统中的一个重要概念,它表示一组不可分割的操作序列,这些操作要么全部执行成功,要么全部不执行,以确保数据库从一个一致性状态转换到另一个一致性状态。事务具有以下四个基本特性,通常被称为ACID特性.
2. ACID特性
原子性(Atomicity):事务中的所有操作要么全部完成,要么全部不完成,不会结束在中间某个点。
一致性(Consistency):事务必须使数据库从一个一致性状态转换到另一个一致性状态。也就是说,事务执行的结果必须符合所有预定义的规则和约束,例如数据的完整性和业务规则
比如:(转账:我有100 张三也有100,我和张三两个人的钱总数是200,这时张三转账给我50,张三只剩50,而我收款了有了150,那么我们两个人加起来的钱还是200,不管谁转给谁总数是不变的)
隔离性(Isolation):并发执行的事务之间不会互相影响。事务的隔离性描述了多个事务并发执行时,事务之间的相互影响程度。不同的数据库系统可能提供不同级别的隔离性,例如读未提交(Read
Uncommitted)、读已提交(Read Committed)、可重复读(Repeatable
Read)和串行化(Serializable)。
持久性(Durability):一旦事务被提交,它对数据库所做的更改就是永久性的。即使系统发生故障,事务的结果也不会丢失。
3. 事务注解@Transactional
事务注解标注的地方:
1.类:当把@Transactional 注解放在类上时,表示所有该类的public方法都配置相同的事务属性信息,会导致事务控制的粒度太大,注解参数无法根据每个类方法的实际需求设置;因此,一般@Transactional注解都会直接添加的需要的方法上;
2. 方法: 当类配置了@Transactional,方法也配置了@Transactional,方法的事务会覆盖类的事务配置信息;
3. 接口:不推荐这种使用方法,因为一旦标注在Interface上并且配置了Spring AOP 使用CGLib动态代理,将会导致@Transactional注解失效;
4. @Transactional注解两个属性
rollbackFor属性:rollbackFor属性指定遇到什么类型的异常就进行回滚,默认是RuntimeException
propagation属性:propagation属性用于设置事务的椽笔级别,默认的级别就是REQUIRED
5. 事务的传播级别
在Spring框架中,事务管理是通过声明式事务管理来实现的,它允许我们以声明的方式配置事务。Spring事务的传播级别(Propagation
Levels)定义了事务的边界和范围,即当一个事务方法被另一个事务方法调用时,事务如何被传播。Spring定义了7种事务传播级别,它们分别如下:
REQUIRED:表示当前方法必须在一个具有事务的上下文中运行,如有客户端有事务在进行,那么被调用端将在该事务中运行,否则的话重新开启一个事务。(如果被调用端发生异常,那么调用端和被调用端事务都将回滚)
SUPPORTS:表示当前方法不必需要具有一个事务上下文,但是如果有一个事务的话,它也可以在这个事务中运行,没有事务则已非事务执行,就是纯方法
MANDATORY:如果当前存在事务,则加入该事务;如果当前没有事务,就抛出异常。表示当前方法必须在一个事务中运行,如果没有事务,将抛出异常
REQUIRES_NEW:新建事务,如果当前存在事务,就把当前事务挂起。这是用于需要创建独立事务的方法。表示当前方法必须运行在它自己的事务中。一个新的事务将启动,而且如果有一个现有的事务在运行的话,则这个方法将在运行期被挂起,直到新的事务提交或者回滚才恢复执行。
NOT_SUPPORTED:以非事务方式执行,如果当前存在事务,就把当前事务挂起。
NEVER:以非事务方式执行,如果当前存在事务,就抛出异常。
NESTED:如果当前存在事务,则在嵌套事务内执行。如果当前没有事务,行为与REQUIRED相同。表示如果当前方法正有一个事务在运行中,则该方法应该运行在一个嵌套事务中,被嵌套的事务可以独立于被封装的事务中进行提交或者回滚。如果封装事务存在,并且外层事务抛出异常回滚,那么内层事务必须回滚,反之,内层事务并不影响外层事务。如果封装事务不存在,则同PROPAGATION_REQUIRED的一样
6. 事务流程图
Spring AOP将通用的功能横向抽取出来作为切面,避免非业务代码侵入到业务代码中;通过@Transactional注解就能让Spring为我们管理事务,免去了重复的事务管理逻辑,减少对业务代码的侵入,让开发人员能够专注于业务层面开发;【这里我们知道事务时基于spring中的AOP(切面)进行实现的】
7. 必要依赖
<dependency>
<!--mysql连接依赖-->
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<version>8.3.0</version>
</dependency>
<!-- druid连接池依赖 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.2.23</version>
</dependency>
<!--spring框架依赖-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>6.1.12</version>
</dependency>
<!--spring框架操作使用jdb操作数据库依赖(包括了事务(spring-tx)依赖)-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
<version>6.1.12</version>
</dependency>
<!--mybatis依赖-->
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.5.16</version>
</dependency>
<!-- spring整合mybatis插件依赖 -->
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis-spring</artifactId>
<version>3.0.4</version>
</dependency>
<!--lombox依赖-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.32</version>
</dependency>
8. 整合(druid数据源,mybatis,事务)
<?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:mybatis="http://mybatis.org/schema/mybatis-spring"
xmlns:context="http://www.springframework.org/schema/context" 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://mybatis.org/schema/mybatis-spring http://mybatis.org/schema/mybatis-spring.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd">
<!-- 扫描包 -->
<context:component-scan base-package="edu.nf.ch03"/>
<!-- 整合Druid数据源连接池 -->
<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource">
<!-- 注入相关的连接属性 -->
<property name="driverClassName" value="com.mysql.cj.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://localhost:3306/demo?useSSL=false&useUnicode=true&characterEncoding=utf-8&timeZone=Asia/Shanghai"/>
<property name="username" value="root"/>
<property name="password" value="1234"/>
<!-- 连接池的属性配置-->
<!-- 最大连接池数量-->
<property name="maxActive" value="200"/>
<!-- 初始化连接池的时候建立的连接个数-->
<property name="initialSize" value="10"/>
<!-- 连接池最小连接数-->
<property name="minIdle" value="10"/>
<!-- 获取连接的最大等待时间,单位:毫秒-->
<property name="maxWait" value="2000"/>
<!-- 检测连接是否有效-->
<property name="testWhileIdle" value="true"/>
<property name="testOnReturn" value="false"/>
<!-- 用一条伪sql来检查连接-->
<property name="validationQuery" value="select 1"/>
<!-- 是否缓存PreparedStatement,mysql中建议关闭-->
<property name="poolPreparedStatements" value="false"/>
</bean>
<!-- 整合mybatis, 目的就是将SqlSessionFactory纳入IOC容器,
SqlSessionFactoryBean实现了Spring的FactoryBean接口,这样spring
容器就可以通过接口的getObject方法得到SqlSessionFactory并纳入容器中
-->
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
<!-- 注入数据源连接池 -->
<property name="dataSource" ref="dataSource"/>
<!-- 实体类的别名-->
<property name="typeAliasesPackage" value="edu.nf.ch03.entity"/>
<!-- 指定mapper映射配置文件的路径-->
<property name="mapperLocations" value="classpath:mappers/*.xml"/>
</bean>
<!-- 指定扫描的dao接口,这样就会基于动态代理在运行时创建Dao接口的代理实现,
这个代理实现也会自动纳入Spring容器-->
<mybatis:scan base-package="edu.nf.ch03.dao"/>
<!-- 装配JDBC本地事务管理器 -->
<bean id="txManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<!-- 注入数据源 -->
<property name="dataSource" ref="dataSource"/>
</bean>
<!-- 启用事务注解处理器,transaction-manager引用JDBC事务管理器的id -->
<tx:annotation-driven transaction-manager="txManager"/>
</beans>
9. 创建账户实体类(Account)
Account类
/**
* 账户实体类
*/
@Data
public class Account {
// 账户id
private Integer cardId;
// 账户卡号
private String cardNum;
// 账户余额
private BigDecimal balance;
}
10. 创建转账业务的接口(TransferService),并创建对应的实现类
TransferService接口
public interface TransferService {
/**
* 检查账户余额
* @param fromUser
* @param money
*/
void check(Account fromUser, BigDecimal money);
/**
* 转账
* @param money 转账金额
* @param formUser 转账人
* @param toUser 接收人
*/
void transfer(BigDecimal money, Account formUser, Account toUser);
}
TransferServiceImpl实现类
/**
* @author xiruo
* 事务注解@Transactional可以声明在类上(类中的所有方法参与事务),
* 也可以声明在方法上(当前方法参与事务);
* rollbackFor属性指定遇到什么类型的异常就进行回滚,默认是RuntimeException
* propagation属性用于设置事务的级别,默认的级别就是REQUIRED
*/
@Service("transferService")
@RequiredArgsConstructor
@Transactional(rollbackFor = RuntimeException.class,
propagation = Propagation.REQUIRED)
public class TransferServiceImpl implements TransferService {
private final AccountDao dao;
// readOnly为true时,该方法为只读事务,引文查询不需要回滚,只要查询即可
// 方法和类都标注@Transactional注解时,方法会覆盖类的事务
@Transactional(readOnly = true)
@Override
public void check(Account fromUser, BigDecimal money) {
//1. 获取转账人信息
fromUser = dao.getOverByAccount(fromUser.getCardNum());
//2. 检查转账人余额
if(fromUser.getBalance().compareTo(money) < 0) {
throw new RuntimeException("对不起,您的余额不足");
}
}
@Override
public void transfer(BigDecimal money, Account formUser, Account toUser) {
//1. 获取转账人信息
formUser = dao.getOverByAccount(formUser.getCardNum());
//2. 获取接收人信息
toUser = dao.getOverByAccount(toUser.getCardNum());
//3. 转账人扣除转账金额
formUser.setBalance(formUser.getBalance().subtract(money));
dao.updateAccount(formUser);
//4. 接收人添加转账金额
toUser.setBalance(toUser.getBalance().add(money));
dao.updateAccount(toUser);
//引发异常
//System.out.println(10/0);
}
}
11. 创建AccountDao接口,以及对应的mapper.xml文件(AccountMapeer)
AccountDao接口
public interface AccountDao {
/**
* 查询账户余额
* @param cardNum 卡号
* @return 账户信息
*/
Account getOverByAccount(String cardNum);
/**
* 更新账户
* @param account 账户信息
*/
void updateAccount(Account account);
}
AccountMapeer
<?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="edu.nf.ch03.dao.AccountDao">
<resultMap id="accountMap" type="edu.nf.ch03.entity.Account">
<id property="cardId" column="card_id"/>
<result property="cardNum" column="card_num"/>
<result property="balance" column="balance"/>
</resultMap>
<select id="getOverByAccount" resultMap="accountMap">
select card_id, card_num, balance from account_info where card_num = #{cardNum}
</select>
<update id="updateAccount">
update account_info set balance = #{balance} where card_num = #{cardNum}
</update>
</mapper>