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

开心一刻

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

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

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

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

又过了几秒钟,厕间门缝塞出一张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 QueueKafkaRacketMQRabbitMQActiveMQRedis 也能实现消息队列的功能,用这些组件实现上述功能的时候,都有可能出现类似问题

异步处理

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

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 事务消息是方案之一

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

总结

  1. 事务最小化原则

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

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

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

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

    推荐 2 种做法

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