当前位置: 首页 > news >正文

那些年不该放到事务中的操作,你实现过哪些

开心一刻

一天在公厕里,忽然听到厕间有人说话:朋友,有手纸吗

我翻了翻口袋:抱歉,没有

过了几秒钟,那人又问:朋友,有小块报纸吗

我无奈一笑,说到:对不起,没有,我只是来尿尿

又过了几秒钟,厕间门缝塞出一张10元人民币:朋友,能破成10张1块的吗

我默默的接过10元,掏出10个钢镚递了过去:朋友,10个够吗,不够我兜里还有

开心一刻

事务最小化

关于 事务最小化 原则

尽可能缩短事务的持续时间、减少事务内部的操作数量和锁定的数据量

我相信大家都知道,也都是这么执行的

哪些操作应该是一个事务,你们肯定也知道

但哪些操作不应该放到事务中,你们肯定容易忽略,为什么是 肯定,因为我也经常忽略

贱打

接下来,我们一起捋一下那些不该放到事务中的操作,来看看你们是不是也这么干过

消息发送

我们来看一个场景,上游系统完成数据持久化后,往下游推送一条消息

@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 请求从上游获取数据,然后进行相关处理

@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 QueueKafkaRacketMQRabbitMQActiveMQRedis 也能实现消息队列的功能,用这些组件实现上述功能的时候,都有可能出现类似问题

异步处理

在同个系统中,有些费时的操作需要异步处理,我们会这么实现

@Override
@Transactional(rollbackFor = Exception.class)
public boolean update(User user) {// 更新 Userboolean updated = this.saveOrUpdate(user);// TODO 其他持久化操作// 异步处理CompletableFuture.runAsync(() -> {// 费时处理});return updated;
}

如果费时处理中,去数据库中查询了当前 User,并且用到该 User 的相关数据,是不是就出现上述 消息发送 中的问题呢?

事务还未提交,异步处理中查询 User,查到的旧的,甚至查不到,这是不是 bug

你们可能会说,异步处理的时候把 User 作为参数传进去,不去数据库查,不就没问题呢?

你们说的非常对,但如果实现更 装逼 一些,引入 生产者与消费者 模式

事务中往队列添加消息,作为 生产者,费时处理作为 消费者

考虑到消息体的简单性,往往只会传递相关 id,消费者消费的过程中再通过这些 id 去查数据库

查到的数据是不是就可能是旧的,或者查不到?

RPC

分布式系统和微服务架构,肯定少不了远程调用

RPC : Remote Procedure Call

如果在事务中进行远程调用,例如

@Override
@Transactional(rollbackFor = Exception.class)
public boolean update(User user) {// 更新 Userboolean updated = this.saveOrUpdate(user);// TODO 其他持久化操作// 远程调用vip服务vipServer.update(user.getId());return updated;
}

vip 服务的 update 中,根据 id 查询 User 信息

是不是也会出现查到的是旧数据,甚至查不到?

拎出非事务操作

如果确定有些操作不需要放到一个事务中,一定要把这些操作从事务中拎出来,保证事务最小化

怎么拎,我已经替你们总结好

事务提交之后再执行某些操作 → 你有哪些实现方式?

如果涉及到分布式事务,那就要用分布式事务解决方案了,RocketMQ 事务消息是方案之一

关于 RocketMQ 事务消息的正确打开方式 → 你学废了吗

总结

  1. 事务最小化原则

    尽可能缩短事务的持续时间、减少事务内部的操作数量和锁定的数据量

    相关的查询也尽量不要放到事务中

    能够大大降低死锁概率,同时也能大大提高系统吞吐量和并发量

  2. 从事务中拎出非事务操作

    推荐 2 种做法

    1. 新增一层 Manager,先调事务操作,然后调非事务操作;Manager 中不要加事务注解
    2. TransactionSynchronizationManager,Spring 框架中提供的一个工具类,操作事务很方便
  3. RocketMQ 事务消息只能保证最终一致性,并不能做到事务回滚

http://www.wxhsa.cn/company.asp?id=1719

相关文章:

  • Java学习笔记
  • Redis容量评估模型
  • [译] 我最爱的PostgreSQL 18特性:虚拟生成列
  • nasm 的 Hello, world 在 Windows 10 x64 上
  • 实用指南:52.前端的后端模式:为每个客户端定制专属「管家服务」
  • Agilent 34401A台式万用表远程读表
  • Java 在大数据处理与人工智能中的应用
  • 马克思,本就是一位独立研究者
  • 产品二期,从GPT5规划开始
  • Redis能抗住百万并发的秘密
  • 接受 “未完成态”,是一种能力
  • 深入理解JNI、安全点与循环优化:构建高健壮性Java应用
  • 英语_阅读_fascinating facts about water_待读
  • AI自动化测试全攻略:从AI 自动化测试实战到AI 智能测试平台开发!
  • LG9691
  • 即时通讯小程序 - 实践
  • PHP serialize 序列化完全指南
  • CF2112D
  • CF2112C
  • CF342C
  • ICPC/XCPC 做题记录
  • LG9648
  • LG5689
  • 近五年 CSP NOIP 补题记录
  • CF2111C
  • 唐人日记
  • CF2111B
  • ABC394F
  • LG11793
  • ABC394G