Spring声明式事务业务bug

Spring 针对 Java Transaction API (JTA)、JDBC、Hibernate 和 Java Persistence API (JPA) 等事务 API,实现了一致的编程模型,而 Spring 的声明式事务功能更是提供了极其方便的事务配置方式,配合 Spring Boot 的自动配置,大多数 Spring Boot 项目只需要在方法上标记 @Transactional 注解,即可一键开启方法的事务性配置。

大多数业务开发同学都有事务的概念,也知道如果整体考虑多个数据库操作要么成功要么失败时,需要通过数据库事务来实现多个操作的一致性和原子性。但,在使用上大多仅限于为方法标记 @Transactional,不会去关注事务是否有效、出错后事务是否正确回滚,也不会考虑复杂的业务代码中涉及多个子业务逻辑时,怎么正确处理事务。

事务没有被正确处理,一般来说不会过于影响正常流程,也不容易在测试阶段被发现。但当系统越来越复杂、压力越来越大之后,就会带来大量的数据不一致问题,随后就是大量的人工介入查看和修复数据。

小心 Spring 的事务可能没有生效

在使用 @Transactional 注解开启声明式事务时, 第一个最容易忽略的问题是,很可能事务并没有生效。

实现下面的 Demo 需要一些基础类,首先定义一个具有 ID 和姓名属性的 UserEntity,也就是一个包含两个字段的用户表:

@Entity
@Data
public class UserEntity {
    @Id
    @GeneratedValue(strategy = AUTO)
    private Long id;
    private String name;

    public UserEntity() { }

    public UserEntity(String name) {
        this.name = name;
    }
}

为了方便理解,我使用 Spring JPA 做数据库访问,实现这样一个 Repository,新增一个根据用户名查询所有数据的方法:

@Repository
public interface UserRepository extends JpaRepository<UserEntity, Long> {
    List<UserEntity> findByName(String name);
}

定义一个 UserService 类,负责业务逻辑处理。如果不清楚 @Transactional 的实现方式,只考虑代码逻辑的话,这段代码看起来没有问题。

定义一个入口方法 createUserWrong1 来调用另一个私有方法 createUserPrivate,私有方法上标记了 @Transactional 注解。当传入的用户名包含 test 关键字时判断为用户名不合法,抛出异常,让用户创建操作失败,期望事务可以回滚:

@Service
@Slf4j
public class UserService {
    @Autowired
    private UserRepository userRepository;

    //一个公共方法供Controller调用,内部调用事务性的私有方法
    public int createUserWrong1(String name) {
        try {
            this.createUserPrivate(new UserEntity(name));
        } catch (Exception ex) {
            log.error("create user failed because {}", ex.getMessage());
        }
        return userRepository.findByName(name).size();
    }

    //标记了@Transactional的private方法
    @Transactional
    private void createUserPrivate(UserEntity entity) {
        userRepository.save(entity);
        if (entity.getName().contains("test"))
            throw new RuntimeException("invalid username!");
    }

    //根据用户名查询用户数
    public int getUserCount(String name) {
        return userRepository.findByName(name).size();
    }
}

 下面是 Controller 的实现,只是调用一下刚才定义的 UserService 中的入口方法 createUserWrong1

@Autowired
private UserService userService;

@GetMapping("wrong1")
public int wrong1(@RequestParam("name") String name) {
    return userService.createUserWrong1(name);
}

调用接口后发现,即便用户名不合法,用户也能创建成功。刷新浏览器,多次发现有十几个的非法用户注册。

这里给出 @Transactional 生效原则 1,除非特殊配置(比如使用 AspectJ 静态织入实现 AOP),否则只有定义在 public 方法上的 @Transactional 才能生效。原因是,Spring 默认通过动态代理的方式实现 AOP,对目标方法进行增强,private 方法无法代理到,Spring 自然也无法动态增强事务处理逻辑。

你可能会说,修复方式很简单,把标记了事务注解的 createUserPrivate 方法改为 public 即可。在 UserService 中再建一个入口方法 createUserWrong2,来调用这个 public 方法再次尝试:

public int createUserWrong2(String name) {
    try {
        this.createUserPublic(new UserEntity(name));
    } catch (Exception ex) {
        log.error("create user failed because {}", ex.getMessage());
    }
  return userRepository.findByName(name).size();
}

//标记了@Transactional的public方法
@Transactional
public void createUserPublic(UserEntity entity) {
    userRepository.save(entity);
    if (entity.getName().contains("test"))
        throw new RuntimeException("invalid username!");
}

测试发现,调用新的 createUserWrong2 方法事务同样不生效。这里,我给出 @Transactional 生效原则 2,必须通过代理过的类从外部调用目标方法才能生效。

Spring 通过 AOP 技术对方法进行增强,要调用增强过的方法必然是调用代理后的对象。我们尝试修改下 UserService 的代码,注入一个 self,然后再通过 self 实例调用标记有 @Transactional 注解的 createUserPublic 方法。设置断点可以看到,self 是由 Spring 通过 CGLIB 方式增强过的类:

  • CGLIB 通过继承方式实现代理类,private 方法在子类不可见,自然也就无法进行事务增强;
  • this 指针代表对象自己,Spring 不可能注入 this,所以通过 this 访问方法必然不是代理;

把 this 改为 self 后测试发现,在 Controller 中调用 createUserRight 方法可以验证事务是生效的,非法的用户注册操作可以回滚。

虽然在 UserService 内部注入自己调用自己的 createUserPublic 可以正确实现事务,但更合理的实现方式是,让 Controller 直接调用之前定义的 UserService 的 createUserPublic 方法,因为注入自己调用自己很奇怪,也不符合分层实现的规范: 

@GetMapping("right2")
public int right2(@RequestParam("name") String name) {
    try {
        userService.createUserPublic(new UserEntity(name));
    } catch (Exception ex) {
        log.error("create user failed because {}", ex.getMessage());
    }
    return userService.getUserCount(name);
}

我们再通过一张图来回顾下 this 自调用、通过 self 调用,以及在 Controller 中调用 UserService 三种实现的区别:

通过 this 自调用,没有机会走到 Spring 的代理类;后两种改进方案调用的是 Spring 注入的 UserService,通过代理调用才有机会对 createUserPublic 方法进行动态增强。

你可能还会考虑一个问题,这种实现在 Controller 里处理了异常显得有点繁琐,还不如直接把 createUserWrong2 方法加上 @Transactional 注解,然后在 Controller 中直接调用这个方法。这样一来,既能从外部(Controller 中)调用 UserService 中的方法,方法又是 public 的能够被动态代理 AOP 增强。

你可以试一下这种方法,但很容易就会踩第二个坑,即因为没有正确处理异常,导致事务即便生效也不一定能回滚。

事务即便生效也不一定能回滚

通过 AOP 实现事务处理可以理解为,使用 try…catch…来包裹标记了 @Transactional 注解的方法,当方法出现了异常并且满足一定条件的时候,在 catch 里面我们可以设置事务回滚,没有异常则直接提交事务。 

这里的“一定条件”,主要包括两点。

第一,只有异常传播出了标记了 @Transactional 注解的方法,事务才能回滚。在 Spring 的 TransactionAspectSupport 里有个 invokeWithinTransaction 方法,里面就是处理事务的逻辑。可以看到,只有捕获到异常才能进行后续事务处理:

try {
   // This is an around advice: Invoke the next interceptor in the chain.
   // This will normally result in a target object being invoked.
   retVal = invocation.proceedWithInvocation();
}
catch (Throwable ex) {
   // target invocation exception
   completeTransactionAfterThrowing(txInfo, ex);
   throw ex;
}
finally {
   cleanupTransactionInfo(txInfo);
}

第二,默认情况下,出现 RuntimeException(非受检异常)或 Error 的时候,Spring 才会回滚事务

打开 Spring 的 DefaultTransactionAttribute 类能看到如下代码块,可以发现相关证据,通过注释也能看到 Spring 这么做的原因,大概的意思是受检异常一般是业务异常,或者说是类似另一种方法的返回值,出现这样的异常可能业务还能完成,所以不会主动回滚;而 Error 或 RuntimeException 代表了非预期的结果,应该回滚:

/**
 * The default behavior is as with EJB: rollback on unchecked exception
 * ({@link RuntimeException}), assuming an unexpected outcome outside of any
 * business rules. Additionally, we also attempt to rollback on {@link Error} which
 * is clearly an unexpected outcome as well. By contrast, a checked exception is
 * considered a business exception and therefore a regular expected outcome of the
 * transactional business method, i.e. a kind of alternative return value which
 * still allows for regular completion of resource operations.
 * <p>This is largely consistent with TransactionTemplate's default behavior,
 * except that TransactionTemplate also rolls back on undeclared checked exceptions
 * (a corner case). For declarative transactions, we expect checked exceptions to be
 * intentionally declared as business exceptions, leading to a commit by default.
 * @see org.springframework.transaction.support.TransactionTemplate#execute
 */
@Override
public boolean rollbackOn(Throwable ex) {
   return (ex instanceof RuntimeException || ex instanceof Error);
}

接下来,我和你分享 2 个反例。

重新实现一下 UserService 中的注册用户操作:

  • 在 createUserWrong1 方法中会抛出一个 RuntimeException,但由于方法内 catch 了所有异常,异常无法从方法传播出去,事务自然无法回滚。
  • 在 createUserWrong2 方法中,注册用户的同时会有一次 otherTask 文件读取操作,如果文件读取失败,我们希望用户注册的数据库操作回滚。虽然这里没有捕获异常,但因为 otherTask 方法抛出的是受检异常,createUserWrong2 传播出去的也是受检异常,事务同样不会回滚。
@Service
@Slf4j
public class UserService {
    @Autowired
    private UserRepository userRepository;
    
    //异常无法传播出方法,导致事务无法回滚
    @Transactional
    public void createUserWrong1(String name) {
        try {
            userRepository.save(new UserEntity(name));
            throw new RuntimeException("error");
        } catch (Exception ex) {
            log.error("create user failed", ex);
        }
    }

    //即使出了受检异常也无法让事务回滚
    @Transactional
    public void createUserWrong2(String name) throws IOException {
        userRepository.save(new UserEntity(name));
        otherTask();
    }

    //因为文件不存在,一定会抛出一个IOException
    private void otherTask() throws IOException {
        Files.readAllLines(Paths.get("file-that-not-exist"));
    }
}

Controller 中的实现,仅仅是调用 UserService 的 createUserWrong1 和 createUserWrong2 方法,这里就贴出实现了。这 2 个方法的实现和调用,虽然完全避开了事务不生效的坑,但因为异常处理不当,导致程序没有如我们期望的文件操作出现异常时回滚事务。

现在,我们来看下修复方式,以及如何通过日志来验证是否修复成功。针对这 2 种情况,对应的修复方法如下。

第一,如果你希望自己捕获异常进行处理的话,也没关系,可以手动设置让当前事务处于回滚状态:

@Transactional
public void createUserRight1(String name) {
    try {
        userRepository.save(new UserEntity(name));
        throw new RuntimeException("error");
    } catch (Exception ex) {
        log.error("create user failed", ex);
        TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
    }
}

第二,在注解中声明,期望遇到所有的 Exception 都回滚事务(来突破默认不回滚受检异常的限制):

@Transactional(rollbackFor = Exception.class)
public void createUserRight2(String name) throws IOException {
    userRepository.save(new UserEntity(name));
    otherTask();
}

在这个例子中,我们展现的是一个复杂的业务逻辑,其中有数据库操作、IO 操作,在 IO 操作出现问题时希望让数据库事务也回滚,以确保逻辑的一致性。在有些业务逻辑中,可能会包含多次数据库操作,我们不一定希望将两次操作作为一个事务来处理,这时候就需要仔细考虑事务传播的配置了,否则也可能踩坑。

请确认事务传播配置是否符合自己的业务逻辑

有这么一个场景:一个用户注册的操作,会插入一个主用户到用户表,还会注册一个关联的子用户。我们希望将子用户注册的数据库操作作为一个独立事务来处理,即使失败也不会影响主流程,即不影响主用户的注册。

接下来,我们模拟一个实现类似业务逻辑的 UserService:

@Autowired
private UserRepository userRepository;

@Autowired
private SubUserService subUserService;

@Transactional
public void createUserWrong(UserEntity entity) {
    createMainUser(entity);
    subUserService.createSubUserWithExceptionWrong(entity);
}

private void createMainUser(UserEntity entity) {
    userRepository.save(entity);
    log.info("createMainUser finish");
}

SubUserService 的 createSubUserWithExceptionWrong 实现正如其名,因为最后我们抛出了一个运行时异常,错误原因是用户状态无效,所以子用户的注册肯定是失败的。我们期望子用户的注册作为一个事务单独回滚,不影响主用户的注册,这样的逻辑可以实现吗?

@Service
@Slf4j
public class SubUserService {

    @Autowired
    private UserRepository userRepository;

    @Transactional
    public void createSubUserWithExceptionWrong(UserEntity entity) {
        log.info("createSubUserWithExceptionWrong start");
        userRepository.save(entity);
        throw new RuntimeException("invalid status");
    }
}

我们在 Controller 里实现一段测试代码,调用 UserService:

@GetMapping("wrong")
public int wrong(@RequestParam("name") String name) {
    try {
        userService.createUserWrong(new UserEntity(name));
    } catch (Exception ex) {
        log.error("createUserWrong failed, reason:{}", ex.getMessage());
    }
    return userService.getUserCount(name);
}

调用后可以在日志中发现如下信息,很明显事务回滚了,最后 Controller 打出了创建子用户抛出的运行时异常:

[22:50:42.866] [http-nio-45678-exec-8] [DEBUG] [o.s.orm.jpa.JpaTransactionManager       :555 ] - Rolling back JPA transaction on EntityManager [SessionImpl(103972212<open>)]
[22:50:42.869] [http-nio-45678-exec-8] [DEBUG] [o.s.orm.jpa.JpaTransactionManager       :620 ] - Closing JPA EntityManager [SessionImpl(103972212<open>)] after transaction
[22:50:42.869] [http-nio-45678-exec-8] [ERROR] [t.d.TransactionPropagationController:23  ] - createUserWrong failed, reason:invalid status

你马上就会意识到,不对呀,因为运行时异常逃出了 @Transactional 注解标记的 createUserWrong 方法,Spring 当然会回滚事务了。如果我们希望主方法不回滚,应该把子方法抛出的异常捕获了。

也就是这么改,把 subUserService.createSubUserWithExceptionWrong 包裹上 catch,这样外层主方法就不会出现异常了:

@Transactional
public void createUserWrong2(UserEntity entity) {
    createMainUser(entity);
    try{
        subUserService.createSubUserWithExceptionWrong(entity);
    } catch (Exception ex) {
        // 虽然捕获了异常,但是因为没有开启新事务,而当前事务因为异常已经被标记为rollback了,所以最终还是会回滚。
        log.error("create sub user error:{}", ex.getMessage());
    }
}

运行程序后可以看到如下日志:

1 [22:57:21.722] [http-nio-45678-exec-3] [DEBUG] [o.s.orm.jpa.JpaTransactionManager       :370 ] - Creating new transaction with name [org.geekbang.time.commonmistakes.transaction.demo3.UserService.createUserWrong2]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT
2 [22:57:21.739] [http-nio-45678-exec-3] [INFO ] [t.c.transaction.demo3.SubUserService:19  ] - createSubUserWithExceptionWrong start
3 [22:57:21.739] [http-nio-45678-exec-3] [DEBUG] [o.s.orm.jpa.JpaTransactionManager       :356 ] - Found thread-bound EntityManager [SessionImpl(1794007607<open>)] for JPA transaction
4 [22:57:21.739] [http-nio-45678-exec-3] [DEBUG] [o.s.orm.jpa.JpaTransactionManager       :471 ] - Participating in existing transaction
5 [22:57:21.740] [http-nio-45678-exec-3] [DEBUG] [o.s.orm.jpa.JpaTransactionManager       :843 ] - Participating transaction failed - marking existing transaction as rollback-only
6 [22:57:21.740] [http-nio-45678-exec-3] [DEBUG] [o.s.orm.jpa.JpaTransactionManager       :580 ] - Setting JPA transaction on EntityManager [SessionImpl(1794007607<open>)] rollback-only
7 [22:57:21.740] [http-nio-45678-exec-3] [ERROR] [.g.t.c.transaction.demo3.UserService:37  ] - create sub user error:invalid status
8 [22:57:21.740] [http-nio-45678-exec-3] [DEBUG] [o.s.orm.jpa.JpaTransactionManager       :741 ] - Initiating transaction commit
9 [22:57:21.740] [http-nio-45678-exec-3] [DEBUG] [o.s.orm.jpa.JpaTransactionManager       :529 ] - Committing JPA transaction on EntityManager [SessionImpl(1794007607<open>)]
10 [22:57:21.743] [http-nio-45678-exec-3] [DEBUG] [o.s.orm.jpa.JpaTransactionManager       :620 ] - Closing JPA EntityManager [SessionImpl(1794007607<open>)] after transaction
11 [22:57:21.743] [http-nio-45678-exec-3] [ERROR] [t.d.TransactionPropagationController:33  ] - createUserWrong2 failed, reason:Transaction silently rolled back because it has been marked as rollback-only
12 org.springframework.transaction.UnexpectedRollbackException: Transaction silently rolled back because it has been marked as rollback-only
...

需要注意以下几点:

  • 如第 1 行所示,对 createUserWrong2 方法开启了异常处理;
  • 如第 5 行所示,子方法因为出现了运行时异常,标记当前事务为回滚;
  • 如第 7 行所示,主方法的确捕获了异常打印出了 create sub user error 字样;
  • 如第 9 行所示,主方法提交了事务;
  • 奇怪的是,如第 11 行和 12 行所示,Controller 里出现了一个 UnexpectedRollbackException,异常描述提示最终这个事务回滚了,而且是静默回滚的。之所以说是静默,是因为 createUserWrong2 方法本身并没有出异常,只不过提交后发现子方法已经把当前事务设置为了回滚,无法完成提交。

这挺反直觉的。我们之前说,出了异常事务不一定回滚,这里说的却是不出异常,事务也不一定可以提交。原因是,主方法注册主用户的逻辑和子方法注册子用户的逻辑是同一个事务,子逻辑标记了事务需要回滚,主逻辑自然也不能提交了。

看到这里,修复方式就很明确了,想办法让子逻辑在独立事务中运行,也就是改一下 SubUserService 注册子用户的方法,为注解加上 propagation = Propagation.REQUIRES_NEW 来设置 REQUIRES_NEW 方式的事务传播策略,也就是执行到这个方法时需要开启新的事务,并挂起当前事务:

@Transactional(propagation = Propagation.REQUIRES_NEW)
public void createSubUserWithExceptionRight(UserEntity entity) {
    log.info("createSubUserWithExceptionRight start");
    userRepository.save(entity);
    throw new RuntimeException("invalid status");
}

运行测试程序看到如下结果,getUserCount 得到的用户数量为 1,代表只有一个用户也就是主用户注册完成了,符合预期:

重点回顾

今天,我针对业务代码中最常见的使用数据库事务的方式,即 Spring 声明式事务,与你总结了使用上可能遇到的三类坑,包括:

第一,因为配置不正确,导致方法上的事务没生效。我们务必确认调用 @Transactional 注解标记的方法是 public 的,并且是通过 Spring 注入的 Bean 进行调用的。

第二,因为异常处理不正确,导致事务虽然生效但出现异常时没回滚。Spring 默认只会对标记 @Transactional 注解的方法出现了 RuntimeException 和 Error 的时候回滚,如果我们的方法捕获了异常,那么需要通过手动编码处理事务回滚。如果希望 Spring 针对其他异常也可以回滚,那么可以相应配置 @Transactional 注解的 rollbackFor 和 noRollbackFor 属性来覆盖其默认设置。

第三,如果方法涉及多次数据库操作,并希望将它们作为独立的事务进行提交或回滚,那么我们需要考虑进一步细化配置事务传播方式,也就是 @Transactional 注解的 Propagation 属性。

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

openGauss + Datakit

openGauss Datakit 1. 简介1.1 openGauss1.2 Datakit 2. 环境准备2.1 支持系统2.2 安装包获取2.3 注意事项2.4 系统环境设置 3. openGauss 安装3.1 创建用户和组3.2 创建工作目录3.3 关闭HISTORY记录/关闭交换内存3.4 解压安装包3.5 安装3.6 启动数据库3.7 连接数据库3.8 添加…

数据库设计——DQL

D Q L \huge{DQL} DQL ⭐⭐⭐⭐⭐ DQL&#xff1a;数据库查询语言&#xff0c;用来查询数据库中的记录&#xff0c;非常的重要&#xff0c;对于数据库的操作修改相对来讲还是较少部分&#xff0c;绝大多数操作都是数据查询。 整体的语法结构&#xff1a; 基本查询 示例&#…

【Java】LockSupport原理与使用

LockSupport&#xff1a; 关键字段&#xff1a; private static final sun.misc.Unsafe UNSAFE;private static final long parkBlockerOffset; Unsafe&#xff1a;"魔法类"&#xff0c;较为底层&#xff0c;在LockSupport类中用于线程调度(线程阻塞、线程恢复等)。…

【Unity】如何在Unity中使用C#的NuGet 包资源

【背景】 Unity的脚本语言是C#&#xff0c;而C#有很多功能和能力可以通过nuget包提供。有没有办法把这些能力结合到Unity中一起使用呢&#xff1f;如果可以&#xff0c;那将大大扩展Unity中各类功能实现的便捷性。 【方法】 答案是&#xff1a;你可以&#xff01; 获取Nuge…

经典八股文之RocketMQ

核心概念 NameServer nameserver是整个rocketmq的大脑&#xff0c;是rocketmq的注册中心。broker在启动时向所有nameserver注册。生产者在发送消息之前先从 NameServer 获取 Broker 服务器地址列表(消费者一 样)&#xff0c;然后根据负载均衡算法从列表中选择一台服务器进行消…

SSM在线手机品牌商城----计算机毕业设计

项目介绍 该项目为前后台项目&#xff0c;分为普通用户与管理员两种角色&#xff0c;前台普通用户登录&#xff0c;后台管理员登录&#xff1b; 管理员角色包含以下功能&#xff1a; 管理员登录,用户管理,品牌管理,子品牌管理,商品管理,订单管理,留言板管理等功能。 用户角…

Note: A Journey Across Canada

A Journey Across Canada 一场横穿加拿大的旅行 across journey After a quiz last autumn, Kuang crossed the continent eastward to Toronto to visit his schoolmate, the distance measuring approximately 5000 kilometers. 去年秋天一次考试后&#xff0c;Kuang向东穿…

Ubuntu 安装 JMeter:为你的服务器配置做好准备

Apache JMeter 是一个开源的负载测试工具&#xff0c;可以用于测试静态和动态资源&#xff0c;确定服务器的性能和稳定性。在本文中&#xff0c;我们将讨论如何下载和安装 JMeter。 安装 Java&#xff08;已安装 Java 的此步骤可跳过&#xff09; 要下载 Java&#xff0c;请遵…

AI小冰入驻淘宝 将提供虚拟人陪伴服务

AI小冰正式入驻淘宝&#xff01; 据悉&#xff0c;小冰在淘宝开出了“小冰旗舰店”、以及手淘小程序“X Eva 克隆人的平行世界”&#xff0c;为消费者提供基于KOL虚拟人带来的陪伴服务体验。用户搜索“小冰旗舰店”就可以直达店铺进行选购。 ​小冰旗舰店的首批商品包括冰花直充…

【设计模式-5】抽象工厂模式的代码实现及使用场景

前面我们了解到工厂方法模式通过引入抽象工厂的概念&#xff0c;使得产品对象的创建可以依赖于具体工厂&#xff0c;但是这种设计模式最大的问题是会造成类的数量爆炸式增长。对于这个问题&#xff0c;抽象工厂模式通过引入两个新的概念&#xff1a;产品等级与产品簇&#xff0…

ant-design-vue 使用本地iconfont.js

createFromIconfontCN只能使用【在线资源】&#xff0c;但是在线资源存在不稳定的风险 有人提了issue&#xff0c;不过目前也没有解决&#xff0c;但是有人提出了一种新的的解决方案 参考链接&#xff1a; https://github.com/ant-design/ant-design/issues/16480 main.js im…

【UML建模】部署图(Deployment Diagram)

1.概述 部署图是一种结构图&#xff0c;用于描述软件系统在不同计算机硬件或设备上的部署和配置情况&#xff0c;以图形化的方式展示系统中组件、节点和连接之间的物理部署关系。 通过部署图&#xff0c;可以清晰地了解系统的物理结构和部署方式&#xff0c;包括系统组件和节…

prometheus grafana mysql监控配置使用

文章目录 前传bitnami/mysqld-exporter:0.15.1镜像出现了问题.my.cnf可以用这个"prom/mysqld-exporter:v0.15.0"镜像重要的事情mysql监控效果外传 前传 prometheus grafana的安装使用&#xff1a;https://nanxiang.blog.csdn.net/article/details/135384541 本文说…

软件测试|SQL AND和OR运算符解析

简介 在SQL&#xff08;Structured Query Language&#xff09;中&#xff0c;AND和OR是两个常用的逻辑运算符。它们用于组合条件来构建复杂的查询语句&#xff0c;帮助我们更精确地过滤和检索数据。本文将详细介绍SQL中的AND和OR运算符&#xff0c;包括其语法、用法以及使用时…

Matlab绘制动态心形线

1. 代码 for alpha0:0.1:30 x-1.8:0.001:1.8; y(x.^2).^(1/3)0.9*(3.3-x.^2).^(1/2).*sin(alpha*pi*x); plot(x,y,r-,LineWidth,1.2); set(gca,YGrid,on); axis([-3,3,-2,4]); text(-2,3.35,$f(x)x^{\frac{2}{3}}0.9(3.3-x^2)^{\frac{1}{2}}sin(\alpha\pi x)$,Interpreter,lat…

Python Gui图形化开发

PyQt5、Tkinter、Kivy等GUI工具&#xff0c;助你轻松构建Python应用。新手友好的PySimpleGUI&#xff0c;高交互性的PyForms&#xff0c;助你搭建理想用户界面。 学习编程&#xff0c;不仅要学习MySQL以及编程语言和并行架构之间的关系这类基础知识&#xff0c;还有一个重要的…

openGauss 5.0.0企业版一主一备安装部署

目录 一、环境准备 1. 华为云购买两台ECS 1.1查看openEuler版本&#xff0c;操作系统版本及CPU的制式是基础 1.2查看CPU模式 1.3操作系统环境准备 2. 集群配置XML文件准备&#xff1a; 2.1集群参数配置&#xff1a; 2.2主机参数配置&#xff1a; 2.3备机参数配置&…

静态网页设计——校园官网(HTML+CSS+JavaScript)

前言 声明&#xff1a;该文章只是做技术分享&#xff0c;若侵权请联系我删除。&#xff01;&#xff01; 使用技术&#xff1a;HTMLCSSJS 主要内容&#xff1a;对学校官网的结构进行模仿&#xff0c;对布局进行模仿。 主要内容 1、首页 首页以多个div对页面进行分割和布局…

Fiddler抓包工具之fiddler界面工具栏介绍

Fiddler界面工具栏介绍 &#xff08;1&#xff09;WinConfig&#xff1a;windows 使用了一种叫做“AppContainer”的隔离技术&#xff0c;使得一些流量无法正常捕获&#xff0c;在 fiddler中点击 WinConfig 按钮可以解除这个诅咒&#xff0c;这个与菜单栏 Tools→Win8 Loopback…

CSS 缩小旋转动画

<template><div class="container" @mouseenter="startAnimation" @mouseleave="stopAnimation"><!-- 旋方块 --><div class="box" :class="{ rotate-scale-down: isAnimating }"><!-- 元素内容…