spring 声明式事务

97 阅读6分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第1天,点击查看活动详情

一、什么是事务

把一组业务当成一个业务来做;要么都成功,要么都失败,保证业务操作完整性的一种数据库机 制。比如常常被拿来举例的转账。
下面我们来看一下事务的相关核心内容,因为事务需要实现对数据库的操作,我们先使用spring jdbTemplate来进行测试,下面我们搭建一个spring jdbcTemplate环境。

二、spring jdbcTemplate的搭建

在spring中为了更加方便的操作JDBC,在JDBC的基础之上定义了一个抽象层,此设计的目的是为不同类型的JDBC操作提供模板方法,每个模板方法都能控制整个过程,并允许覆盖过程中的特定任务,通过这种方式,可以尽可能保留灵活性,将数据库存取的工作量降到最低。

2-1、添加相关maven的依赖

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.jony</groupId>
    <artifactId>spring_transaction</artifactId>
    <version>1.0-SNAPSHOT</version>

    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
    </properties>

    <dependencies>
        <!--spring -->
        <!-- https://mvnrepository.com/artifact/org.springframework/spring-context -->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context</artifactId>
            <version>5.2.19.RELEASE</version>
        </dependency>
        <!--junit 测试包-->
        <!-- https://mvnrepository.com/artifact/junit/junit -->
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.13.2</version>
            <scope>test</scope>
        </dependency>

        <!--阿里的数据库连接池-->
        <!-- https://mvnrepository.com/artifact/com.alibaba/druid -->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid</artifactId>
            <version>1.2.8</version>
        </dependency>

        <!--mysql 包-->
        <!-- https://mvnrepository.com/artifact/mysql/mysql-connector-java -->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>8.0.28</version>
        </dependency>

        <!--spring aop+transaction-->
        <!-- https://mvnrepository.com/artifact/org.springframework/spring-aop -->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-aop</artifactId>
            <version>5.3.16</version>
        </dependency>

        <!-- https://mvnrepository.com/artifact/org.aspectj/aspectjweaver -->
        <dependency>
            <groupId>org.aspectj</groupId>
            <artifactId>aspectjweaver</artifactId>
            <version>1.9.8</version>
            <scope>runtime</scope>
        </dependency>

        <!-- https://mvnrepository.com/artifact/org.springframework/spring-aspects -->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-aspects</artifactId>
            <version>5.3.1</version>
        </dependency>

        <!-- https://mvnrepository.com/artifact/org.springframework/spring-orm -->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-orm</artifactId>
            <version>5.3.16</version>
        </dependency>

    </dependencies>

</project>

2-2、mysql数据源配置

mysql.username=root
mysql.password=root
mysql.url=jdbc:mysql://localhost:3306/jony
mysql.driverClassName=com.mysql.jdbc.Driver

2-3、使用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:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd">

    <!--引入外部属性资源文件-->
    <context:property-placeholder location="db.properties"></context:property-placeholder>
    
    <!--配置第三方bean-->
    <bean class="com.alibaba.druid.pool.DruidDataSource" id="dataSource">
        <property name="username" value="${mysql.username}"></property>
        <property name="password" value="${mysql.password}"></property>
        <property name="url"  value="${mysql.url}"></property>
        <property name="driverClassName" value="${mysql.driverClassName}"></property>
    </bean>

</beans>

2-4、测试数据库连接

package com.jony.test;

import com.alibaba.druid.pool.DruidDataSource;
import org.junit.Before;
import org.junit.Test;
import org.springframework.context.support.ClassPathXmlApplicationContext;

import java.sql.SQLException;

public class MyTest {

    ClassPathXmlApplicationContext ioc;

    @Before
    public void init(){
        ioc=new ClassPathXmlApplicationContext("applicationContext.xml");
    }

    @Test
    public void test() throws SQLException {
        DruidDataSource dataSource = ioc.getBean("dataSource", DruidDataSource.class);
        System.out.println(dataSource);
        System.out.println(dataSource.getConnection());
    }
}

image.png

2-5、在配置文件中添加jdbcTemplate

<bean class="org.springframework.jdbc.core.JdbcTemplate" id="jdbcTemplate">
    <constructor-arg name="dataSource" ref="dataSource"></constructor-arg>
</bean>

2-5-1、测试jdbcTemplate

可以看到已经成功获取jdbcTemplate对象 image.png

三、使用jdbcTemplate CURD

3-1基础准备工作

3-1-1、创建MVC三层代码

image.png

3-2、将jdbcTemplate 注入到 UserDaoImpl中

image.png

3-2、保存

@Override
public void saveUser(User user) {
    String sql="insert into user(name,score) values (?,?)";
    jdbcTemplate.update(sql,user.getName(),user.getScore());
}

3-2-1、测试

@Test
public void test02(){
    User user=new User();
    user.setName("张三");
    user.setScore(100);
    UserService userService=ioc.getBean(UserService.class);
    userService.saveUser(user);
}

3-2-2、执行结果

image.png

3-3、查询某个值,并以对象返回

这个地方需要注意的是,如果对象字段名称和数据库字段名称一样,我们就可以使用new BeanPropertyRowMapper<>(User.class)来告诉jdbcTemplate 要返回什么类型,否则就只能挨个字段进行接收

//查询对象
    @Override
    public User getUser(int id) {
        String sql="select * from user where id=?";
        User user=jdbcTemplate.queryForObject(sql, new RowMapper<User>() {
            @Override
            public User mapRow(ResultSet rs, int rowNum) throws SQLException {
                User user=new User();
                user.setName(rs.getString("name"));
                user.setScore(rs.getInt("score"));
                return user;
            }
        },id);

        //如果数据库字段名称和对象名称一样,可以使用BeanPropertyRowMapper
//        User user = jdbcTemplate.queryForObject(sql, new BeanPropertyRowMapper<>(User.class),id);
        return user;
    }

3-3-1、测试

@Test
public void test03(){
    UserService userService=ioc.getBean(UserService.class);
    User user=userService.getUser(1);
    System.out.println(user);
}

3-3-2、执行结果

image.png

3-4、批量保存

//批量保存
public int[] saveBatch(List<User> userlist){
    String sql="insert into user (name,score) values(?,?)";
    List<Object[]> listObj=new ArrayList<>();
    for(User user:userlist){
        listObj.add(new Object[]{user.getName(),user.getScore()});
    }
    int[] result=jdbcTemplate.batchUpdate(sql,listObj);
    return result;
}

3-4-1、测试

//批量保存
@Test
public void test04(){
    UserService userService=ioc.getBean(UserService.class);
    List<User> userList=new ArrayList<>();
    userList.add(new User(2,"李四",99));
    userList.add(new User(3,"王五",80));
    userList.add(new User(4,"赵六",95));
    int[] ints = userService.saveBatch(userList);
    for(int i:ints){
        System.out.println(i);
    }
}

3-4-2、执行结果

image.png

image.png

3-5、查询对象集合

//根据条件批量查询对象
public List<User> getUserList(int score){
    String sql="select * from user where score>?";
    List<User> query = jdbcTemplate.query(sql, new BeanPropertyRowMapper<>(User.class), score);
    return query;
}

3-5-1、测试

//根据条件查询对象集合
@Test
public void getUserList(){
    UserService userService=ioc.getBean(UserService.class);
    List<User> userList = userService.getUserList(80);
    System.out.println(userList);
}

3-5-2、执行结果

image.png

3-6、查询函数

//查询最大值
public int getMaxScore(){
    String sql="select max(score) from user";
    return jdbcTemplate.queryForObject(sql,int.class);
}

3-6-1、测试

//查询最大分值
@Test
public void getMaxScore(){
    UserService userService=ioc.getBean(UserService.class);
    int maxScore = userService.getMaxScore();
    System.out.println(maxScore);
}

3-6-2、执行结果

image.png

3-7、删除

//删除对象
public int delUserById(int id){
    String sql="delete from user where id=?";
    int update = jdbcTemplate.update(sql, id);
    return update;
}

3-7-1、测试

//根据ID删除用户
@Test
public void deleUser(){
    UserService userService=ioc.getBean(UserService.class);
    int num=userService.delUserById(1);
    System.out.println(num);
}

3-7-2、执行结果

image.png

3-8、更新

//根据ID更新分值
public void  updateUserById(int id,int scoreNum){
    String sql="update user set score=score+? where id=?";
    jdbcTemplate.update(sql,scoreNum,id);
}

3-8-1、测试

@Test
public void updateUser(){
    UserService userService=ioc.getBean(UserService.class);
    userService.updateUserById(2,10);
}

四、声明事务

ACID 四大特性

1、原子性(Atomicity): 事务中所有操作是不可再分割的原子单元。事务中所有操作要么都执行成功,要么都执行失败。

2、一致性(Consistency): 事务执行后,数据库状态与其他业务规则保持一致。如转账业务,无论事务执行成功与否,参与转账的两个账户余额之和应该保持不变。

3、隔离性(Isolation): 隔离性是指在并发操作中,不同事务之间应该隔离开来,使每个并发中的事务不会互相干扰。

4、持久性(Durability): 一旦事务提交成功,事务中所有的数据操作都必须被持久化保存到数据库中,即使提交事务后,数据库崩溃,在数据库重启时,也必须能保证通过某种机制恢复数据。

在事务控制方面,主要有两大类: 编程式事务:我们需要在代码中添加一些事务相关代码,beginTransaction()、commit()、rollback()等事务相关方法。

声明式事务:在方法的外部添加注解或者直接在配置文件中定义,将事务管理代码从业务方法中分离出来,以声明的方式来实现事务管理。spring的AOP恰好可以完成此功能:事务管理代码的固定模式作为一种横切关注点,通过AOP方法模块化,进而实现声明式事务。

4-1、声明式事务的简单配置

Spring从不同的事务管理API中抽象出了一整套事务管理机制,让事务管理代码从特定的事务技术中独立出来。开发人员通过配置的方式进行事务管理,而不必了解其底层是如何实现的。

Spring的核心事务管理抽象是PlatformTransactionManager。它为事务管理封装了一组独立于技术的方法。无论使用Spring的哪种事务管理策略(编程式或声明式),事务管理器都是必须的。

事务管理器可以以普通的bean的形式声明在Spring IOC容器中。

4-1-1、配置文件中添加事务管理器

<!--事务控制-->
<!--配置事务管理器的bean-->
<bean class="org.springframework.jdbc.datasource.DataSourceTransactionManager" id="transactionManager">
    <property name="dataSource" ref="dataSource"></property>
</bean>
<tx:annotation-driven transaction-manager="transactionManager"></tx:annotation-driven>

配置了以上事务的管理,我们就可以使用@Transaction注解了。

4-4-2、@Transactional注解应该写在哪:

@Transactional 可以标记在类上面(当前类所有的方法都运用上了事务)
@Transactional 标记在方法则只是当前方法运用事务

也可以类和方法上面同时都存在, 如果类和方法都存在@Transactional会以方法的为准。

如果方法上面没有@Transactional会以类上面的为准

建议:@Transactional写在方法上面,控制粒度更细, 建议@Transactional写在业务逻

辑层上,因为只有业务逻辑层才会有嵌套调用的情况。

4-2、事务配置的属性

isolation:设置事务的隔离级别

propagation:事务的传播行为

noRollbackFor:那些异常事务可以不回滚

noRollbackForClassName:填写的参数是全类名

rollbackFor:哪些异常事务需要回滚

rollbackForClassName:填写的参数是全类名

readOnly:设置事务是否为只读事务

timeout:事务超出指定执行时长后自动终止并回滚,单位是秒

4-2-1、设置隔离级别(isolation)

在并发情况下,对同一个数据(变量、对象)进行读写操作才会产生并发问题。

4-2-1-1、脏读

未命名文件.jpg 如上图,最开始score=90,然后事务1,事务2,开始执行,事务2优先执行,将数据库score=100,这时候事务1开始查询得到的score是100,但是后面事务2进行了回滚,score又回归到最开始的90,那这时候事务1还以为score=100,这就是脏读。

4-2-1-1-1、解决方案

@Transactional(isolation = Isolation.READ_COMMITTED)
读已提交:READ_COMMITTED
要求Transaction01只能读取Transaction02已提交的修改。

REQUIRES_NEW 内外部方法需要在不同的类里面,内部的类声明REQUIRES_NEW,才可以成功执行,自己会成功提交,不管外部事务是否成功