Spring 事务--如何在开发中熟练使用事务

Spring IoC和AOP看这里:Spring IoC和AOP–基于SpringBoot AOP开发

事务简介

  在JavaEE企业及开发的应用中,为了保证数据的完整性和一致性,必须引入数据库事务的概念,所以事务管理是企业级应用程序开发中必不可少的一部分。事务就是一组由于逻辑紧密关联而合并成一个整体(工作单元)的多个数据库操作,这些操作要么都执行,要么都不执行。

事务特性(ACID):

  • 原子性(Atomicity):”原子“的本意为”不可分割“,事务中的原子性就是一个事务中涉及多个操作在逻辑上缺一不可。事务的原子性要求事务中的操作要么都执行,要么都不执行。
  • 一致性(Consistency):一致性是指数据一致,所有数据都满足业务规则的一致性状态。不管事务中有多少操作都必须保证执行前数据都是正确的,执行后数据仍是正确的。如果事务执行的过程中某一个操作失败了,必须将其他所有操作撤销,将数据恢复到事务执行之前的状态,这就是回滚。
  • 隔离性(Isolation):在应用程序中事务往往是并发的执行的,所有很有可能有多个事务处理相同的数据,因此每个事务都应该与其他事务隔离开来,防止数据损坏。事务的隔离性要求多个事务在并发过程中不会相互干扰。
  • 持久性(Durability):持久性要求事务执行完成后,对数据的修改要永久保存,不会因为各种系统错误或者其他意外情况而受到影响,事务对数据的修改通常应该被写入到持久化的存储器中。

Spring事务

  Spring支持编程式事务管理和声明式事务管理两种方式。

  编程式事务管理:是侵入性事务管理,使用TransactionTemplate或者直接使用PlatformTransactionManager。

  声明式事务管理:是基于AOP在方法前后对其拦截,在目标方法执行之前创建事务,并根据目标方法执行情况提交或者回滚。

  编程式事务每次都要单独实现,在业务功能大的情况下使用很头疼;而声明式事务不同,它属于无侵入式,不会影响业务代码,只需要配置相关事务声明或者通过注解的方式,便可将事务应用在业务代码中。

基于SpringBoot注解事务示例

  这里的演示基于SpringBoot,持久层使用MyBatis:

  新建maven工程,pom.xml加入以下依赖,为了方便测试我们也引入test启动器:

<?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>org.example</groupId>
    <artifactId>test-aspect</artifactId>
    <version>1.0-SNAPSHOT</version>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.2.7.RELEASE</version>
    </parent>
    <dependencies>
        <!-- mybatis依赖 -->
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>1.3.2</version>
        </dependency>
        <!-- mysql依赖 -->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <scope>runtime</scope>
        </dependency>
        <!-- 测试依赖 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>
</project>

  创建启动类Application.java:

@SpringBootApplication
@MapperScan("com.**.mapper*") //扫描mapper接口
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class,args);
    }
}

  配置文件application.yml(数据库、用户和密码改成自己的):

spring:
  datasource:
    url: jdbc:mysql://localhost/test?characterEncoding=UTF-8&useUnicode=true&useSSL=false&autoReconnect=true&serverTimezone=UCT
    username: root
    password: root
    driverClassName: com.mysql.cj.jdbc.Driver
mybatis:
  mapper-locations: classpath:mapping/*Mapping.xml
logging:
  level:
    com.spring.mapper: debug

  建表语句:

DROP TABLE IF EXISTS `book`;
CREATE TABLE `book` (
  `id` int(11) NOT NULL,
  `bname` varchar(100) DEFAULT NULL,
  `price` int(11) DEFAULT NULL
) ;
INSERT INTO `book` VALUES (1,'坏蛋是怎么样炼成的',100),(2,'时间管理',100),(3,'Java虚拟机',100),(4,'数据库概述',100);

DROP TABLE IF EXISTS `user`;
CREATE TABLE `user` (
  `id` bigint(20) NOT NULL,
  `name` varchar(100) DEFAULT NULL,
  `balance` int(11) DEFAULT NULL
) ;
INSERT INTO `user` VALUES (1,'张三',10000),(2,'李四',10000),(3,'王五',10000),(4,'赵六',10000);

DROP TABLE IF EXISTS `stock`;
CREATE TABLE `stock` (
  `id` int(11) NOT NULL,
  `bid` int(11) DEFAULT NULL,
  `stock` int(11) DEFAULT NULL
) ;
INSERT INTO `stock` VALUES (1,1,500),(2,2,500),(3,3,500),(4,4,500);

  创建三个实体(需要Getter和Setter方法):

  Book.java:

public class Book { //图书实体
    private int id;
    private String bName; //书名
    private int price; //销售价格
}

  Stock.java:

public class Stock { //库存实体
    private int id;
    private int bId; //书id
    private int stock; //库存
}

  User.java:

public class User { //用户实体
    private Integer id;
    private String name; //用户姓名
    private Integer balance; //用户余额
}

  创建三个mapper接口:
  BookMapper.java:

@Mapper
public interface BookMapper {
    Book selectPrice(@Param("id") int id); //用id获取图书信息
}

  StockMapper.java:

@Mapper
public interface StockMapper {
    int update(@Param("stock") int stock, @Param("bid") int bid); //修改库存
}

  UserMapper.java:

@Mapper
public interface UserMapper {
    int update(@Param("balance") int balance, @Param("name") String name); //修改余额
}

  创建三个映射文件:

  BookMapping.xml:

<?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="com.spring.mapper.BookMapper">

    <select id="selectPrice" resultType="com.spring.entity.Book">
        select * from book where id = #{id}
    </select>
</mapper>

  StockMapping.xml:

<?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="com.spring.mapper.StockMapper">
    <update id="update">
        update stock set stock = stock - #{stock} where bid = #{bid}
    </update>
</mapper>

  UserMapping.xml:

<?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="com.spring.mapper.UserMapper">
    <update id="update">
        update user set balance = balance - #{balance} where name = #{name}
    </update>
</mapper>

  创建service接口UserService.java:

public interface UserService {
    void buyBook();
}

  创建service实现类UserServiceImpl.java:

@Service
public class UserServiceImpl implements UserService {
    @Autowired
    UserMapper userMapper;

    @Autowired
    BookMapper bookMapper;

    @Autowired
    StockMapper stockMapper;

    @Override
    @Transactional
    public void buyBook() {
        //模拟张三买了一本书,需要减去库存,减去张三账户余额
        Book book = bookMapper.selectPrice(1); //用id获取图书信息

        stockMapper.update(1,book.getId()); //修改库存

        userMapper.update(book.getPrice(),"张三"); //修改余额
    }
}

  新建测试类TestSpringTx.java:

@RunWith(SpringRunner.class)
@SpringBootTest(classes = Application.class) //作用
public class TestSpringTx {

    @Autowired
    UserService userService;

    @Test
    public void test01(){
        userService.buyBook();
    }
}

  工程结构:
在这里插入图片描述

  执行测试方法,正常执行,查看表中数据:

: ==>  Preparing: select * from book where id = ? 
: ==> Parameters: 1(Integer)
: <==      Total: 1
: ==>  Preparing: update stock set stock = stock - ? where bid = ? 
: ==> Parameters: 1(Integer), 1(Integer)
: <==    Updates: 1
: ==>  Preparing: update user set balance = balance - ? where name = ? 
: ==> Parameters: 100(Integer), 张三(String)
: <==    Updates: 1

在这里插入图片描述
在这里插入图片描述
  这是我们改一下StockMapping.xml里面的SQL:
在这里插入图片描述

  再次执行测试方法,报错,然后查看表数据:

: ==>  Preparing: select * from book where id = ? 
: ==> Parameters: 1(Integer)
: <==      Total: 1
: ==>  Preparing: update stock set stock = stock - ? where bid = ? 
: ==> Parameters: 1(Integer), 1(Integer)
: <==    Updates: 1
: ==>  Preparing: updateaa user set balance = balance - ? where name = ? 
: ==> Parameters: 100(Integer), 张三(String)

在这里插入图片描述

在这里插入图片描述
在这里插入图片描述
  在执行更新库存表的时候没有异常,执行更新用户余额的时候报错,但是库存更新也回滚了。

@Transactional

  @Transactional是声明式事务管理编程中使用的注解,用于接口实现类或接口实现方法上,只对public修饰的方法起作用。读取数据的接口尽量不要用此注解,注解底层采用AOP的动态代理拦截事务会影响系统性能。
  这里看一下@Transactional的实现:

@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface Transactional {
    @AliasFor("transactionManager")
    String value() default "";

    @AliasFor("value")
    String transactionManager() default "";

    Propagation propagation() default Propagation.REQUIRED;

    Isolation isolation() default Isolation.DEFAULT;

    int timeout() default -1;

    boolean readOnly() default false;

    Class<? extends Throwable>[] rollbackFor() default {};

    String[] rollbackForClassName() default {};

    Class<? extends Throwable>[] noRollbackFor() default {};

    String[] noRollbackForClassName() default {};
}

常用参数:

1. 超时 timeoutint

  默认值-1(不设置超时),单位秒,设定的时间内没有执行完成会报事务超时异常。

@Transactional(timeout = 3)
public void buyBook() throws InterruptedException {
    //模拟张三买了一本书,需要减去库存,减去张三账户余额
    Book book = bookMapper.selectPrice(1);

    stockMapper.update(1,book.getId());

    Thread.sleep(3000);

    userMapper.update(book.getPrice(),"张三");
}

  执行输出:
org.springframework.transaction.TransactionTimedOutException: Transaction timed out: deadline was Mon Sep 06 15:50:36 CST 2021

2. 只读 readOnly

  默认值false,设置事务是否只读,对查询事务设为只读可加快查询速度;有增删改的事务不可设置为true。

3. 异常回滚 rollbackFor和rollbackForClassName

  默认值都是{},rollbackFor参数类型是Class<? extends Throwable>[],rollbackForClassName参数类型是String[ ]。
  这里需要了解异常分类:
  运行时异常(非检查异常):这种异常不用处理(例如除零异常),事务中发生运行时异常默认回滚。
  编译时异常(检查异常):要么try-catch,要么throws(例如FileNotFoundException),事务中发生编译时异常默认不回滚。
  事务中出现编译时异常哪些异常需要回滚:
  这里我们用FileInputStream构造一个不存在的文件,然后抛出异常,把库存全部改为500,把用户余额全部改为10000,然后执行测试方法:

    @Transactional
    public void buyBook() throws FileNotFoundException {
        //模拟张三买了一本书,需要减去库存,减去张三账户余额
        Book book = bookMapper.selectPrice(1);

        stockMapper.update(1,book.getId());

        userMapper.update(book.getPrice(),"张三");

        new FileInputStream("D://qezxadfasre.qerq");
    }

这时发现虽然有异常发生了,但是事务并没有回滚,表中数据也更新了:
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述
  如果想在发生编译时异常回滚,使用noRollbackFor 或noRollbackForClassName 。

    @Transactional(noRollbackFor = {FileNotFoundException.class})
//    @Transactional(noRollbackForClassName = {"java.io.FileNotFoundException"})
    public void buyBook() throws FileNotFoundException {
        //模拟张三买了一本书,需要减去库存,减去张三账户余额
        Book book = bookMapper.selectPrice(1);

        stockMapper.update(1,book.getId());

        userMapper.update(book.getPrice(),"张三");

        new FileInputStream("D://qezxadfasre.qerq");
    }

4. 异常不回滚 noRollbackFor和noRollbackForClassName

  默认值都是{},noRollbackFor参数类型是Class<? extends Throwable>[],noRollbackForClassName参数类型是String[ ]。
  事务中出现运行时异常哪些异常不需要回滚:
  这里我们用除零触发运行时异常,把库存全部改为500,把用户余额全部改为10000,然后执行测试方法:

    @Transactional
    public void buyBook() {
        //模拟张三买了一本书,需要减去库存,减去张三账户余额
        Book book = bookMapper.selectPrice(1);

        stockMapper.update(1,book.getId());

        userMapper.update(book.getPrice(),"张三");
        
        int a = 1/0;
    }

  发生运行时异常,回滚:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

  如果发生运行时异常不回滚,可以使用noRollbackFor或noRollbackForClassName。

    @Transactional(noRollbackFor = {ArithmeticException.class})
//    @Transactional(noRollbackForClassName = {"java.lang.ArithmeticException"})
    public void buyBook() {
        //模拟张三买了一本书,需要减去库存,减去张三账户余额
        Book book = bookMapper.selectPrice(1);

        stockMapper.update(1,book.getId());

        userMapper.update(book.getPrice(),"张三");

        int a = 1/0;
    }

  执行测试方法,并查看表:
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

5. 隔离级别 isolation

  默认值Isolation.DEFAULT,参数类型Isolation。
  事务隔离级别:一个事务和其他事务的隔离程度成为隔离级别,这是数据库具有隔离并发运行各个事务的能力,使它们不会互相影响,避免各种并发问题。SQL标准中定义了多种事务隔离级别,不同的隔离级别对应不同的干扰程度,隔离级别越高,数据一致性越好,但并发越弱。
  没有隔离级别的时候,事务并发会出现什么问题,假设现在有Tx1和Tx2两个事务并发执行:

  • 脏读
    (1)Tx1将某条记录的stock的值从500改成499;
    (2)Tx2读取了Tx2更新后的值499;
    (3)Tx1发生异常回滚,stock的值恢复到了500;
    (4)Tx2读取到的499就是一个无效的脏数据。
  • 不可重复读
    (1)Tx1读取到stock的值为500;
    (2)Tx2将stock的值从500改为400;
    (3)Tx1再次读取stock的值为400。
  • 幻读
    (1)Tx1读取了book表中的一部分数据;
    (2)Tx2向book表中插入了新的行;
    (3)Tx1读取book表时,多出了一些行。

隔离级别

  1. 读未提交(READ UNCOMMITTED):允许Tx1读取Tx2未提交的数据。
  2. 读已提交(READ COMMITTED):要求Tx1只能读取Tx2已提交的数据。
  3. 可重复读(REPEATABLE READ):Tx2执行事务期间,确保Tx1多次从一个字段中取到相同值。
  4. 串行化(SERIALIZABLE):Tx2执行事务期间,确保Tx1多次从一个表读取到相同行,在Tx2执行期间,禁止其他事务对这个表进行添加、更新和删除操作,可以避免任何并发问题,但是性能十分低下。

  各个隔离级别解决并发问题:

隔离级别脏读不可重复读幻读
READ UNCOMMITTED
READ COMMITED
REPEATABLE READ
SERIALIZABLE

  Oracle和MySQL支持的隔离级别:

隔离级别OrcaleMySQL
READ UNCOMMITTED
READ COMMITED
REPEATABLE READ
SERIALIZABLE

  @Transaction设置隔离级别用Isolation参数,参数类型是Isolation枚举:

public enum Isolation {
    DEFAULT(-1), //不设置隔离级别
    READ_UNCOMMITTED(1), //读未提交
    READ_COMMITTED(2), //读已提交
    REPEATABLE_READ(4), //可重复读
    SERIALIZABLE(8); //串行化
}

MySQL数据库中事务隔离的现象:
查看MySQL全局隔离级别:

SELECT @@global.tx_isolation; -- 查询全局隔离级别
SELECT @@session.tx_isolation; -- 查询当前会话隔离级别
SELECT @@tx_isolation;  -- 同上

  打开cmd,执行mysql -uroot -proot(-u后面是数据库用户名,-p后面是密码;如果报’mysql’ 不是内部或外部命令,在mysql安装路径运行即可):
在这里插入图片描述
  设置当前会话隔离级别:

SET [SESSIONI GLOBAL] TRANSACTION ISOLATION LEVEL (READ UNCOMMITTED | READ COMMITTED | REPEATABLE READ | SERIALIZABLE )
-- 如:SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;        

1、读未提交的脏读问题

  修改当前会话隔离级别为读未提交(READ UNCOMMITTED):

SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;

在这里插入图片描述

  再打开一个cmd窗口,执行mysql -uroot -proot,模拟两个事务在读未提交下的现象,用start transaction;在两个客户端都开启事务(蓝色为客户端1,灰色为客户端2):

  1. 客户端1执行select price from book where bname ='Java虚拟机';
  2. 客户端2执行update book set price=112 where bname = 'Java虚拟机';
  3. 客户端1执行select price from book where bname ='Java虚拟机';
  4. 客户端2回滚,执行rollback;
  5. 客户端1执行select price from book where bname ='Java虚拟机';

在这里插入图片描述
  那么在第三步读取到的数据112就是脏数据。

2、读已提交避免脏读,没有避免不可重复读

  修改客户端1会话隔离级别为读已提交(READ COMMITTED):

SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;

在这里插入图片描述
  在两个客户端都开启事务:
   1. 客户端1执行select price from book where bname ='Java虚拟机';
   2. 客户端2执行update book set price=112 where bname = 'Java虚拟机';
   3. 客户端1执行select price from book where bname ='Java虚拟机';
   4. 客户端2提交,执行commit;
   5. 客户端1执行select price from book where bname ='Java虚拟机';

在这里插入图片描述
  在客户端1中,在一个事务中读取了三次,最后一次读取的值和其他数据不一样。

3、可重复读级别下,只要在一个事务中,读取的数据一直相同

  修改客户端1会话隔离级别为读已提交(REPEATABLE READ)

SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ;

在这里插入图片描述
  在两个客户端都开启事务:
   1. 客户端1执行select price from book where bname ='Java虚拟机';
   2. 客户端2执行update book set price=112 where bname = 'Java虚拟机';
   3. 客户端1执行select price from book where bname ='Java虚拟机';
   4. 客户端2执行delete from book where bname = 'Java虚拟机';
   5. 客户端2提交,执行commit;
   6. 客户端1执行select price from book where bname ='Java虚拟机';

在这里插入图片描述
  客户端1开启事务后读取的每次数据都一致,即便数据被删掉了,在开启事务的时候会保存一个快照把数据定格。

并发修改
  在两个客户端都开启事务,同时改修一条数据,先执行语句的事务不执行提交或者回滚,后执行的语句就一直在等待:
在这里插入图片描述
  先执行语句的事务提交或者回滚,后执行的客户端才能执行语句。
在这里插入图片描述
  无论哪种隔离级别,两个事务同时修改一行数据,先执行语句的提交或者回滚完成之后,后执行语句的事务才能执行语句,所以隔离级别是隔离读的。

6. 传播行为 ropagation

  事务传播行为,默认值Propagation.REQUIRED,参数类型枚举Propagation:

public enum Propagation {
    REQUIRED(0),
    SUPPORTS(1),
    MANDATORY(2),
    REQUIRES_NEW(3),
    NOT_SUPPORTED(4),
    NEVER(5),
    NESTED(6);
}

  事务的传播行为,当事务方法被另一个事务方法调用时,指定事务如何传播,也就是说两个事务方法是否共享同一个事务。例如:方法可继续在现有的事务中运行,也可开启一个新事务,在新启的事务中运行,Spring中定义了期七种传播类型:

传播类型描述
REQUIRED如果有事务运行,当前的方法就在这个事务运行;否则就启一个新的事务,并在新启的事务中运行
REQUIRES_NEW当前方法必须新启一个事务,并在新启的事务中运行
SUPPORTS如果有事务在运行,当前方法就在事务中运行;否则它不在事务中运行
NOT_SUPPORTED当前方法不再事务中运行,如果有运行事务就将它挂起
MANDATORY当前方法必须在运行的事务内部,如果没有正常运行的事务,就抛出异常
NEVER当前方法不应该运行在事务中,如果有运行的事务,就抛出异常
NESTED如果有事务在运行,当前方法就在这个事务的嵌套事务中运行;否则就启动一个新的事务,并在新启的事务中运行

  被调用方法在运行的事务中运行时,事务的属性由运行中的事务决定,被调用方法的事务属性不起作用(例如timeout)。同一个类下面的事务方法调用,就只有一个事务,即使传播属性都为REQUIRES_NEW也不起作用。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值