本地事务
什么是本地事务,现在有一个单体应用,就连接了一个数据库,也没有多个数据库,也没有多个项目,也不牵扯到远程调用,现在有一个这样的业务流程,下订单,减库存,减积分,这三个多数据库的操作要么同时成功要么同时失败,这就是本地事务!
事务的基本性质
原子性(Atomicity)、一致性( Consistency) 、隔离性或独立性(Isolation)和持久性(Durabilliy,简称就是ACID;
**原子性:**一系列的操作整体不可拆分,要么同时成功,要么同时失败
**一致性:**数据在事务的前后,业务整体一致。转账。A:1000; B:1000;转200 事务成功; A: 800 B: 1200
**隔离性:**事务之间互相隔离。
**持久性:**一旦事务成功,数据一定会罗盘到数据库
在上面的下订单,减库存,减积分这三个对数据库的操作是同一个事务,要么同时成功或者同时失败,本地事务指的这三个操作用的是同一个数据库,并且建立的数据库连接也是同一个,
一个事务开始,代表一下的所有操作都在同一个数据库连接里面,本地事务特别简单Spring提供非常便捷的 @Transactional
在使用事务的时候要注意事务的隔离级别
事务的隔离级别
(读未提交)READ UNCOMMITTED: 该隔离级别的事务会读到其它未提交事务的数据,此现象也称之为脏读。
(读提交)READ COMMITTED : 一个事务可以读取另一个已提交的事务,多次读取会造成不一样的结果,此现象称为不可重复读问题, Oracle和SQL Server的默认隔离级别。
(可重复读)REPEATABLE READ: 该隔离级别是MySQL默认的隔离级别,在同一个事务里, select的结果是事务开始时时间点的状态,因此,同样的select操作读到的结果会是一致的,但是,会有幻读现象。MysaL的InnoDB引擎可以通过next-key locks机制(参考下文"行锁的算法"一节)来避免幻读。
(序列化)SERIALIZABLE: 在该隔离级别下事务都是串行顺序执行的, MysQL,数据库的InnoDB引擎会给读操作隐式加一把读共享锁,从而避免了脏读、不可重读复读和幻读问题。
隔离级别越高,数据库并发读写的能力越弱!
强—>弱
序列化>可重复读>读提交>读未提交
//注解可以直接调整当前事务的隔离级别
@Transactional(isolation = Isolation.READ_COMMITTED)
@Transactional(isolation = Isolation.DEFAULT)
@Transactional(isolation = Isolation.READ_UNCOMMITTED)
@Transactional(isolation = Isolation.REPEATABLE_READ)
@Transactional(isolation = Isolation.SERIALIZABLE)
事务的传播行为
PROPAGATION-REQUIRED: 如果当前没有事务,就创建一个新事务,如果当前存在事务,就加入该事务,该设置是最常用的设置。
PROPAGATION-SUPPORTS: 支持当前事务,如果当前存在事务,就加入该事务,如果当前不存在事务,就以非事务执行。
PROPAGATION_MANDATORY: 支持当前事务,如果当前存在事务,就加入该事务,如果当前不存在事务,就抛出异常。
PROPAGATION-REQUIRES_NEW: 创建新事务,无论当前存不存在事务,都创建新事务。
PROPAGATION-NOT-SUPPORTED: 以非事务方式执行操作,如果当前存在事务,就把当前事务挂起。
PROPAGATION-NEVER: 以非事务方式执行,如果当前存在事务,则抛出异常。
PROPAGATION-NESTED: 如果当前存在事务,则在嵌套事务内执行。如果当前没有事务,执行与PROPAGATION REQUIRED类似的操
@Transactional(timeout = 30)//a事务30秒超时回滚
public void a(){
b();//和a公用同一个事务和a同时成功同时失败
c();//不和a公用同一个事务和a没有半毛钱关系
int i=10/0;//这里手动异常,那么a必然回滚,b和a在同一个事务里面,那么也会回滚,但是c没有和a在同一个事务里面,所以无法回滚
}
@Transactional(propagation = Propagation.REQUIRED,timeout = 2)
//如果当前没有事务,就创建一个新事务,如果当前存在事务,就加入该事务,该设置是最常用的设置。
//这里也设置了超时时间,但是由于和a公用同一个事务,所以设置无法生效
public void b(){
}
@Transactional(propagation = Propagation.REQUIRES_NEW)//创建新事务,无论当前存不存在事务,都创建新事务。
public void c(){
}
上面这是伪代码,这个伪代码是为了方便解释事务的传播行为的,但是实际上这些b、c的事务是无法生效的,这也是SpringBoot事务的一个坑
解释这个坑: 原因就是b、c事务是在同一个service里面的事务,而且a调用额是同一个service里面的b、c方法,b、c方法设置任何的事务都是无效的,都是和a公用同一个事务的,事务最大的特点就是使用代理对象来控制的,如果b、c事务方法是其他的service的话就可以,入bService. b(); cService.c();
**注意:**这里前往不要自己的service注入使用 @Autowired将自己的service注入进来,否则会循环应用
本地事务失效解决方案:
1.你也可以直接将这个b方法和c方法写到其他的service中
2.使用aop实现动态代理
1.导入AOP依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
主要是使用aspectj动态代理,这个的动态代理更强大,不是用aspectj动态代理的话默认使用jdk自带的
2.主启动类开启aspectj动态代理
@EnableAspectJAutoProxy
开启后,以后所有的动态代理都是由aspectj来实现代理的,aspectj创建的动态代理的好处就是即使没有接口也能实现动态代理,使用jdk的动态代理必须要有接口
3.暴露动态代理对象
@EnableAspectJAutoProxy(exposeProxy = true)
4.本类互相调用对象
@Transactional(timeout = 30)//a事务30秒超时回滚
public void a(){
YouService youService =(YouService )AopContext.currentProxy();
youService .b();//和a公用同一个事务和a同时成功同时失败
youService .c();//不和a公用同一个事务和a没有半毛钱关系
int i=10/0;//这里手动异常,那么a必然回滚,b和a在同一个事务里面,那么也会回滚,但是c没有和a在同一个事务里面,所以无法回滚
}
演示代码
controller
@Autowired
private UserService userService;
@PostMapping("/save")
public String save(){
userService.testT();
return "ok";
}
@PostMapping("/readUncommitted")//读未提交
public String readUncommitted(){
userService.readUncommitted();
return "ok";
}
@PostMapping("/readCommitted")//读提交
public String readCommitted(){
userService.readCommitted();
return "ok";
}
@PostMapping("/repeatableRead")//可重复读
public String repeatableRead(){
userService.repeatableRead();
return "ok";
}
@PostMapping("/serializable")//序列化
public String serializable(){
userService.serializable();
return "ok";
}
@PostMapping("/add")//测试事务开启后添加数据
public String add(){
userService.add();
return "ok";
}
service
@Transactional
@Override
public void add() {
UserEntity userEntity=new UserEntity();
String uuid= String.valueOf(UUID.randomUUID());
userEntity.setUsername(uuid);
boolean res=this.save(userEntity);
}
@Transactional
@Override
public boolean testT() {//模拟报错事务回滚
UserEntity userEntity=new UserEntity();
String uuid= String.valueOf(UUID.randomUUID());
userEntity.setUsername(uuid);
boolean res=this.save(userEntity);
log.info(String.valueOf(res));
try {
Thread.sleep(60000);
} catch (InterruptedException e) {
e.printStackTrace();
}
int i=10/0;
return false;
}
@Transactional(isolation = Isolation.READ_UNCOMMITTED)
@Override
public void readUncommitted() {
log.info("读未提交");
//在testT方法执行完save后,线程随眠后,方法还没执行完,事务也没提交
//这里的事务隔离级别是读未提交,所以这里是能直接读取到休眠是未提交的数据的
getAll();
}
@Transactional(isolation = Isolation.READ_COMMITTED)
@Override
public void readCommitted () {
log.info("读提交");
//在testT方法执行完save后,线程随眠后,方法还没执行完,事务也没提交
//这里的事务隔离级别是读提交,所以这里是无法读取到休眠是未提交的数据的
getAll();
}
@Transactional(isolation = Isolation.REPEATABLE_READ)
@Override
public void repeatableRead() {
for (int i=0;i<20;i++){
try {
log.info("可重复读");
//在testT方法执行完save后,线程随眠后,方法还没执行完,事务也没提交
//并且在repeatableRead方法进入后循环后,每隔一秒开始读取数据
//这里的事务隔离级别是可重复读,读取到的数据是不会改变的
//哪怕一直频繁调用add添加数据的方法,添加数据成功,并且Mysql客户端也能看到数据
//但是这里可重复读,只能感知到事务开启时数据库中的数据情况,
//进入事务后的数据便更了是无法感知的
Thread.sleep(1000);
getAll();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
@Transactional(isolation = Isolation.SERIALIZABLE)
@Override
public void serializable() {
log.info("序列化");
getAll();
}
public void getAll(){
List<UserEntity> userList=this.baseMapper.selectList(null);
log.info("获取到的数据"+userList.size());
}