开心一刻
一天在公厕里,忽然听到厕间有人说话:朋友,有手纸吗
我翻了翻口袋:抱歉,没有
过了几秒钟,那人又问:朋友,有小块报纸吗
我无奈一笑,说到:对不起,没有,我只是来尿尿
又过了几秒钟,厕间门缝塞出一张10元人民币:朋友,能破成10张1块的吗
我默默的接过10元,掏出10个钢镚递了过去:朋友,10个够吗,不够我兜里还有
事务最小化
关于 事务最小化
原则
尽可能缩短事务的持续时间、减少事务内部的操作数量和锁定的数据量
我相信大家都知道,也都是这么执行的
哪些操作应该是一个事务,你们肯定也知道
但哪些操作不应该放到事务中,你们肯定容易忽略,为什么是 肯定
,因为我也经常忽略
接下来,我们一起捋一下那些不该放到事务中的操作,来看看你们是不是也这么干过
消息发送
我们来看一个场景,上游系统完成数据持久化后,往下游推送一条消息
java
@Override
@Transactional(rollbackFor = Exception.class)
public boolean update(User user) {
// 更新数据
boolean updated = this.saveOrUpdate(user);
// TODO 其他持久化操作
// 往下游发送消息
rabbitTemplate.convertAndSend(QSL_FANOUT_EXCHANGE, null, user.getUserId());
return updated;
}
下游收到消息后,通过 HTTP
请求从上游获取数据,然后进行相关处理
java
@Value("${qsl-front.url}")
private String frontUrl;
@Resource
private RestTemplate restTemplate;
@Override
@RabbitListener(queues = QSL_QUEUE)
public void onMessage(Message message, Channel channel) {
String userId = new String(message.getBody(), StandardCharsets.UTF_8);
log.info("收到front消息,userId={}", userId);
// 1、调接口查询数据
ResponseEntity<User> userResp = restTemplate.getForEntity(frontUrl.replace("{userId}", userId), User.class);
if (HttpStatus.OK != userResp.getStatusCode()) {
log.error("查询front接口失败,status = {}", userResp.getStatusCode());
// TODO 请求失败处理
} else {
User user = userResp.getBody();
log.info("front接口响应值:{}", user);
// TODO 用 user 数据进行业务处理
}
}
这代码有没有很眼熟,你们平时是不是也经常这么写?
我们来分析下,这代码是不是有 bug
有没有可能下游收到消息,然后通过 HTTP 请求上游查 User 数据时,上游事务还未提交?
如果有可能,那下游查到的 User 数据是不是有可能是旧的?
如果 User 数据是旧的,下游业务处理是不是就不对了?
上游 UPDATE
操作,下游查到的数据可能是旧的,如果上游是 INSERT
操作,下游是不是可能都查不到数据?
这代码确实有 bug
吖!
除了常规的
Message Queue
:Kafka
、RacketMQ
、RabbitMQ
、ActiveMQ
,Redis
也能实现消息队列的功能,用这些组件实现上述功能的时候,都有可能出现类似问题
异步处理
在同个系统中,有些费时的操作需要异步处理,我们会这么实现
java
@Override
@Transactional(rollbackFor = Exception.class)
public boolean update(User user) {
// 更新 User
boolean updated = this.saveOrUpdate(user);
// TODO 其他持久化操作
// 异步处理
CompletableFuture.runAsync(() -> {
// 费时处理
});
return updated;
}
如果费时处理中,去数据库中查询了当前 User,并且用到该 User 的相关数据,是不是就出现上述 消息发送
中的问题呢?
事务还未提交,异步处理中查询 User,查到的旧的,甚至查不到,这是不是 bug
?
你们可能会说,异步处理的时候把 User
作为参数传进去,不去数据库查,不就没问题呢?
你们说的非常对,但如果实现更 装逼
一些,引入 生产者与消费者
模式
事务中往队列添加消息,作为 生产者
,费时处理作为 消费者
考虑到消息体的简单性,往往只会传递相关 id
,消费者消费的过程中再通过这些 id 去查数据库
查到的数据是不是就可能是旧的,或者查不到?
RPC
分布式系统和微服务架构,肯定少不了远程调用
RPC : Remote Procedure Call
如果在事务中进行远程调用,例如
java
@Override
@Transactional(rollbackFor = Exception.class)
public boolean update(User user) {
// 更新 User
boolean updated = this.saveOrUpdate(user);
// TODO 其他持久化操作
// 远程调用vip服务
vipServer.update(user.getId());
return updated;
}
vip 服务的 update
中,根据 id 查询 User 信息
是不是也会出现查到的是旧数据,甚至查不到?
拎出非事务操作
如果确定有些操作不需要放到一个事务中,一定要把这些操作从事务中拎出来,保证事务最小化
怎么拎,我已经替你们总结好
如果涉及到分布式事务,那就要用分布式事务解决方案了,RocketMQ
事务消息是方案之一
总结
-
事务最小化原则
尽可能缩短事务的持续时间、减少事务内部的操作数量和锁定的数据量
相关的查询也尽量不要放到事务中
能够大大降低死锁概率,同时也能大大提高系统吞吐量和并发量
-
从事务中拎出非事务操作
推荐 2 种做法
- 新增一层
Manager
,先调事务操作,然后调非事务操作;Manager 中不要加事务注解 - TransactionSynchronizationManager,Spring 框架中提供的一个工具类,操作事务很方便
- 新增一层
-
RocketMQ 事务消息只能保证最终一致性,并不能做到事务回滚