本篇文章是根据B站狂神说Java系列的Spring5的笔记记录 —— 第五篇Spring5事务管理入门
gitee:https://gitee.com/ywq869819435/spring5
事务概述
- 一组操作,要么都成功要么都失败
- 事务在项目开发中十分重要,涉及到数据的一致性问题
- 确保完整性和一致性
- ACID原则:
- 原子性
- 一致性
- 隔离性
- 多个业务操作同一个资源的时候,防止数据损坏
- 持久性
- 事务一旦提交,无论系统发送什么问题,结果都不会被影响
- 持久化写入存储器中(一般指数据库)
Spring事务管理
分为编程式事务和声名式事务
声名式事务
- 利用Spring的AOP进去横切
事物的传播性
Spring事物在运用过程总存在事物嵌套的情况,根据不同的业务场景Spring提供了7种值来处理,前三个都保证同一事物内,后四个保证不在同一事物中,一下所有示例中都为
// 是否存在事物A???
A(){
B();
}
@Transactional(propagation=...)
B(){
//
}
- PROPAGATION_REQUIRED
- Spring的默认传播级别
- 支持当前事物,如果没有当前事物则新建一个事物
- 示例中若…表示PROPAGATION_REQUIRED,分为两种情况
- 方法A有创建事物A时,事物B加入事物A,也就是一起成功或失败
- 方法A没有创建事物A时,新建一个事物A,然后再事物B加入事物A
- 适用于整个业务逻辑不允许出现任何失败步骤,且外层方法运行不定义事物
- PROPAGATION_SUPPORTS
- 支持当前事物,如果没有当前事物就按没有事物执行
- 示例中若…表示PROPAGATION_SUPPORTS,分为两种情况
- 方法A有创建事物A时,事物B加入事物A,也就是一起成功或失败
- 方法A没有创建事物A时,方法A的执行不影响事物B的执行,而事物B整个事物执行是否成功还跟采用的数据源的是否默认自动提交(defaultAutoCommit,这个值是可以自定义的,其粒度是当次发送给数据库执行的SQL)有关,若是true,则会在B方法中在遇到异常之前的都执行成功,若是false则遇到异常之前的都会失败
- 适用于整个业务逻辑是否需要保持同步失败或成功由外层方法是否有事物决定
- PROPAGATION_MANDATORY
- 支持当前事物,当前没有事物就抛出异常
- 示例中若…表示PROPAGATION_MANDATORY,分为两种情况
- 方法A有创建事物A时,事物B加入事物A,也就是一起成功或失败
- 方法A没有创建事物A时,方法B直接抛出异常【方法A若有多个方法调用了则根据defaultAutoCommit可能会有不同的结果,实践出真理】
- 适用于整个业务逻辑必须保持同步失败或成功,且外层方法也要严格设定好
- PROPAGATN_REQUIRES_NEW
- 新建一个独立事物,若有当前事物,则当前事物挂起
- 示例中若…表示PROPAGATN_REQUIRES_NEW,分为两种情况
- 方法A有创建事物A时,事物A被挂起,等待事物B执行完成之后继续事物A,其中若是事物A失败回滚了,不允许事物B,若是事物B失败回滚了,方法A中有捕获异常,则事物A不受影响,若是没有捕获异常则A也会回滚
- 方法A没有创建事物A时,事物B独立执行
- 适用于业务逻辑中某个内调的方法逻辑不能影响其他的方法运行的场景
- PROPAGATION_NOT_SUPPORTED
- 不支持事物,若有当前事物,则当前事物挂起
- 示例中若…表示PROPAGATION_NOT_SUPPORTED,分为两种情况
- 方法A有创建事物A时,事物A被挂起,等待方法B执行完成,之后继续事物A,此时方法B没有没有被Spring事物管理,所以其sql的是否能提交依赖于defaultAutoCommit属性值同时方法A是否捕获方法B的异常也会对事物A是否回滚产生影响
- 方法A没有创建事物A时,方法B正常运行
- 适用于业务逻辑中某个内调的方法逻辑不需要支持事物
- PROPAGATION_NEVER
- 不支持事物,若有当前事物,直接抛出异常
- 示例中若…表示PROPAGATION_NEVER,分为两种情况
- 方法A有创建事物A时,方法B抛出异常,若方法A没有捕获则直接事物A回滚,若有捕获则事物A正常运行
- 方法A没有创建事物A时,方法A和B常规运行
- 适用于业务逻辑中某个内调的方法逻辑不需要事物管理【因为事物管理消耗资源,但实际又不要用到】
- PROPAGATION_NESTED
- 无论有没有当前事物,都新建独立事物(内嵌事物)执行,内嵌事物不直接影响外部,外部可以直接影响内部【如果当前存在事务,Save Point技术把当前事务状态进行保存,然后底层共用一个连接,当NESTED内部出错的时候,自行回滚到 Save Point这个状态,只要外部捕获到了异常,就可以继续进行外部的事务提交,而不会受到内嵌业务的干扰,但是,如果外部事务抛出了异常,整个内外所有事务都会回滚】
- 示例中若…表示PROPAGATION_NESTED,分为两种情况
- 方法A有创建事物A时,方法B抛出异常,若方法A没有捕获则直接事物A和B回滚,若有捕获则事物A正常运行
- 方法A有创建事物A时,方法A抛出异常,则直接事物A和B回滚
- 方法A没有创建事物A时,新建事物B
- 适用于业务逻辑中某个内调的方法逻辑会因外部方法失效而失去意义的场景
事物的隔离性
相比MySQL,Spring设置隔离级别就多了一个,采用数据库默认隔离级别,Spring改变隔离性是在开启事物时,对Connection[建立连接]的隔离级别进行设置的,所以主要还是根据数据库底层实现隔离性,参考我之前的笔记:https://blog.csdn.net/weixin_43864861/article/details/117670098
隔离级别 | 描述 |
---|---|
ISOLATION_DEFAULT | Spring的默认隔离级别,采用数据源的默认隔离级别(MySQL是REPEATABLE_READ) |
ISOLATION_READ_UNCOMMITTED | 读未提交,允许读取未提交的事物,最低的隔离级别 |
ISOLATION_READ_COMMITTED | 读已提交,只允许读已提交的事物,解决脏读 |
ISOLATION_REPEATABLE_READ | 可重复读,MVCC解决不可重读和部分幻读,解决脏读,不可重复读 |
ISOLATION_SERIALIZABLE | 串行化,事物不并发执行,解决脏读,不可重复读,幻读 |
示例
配置事务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"
xsi:schemaLocation="http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/aop
https://www.springframework.org/schema/aop/spring-aop.xsd
http://www.springframework.org/schema/tx
https://www.springframework.org/schema/tx/spring-tx.xsd">
<!-- DataSource数据源:使用spring的数据源替代Mybatis的配置
(除了使用Spring之外还有c3p0、dbcp、druid)
这里使用Spring提供的jdbc类,注入ioc中-->
<bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
<property name="driverClassName" value="com.mysql.cj.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://localhost:3306/mybatis?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=Asia/Shanghai&autoReconnect=true&failOverReadOnly=false"/>
<property name="username" value="root"/>
<property name="password" value="123456"/>
</bean>
<!-- sqlSessionFactory(链接sql的工厂) -->
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
<property name="dataSource" ref="dataSource"/>
<!-- 绑定Mybatis配置文件
(也通过在这里将配置文件里有的属性配置好,然后完全省略掉) -->
<property name="configLocation" value="classpath:mybatis-config.xml"/>
<!-- 绑定mapper资源路径,*表示任意文件名称 -->
<property name="mapperLocations" value="classpath:mapper/*.xml"/>
</bean>
<!--注入sqlSession到ioc中,采用自带的sqlSessionTemplate模板类创建
sqlSession:Java的mapper去获取到数据库的结果数据的缓存类
方式2中这钟方式是不需要的-->
<bean id="sqlSession" class="org.mybatis.spring.SqlSessionTemplate">
<!--源码中表示只能使用构造器注入,源码中没有set-->
<constructor-arg index="0" ref="sqlSessionFactory"/>
</bean>
<!--配置声名式事务-->
<bean id="transactionManger" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"/>
</bean>
<!--结合AOP实现事务的植入-->
<!-- 1.配置事务通知 -->
<tx:advice id="txAdvice" transaction-manager="transactionManger">
<!--指定那些方法需要植入[扫描方法开头含有这些]-->
<!--配置事务的传播性 propagation 默认为Required(如果当前没有事务就创建事务,有就用)-->
<tx:attributes>
<tx:method name="add" propagation="REQUIRED"/>
<tx:method name="delete" propagation="REQUIRED"/>
<tx:method name="update" propagation="REQUIRED"/>
<!-- read-only:只读模式,不进行任何干预 -->
<tx:method name="query" read-only="true"/>
<tx:method name="*" propagation="REQUIRED"/>
</tx:attributes>
</tx:advice>
<!-- 2.配置事务切入 -->
<aop:config>
<!-- 切入点(多路径用|| 分隔) -->
<aop:pointcut id="txPointCut" expression="execution(* com.yang.dao.*.*(..))"/>
<!-- 切入通知 -->
<aop:advisor advice-ref="txAdvice" pointcut-ref="txPointCut"/>
</aop:config>
</beans>
对应的mapper
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.yang.dao.UserMapper">
<select id="queryUser" resultType="com.yang.entity.User">
SELECT * FROM mybatis.user;
</select>
<insert id="addUser" parameterType="com.yang.entity.User">
INSERT INTO mybatis.user (id, name, password)
VALUE (#{id}, #{name}, #{password})
</insert>
<!-- DELETES故意写错用于测试 -->
<delete id="deleteUser" parameterType="integer">
DELETES FROM mybaits.user WHERE id = #{id}
</delete>
</mapper>
测试
public class UserMapperImpl extends SqlSessionDaoSupport implements UserMapper {
@Override
public List<User> queryUser(){
UserMapper mapper = getSqlSession().getMapper(UserMapper.class);
return mapper.queryUser();
}
@Override
public Integer addUser(User user) {
return getSqlSession().getMapper(UserMapper.class).addUser(user);
}
@Override
public Integer deleteUser(Integer id) {
return getSqlSession().getMapper(UserMapper.class).deleteUser(id);
}
@Override
public void testPointCut(){
UserMapper userMapper = getSqlSession().getMapper(UserMapper.class);
List<User> userList = userMapper.queryUser();
userList.forEach(System.out::println);
User user = new User(4,"xiao","123456");
userMapper.addUser(user);
userMapper.deleteUser(4);
userList = userMapper.queryUser();
userList.forEach(System.out::println);
}
}
@Test
public void userTest(){
ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
UserMapper userMapper = context.getBean("userMapper", UserMapper.class);
userMapper.testPointCut();
}
编程式事务
- 侵入性事务管理,直接通过代码的改变实现,比如用try-catch去做尝试,出现失败的时候就会回滚
- 使用
TransactionTemplate
或者直接使用PlatformTransactionManager
,对于编程式事务管理,Spring推荐使用TransactionTemplate
- 当处理的业务复杂起来时候,代码的重复性将会使开发人员十分痛苦
- 编程式事物的优势在于其粒度可以是代码块,而不像声明式事物最低是方法级,但是一般设计好的代码逻辑,应该是保证方法的原子性的,所以这里不做多解释
为什么需要事务
- 如果没有,可能存在数据提交不一致的情况下
- 如果不在Spring中配置声名式事务,就需要在代码中手动配置事务,保证事务安全性
- 事务在项目的开发中很重要,涉及到数据的一致性和完整性
JavaConfig配置Mybatis
Config
ApplicationConfig
@Configuration
/** 扫描Bean并注入 */
@ComponentScan({"com.yang.dao","com.yang.service"})
@Import(value = SpringDaoConfig.class)
/** 开启事务支持 */
@EnableTransactionManagement
public class ApplicationConfig {
}
SpringDaoConfig
@Configuration
public class SpringDaoConfig {
/**
* 配置数据源
* @return
*/
@Bean
public DriverManagerDataSource getDataSource(){
// 返回要注入bean的对象
DriverManagerDataSource dataSource = new DriverManagerDataSource();
dataSource.setDriverClassName("com.mysql.cj.jdbc.Driver");
dataSource.setUrl("jdbc:mysql://localhost:3306/mybatis?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=Asia/Shanghai&autoReconnect=true&failOverReadOnly=false");
dataSource.setUsername("root");
dataSource.setPassword("123456");
return dataSource;
}
/**
* 配置sqlSessionFactory(链接sql的工厂)
* @return
* @throws Exception
*/
@Bean
public SqlSessionFactory getSqlSessionFactory() throws Exception {
SqlSessionFactoryBean sqlSessionFactory = new SqlSessionFactoryBean();
sqlSessionFactory.setDataSource(this.getDataSource());
PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
try {
sqlSessionFactory.setConfigLocation(resolver.getResource("classpath:mybatis-config.xml"));
// @MapperScan(basePackages = { "com.yang.dao" })可以在配置文件开头写入这代替下面的扫描
sqlSessionFactory.setMapperLocations(resolver.getResource("classpath:mapper/UserMapper.xml"));
sqlSessionFactory.setTypeAliasesPackage("mapper");
return sqlSessionFactory.getObject();
} catch (Exception e) {
e.printStackTrace();
throw e;
}
}
/**
* 注入sqlSession到Spring
* @return
* @throws Exception
*/
@Bean
public SqlSessionTemplate getSqlSession() throws Exception {
try {
return new SqlSessionTemplate(this.getSqlSessionFactory());
} catch (Exception e) {
e.printStackTrace();
throw e;
}
}
/**
* 声名式事务(mybatis-spring会自动植入切点)
* @return
*/
@Bean
public DataSourceTransactionManager transactionManger(){
return new DataSourceTransactionManager(this.getDataSource());
}
@Bean
public UserMapper userMapper(@NotNull SqlSessionTemplate sqlSessionTemplate){
return sqlSessionTemplate.getMapper(UserMapper.class);
}
}
mybatis-config
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<!--核心配置文件-->
<configuration>
<!-- 别名管理:
指定的路径下的会自己动取别名,采用实体类的名称代替全路径 -->
<typeAliases>
<package name="com.yang.entity"/>
</typeAliases>
</configuration>
Mapper
@Mapper
public interface UserMapper {
/**
* 查询用户信息
* @return
*/
List<User> queryUser();
Integer addUser(User user);
Integer deleteUser(Integer id);
}
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.yang.dao.UserMapper">
<select id="queryUser" resultType="com.yang.entity.User">
SELECT * FROM mybatis.user;
</select>
<insert id="addUser" parameterType="com.yang.entity.User">
INSERT INTO mybatis.user (id, name, password)
VALUE (#{id}, #{name}, #{password})
</insert>
<delete id="deleteUser" parameterType="integer">
DELETES FROM mybaits.user WHERE id = #{id}
</delete>
</mapper>
Service
public interface UserService {
/**
* 查询用户信息
* @return
*/
List<User> queryUser();
Integer addUser(User user);
Integer deleteUser(Integer id);
void testPointCut();
}
@Service
public class UserServiceImpl implements UserService {
@Autowired
private UserMapper userMapper;
@Override
public List<User> queryUser(){
return userMapper.queryUser();
}
@Override
public Integer addUser(User user) {
return userMapper.addUser(user);
}
@Override
public Integer deleteUser(Integer id) {
return userMapper.deleteUser(id);
}
@Override
@Transactional(rollbackFor = Exception.class)
/**
* @Transactional
* * 开启注解事务配置,一个 @Transactional只能对当前类有效,当前类下面所有方法配置一样
* * isolation=Isolation.REPEATABLE_READ, 隔离级别
* propagation=Propagation.REQUIRED, 传播行为(规则)
* timeout=5, 超时时间
* readOnly=true 是否是只读事务
*/
public void testPointCut(){
List<User> userList = userMapper.queryUser();
userList.forEach(System.out::println);
User user = new User(4,"xiao","123456");
userMapper.addUser(user);
userMapper.deleteUser(4);
userList = userMapper.queryUser();
userList.forEach(System.out::println);
}
}
测试类
public class MyTest {
@Test
public void userTest(){
AnnotationConfigApplicationContext config = new AnnotationConfigApplicationContext(ApplicationConfig.class);
UserMapper userMapper = config.getBean(UserMapper.class);
UserService userService = config.getBean(UserService.class);
// List<User> userList = userMapper.queryUser();
// userList.forEach(System.out::println);
// User user = new User(4,"xiao","123456");
// userMapper.addUser(user);
// userMapper.deleteUser(4);
// userList = userMapper.queryUser();
// userList.forEach(System.out::println);
// List<User> userList = userMapper.queryUser();
// userList.forEach(System.out::println);
userService.testPointCut();
}
}