Spring Boot教程 | 第七篇:Spring Data JPA (Hibernate) 事务控制 - Go语言中文社区

Spring Boot教程 | 第七篇:Spring Data JPA (Hibernate) 事务控制


一、前言

[此篇很多是我自己的理解和总结,如果有错误的地方欢迎批评指正。部门后端人员务必全部阅读完此篇,尤其是红色字体部分!]

1.1 事务的概念

事务是数据库管理系统执行过程中的一个逻辑单位,一个操作序列。就数据库事务而言它的存在包含有以下两个目的:

  1. 为数据库操作序列提供了一个从失败中恢复到正常状态的方法,同时提供了数据库即使在异常状态下仍能保持一致性的方法。
  2. 当多个应用程序在并发访问数据库时,可以在这些应用程序之间提供一个隔离方法,以防止彼此的操作互相干扰。

事务有四大特性(ACID):

  1. 原子性(Atomicity):事务是数据库的逻辑工作单位,而且是必须是原子工作单位,对于其持久化的操作,要么全部执行,要么全部不执行。比如A向B转账100元,就一定要保证原子性(要么同时成功,要么同时失败)。
  2. 一致性(Consistency):在事务开始以前,被操作的数据的完整性处于一致性的状态,事务结束后,被操作的数据的完整性也必须处于一致性状态。比如A向B转账,不可能A扣了钱,B却没收到(这就是非一致性)。
  3. 隔离性(Isolation):一个事务的执行不能被其他事务所影响。比如A和C同时向B转账,那B同一时间只能和一个人交易(同时只能有一个交易在执行)。
  4. 持久性(Durability):一个事务一旦提交,事物的操作便永久性的保存在DB中。即使此时再执行回滚操作也不能撤消所做的更改。A向B转账,成功以后B的账户就存入了转账数额,在不做其他的操作前提下数据是永久性的。

如果某个系统事务是的原子性和一致性不能保证,就会遇到主从不一(主更新或生成了,子没更新或生成)、库存为负数、金额为负数等。一个复杂的业务系统,一旦事务失控,那这个系统必然是不可靠的。

 

1.2 Hibernate事务

*Spring Data JPA它的底层是用Hibernate实现的,Spring Data JPA事务即为Hibernate事务。Hibernate是对JDBC的轻量级对象封装,Hibernate本身是不具备事务处理功能的,Hibernate的事务实际上是底层的JDBC事务的封装。先贴一段代码:

public synchronized void receiveMessage(String _message) {
	if(sessionFactory==null){
		sessionFactory=(SessionFactory) SpringBeanUtil.getBeanFromSpringByBeanName("sessionFactory");
	}
	session = sessionFactory.openSession();
	te = session.getTransaction();
	te.begin();
	try {
		//保存数据
		.
		.
		fqsArrXmlHome.persist();
		te.commit();
	} catch (RuntimeException re) {
		if(te != null){
		  if(session!=null){
			  if(session.isOpen()){
				  te.rollback();
			  }
		  }
		 
		}
		re.printStackTrace();
		throw re;
	}finally {
		if(session!=null){
			if(session.isOpen()){
				session.close();
			}
		}
	}
}

这是我们很久以前项目中的一段代码,写法和JDBC或者数据库的存储过程套路是差不多的,基本流程就是开会话,执行,如果有异常就回滚,最后关闭会话。这种写法有两大弊端:一、代码量大,而且还是重复的代码,二、如果忘记关闭会话,随着连接数的增多,可能会造成连接池溢出,系统假死等情况。那我们有没有什么更简洁的方法或工具去管理Hibernate的事务呢?答案就是Spring。

 

二、Spring管理Hibernate事务

Spring以非侵入式方式对Hibernate事务进行管理。声明式事务管理使业务代码不受污染,只要加上注解就可以获得完全的事务支持。

疑问1:我们前几篇中,好像没有看到在哪里声明了事务,但是确能将数据持久化到数据库中去,是怎么实现的?

答:上图是JpaRepository接口实现类,我们可以看到在方法上加了@Transactional注解,加了此注解就开启了声明式事务,也就是可以让Spring去帮我们管理Hibernate的事务了,所以在使用data jpa操作数据库时,即使我们没有在Service层上开启事务也能保存数据。

疑问2:以前我们搭建(SSH)项目,需要在配置文件中配置事务管理器TransactionManager后@Transactional注解生效,但是我们的Spring Boot项目好像直接使用就可以了,是怎么实现的呢?

答:Spring Boot就是简化配置的,在引用 spring-boot-starter-data-jpa,使用data-jpa作为数据库访问技术的时候,就自动帮我们配置好了,引入starter-data-jpa依赖后直接使用@Transactional即可。


下面我们进行代码实现:

学生信息表及相关类前几篇已创建:Spring Boot教程 | 第二篇:使用Spring Data JPA、Hikari连接池操作MySQL数据库

 新建一个班级信息表,实体类如下:

package com.kcsm.training.bootdemo.entity;

import lombok.Data;
import org.hibernate.annotations.GenericGenerator;
import javax.persistence.*;

/**
 * 班级信息实体类
 *
 * @author lqk
 * @date 2019/7/18 13:00
 */
@Entity
@Table(name = "CLASSES")
@Data
public class Classes {
    
    /**
     * 主键
     */
    @Id
    @GeneratedValue(generator = "guidGenerator")
    @GenericGenerator(name = "guidGenerator", strategy = "uuid")
    @Column(name = "ID", unique = true, nullable = false, length = 32)
    private String id;

    /**
     * 班级名称
     */
    @Column(name = "NAME",length = 50)
    private String name;

    /**
     * 班级人数
     */
    @Column(name = "TOTAL_NUMBER")
    private Integer totalNumber;
}

创建ClassesDao

package com.kcsm.training.bootdemo.dao;

import com.kcsm.training.bootdemo.entity.Classes;
import com.kcsm.training.bootdemo.entity.Student;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
import org.springframework.data.rest.core.annotation.RepositoryRestResource;

import java.util.List;

/**
 * 班级信息数据库访问接口
 *
 * @author lqk
 * @date 2019/7/9 15:43
 */
@RepositoryRestResource(path = "classes")
public interface ClassesDao extends JpaRepository<Classes, String>, JpaSpecificationExecutor<Classes> {

}

新建一个学生离校的方法,学习离校会进行两步操作:1、删除离校的学生;2、班级学生人数减1。

StudentService[本篇只需关注学生离校的方法]

package com.kcsm.training.bootdemo.service;

import com.kcsm.training.bootdemo.common.BaseService;
import com.kcsm.training.bootdemo.entity.Student;

import java.util.List;

/**
 * 学生信息业务接口
 *
 * @author lqk
 * @date 2019/7/9 18:29
 */
public interface StudentService extends BaseService<Student> {
    /**
     * 学生离校
     * 第一步、删除学生表中离校学生的数据,第二步:班级学生数量减1
     *
     * @author lqk
     * @param  studentId 离校的学生主键
     * @return [String]执行结果说明
     * @date 2019/7/10 9:19
     */
    public String leaveSchool(String studentId);
}

StudentServiceImpl[本篇只需关注学生离校的方法]

package com.kcsm.training.bootdemo.service.impl;

import com.kcsm.training.bootdemo.common.BaseServiceImpl;
import com.kcsm.training.bootdemo.dao.ClassesDao;
import com.kcsm.training.bootdemo.dao.StudentDao;
import com.kcsm.training.bootdemo.entity.Classes;
import com.kcsm.training.bootdemo.entity.Student;
import com.kcsm.training.bootdemo.service.StudentService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.List;

/**
 * 学生信息业务接口实现
 *
 * @author lqk
 * @date 2019/7/9 19:52
 */
@Service
public class StudentServiceImpl extends BaseServiceImpl<Student> implements StudentService {
    @Autowired
    StudentDao studentDao;
    @Autowired
    ClassesDao classesDao;

    @Override
    @Transactional
    public String leaveSchool(String studentId){
        //第一步:删除离校的学生
        Student student = studentDao.findById(studentId).orElse(null);
        studentDao.delete(student);

        //第二步:班级学生数减1
        //classId 根据自己做的数据赋值
        String classId = "abc";
        Classes classes = classesDao.findById(classId) .orElse(null);
        classes.setTotalNumber(classes.getTotalNumber() - 1);
        classesDao.save(classes);

        return "执行成功!";
    }
}

StudentController[本篇只需关注学生离校的方法]

package com.kcsm.training.bootdemo.controller;

import com.kcsm.training.bootdemo.common.BaseController;
import com.kcsm.training.bootdemo.dao.StudentDao;
import com.kcsm.training.bootdemo.entity.Student;
import com.kcsm.training.bootdemo.service.StudentService;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
import java.util.List;

/**
 * 学生信息控制类
 *
 * @author lqk
 * @date 2019/7/9 20:00
 */
@RestController
@RequestMapping(value = "student")
public class StudentController extends BaseController<Student> {

    @Resource(name = "studentServiceImpl")
    StudentService studentService;

    public StudentController(StudentService studentService) {
        this.baseService = studentService;
        this.studentService = studentService;
    }

    /**
     * 学生离校
     * 第一步、删除学生表中离校学生的数据,第二步:班级学生数量减1
     *
     * @author lqk
     * @param  studentId 离校的学生主键
     * @return [String]执行结果说明
     * @date 2019/7/10 9:19
     */
    @PostMapping(value = "leaveSchool")
    public String leaveSchool(@Param("studentId")String studentId){
        try{
            return  studentService.leaveSchool(studentId);
        }catch (Exception ex){
            ex.printStackTrace();
            return ex.toString();
        }
    }
}

数据库端做点数据

*我们日常生产开发中,如果没有特殊需求,无需配置只读属性、事务传播性与隔离级别(详见下节),只需指定所有异常全部回滚。通常用法如下:

在类上加注解,则这个类的所有方法都开启了事务声明,如果方法上也开启了,以方法上的为准。rollbackFor=Exception.class代表所有的异常都回滚。

启动项目,执行http://localhost:8080/student/leaveSchool?studentId=402881346bda7c5d016bda9375623478 (参数根据自己的数据做)执行完成,无报错,数据正确。

现在模拟出异常时事务回滚,做一个分母为0的异常

    @Transactional(rollbackFor=Exception.class)
    public String leaveSchool(String studentId){
        //第一步:删除离校的学生
        Student student = studentDao.findById(studentId).orElse(null);
        studentDao.delete(student);

        //第二步:班级学生数减1
        //生成报错信息
        int i = 1/0;
        String classId = "abc";
        Classes classes = classesDao.findById(classId) .orElse(null);
        classes.setTotalNumber(classes.getTotalNumber() - 1);
        classesDao.save(classes);

        return "执行成功!";
    }

执行张倩离校:http://localhost:8080/student/leaveSchool?studentId=402881b66bda7c5d016bda9373451278

后台报错,学生数据并没有被删除,事务回滚,就像什么都没发生一样

再做点改造,模拟如果我们把异常处理掉会发生什么

    @Transactional(rollbackFor=Exception.class)
    public String leaveSchool(String studentId){
        try{
            //第一步:删除离校的学生
            Student student = studentDao.findById(studentId).orElse(null);
            studentDao.delete(student);

            //第二步:班级学生数减1
            //生成报错信息
            int i = 1/0;
            String classId = "abc";
            Classes classes = classesDao.findById(classId) .orElse(null);
            classes.setTotalNumber(classes.getTotalNumber() - 1);
            classesDao.save(classes);
            return "执行成功!";
        }catch (Exception ex){
            return "执行失败!";
        }
    }

执行,发现学生信息被删除了,但是班级人数却没有减少!!主从不一重现了,系统不可靠了!害不害怕?恐怖恐怖!这是很多不清楚Spring事务管理用法的同学常犯的错误,*要让Spring去回滚事务,必须让它在开启事务的这一层或这一个方法上捕捉到异常。如果没有异常Spring认为执行是没有问题,也就正常提交这一次会话中所有的数据持久化操作!Hibernate还有个细节,默认配置下,如果你取到某一个对象,set它的某一个属性,即使你不执行save,只要不报错,哪怕你只是拿来做个中间变量,不想保存到数据库中,Hibernate也会帮你自动执行save操作!

 

三、@Transactional配置参数详解

@Transactional有四个常用配置属性:rollbackFor,readOnly、propagation、isolation

1、 rollbackFor:配置何种异常回滚

在@Transactional注解中如果不配置rollbackFor属性,那么只会在遇到RuntimeException的时候才会回滚,加上rollbackFor=Exception.class,可以让事务在遇到非运行时异常时也回滚。一般在日常生产开发中,我们配置成rollbackFor=Exception.class

 

2、readOnly:读写事务控制

readOnly=true表明所注解的方法或类只是读取数据,我们的某个方法只提供查询时,可以进行此种配置。readOnly=false表明所注解的方法或类是增加,删除,修改数据。默认是false,一般使用默认即可,无需配置。

 

3、Propagation事务传播行为

*开发人员不得进行此项配置,只能与项目负责人申请评估后方可进行配置

Propagation属性用来枚举事务的传播行为。所谓事务传播行为就是多个事务方法相互调用时,事务如何在这些方法间传播。Spring支持7种事务传播行为,默认为REQUIRED。

1、REQUIRED

REQUIRED是常用的事务传播行为,如果当前没有事务,就新建一个事务,如果已经存在一个事务中,加入到这个事务中。

我们使用sping data jpa时,它的实现类的方法就是使用了此项默认配置,所以我们操作各表时,事务能绑定到同一个,异常时全部回滚。

2、SUPPORTS

SUPPORTS表示当前方法不需要事务上下文,但是如果存在当前事务的话,那么这个方法会在这个事务中运行。

3、MANDATORY

MANDATORY表示该方法必须在事务中运行,如果当前事务不存在,则会抛出一个异常。不会主动开启一个事务。

4、REQUIRES_NEW

REQUIRES_NEW表示当前方法必须运行在它自己的事务中。一个新的事务将被启动,如果存在当前事务,在该方法执行期间,当前事务会被挂起(如果一个事务已经存在,则先将这个存在的事务挂起)。如果使用JTATransactionManager的话,则需要访问TransactionManager。

5、NOT_SUPPORTED

NOT_SUPPORTED表示该方法不应该运行在事务中,如果存在当前事务,在该方法运行期间,当前事务将被挂起。如果使用JTATransactionManager的话,则需要访问TransactionManager。

6、NEVER

NEVER表示当前方法不应该运行在事务上下文中,如果当前正有一个事务在运行,则会抛出异常。

7、NESTED

NESTED表示如果当前已经存在一个事务,那么该方法将会在嵌套事务中运行。嵌套的事务可以独立于当前事务进行单独地提交或回滚。如果当前事务不存在,那么其行为与REQUIRED一样。嵌套事务一个非常重要的概念就是内层事务依赖于外层事务。外层事务失败时,会回滚内层事务所做的动作。而内层事务操作失败并不会引起外层事务的回滚。

 

4、isolation:事务隔离级别 [详见数据库篇]

*开发人员不得进行此项配置,只能与项目负责人申请评估后方可进行配置

我们在使用事务过程中,通常会发生以下三种情况:

1、脏读(dirty read):当一个事务读取另一个事务尚未提交的修改时,产生脏读。

2、不可重复读(non-repeatable read):同一查询在同一事务中多次进行,由于其他提交事务所做的修改或删除,每次返回不同的结果集,此时发生非重复读。

3、幻像读(phantom read):同一查询在同一事务中多次进行,由于其他提交事务所做的插入操作,每次返回不同的结果集,此时发生幻像读。

针对上述三种情况,Spring提供了5种事务隔离级别予以解决:

1、DEFAULT默认级别

DEFAULT为数据源(数据库)的默认隔离级别,以目前常用的MySQL为例,默认的隔离级别通常为REPEATABLE_READ。

2、READ_UNCOMMITTED未授权读取级别

这是最低的隔离级别,一个事务能读取到别的事务未提交的更新数据,很不安全,可能出现丢失更新、脏读、不可重复读、幻读。

3、READ_COMMITTED授权读取级别

以操作同一行数据为前提,读事务允许其他读事务和写事务,未提交的写事务禁止其他读事务和写事务。此隔离级别可以防止更新丢失、脏读,但不能防止不可重复读、幻读。此隔离级别可以通过“瞬间共享读锁”和“排他写锁”实现。

4、REPEATABLE_READ可重复读取级别

保证同一事务中先后执行的多次查询将返回同一结果,不受其他事务影响。以操作同一行数据为前提,读事务禁止其他写事务,但允许其他读事务,未提交的写事务禁止其他读事务和写事务。此隔离级别可以防止更新丢失、脏读、不可重复读,但不能防止幻读。

5、SERIALIZABLE序列化级别

所有的事务依次逐个执行,这样事务之间就完全不可能产生干扰。提供严格的事务隔离,此隔离级别可以防止更新丢失、脏读、不可重复读、幻读。如果仅仅通过“行级锁”是无法实现事务序列化的,必须通过其他机制保证新插入的数据不会被刚执行查询操作的事务访问到。

隔离级别越高,越能保证数据的完整性和一致性,但是对并发性能的影响也越大。对于多数应用程序,可以优先考虑把数据库系统的隔离级别设为Read Committed。它能够避免更新丢失、脏读,而且具有较好的并发性能。尽管它会导致不可重复读、幻读这些并发问题,在可能出现这类问题的个别场合,可以由应用程序采用悲观锁[详见后续悲观锁篇]或乐观锁来控制。

 

 

版权声明:本文来源CSDN,感谢博主原创文章,遵循 CC 4.0 by-sa 版权协议,转载请附上原文出处链接和本声明。
原文链接:https://blog.csdn.net/tuoyun6647/article/details/96348108
站方申明:本站部分内容来自社区用户分享,若涉及侵权,请联系站方删除。

0 条评论

请先 登录 后评论

官方社群

GO教程

猜你喜欢