结合实际业务分析事务失效场景

目录

一.业务场景

二.事务的优化

三,事务失效

必要条件

纠错

四,问题总结

五,问题解决


一.业务场景

当前正在做一个图片上传的功能。

图片上传的流程为:

1.前端上传图片,请求服务

2.媒资服务将图片文件上传文件系统Minio

3.构建文件信息(图片地址和其他属性信息)保存到数据库

二.事务的优化

在2中涉及到了网络连接,这个过程是比较耗时的。我们知道,数据库连接是有限的,大概就几十个,如果一个事务长时间无法结束,会直接导致系统崩溃。

所以需要尽可能缩小事务粒度,在本项目中,其实只需要在3上面加事务,所以我们将两个过程拆解开。

可以看到,只在addMediaFilesToDb这个写入数据库的方法上面加了事务控制。

代码如下:

@Override
    public UploadFileResultDto uploadFile(Long companyId, UploadFileParamsDto uploadFileParamsDto, byte[] bytes, String folder, String objectName) {
        //生成文件id,文件的md5值
        String fileId = DigestUtils.md5Hex(bytes);
        //文件名称
        String filename = uploadFileParamsDto.getFilename();
        //构造objectname
        if (StringUtils.isEmpty(objectName)) {
            objectName = fileId + filename.substring(filename.lastIndexOf("."));
        }
        if (StringUtils.isEmpty(folder)) {
            //通过日期构造文件存储路径
            folder = getFileFolder(new Date(), true, true, true);
        } else if (folder.indexOf("/") < 0) {
            folder = folder + "/";
        }
        //对象名称
        objectName = folder + objectName;
        MediaFiles mediaFiles = null;
        try {
            //上传至文件系统
            addMediaFilesToMinIO(bytes,bucket_Files,objectName);
            //写入文件表 向数据库中写入
//            mediaFiles = this.addMediaFilesToDb(companyId,fileId,uploadFileParamsDto,bucket_Files,objectName);
            mediaFiles = addMediaFilesToDb(companyId,fileId,uploadFileParamsDto,bucket_Files,objectName);
            UploadFileResultDto uploadFileResultDto = new UploadFileResultDto();
            BeanUtils.copyProperties(mediaFiles, uploadFileResultDto);
            return uploadFileResultDto;
        } catch (Exception e) {
            e.printStackTrace();
            throw new RuntimeException("上传过程中出错");
        }
    }

    /**
     * @description 将文件写入minIO
     * @param bytes  文件字节数组
     * @param bucket  桶
     * @param objectName 对象名称
     * @return void
     * @author Mr.M
     * @date 2022/10/12 21:22
     */
    public void addMediaFilesToMinIO(byte[] bytes, String bucket, String objectName) {
        //转为流
        ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(bytes);
        //扩展名
        String extension = null;
        if(objectName.indexOf(".")>=0){
            //文件扩展名
            extension = objectName.substring(objectName.lastIndexOf("."));
        }
        String  contentType = getMimeTypeByExtension(extension);
        try {
            PutObjectArgs putObjectArgs = PutObjectArgs.builder().bucket(bucket).object(objectName)
                    //-1表示文件分片按5M(不小于5M,不大于5T),分片数量最大10000,
                    .stream(byteArrayInputStream, byteArrayInputStream.available(), -1)
                    .contentType(contentType)
                    .build();

            minioClient.putObject(putObjectArgs);
        } catch (Exception e) {
            e.printStackTrace();
            throw new RuntimeException("上传文件到文件系统出错");
        }
    }

    private String getMimeTypeByExtension(String extension){
        String contentType = MediaType.APPLICATION_OCTET_STREAM_VALUE;
        if(StringUtils.isNotEmpty(extension)){
            ContentInfo extensionMatch = ContentInfoUtil.findExtensionMatch(extension);
            if(extensionMatch!=null){
                contentType = extensionMatch.getMimeType();
            }
        }
        return contentType;

    }

    /**
     * @description 将文件信息添加到文件表
     * @param companyId  机构id
     * @param fileMd5  文件md5值
     * @param uploadFileParamsDto  上传文件的信息
     * @param bucket  桶
     * @param objectName 对象名称
     * @return com.xuecheng.media.model.po.MediaFiles
     * @author Mr.M
     * @date 2022/10/12 21:22
     */
    @Transactional
    public MediaFiles addMediaFilesToDb(Long companyId,String fileMd5,UploadFileParamsDto uploadFileParamsDto,String bucket,String objectName){
        //从数据库查询文件
        MediaFiles mediaFiles = mediaFilesMapper.selectById(fileMd5);
        if (mediaFiles == null) {
            mediaFiles = new MediaFiles();
            //拷贝基本信息
            BeanUtils.copyProperties(uploadFileParamsDto, mediaFiles);
            mediaFiles.setId(fileMd5);
            mediaFiles.setFileId(fileMd5);
            mediaFiles.setCompanyId(companyId);
            mediaFiles.setUrl("/" + bucket + "/" + objectName);
            mediaFiles.setBucket(bucket);
            mediaFiles.setFilePath(objectName);
            mediaFiles.setCreateDate(LocalDateTime.now());
            mediaFiles.setAuditStatus("002003");
            mediaFiles.setStatus("1");
            //保存文件信息到文件表
            int insert = mediaFilesMapper.insert(mediaFiles);
//            System.out.println(1/0);
            if (insert < 0) {
                throw new RuntimeException("保存文件信息失败");
            }
        }
        return mediaFiles;
    }

接着来测试一下事务控制是否生效,这里在addMediaFilesToDb中模拟一个除0异常,调用上传图片的接口,很不幸的发现,似乎事务并没有生效,数据依旧插入到了数据库。

三,事务失效

必要条件

我们知道事务生效的两个必要条件

1.方法需要被代理对象来调用

2.方法上面加@Transactional注解(这个不可能出错吧)

纠错

接着打几个断点来调试一下这段程序,

1.在Controller调用uploadFile的位置打上断点,查看service对象是否是代理对象。

发现这里没啥问题。


  

2.进入service中addMediaFilesToDb方法的实际调用位置,可以看到this并不是一个代理对象。而是就是当前对象,并没有被AOP接管。那么在addMediaFilesToDb出现异常,就无法回滚。

四,问题总结

原理在于Spring的事务管理依靠的动态代理模式,当在同一个类中调用非事务方法,是不会生成代理对象的,自然也不会触发事务。

1)@Transactional Spring 事务注解是基于 Spring AOP 来实现的,而 Spring AOP 又是基于动态代理实现的;

2)动态代理分 JDK 动态代理和 Cglib 动态代理,Spring AOP 是基于 Cglib 动态代理实现的;

3)也就是说 Spring AOP 是在动态代理类中在切点位置切了一刀,去执行而外的处理;

4)非事务方法调事务方法时实际走的不是动态代理类,而是被代理类包裹的实际实现类中自己的方法,所以不会被 Spring AOP 切到,也就导致 @Transactional 事务注解不生效;

那怎么样让代理对象来调用事务方法呢?

五,问题解决

在service类中注入service对象,这个对象本身是代理对象,然后通过它来调用方法。

@Autowired
private MediaFileService mediaFileService;

@Override
    public UploadFileResultDto uploadFile(Long companyId, UploadFileParamsDto uploadFileParamsDto, byte[] bytes, String folder, String objectName) {
        //生成文件id,文件的md5值
        String fileId = DigestUtils.md5Hex(bytes);
        //文件名称
        String filename = uploadFileParamsDto.getFilename();
        //构造objectname
        if (StringUtils.isEmpty(objectName)) {
            objectName = fileId + filename.substring(filename.lastIndexOf("."));
        }
        if (StringUtils.isEmpty(folder)) {
            //通过日期构造文件存储路径
            folder = getFileFolder(new Date(), true, true, true);
        } else if (folder.indexOf("/") < 0) {
            folder = folder + "/";
        }
        //对象名称
        objectName = folder + objectName;
        MediaFiles mediaFiles = null;
        try {
            //上传至文件系统
            addMediaFilesToMinIO(bytes, bucket_Files, objectName);
            //写入文件表 向数据库中写入
//            mediaFiles = this.addMediaFilesToDb(companyId,fileId,uploadFileParamsDto,bucket_Files,objectName);
            mediaFiles = mediaFileService.addMediaFilesToDb(companyId, fileId, uploadFileParamsDto, bucket_Files, objectName);
            UploadFileResultDto uploadFileResultDto = new UploadFileResultDto();
            BeanUtils.copyProperties(mediaFiles, uploadFileResultDto);
            return uploadFileResultDto;
        } catch (Exception e) {
            e.printStackTrace();
            throw new RuntimeException("上传过程中出错");
        }
    }

六,事务失效场景

1.异常被捕获之后没有抛出(常见)

@Transactional
public void deleteUser() {
    userMapper.deleteUserA();
    try {
        int i = 1 / 0;
        userMapper.deleteUserB();
    } catch (Exception e) {
        e.printStackTrace();
    }
}

2.抛出非运行时一场

3.方法内部直接调用(本文所述)

4.新开启一个线程

如下的方式deleteUserA()也不会回滚,因为spring实现事务的原理是通过ThreadLocal把数据库连接绑定到当前线程中,新开启一个线程获取到的连接就不是同一个了。

@Transactional
public void deleteUser() throws MyException{
    userMapper.deleteUserA();
 try {
  //休眠1秒,保证deleteUserA先执行
        Thread.sleep(1000);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    new Thread(() -> {
        int i = 1/0;
        userMapper.deleteUserB();
    }).start();    
}

5.注解方法是private

6.数据库引擎不支持事务控制(emmmm)

相关推荐

  1. spring事务失效场景

    2024-04-12 20:06:01       37 阅读
  2. Spring事务失效场景

    2024-04-12 20:06:01       36 阅读
  3. Spring事务失效场景

    2024-04-12 20:06:01       36 阅读
  4. 在Spring中事务失效场景

    2024-04-12 20:06:01       48 阅读
  5. Spring中事务失效场景

    2024-04-12 20:06:01       38 阅读
  6. spring 事务失效的几种场景

    2024-04-12 20:06:01       26 阅读
  7. Spring Boot 事务管理(事务失效常见场景

    2024-04-12 20:06:01       42 阅读

最近更新

  1. docker php8.1+nginx base 镜像 dockerfile 配置

    2024-04-12 20:06:01       94 阅读
  2. Could not load dynamic library ‘cudart64_100.dll‘

    2024-04-12 20:06:01       101 阅读
  3. 在Django里面运行非项目文件

    2024-04-12 20:06:01       82 阅读
  4. Python语言-面向对象

    2024-04-12 20:06:01       91 阅读

热门阅读

  1. macOS idea配置mysql

    2024-04-12 20:06:01       38 阅读
  2. CSS中的类命名

    2024-04-12 20:06:01       32 阅读
  3. WPF 跨线程-Dispatcher:详解与示例

    2024-04-12 20:06:01       36 阅读
  4. C++Book对象数组初始化

    2024-04-12 20:06:01       34 阅读
  5. SpringBoot多数据源配置及使用

    2024-04-12 20:06:01       35 阅读
  6. AcWing 791. 高精度加法——算法基础课题解

    2024-04-12 20:06:01       39 阅读
  7. UI设计需要学习什么?我们应该掌握什么软件?

    2024-04-12 20:06:01       37 阅读
  8. 命名实体识别模型和分词的不同

    2024-04-12 20:06:01       36 阅读