大事务提交优化

经常性的报死锁异常,经常性的主从延迟......通过报错信息按图索骥,发现代码是这样的。

 

这是一段商品发布的逻辑,我们可以看到参数校验、查询、最终的insert以及update全部揉在一个事务中。遇到批量发布商品的时候就经常出现问题了,数据库主从延迟是肯定少不了的。

开启优化

其实像上述小猫遇到的这种状况我们就称其为大事务,那么我们就大概有这么一个定义。我们将执行时间长,并且操作数据比较多的事务叫做大事务。

大事务产生的原因

在我们日常开发过程中,其实经常会遇到大事务,老猫总结了一下,往往原因其实总结下来有这么几点(当然存在纰漏的地方,也欢迎大家评论区留言补充)

  1. 一次性操作的数据量确实多,大量的锁竞争,比如批量操作这种行为。
  2. 事务粒度过大,代码中的 @Transactional使用不当,其他非DB操作比较多,耗时久。比如调用RPC接口,在例如上述小猫遇到的check逻辑甚至都揉在一起等等。
造成的影响

那么大事务造成的影响又是什么呢?

  1. 从开发者的角度来看的话,部分大事务必定对应的复杂的业务逻辑,代码封装事务拆解不合理,研发侧维护困难,维护成本高。
  2. 从最终系统以及运维角度来看
    • 出现了死锁。
    • 造成了主从延迟。
    • 大事务消耗更多的磁盘空间,回滚成本高。
    • 大事务发生的过程中,由于连接池持续被打开,很容易造成数据库连接池被沾满。
    • 接口响应慢导致接口超时,甚至导致服务不可用等等
      (欢迎大家补充)
优化方案

大事务既然有这么多坑,那么我们来看一下我们日常开发过程中,应该如何做到尽量规避呢?老猫整理了以下几种优化方法。

  1. 降低事务颗粒度,大事务拆解小事务
    • 编程式事务代替@Transactional。
    • 非update以及insert动作外移。
  2. 大数据量一次性提交尽可能拆解分批处理。
  3. 拆解原始事务,异步化处理。
降低事务颗粒度

1、我们对@Transactional的事务粒度把控不好,有时候如果使用不当的话事务功能可能会失效,如果经验不足,很难排查,那么我们不如直接使用粗细粒度更好把控的编程式事务。TransactionTemplate。这样的话咱们的优化代码就可以写好才能如下方式。

@Autowired
private TransactionTemplate transactionTemplate;
public boolean publishProduct(PublishProductRequest request) {
externalSellerAuthorizeService.checkAuthorizeValid(request.getSellerId(),request.getThirdCategoryId(),request.getBrandId());
......
transactionTemplate.execute((status) -> {
try{
//执行insert
productDao.insert(productDO);
productDescDao.insert(productDescDO);
....
//其他insert以及update操作
}catch (Exception e) {
//回滚
status.setRollbackOnly();
return true;
}
return false;
});
return true;
}
非update以及insert动作外移。

原始代码:

@Transactional(rollbackFor=Exception.class)
public void save(Req req) {
checkParam(req);
saveData1(req);
updateData2(req);
}
private void checkParam(Req req){
Data1 data = selectData1();
Data2 data2 = selectData2();
if(data.getSomeThing() != STATUS_YES){
throw new BusinessTimeException(.....);
}
}

然后部分小伙伴就觉得外移么,如果不用@Transactional的情况,那直接这样不就行了么。

错误改造案例:

class ServiceAImpl implements ServiceA {
@Transactional(rollbackFor=Exception.class)
public void save(Req req) {
saveData1(req);
updateData2(req);
}
private void checkParam(Req req){
Data1 data = selectData1();
Data2 data2 = selectData2();
if(data.getSomeThing() != STATUS_YES){
throw new BusinessTimeException(.....);
}
}
public void save(Req req){
checkParam(req);
doSave(req);
}
}

这个例子是非常经典的错误,这种直接方法调用的做法事务不会生效,老猫以前也踩过这样的坑。因为 @Transactional 注解的声明式事务是通过 spring aop 起作用的,
而 spring aop 需要生成代理对象,直接方法调用使用的还是原始对象,所以事务不会生效。那么我们应该如何改造呢?我们看下正确的改造。

正确改造方案1,当然还是利用上面的TransactionTemplate:

@Autowired
private TransactionTemplate transactionTemplate;
public void save(Req req) {
checkParam(req);
transactionTemplate.execute((status) -> {
try{
saveData1(req);
updateData2(req);
....
//其他insert以及update操作
}catch (Exception e) {
//回滚
status.setRollbackOnly();
return true;
}
return false;
});
}
private void checkParam(Req req){
Data1 data = selectData1();
Data2 data2 = selectData2();
if(data.getSomeThing() != STATUS_YES){
throw new BusinessTimeException(.....);
}
}

正确改造方案2,把 @Transactional 注解加到新Service方法上,把需要事务执行的代码移到新方法中。

@Servcie
public class ServiceA {
@Autowired
private ServiceB serviceB;
private void checkParam(Req req){
Data1 data = selectData1();
Data2 data2 = selectData2();
if(data.getSomeThing() != STATUS_YES){
throw new BusinessTimeException(.....);
}
}
public void save(Req req) {
checkParam(req);
serviceB.save(req);
}
}
@Servcie
public class ServiceB {
@Transactional(rollbackFor=Exception.class)
public void save(Req req) {
saveData1(req);
updateData2(req);
}
}

正确改造方案3:将ServiceA 再次注入到自身

大数据量一次性提交尽可能拆解分批处理。

我们再来看大数量批量请求的场景,咱们具体来分析一下,假设上游系统存在一个批量导入2w的数据操作。如果我们读取到上游导入的数据,并且直接执行DB一次性执行肯定是不合适的。这种情况就需要我们对其请求的数据量做一个拆解。我们可以采用Lists.partition等等方式将数据拆成多个小的批量然后再进行入库操作处理。

@Servcie
public class ServiceA {
@Autowired
private ServiceB serviceB;
private void batchAdd(List<Long> inventorySkuIdList){
List<List<Long>> partition = Lists.partition(inventorySkuIdList, 1000);
for (List<Long> idList : partition) {
List<InventorySkuDO> inventorySkuDOList = inventorySkuDao.selectByIdList(idList, null);
if (CollectionUtils.isNotEmpty(inventorySkuDOList)) {
serviceB.doInsertUpdate(inventorySkuDOList);
}
}
}
}
@Servcie
public class ServiceB {
@Transactional(rollbackFor=Exception.class)
private void doInsertUpdate(List<InventorySkuDO> inventorySkuDOList){
for (InventorySkuDO inventorySkuDO : inventorySkuDOList) {
doInsert(inventorySkuDO);
doUpdate(inventorySkuDO)
}
}
}
拆解原始事务,异步化处理。

这种异步化处理的方案其实有两种方式进行异步化操作。尤其是涉及到第三方RPC调用或者HTTP调用的时候,这种方案就更加适合。

方案一,采用CompletableFuture异步编排特性,当业务流程比较长的时候,我们可以将一个大业务拆解成多个小的任务进行异步化执行。比如咱们有个批量支付的业务逻辑,因为整个流程是同步的,所以大概有了下面这样的流程。

对应转换成代码逻辑的话,大概是这样的:

 
void doBatchPay() {
CompletableFuture<Object> task1 = CompletableFuture.supplyAsync(() -> {
return "订单信息";
});
CompletableFuture<Object> task2 = CompletableFuture.supplyAsync(() -> {
try {
return doPay();
} catch (InterruptedException e) {
//log add
}
});
//task1、task2 执行完执行task3 ,需要感知task1和task2的执行结果
CompletableFuture<Object> future = task1.thenCombineAsync(task2, (t1, t2) -> {
return "邮件发送成功";
});
}

方案二,Mq异步化处理,还是针对上述业务逻辑,我们是否可以将最终的发送邮件的动作剥离出来,最终再去统一执行发送邮件。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/326855.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

日志高亮 | notepad

高亮显示日志 日志文件无法清晰看到关键问题所在? 看到一堆日志头疼?高亮日志可以清晰展示出日志的 ERROR级等各种等级的问题, 一下浏览出日志关键所在 tailspin 项目地址&#xff1a; https://githubfast.com/bensadeh/tailspin 使用Rust包管理器cargo安装 安装 - Cargo 手…

软件测试|深入学习 Docker Logs

简介 Docker 是一种流行的容器化技术&#xff0c;它能够帮助用户将应用程序及其依赖项打包成一个可移植的容器。Docker logs 是 Docker 提供的用于管理容器日志的命令&#xff0c;本文将深入学习 Docker logs 的使用和管理&#xff0c;帮助用户更好地监测和解决容器问题。 Do…

box-shadow参数学习及渲染过程研究

参数定义 CSS 的 box-shadow 属性用于在元素的框架周围添加阴影效果。它可以接受多个由逗号分隔的阴影效果&#xff0c;每个阴影效果由以下几部分组成&#xff1a; h-offset&#xff1a;水平阴影的位置。正值将阴影向右移动&#xff0c;负值将阴影向左移动。v-offset&#xf…

【Filament】基于物理的光照(PBR)

1 前言 自定义Blinn Phong光照模型中实现了基础的自定义光照&#xff0c;与现实的光照还是有些差别&#xff0c;本文将实现更逼真的光照效果&#xff0c;即基于物理的光照&#xff08;PBR&#xff09;。 读者如果对 Filament 不太熟悉&#xff0c;请回顾以下内容。 Filament环…

debug mccl 02 —— 环境搭建及初步调试

1, 搭建nccl 调试环境 下载 nccl 源代码 git clone --recursive https://github.com/NVIDIA/nccl.git 只debug host代码&#xff0c;故将设备代码的编译标志改成 -O3 (base) hipperhipper-G21:~/let_debug_nccl/nccl$ git diff diff --git a/makefiles/common.mk b/makefiles/…

scanf函数和printf函数

1.scanf函数 int scanf ( const char * format, ... );函数功能&#xff1a; 从键盘读取数据如果读取成功&#xff0c;返回读取到的数据个数如果读取失败&#xff0c;返回EOF 不常见的读取格式&#xff1a; %md -->读取m个宽度的数据 int main() {int n 0;scanf("%4d&…

安装阿里云CLI之配置阿里云凭证信息

有时候需要再主机上通过 OpenAPI 的调用访问阿里云&#xff0c;并完成控制&#xff0c;此时就需要在服务器上安装阿里云CLI&#xff0c;并完成账号的设置。 1. 登录阿里云创建账号 1.1 点击阿里云头像 ——》 控制访问 ——》创建一个拥有DNS权限的用户 这个用户不用太多权限…

ATTCK视角下的信息收集:主机发现

目录 1、利用协议主动探测主机存活 利用ICMP发现主机 利用ARP发现主机 利用NetBIOS协议发现主机 利用TCP/UDP发现主机 利用DNS协议发现主机 利用PRC协议发现主机程序 2、被动主机存活检测 利用Browser主机探测存活主机 利用ip段探测主机存活 利用net命令探测主机存活…

Redis实现订单超时自动关闭真的好吗,MQ更具性价比

由于Redis具有过期监听的功能&#xff0c;于是就有人拿它来实现订单超时自动关闭的功能&#xff0c;但是这个方案并不完美。今天来聊聊11种实现订单超时自动关闭的方案&#xff0c;总有一种适合你&#xff01;这些方案并没有绝对的好坏之分&#xff0c;只是适用场景的不大相同。…

2024--Django平台开发-Web框架和Django基础(二)

day02 Web框架和Django基础 今日概要&#xff1a; 网络底层引入&#xff0c;到底什么是web框架&#xff1f;常见web框架对比django快速上手&#xff08;创建网站&#xff09;常见操作&#xff1a;虚拟环境、django项目、多app应用、纯净版逐点剖析&#xff1a;路由、视图、模…

【Linux 内核源码分析】关于Linux内核源码目录结构

Linux内核源码采用树形结构。功能相关的文件放到不同的子目录下面&#xff0c;使程序更具有可读行。 使用Source Insight打开源码&#xff0c;如下图所示&#xff0c;可以看到源码是树形结构。 目录含义描述arch存放与体系结构相关的代码&#xff0c;包括不同硬件平台的特定代…

聚类分析 | Matlab实现基于RIME-DBSCAN的数据聚类可视化

聚类分析 | Matlab实现基于RIME-DBSCAN的数据聚类可视化 目录 聚类分析 | Matlab实现基于RIME-DBSCAN的数据聚类可视化效果一览基本介绍程序设计参考资料 效果一览 基本介绍 1.聚类分析 | Matlab实现基于RIME-DBSCAN的数据聚类可视化&#xff08;完整源码和数据) 2.多特征输入&…