一、基本概念
事务是由一组sql语句构成的,它由一个用户输入,并以修改成持久的或者回滚到原来状态而终结。
简单来说就是由一个或多个sql语句组成一个事务,在这个事务中,sql语句要么全部执行成功,要么全部失败,并回滚到原来的状态。
二、ACID原则
事务必须遵从原子性(atomicity)、一致性(consistency)、隔离性(isolation)、持久性(durability)原则。
1、原子性:一个事务必须被视为一个不可分割的最小工作单元,整个事务中的所有操作要么全部提交成功,要么全部失败回滚。
2、一致性:事务不管成功还是失败,数据的完整性要保持一致,不应该导致数据的丢失或错乱。
3、隔离性:一个事务所做的修改在最终提交前能否对其它事务可见,这就是事务的隔离性。这就有关于事务的隔离级别,通常情况下是不可见的。
4、持久性:一旦事务提交,则其所做的修改将永久保存到数据库中,即使系统崩溃,修改的数据也不会丢失。
三、mysql事务创建与生命周期
mysql处理一个事务包括以下几个部分:
1、开始事务
start transaction 或者 begin work
2、结束事务
commit语句是提交事务的,它会提交从开始到结束中sql语句对数据的修改,并持久化到数据库中。
START TRANSACTION;
update admin set name ="张三" WHERE id = 9;
commit;
事务被提交,数据被修改
3、撤销事务
rollback语句用于撤销事务所做的修改,并结束当前事务。也就是回滚整个事务。
START TRANSACTION;
update admin set name ="李四" WHERE id = 9;
rollback;
事务被撤销,当前事务结束,数据没有被修改
4、回滚事务
除了撤销整个事务外,mysql还支持将事务回滚到事务中的某个保存点。
这个点用savepoint 来表示:savepoint point1,定义了一个名为point1的保存点
rollback to point1 将事务回滚到point1这个点
START TRANSACTION;
update admin set name ="张三" WHERE id = 9;
SAVEPOINT s1;
update admin set name ="李四" WHERE id = 2;
// work可以省略
ROLLBACK WORK TO s1;
UPDATE admin set `name`="Tom" WHERE id = 10;
COMMIT;
事务被回滚到了s1保存点,执行结果:id为9的记录name字段被修改为张三,id为2的记录因为回滚而没有被修改,id为10的记录发送在回滚之后,所以也被修改了。
四、事务的隔离级别
1、READ UNCOMMITTED(未提交读)
在READ UNCOMMITTED级别,事务中的修改即使没有提交,对其它事务也是可见的。事务可以读取到其他事务未提交的数据,这也被称为脏读。
(脏读:某个事务读取到了另外一个事务提交前或者回滚前的修改结果。也就是说当前事务读取到的是别的事务想要修改但实际上还未成功,没有持久化的数据。)
设置数据库隔离级别为READ UNCOMMITTED:
SET GLOBAL TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
SELECT @@tx_isolation ;
开启一个事务,修改数据,但不提交。
START TRANSACTION;
update admin set name ="张三" WHERE id = 1;
打开另外一个查询窗口(开启另外一个事务)
SELECT * FROM admin WHERE id = 1
READ UNCOMMITTED级别下,一个事务读取到了另外一个事务没有提交的数据,造成脏读。
2、RED COMMITTED(提交读,也叫不可重复读)
一个事务从开始直到提交之前,所做的任何修改对其它事务都不可见,也叫做不可重复读,因为在一个事务中执行两次同样的查询可能会得到不一样得结果。(在未提交的事务1中执行一次查询,接着另外一个事务2对该条数据进行了修改并提交,当事物1再次执行相同的查询时,会得到修改后的结果,两次查询结果不同)
设置数据库隔离级别为READ COMMITTED:
SET GLOBAL TRANSACTION ISOLATION LEVEL READ COMMITTED;
SELECT @@tx_isolation ;
开启一个事务a,修改数据,但不做提交:
START TRANSACTION;
update admin set name ="李四" WHERE id = 1;
开启另外一个事务b,查询该条数据 :
START TRANSACTION;
#第一次查询
SELECT * FROM admin WHERE id = 1;
说明事务a提交之前,对其它事务不可见。
在事务a中使用commit 提交事务,在事务b中再次查询该条记录
START TRANSACTION;
#第一次查询
SELECT * FROM admin WHERE id = 1;
#第二次执行查询
SELECT * FROM admin WHERE id = 1;
在事务b中两次查询的结果不一致。
3、REPEATABLE READ(可重复读)
REPEATABLE READ是mysql默认的隔离级别,解决了脏读的问题,并且保证在同一个事务中多次读取同样的记录得到的结果一致,即使其它事务已经对数据进行了修改和提交。(未提交的事物1,它查询某条记录,接着另外一个事物2对这条数据进行了修改并提交。事物1再次查询这条记录,查询的结果还是和原来的一样,没有被修改。事物1提交过后或者说在新一个事物开始后才能看到被修改的结果。)但这也造成了幻读问题。
所谓幻读指的是某个事务在读取某个范围的数据时,另外一个事务又在该范围内插入了新的记录,当之前的事务再次读取改范围的记录时会出现幻行。(幻读侧重的方面是某一次的查询检查操作的结果所表征的数据状态无法支撑后续的业务操作。更为具体一些的列子:在事务a中需要插入某条数据,在插入前进行查询检测该条数据是否存在,不存在则插入,当事务a查询检测之后,另一个事务b突然插入了该条记录,导致事务a查询检测结果和实际操作结果不一致,也就是说之前做的查询检测对于后续操作来说是不可靠的。在未提交读、可重复读以及不可重复读的隔离级别下都会发送类似的情况)
设置数据库隔离级别为REPEATABLE READ:
SET GLOBAL TRANSACTION ISOLATION LEVEL REPEATABLE READ;
SELECT @@tx_isolation ;
开启一个事务a,修改数据,但不做提交:
START TRANSACTION;
update admin set name ="张三" WHERE id = 1;
开始另外一个事务b,查询该条记录:
START TRANSACTION;
#第一次查询
SELECT * FROM admin WHERE id = 1;
在REPEATABLE READ级别下,一个事务未提交前对其他事务不可见,不能读取到其它事务未提交的修改,解决了脏读问题。
提交事物a,在事务b中再次查询该条记录
START TRANSACTION;
#第一次查询
SELECT * FROM admin WHERE id = 1;
#第二次查询
SELECT * FROM admin WHERE id = 1;
事物b感知不到数据变化,两次查询的结果相同,即使事务a已经对数据进行了修改并提交,同理在insert和delete操作下亦是如此。
开启另外一个事务C,查询该条记录
SELECT * FROM admin WHERE id = 1
在新事务中才能发现数据的变化。
四、SERIALIZABLE(可串行化)
SERIALIZABLE是最高的隔离级别。它通过强制事务串行执行(也就是说,所有的事物都是排队执行的,即使事物只是查询操作,没有对数据库进行任何修改,它也必须等待前面的事物完成,严格保持了一致性。),避免了幻读和脏读问题,但这也造成了严重的性能下降。只有在非常需要确保数据一致性并且可以接受没有并发的情况下才会考虑使用。
五、spring data jpa 事务管理实例
Spring 实现了一套事务管理机制,这里不再赘述,不清楚可以查阅相关资料。这里主要具体讲解如何用spring data jpa实现事务管理。
首先创建实体类:
import javax.persistence.*;
@Entity
@Table(name = "admin")
public class Admin {
private Integer id;
private String name;
private String password;
private Integer status;
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public Integer getStatus() {
return status;
}
public void setStatus(Integer status) {
this.status = status;
}
}
创建Dao:
import com.example.learn.model.Admin;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;
@Repository
public interface AdminDao extends JpaRepository<Admin,Integer> {
@Modifying
@Query("UPDATE Admin a set a.name=:name where id=:id")
Integer updateName(@Param("name") String name,@Param("id") Integer id);
@Modifying
@Query("UPDATE Admin a set a.status=:status where id=:id")
Integer updateStatus(@Param("status") Integer status,@Param("id") Integer id);
}
在spring data jpa 中 修改和删除数据是需要使用@Transactinal切入事务的,我们将注解加在service层,方便事务管理。
创建AdminService接口:
public interface AdminService {
Integer updateName(String name,Integer id);
Integer updateStatus(Integer status,Integer id);
}
创建接口实现类:
import com.example.learn.Service.AdminService;
import com.example.learn.dao.AdminDao;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
@Service
public class AdminServiceImpl implements AdminService {
@Autowired
AdminDao adminDao;
@Override
@Transactional(propagation = Propagation.REQUIRED,rollbackFor = RuntimeException.class)
public Integer updateName(String name, Integer id) {
Integer res = adminDao.updateName(name,id);
return res;
}
@Override
@Transactional(propagation = Propagation.REQUIRED,rollbackFor = RuntimeException.class)
public Integer updateStatus(Integer status, Integer id) {
Integer res = adminDao.updateStatus(status,id);
return res;
}
}
创建AdminService2接口
public interface AdminService2 {
Integer updateName(String name,Integer id);
}
创建接口实现类:
import com.example.learn.Service.AdminService2;
import com.example.learn.dao.AdminDao;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
@Service
public class AdminService2Impl implements AdminService2 {
@Autowired
AdminDao adminDao;
@Override
@Transactional(propagation = Propagation.REQUIRED,rollbackFor = RuntimeException.class)
public Integer updateName(String name, Integer id) {
Integer res = adminDao.updateName(name,id);
return res;
}
@Transactional(propagation = Propagation.REQUIRED,rollbackFor = RuntimeException.class)
public Integer updateNameThrowException(String name, Integer id) {
Integer res = adminDao.updateName(name,id);
throw new RuntimeException("service2抛出运行时异常");
}
}
在spring data jpa中使用@Transactional开始注解式事务
@Transactional注解中常用参数说明
其中propagation 用来设置事务的传播行为,事务传播行为用来描述由某一个事务传播行为修饰的方法被嵌套进另一个方法时事务如何传播。
public void A(){
B();
}
@Transactional(Propagation=XXX)
public void B(){
}
代码中A()
方法嵌套调用了B()
方法,B()
的事务传播行为由@Transaction(Propagation=XXX)
设置决定。这里需要注意的是A()
并没有开启事务,某一个事务传播行为修饰的方法并不是必须要在开启事务的外围方法中调用。
spring事务管理中定义了7中事务传播行为:
前面我们已经创建好了实体和服务层,接下来我们将使用具体的案例来让我们清楚的理解spring事务的传播行为:
1、@Transactional(propagation = Propagation.REQUIRED,rollbackFor = RuntimeException.class)
当事务中抛出运行时异常时,事物将会回滚
案例一:外层方法没有开启事务
(1)外层的RollBack()方法没有定义事务并且嵌套了两个事务(adminService和adminService2中定义的事务),他们的传播行为是REQUIRED
public class SQLController {
@Autowired
AdminDao adminDao;
@Autowired
AdminServiceImpl adminService;
@Autowired
AdminService2Impl adminService2;
@RequestMapping("/rollback")
public void RollBack() {
adminService.updateName("张三",9);
adminService2.updateName("李四",10);
throw new RuntimeException(“外层方法抛出运行时异常”);
}
}
运行结果:没有定义事务的外层方法抛出异常,内层事务并没有回滚,两条记录都被修改了。
在adminService2定义的事务中抛出运行时异常
public void RollBack() {
adminService.updateName("张三",9);
adminService2.updateNameThrowException("李四",10);
}
结果:内层中定义事务的方法抛出运行时异常,该事务回滚。中id为9的记录被修改了,id为10的记录被回滚,没有修改。
案例二:外层方法定义事务
@RequestMapping("/rollback")
@Transactional(propagation = Propagation.REQUIRED)
public void RollBack() {
adminService.updateName("张三",9);
adminService2.updateName("李四",10);
throw new RuntimeException("外层方法抛出异常");
}
运行结果:两条记录均未被修改。外围方法开启事务,内部方法加入外围方法事务,外围方法回滚,内部方法也要回滚
@RequestMapping("/rollback")
@Transactional(propagation = Propagation.REQUIRED)
public void RollBack() {
adminService.updateName("张三",9);
adminService2.updateNameThrowException("李四",10);
}
运行结果:两条记录均未被修改。外围方法开启事务,内部方法加入外围方法事务,内部方法抛出异常,外部方法也会回滚
@RequestMapping("/rollback")
@Transactional(propagation = Propagation.REQUIRED)
public void RollBack() {
adminService.updateName("张三",9);
try {
adminService2.updateNameThrowException("李四",10);
}catch (Exception e){
System.out.println("捕捉异常内部异常,不抛出");
}
}
运行结果:两条数据均未被修改,并抛出org.springframework.transaction.UnexpectedRollbackException异常
内部事务抛出异常,应该是做回滚操作,外部事务捕获内部异常,外部方法就感知不到错误,做commit提交事务操作,这样就导致了回滚事务和提交事务的冲突,最后还是选择的做回滚操作。这时外部方法就认为,要做commit操作然而却做了回滚,就抛出Unexpected异常。
结论:REQUIRED 如果外层方法中存在事务,嵌套的方法就加入到外层方法的事务中,任何一个抛出异常都会回滚。如果外层方法没有事务,就创建一个新的事务,内外层事务相互隔离互不干扰。
明白这个,其他类型事务就容易理解了,这里不再赘述,请参见参考文献
参考文献:
[1] Baron.高性能MySQL(第3版).北京.电子工业出版社
[2] 张郑华.spring data jpa 从入门到精通.北京.清华大学出版社
[3] 郑阿奇.mysql实用教程(第2版).北京.电子工业出版社
[4] https://segmentfault.com/a/1190000013341344?utm_source=tag-newest