社区微信群开通啦,扫一扫抢先加入社区官方微信群
社区微信群
[此篇很多是我自己的理解和总结,如果有错误的地方欢迎批评指正。部门后端人员务必全部阅读完此篇,尤其是红色字体部分!]
事务是数据库管理系统执行过程中的一个逻辑单位,一个操作序列。就数据库事务而言它的存在包含有以下两个目的:
事务有四大特性(ACID):
如果某个系统事务是的原子性和一致性不能保证,就会遇到主从不一(主更新或生成了,子没更新或生成)、库存为负数、金额为负数等。一个复杂的业务系统,一旦事务失控,那这个系统必然是不可靠的。
*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事务进行管理。声明式事务管理使业务代码不受污染,只要加上注解就可以获得完全的事务支持。
疑问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有四个常用配置属性:rollbackFor,readOnly、propagation、isolation
在@Transactional注解中如果不配置rollbackFor属性,那么只会在遇到RuntimeException的时候才会回滚,加上rollbackFor=Exception.class,可以让事务在遇到非运行时异常时也回滚。一般在日常生产开发中,我们配置成rollbackFor=Exception.class
readOnly=true表明所注解的方法或类只是读取数据,我们的某个方法只提供查询时,可以进行此种配置。readOnly=false表明所注解的方法或类是增加,删除,修改数据。默认是false,一般使用默认即可,无需配置。
*开发人员不得进行此项配置,只能与项目负责人申请评估后方可进行配置
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一样。嵌套事务一个非常重要的概念就是内层事务依赖于外层事务。外层事务失败时,会回滚内层事务所做的动作。而内层事务操作失败并不会引起外层事务的回滚。
*开发人员不得进行此项配置,只能与项目负责人申请评估后方可进行配置
我们在使用事务过程中,通常会发生以下三种情况:
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。它能够避免更新丢失、脏读,而且具有较好的并发性能。尽管它会导致不可重复读、幻读这些并发问题,在可能出现这类问题的个别场合,可以由应用程序采用悲观锁[详见后续悲观锁篇]或乐观锁来控制。
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!