Java服务_Spring事物与分布式事务

Java服务_Spring事物与分布式事务

一、Spring事务

1.事务的属性

事务具有ACID四个属性,这四种属性也可以看作是一种因果关系,A、I、D是手段,C是目的:

原子性(Atomicity):事务是一个不可分割的工作单位,事务中的操作要么都发生,要么都不发生。

一致性(Consistency):事务必须使数据库从一个一致性状态变换到另外一个一致性状态。

隔离性(Isolation):指一个事务的执行不能被其他事务干扰,即一个事务内部的操作及使用的数据对并发的其他事务是隔离的,并发执行的各个事务之间不能互相干扰。

持久性(Durability):一个事务一旦被提交,它对数据库中数据的改变就是永久性的,接下来的其他操作和数据库故障不应该对其有任何影响。

2.@Transactional注解

@Transactional注解是Spring提供的数据库事务快捷实现组件,本质是通过AOP实现的,可以与任何遵循了Java数据库连接(JDBC)规范且本身支持事务的数据库协同工作。@Transactional注解的常用参数如下:

rollbackFor

该属性用于设置需要进行回滚的异常类数组,当方法中抛出指定异常数组中的异常时,则进行事务回滚。例如: 1. 指定单一异常类:@Transactional(rollbackFor=RuntimeException.class) 2. 指定多个异常类:@Transactional(rollbackFor={RuntimeException.class, BusnessException.class})

isolation

该属性用于设置底层数据库的事务隔离级别,事务隔离级别介绍如下:

1)@Transactional(isolation = Isolation.READ_UNCOMMITTED)读取未提交数据(会出现脏读, 不可重复读) 基本不使用;

2)@Transactional(isolation = Isolation.READ_COMMITTED)读取已提交数据(会出现不可重复读和幻读);

3)@Transactional(isolation = Isolation.REPEATABLE_READ)可重复读(会出现幻读);

4)@Transactional(isolation = Isolation.SERIALIZABLE)串行化。

其中的脏读、幻读、不可重复读分别表示:

1)脏读 : 一个事务读取到另一事务未提交的更新数据;

2)不可重复读 : 在同一事务中, 多次读取同一数据返回的结果有所不同, 换句话说, 后续读取可以读到另一事务已提交的更新数据.。相反, “可重复读”在同一事务中多次读取数据时, 能够保证所读数据一样, 也就是后续读取不能读到另一事务已提交的更新数据;

3)幻读 : 一个事务读到另一个事务已提交的insert数据。

3.@Transactional注解失效的8种场景

1.访问权限问题 (只有public方法会生效)

如果方法的访问权限被定义成了private,这样会导致事务失效,spring要求被代理方法必须得是public的。

2.方法用final或static修饰,不会生效

如果方法被定义成了final或者static的,这样会导致事务失效。查看spring事务的源码,可以知道spring事务底层使用了aop,也就是通过jdk动态代理或者cglib,帮我们生成了代理类,在代理类中实现的事务功能。但如果某个方法用final或static修饰了,那么在它的代理类中,就无法重写该方法,也就无法添加事务功能。

3.同一个类中的方法直接内部调用,会导致事务失效

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Service
public class UserService {

@Autowired
private UserMapper userMapper;

public void add(UserModel userModel) {
userMapper.insertUser(userModel);
updateStatus(userModel);
}

@Transactional
public void updateStatus(UserModel userModel) {
doSameThing();
}
}

我们看到在事务方法add中,直接调用事务方法updateStatus。从前面介绍的内容可以知道,updateStatus方法拥有事务的能力是因为spring aop生成代理了对象,但是这种方法直接调用了this对象的方法,所以updateStatus方法不会生成事务。由此可见,在同一个类中的方法直接内部调用事务方法,会导致事务失效。

如果有些场景,确实想在同一个类的某个方法中,调用它自己的另外一个方法,那么有一些解决办法如下:

1)新建一个类,将事务方法与调用方法放在不同类中;

2)在该类中注入自己,spring ioc内部的三级缓存确保了不会出现循环依赖问题,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Servcie
public class ServiceA {
@Autowired
prvate ServiceA serviceA;

public void save(User user) {
queryData1();
queryData2();
serviceA.doSave(user);
}

@Transactional(rollbackFor=Exception.class)
public void doSave(User user) {
addData1();
updateData2();
}
}

4.事务方法所在类未被spring管理

用spring事务的前提是:对象要被spring管理,需要创建bean实例,如使用@Controller、@Service、@Component、@Repository等注解都可以。

5.多线程调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
@Slf4j
@Service
public class UserService {

@Autowired
private UserMapper userMapper;
@Autowired
private RoleService roleService;

@Transactional
public void add(UserModel userModel) throws Exception {
userMapper.insertUser(userModel);
new Thread(() -> {
roleService.doOtherThing();
}).start();
}
}

@Service
public class RoleService {

@Transactional
public void doOtherThing() {
System.out.println("保存role表数据");
}
}

事务方法add中,调用了事务方法doOtherThing,但是事务方法doOtherThing是在另外一个线程中调用的。这样会导致两个方法不在同一个线程中,获取到的数据库连接不一样,从而是两个不同的事务。如果doOtherThing方法中抛了异常,并不会被add方法捕捉到。

我们所说的同一个事务,其实是指同一个数据库连接,只有拥有同一个数据库连接才能同时提交和回滚。

6.数据库表不支持事务

比如mysql数据库的myisam引擎就不支持事务,所以就算配置了@Transactional也不会生效。

7.未开启事务

如果不是SpringBoot项目,则需要在applicationContext.xml文件中,手动配置Spring事务相关参数。

8.抛出错误异常

Spring事务在默认情况下只会回滚RuntimeException(运行时异常)和Error(错误),对于普通的Exception(非运行时异常),它不会回滚,比如常见的IOExeption和SQLException。对于非运行时异常则需要通过rollbackFor参数手动配置才可以使回滚生效。

二、分布式事务

随着微服务架构的流行,很多大型的业务流程被拆分成为了多个功能单一的基础服务,大家会根据业务的诉求在这些基础服务之上编写一些组合调用服务来满足业务诉求。但是随之就带来了分布式事务的问题,事务只能在一个微服务的一个方法中生效,无法对RPC调用的下游也生效。

1
2
3
4
5
6
7
8
9
10
11
12
@Service
public class UserService {

@Autowired
private RemoteService remoteService;

@Transactional
public void updateStatus(UserModel userModel) {
remoteService.updateStatus(userModel)
doSameThing();
}
}

如上述示例中,调用RPC服务的remoteService.updateStatus()方法没有问题,但是调用自身的doSameThing()方法出现报错抛出异常时,就会导致数据不一致的问题。这种问题往往比较致命,下游的remoteService.updateStatus()方法不一定支持幂等,那么失败重试可能会一直无法成功甚至引发更大的数据不一致问题。

业界也早就意识到了分布式事务的必要性,设计出了完美的补偿(TCC)和非完美补偿(Saga)等各种分布式事务协调协议,可以在一定程度上保障不同服务之间的数据一致性,但这些协议往往伴随着性能损失和复杂性提升。因此,我也思考了两个替代的编程思路,在尽量减少代码开发量的前提下避免分布式事务或降低它们对系统的影响:

1.在一个事务方法中将本服务的数据更新操作放在前面,RPC服务方法的调用放在后面,这样做可以避免上述问题本服务代码报错而RPC服务数据已更新的数据不一致问题,同样如果本服务不报错而RPC服务报错返回异常那么本服务事务也可以正常生效确保数据一致性。

2.将一个事务中的本服务数据更新部分实现幂等,我们不能相信其他人的系统,不能依赖下游实现了幂等,那我们自己就需要幂等,确保在本服务成功但是RPC服务失败的场景下,可以不断重试知道下游RPC服务成功,实现数据一致性。

定义驱动服务中的添加数据源接口就采用了类似思想,先更新本服务中的数据源、数据源策略相关信息,再调用下游的原子服务更新接口的EZD更新接口,也对本服务的数据更新方法checkAndInitDataSource()实现了幂等:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
try {
RedisUtil.lock(lockKey, 1000, 60);
boolean addRoute = routeDataSourceBO.getId() == null;
driveDataSourceRouteManageService.checkAndInitDataSource(erp, addRoute, routeDataSourceBO);
Map<Long, List<Long>> resultMap = driveDataSourceRouteManageService.routeAddOrUpdate(erp, addRoute, routeDataSourceBO);
driveDataSourceRouteManageService.synAtomicServiceAndEzd(routeDataSourceBO);
Long routeId = resultMap.keySet().stream().findFirst().get();
return success(routeId);
} catch (TimeoutException e) {
UmpUtil.makeAlarm(umpInfo, EXTERNAL_SERVICE_EXCEPTION_UMP_KEY, "数据源-集群同步外部系统超时");
throw new CommonException("数据源编辑接口调用超时");
} finally {
RedisUtil.unLock(lockKey);
}

参考文献

基于服务的分布式事务(上)