基于RocketMQ实现分布式事务

前言

在上一篇文章Spring Boot自动装配原理以及实践我们完成了服务通用日志监控组件的开发,确保每个服务都可以基于一个注解实现业务功能的监控。
而本文我们尝试基于RocketMQ实现下单的分布式的事务。可能会有读者会有疑问,之前我们不是基于Seata完成了分布式事务,为什么我们还要用到RocketMQ呢?

我们的再来回顾一下我们下单功能大抵是做以下三件事情:

  1. 创建订单,将订单记录存到数据库中。
  2. 扣款,记录用户扣款后钱包所剩下的额度。
  3. 扣除商品库存,并发放商品。

我们将该场景放到高并发场景下,这个功能势必要考虑性能和可靠性问题,所以我们在业务需求清楚明了的情况下,就希望能有一种方式确保下单功能在高并发场景保证性能、可靠性。
SeataAT模式确实可以保证最终一致性,但由于需要用到undo_loglock_table等涉及数据持久化以及锁相关的操作,可能存在一定的性能问题。而且Seata一旦报错会直接回滚事务,不存在任何重试机制,对于我们这种付款下单的场景是非常不可取的。
RocketMQ实现分布式的方式是基于消息通信的,既确保了业务功能解耦保证了并发场景的性能,而且RocketMQ还对消息消费可靠性做了许多不错的优化,例如:失败重试、死信队列等,所以我们还是尝试使用RocketMQ来改良我们的下单分布式事务问题。

需求介绍以及实现思路

用户下单大抵需要在三个服务中完成:订单创建、钱包扣款、库存扣减等业务逻辑。这其中会跨域三个服务,分别是订单服务创建订单、账户服务扣款、商品服务扣减库存。

在这里插入图片描述

以我们业务为最终目标,RocketMQ实现分布式事务的原理是基于2PC的,流程大抵如下:

  1. 订单服务发送一个事务消息到消息队列,消息内容就是我们的订单信息,这里面包含用户账号、购买的产品代码、购买产品数量等数据。
  2. MQ收到half消息,并回复确认。
  3. 生产者(订单服务order-service)得知我们发送的消息已被收到,订单服务则执行本地事务并提交事务,即将订单数据插入数据库中。
  4. 生产者(订单服务order-service)完成本地事务的提交,告知MQ将事务消息commit,此时消费者就可以消费这条消息了,注意若生产者消费失败,则将消息rollback,一切就当没有发生过。
  5. 如果上述的消息是commit则将消息持久化到commitLog中,以便后续MQ宕机或者服务宕机后依然可以继续消费这条没有被消费的消息。
  6. (非必要步骤)若MQ长时间没有收到生产者的commit或者rollback的信号,则会主动找生产者索要当前消息状态。
  7. 消费者即我们的用户服务或者库存服务收到消息则执行本地事务并提交,若失败则会不断重试,直到达到上限则将消息存到死信队列中。

在这里插入图片描述

常见问题

什么是half消息

half消息即半消息,它和普通消息一样,都是存储在MQ中,唯一区别就是这个消息不会立马被消费者消费到。只有生产者本地事务成功并发送commit通知后,这个消息才会被提交到topic队列中后消费者拿到这个消息并进行消费。

如何发送half消息?

基于MQ事务消息的实现接口完成实现(具体后文会演示)。

为什么要先发送half消息再执行本地事务?先执行本地事务,成功后在发送不行吗?

先发送half消息的原因是为了尽可能确保生产者和消息队列通信正常,只有通信正常了才能确保生产者本地事务提交后发送的commit通知可以消息队列收到通知,从而将消息提交到topic队列中让消费者消费,由此保证分布式事务的可靠性。

如果mq收到half消息,准备发送success的消息给生产者,但因为网络波动导致生产者没有收到这个消息要怎么办?

这也就意味着生产者没有收到确认的通知,随后消息队列就会因为长时间没有收到生产者commit或者rollback的通知而去回调生产者的接口询问事务提交结果。

MQ没有收到生产者(订单服务)commit或者rollback信号我们如何回查?怎么提供回查的依据?

常规的做法就是建立一张表记录日志,只要我们订单信息插入成功就需要日志一下这条数据,所以我们必须保证订单数据插入和日志插入表中的原子性,这一点我们基于spring的事务注解即可实现。

如果生产者执行本地事务失败了怎么办?

首先将本地事务回滚,再向消息队列提交一个rollback的请求,对应的half消息就会回滚,而不会被消费者消费,保证最终一致性。

前面说的都是事务流程?这和事务消息如何保证数据最终一致性有什么关系?

生产者和消息队列事务流程可以确保生产者和消息队列写操作的一致性,确保写操作都是成功或者失败。只有保证两者正常通信,才能确保消费者可以消费MQ中的消息从而完成数据最终一致性。

消费者提交本地事务失败了怎么办?

我们都知道消息队列只能保证消息可靠性,而无法保证分布式事务的强一致性,出现这种情况,消息队列会进行N次重试,如果还是失败,则可以到死信队列中查看失败消息,然后通过补偿机制实现分布式事务最终一致性。

实践-基于RocketMQ实现分布式事务

部署RocketMQ

在编写业务代码之前,我们必须完成一下RocketMQ的部署,首先我们自然要下载一下RocketMQ,下载地址如下,笔者下载的是rocketmq-all-4.8.0-bin-release这个版本

https://rocketmq.apache.org/download/

完成完成后,我们将其解压到自定义的路径,并配置一个名为ROCKETMQ_HOME的环境变量,以笔者为例,因为mq存放在D:\myinstall\rocketmq,所以我们将这个路径配置到环境变量中。

在这里插入图片描述

完成环境变量配置后,我们到达mqbin目录先键入这条命令,启动nameserver

start mqnamesrv.cmd

如果弹窗输出下面这条结果,则说明mqNameServer启动成功。

Java HotSpot(TM) 64-Bit Server VM warning: Using the DefNew young collector with the CMS collector is deprecated and will likely be removed in a future release
Java HotSpot(TM) 64-Bit Server VM warning: UseCMSCompactAtFullCollection is deprecated and will likely be removed in a future release.
The Name Server boot success. serializeType=JSON

然后我们再键入下面这条命令启动broker

start mqbroker.cmd -n 127.0.0.1:9876 autoCreateTopicEnable=true

若弹窗输出下面所示的文字,则说明broker启动成功,自此mq就在windows环境部署成功了。我们就可以开始编码工作了。

The broker[DESKTOP-BI4ATFQ, 192.168.237.1:10911] boot success. serializeType=JSON and name server is 127.0.0.1:9876

服务引入MQ完成下单功能开发

服务引入RocketMQ依赖

完成RocketMQ部署之后,我们就可以着手编码工作了,首先我们要在在三个服务中引入RocketMQ的依赖,由于笔者的spring-boot版本比较老,所以这里笔者为了统一管理在父pom中指定了mq较新的版本号:

   <!--rocketmq--><!-- https://mvnrepository.com/artifact/org.apache.rocketmq/rocketmq-spring-boot-starter --><dependency><groupId>org.apache.rocketmq</groupId><artifactId>rocketmq-spring-boot-starter</artifactId><version>2.1.1</version></dependency>

然后我们分别对orderaccountproduct三个服务中引入依赖

 <dependency><groupId>org.apache.rocketmq</groupId><artifactId>rocketmq-spring-boot-starter</artifactId></dependency>
注册中心配置RocketMQ信息

由于我们的分布式事务涉及3个服务,而且mq的消费模式采用的是发布订阅模式,所以我们的生产者(order-service)和消费者(account-serivce)都配置为cloud-group

rocketmq:name-server: 127.0.0.1:9876producer:group: cloud-group

之所以没有没将消费者2(product-service)也配置到cloud-group中的原因也很简单,同一个消息只能被同一个消费者组中的一个成员消费,假如我们的将product-service配置到同一个消费者组中就会出现一条消息只能被一个Java服务消费。

在这里插入图片描述

对此我们实现思路有两种:

  1. 将服务都放到同一个消费者组,消费模式改为广播模式。
  2. product-service设置到别的消费者组中。

考虑后续扩展笔者选择方案2,设置到别的组中。

rocketmq:name-server: 127.0.0.1:9876producer:group: cloud-group2
创建消息日志表

我们在上文进行需求梳理时有提到一个MQServer没收到生产者本地事务执行状态的情况,所以我们在生产者在执行本地事务时,需要创建一张表记录生产者本地事务执行状态,建表SQL如下:

DROP TABLE IF EXISTS `rocketmq_transaction_log`;
CREATE TABLE `rocketmq_transaction_log` (`id` int(11) NOT NULL AUTO_INCREMENT,`transaction_id` varchar(50) DEFAULT NULL,`log` varchar(500) DEFAULT NULL,PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
完成order服务half消息发送、监听、回查回调逻辑

我们的订单服务需要做以下三件事:

  1. 发送half消息给MQ。
  2. half消息发送成功执行本地事务并记录日志。
  3. 告知MQ可以提交事务消息。

所以我们需要定义一下消息格式,对象类中必须包含订单号、产品编码、用户编码、购买产品数量等信息。

@NoArgsConstructor
@AllArgsConstructor
@Getter
@Setter
public class OrderDto {private static final long serialVersionUID = 1L;//设置主键自增,避免插入时没必要的报错@TableId(value = "ID", type = IdType.AUTO)private Integer id;/*** 订单号*/private String orderNo;/*** 用户编码*/private String accountCode;/*** 产品编码*/private String productCode;/*** 产品扣减数量*/private Integer count;/*** 余额*/private BigDecimal amount;/*** 本次扣减金额*/private BigDecimal price;
}

然后我们就可以编写控制层的代码了,通过获取前端传输的参数调用orderService完成half消息发送。

@PostMapping("/order/createByMQ")public ResultData<String> createByMQ(@RequestBody OrderDto orderDTO) {log.info("基于mq完成用户下单流程,请求参数: " + JSON.toJSONString(orderDTO));orderService.createByRocketMQ(orderDTO);return ResultData.success("基于mq完成用户下单完成");}

orderService的实现逻辑很简单,定义好消息设置消息头内容和消息载体的对象,通过sendMessageInTransaction方法完成半消息发送,需要了解一下消息的主题(topic)createByRocketMQ,只有订阅这个主题的消费者才能消费这条消息。

@Autowiredprivate RocketMQTemplate rocketMQTemplate;@Overridepublic void createByRocketMQ(OrderDto orderDto) {//创建half消息,消息内容为,告知account服务要退款给用户String transactionId = UUID.randomUUID().toString();Message<OrderDto> message = MessageBuilder.withPayload(orderDto).setHeader(RocketMQHeaders.TRANSACTION_ID, transactionId).setHeader("accountCode", orderDto.getAccountCode()).setHeader("productCode", orderDto.getProductCode()).setHeader("count", orderDto.getCount()).setHeader("amount", orderDto.getPrice().multiply(new BigDecimal(orderDto.getCount()))).build();//发送half消息rocketMQTemplate.sendMessageInTransaction("createByRocketMQ", message, orderDto);}

完成half消息发送之后,我们就必须知晓消息发送结果才能确定是否执行本地事务并提交,所以我们的订单服务必须创建一个监听器了解half消息的发送情况,executeLocalTransaction方法就是mq成功收到半消息后的回调函数,一旦我们得知消息成功发送之后,MQ就会执行这个方法,笔者通过这个方法获取消息头的参数创建订单对象,调用createOrderWithRocketMqLog完成订单的创建的本地事务成功的日志记录。

@Slf4j
@RocketMQTransactionListener
@RequiredArgsConstructor(onConstructor = @__(@Autowired))
public class OrderListener implements RocketMQLocalTransactionListener {private final IOrderService orderService;private final RocketmqTransactionLogMapper rocketMqTransactionLogMapper;/*** 监听到发送half消息,执行本地事务*/@Overridepublic RocketMQLocalTransactionState executeLocalTransaction(Message message, Object arg) {log.info("order执行本地事务");try {MessageHeaders headers = message.getHeaders();String amount = (String) headers.get("amount");Order order = Order.builder().accountCode((String) headers.get("accountCode")).amount(new BigDecimal(amount) ).productCode((String) headers.get("productCode")).count(Integer.valueOf(String.valueOf(headers.get("count")))).build();String transactionId = (String) headers.get(RocketMQHeaders.TRANSACTION_ID);orderService.createOrderWithRocketMqLog(order, transactionId);return RocketMQLocalTransactionState.COMMIT;} catch (Exception e) {log.error("创建订单失败,失败原因: " + e.getMessage(), e);return RocketMQLocalTransactionState.ROLLBACK;}}/*** 本地事务的检查,检查本地事务是否成功*/@Overridepublic RocketMQLocalTransactionState checkLocalTransaction(Message message) {MessageHeaders headers = message.getHeaders();//获取事务IDString transactionId = (String) headers.get(RocketMQHeaders.TRANSACTION_ID);log.info("检查本地事务,事务ID:{}", transactionId);//根据事务id从日志表检索QueryWrapper<RocketmqTransactionLog> queryWrapper = new QueryWrapper<>();queryWrapper.eq("transaction_id", transactionId);RocketmqTransactionLog rocketmqTransactionLog = rocketMqTransactionLogMapper.selectOne(queryWrapper);if (null != rocketmqTransactionLog) {return RocketMQLocalTransactionState.COMMIT;}return RocketMQLocalTransactionState.ROLLBACK;}
}

createOrderWithRocketMqLog做了两件事,分别是插入订单信息和创建消息日志,这里笔者用到了事务注解确保了两个操作的原子性。
这样一来,MQserver后续的回查逻辑完全可以基于RocketmqTransactionLog 进行判断,如果消息的事务id在表中存在,则说明生产者本地事务成功,反之就是失败。

  @Transactional(rollbackFor = RuntimeException.class)@Overridepublic void createOrderWithRocketMqLog(Order order, String transactionId) {order.setOrderNo(UUID.randomUUID().toString());orderMapper.insert(order);RocketmqTransactionLog log = RocketmqTransactionLog.builder().transactionId(transactionId).log("执行创建订单操作").build();rocketmqTransactionLogMapper.insert(log);}

补充一下基于MP生成的RocketmqTransactionLog 类代码

@TableName("rocketmq_transaction_log")
@ApiModel(value = "RocketmqTransactionLog对象", description = "")
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class RocketmqTransactionLog implements Serializable {private static final long serialVersionUID = 1L;@TableId(value = "ID", type = IdType.AUTO)private Integer id;private String transactionId;private String log;}
完成account、product监听事件

然后我们就可以实现用户服务和商品服务的监听事件了,一旦生产者提交事务消息之后,这几个消费者都会收到这个topic(主题)的消息,进而完成当前服务的业务逻辑。

先来看看实现扣款的用户服务,我们的监听器继承了RocketMQListener,基于@RocketMQMessageListener注解设置它订阅的主题为createByRocketMQ,一旦收到这个主题的消息时这个监听器就会执行onMessage方法,我们的逻辑很简单,就是获取消息的内容完成扣款,唯一需要注意的就是线程安全问题。我们的压测的情况下,单用户可能会频繁创建订单,在并发期间同一个用户的扣款消息可能同时到达扣款服务中,这就导致单位时间内扣款服务从数据库中查询到相同的余额,执行相同的扣款逻辑,导致金额少扣了。

在这里插入图片描述

所以我们必须保证扣款操作互斥和原子化,考虑到笔者当前项目环境是单体,所以就用简单的synchronized 关键字解决问题。

@Slf4j
@Service
@RocketMQMessageListener(topic = "createByRocketMQ", consumerGroup = "cloud-group")
@RequiredArgsConstructor(onConstructor = @__(@Autowired))
public class SubtracAmountListener implements RocketMQListener<OrderDto> {@Resourceprivate AccountMapper accountMapper;//强制转为runTimeException@SneakyThrows@Overridepublic void onMessage(OrderDto orderDto) {log.info("账户服务收到消息,开始消费");QueryWrapper<Account> query = new QueryWrapper<>();query.eq("account_code", orderDto.getAccountCode());//解决单体服务下线程安全问题synchronized (this){Account account = accountMapper.selectOne(query);BigDecimal subtract = account.getAmount().subtract(orderDto.getAmount());if (subtract.compareTo(BigDecimal.ZERO)<0){throw new Exception("用户余额不足");}account.setAmount(subtract);log.info("更新账户服务,请求参数:{}", JSON.toJSONString(account));accountMapper.updateById(account);}}
}

然后就说商品服务,逻辑也很简单,也同样要注意一下线程安全问题

@Slf4j
@Service
@RocketMQMessageListener(topic = "createByRocketMQ", consumerGroup = "cloud-group2")
@RequiredArgsConstructor(onConstructor = @__(@Autowired))
public class ProductSubtractListener implements RocketMQListener<OrderDto> {@Resourceprivate ProductMapper productMapper;@Overridepublic void onMessage(OrderDto orderDto) {log.info("产品服务收到消息,开始消费");QueryWrapper<Product> queryWrapper=new QueryWrapper<>();queryWrapper.eq("product_code",orderDto.getProductCode());synchronized (this){Product product = productMapper.selectOne(queryWrapper);if (product.getCount()<orderDto.getCount()){throw new RuntimeException("库存不足");}product.setCount(product.getCount()-orderDto.getCount());log.info("更新产品库存信息,请求参数:{}", JSON.toJSONString(product));productMapper.updateById(product);}}
}

测试

完整编码工作后,自测是非常有必要的,我们日常完成开发任务后,都会结合需求场景以及功能编排一些自测用例查看最终结果是否与预期一致。
需要注意的是由于订单业务逻辑较为复杂,很多业务场景一篇博客是不可能全部覆盖,所以这里我们就测试一下基于RocketMQ实现分布式事务常见的几个问题场景是否和预期一致。

在测试前我们必须做好前置准备工作,准备功能测试时涉及到的SQL语句,以本次用户购买产品的业务为例,涉及到订单表、用户账户信息表、产品表、以及生产者本地事务日志表。

SELECT * FROM t_order to2 ;
SELECT * from account a ;
SELECT * from product p ;
SELECT * FROM rocketmq_transaction_log rtl ;

在每次测试完成之后,我们希望数据能够还原,所以这里也需要准备一下每次测试结束后的更新语句,由于订单表和消息日志表都是主键自增,考虑到这两张表只涉及插入,所以笔者为了重置主键的值采取的是truncate语句。

truncate  table  t_order;
truncate rocketmq_transaction_log ;
UPDATE account set amount=10000 ;
UPDATE product set count=10000;

测试用例1

第一个用例是查看所有服务都正常的情况下,订单表是否有数据,用户表的用户是否会正常扣款,以及商品表库存是否会扣减。

测试前,我们先查看订单表,确认没有数据

在这里插入图片描述

查看我们的测试用户,钱包额度为10000

在这里插入图片描述

再查看库存表,可以看到数量为1000

在这里插入图片描述

确认完数据之后,我们就可以测试服务是否按照预期的方式执行,将所有服务启动

在这里插入图片描述

我们通过网关发起调用,请求地址如下:

http://localhost:8090/order/order/createByMQ

请求参数如下,从参数可以看出这个请求意为用户代码(accountCode)为demoData这个用户希望购买1个(count)产品代码(productCode)P001的产品,该产品当前售价(price)为1元。

{"accountCode": "demoData","productCode": "P001","count": 1,"amount": 1,"price": 1
}

调用完成后,查看订单表,订单数据生成无误:

在这里插入图片描述

查看用户服务是否完成用户扣款,扣款无误:

在这里插入图片描述

查看产品表,可以看到产品数量也准确扣减:

在这里插入图片描述

测试用例2

我们希望测试一下发送完half消息之后,执行本地事务完成,但是未提交commit请求时,MQServer是否会调用回查逻辑。

为了完成这一点我们必须按照以下两个步骤执行:

  1. 在订单服务提交事务消息处打个断点。

在这里插入图片描述

  1. 发起请求,当代码执行到这里的时候通过jps定位到进程号,将其强制杀死。如下所示,我们的代码执行到了提交事务消息这一步:

在这里插入图片描述

我们通过jps定位并将其杀死

在这里插入图片描述

  1. 完成这些步骤后,我们再次将服务启动,等待片刻之后可以发现,MQServer会调用checkLocalTransaction回查生产者本地事务的情况。我们放行这块代码让程序执行下去,最后再查看数据库中的数据结果是否符合预期。

在这里插入图片描述

测试用例3

测试消费者执行报错后是否会进行重试,这一点就比较好测试了,我们在消费者监听器中插入随便插入一个报错查看其是否会不断重试。这里笔者就不多做演示,实验结果是会进行不断重试,当重试次数达到阈值时会将结果存到死信队列中。

在这里插入图片描述

压测MQ和Seata的性能

由于MQ是采用异步消费的形式解耦了服务间的业务,而我们的Seata采用默认的AT模式每次执行分布式事务时都会需要借助undo-log全局锁等的方式保证最终一致性。所以理论上RocketMQ的性能肯定是高于Seata的,对此我们不妨使用Jmeter进行压测来验证一下。

本次压测只用了10个并发,MQ和seata的压测结果如下,可以看到MQ无论从执行时间还是成功率都远远优秀于Seata的。

MQ的压测结果:

在这里插入图片描述

Seata的压测结果:

在这里插入图片描述

参考文献

SpringCloud Alibaba微服务实战三十二 - 集成RocketMQ实现分布式事务

Lombok注解-@SneakyThrows

RocketMq 广播模式

使用RocketMQTemplate发送各种消息

RocketMQ事务消息如何保证数据的最终一致性

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

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

相关文章

AIGC:阿里开源大模型通义千问部署与实战

1 引言 通义千问-7B&#xff08;Qwen-7B&#xff09;是阿里云研发的通义千问大模型系列的70亿参数规模的模型。Qwen-7B是基于Transformer的大语言模型, 在超大规模的预训练数据上进行训练得到。预训练数据类型多样&#xff0c;覆盖广泛&#xff0c;包括大量网络文本、专业书籍…

百度侯震宇:AI原生与大模型将从三个层面重构云计算

12月20日&#xff0c;2023百度云智大会智算大会在北京举办&#xff0c;大会以「大模型重构云计算&#xff0c;Cloud for AI」为主题&#xff0c;深度聚焦大模型引发的云计算变革。 百度智能云表示&#xff0c;为满足大模型落地需求&#xff0c;正在基于「云智一体」战略重构…

ubuntu qt 源码编译

官方源码下载地址 : 源码地址 选择要下载的版本 dmg结尾的是MacOS系统里使用的Qt库&#xff0c;qt-everywhere-opensource-src-4.7.0是Qt源码包&#xff0c;有zip和tar.gz两个压缩格式的&#xff0c;两个内容是一样的&#xff0c;只是zip一般在Windows下比较流行&#xff0c;…

Java:语法速通

参考 菜鸟教程 java 继承 class 父类 { }class 子类 extends 父类 { }继承的特性&#xff1a; 子类拥有父类非private的属性和方法子类可以对父类进行扩展子类可以重写父类的方法使用extends只能单继承&#xff0c;使用implements可以变相的多继承&#xff0c;即一个类继承…

无人机支持的空中无蜂窝大规模MIMO系统中上行链路分布式检测

无人机支持的空中无蜂窝大规模MIMO系统中上行链路分布式检测 无人机支持的空中无蜂窝大规模MIMO系统中上行链路分布式检测介绍题目一. 背景&#xff08;解决的问题&#xff09;二. 系统模型2.1 信道模型2.1.1 信道系数2.1.2 进行标准化 2.2 信道估计 和 数据传输2.2.1 信道估计…

在Windows系统平台下部署运行服务端Idea工程的jar服务

前言 目前云原生docker等技术&#xff0c;加上部署流水线大大的简化了各种流程&#xff0c;我们后端开发的人员只需要提交代码后&#xff0c;构建、部署、测试、发布等环节都无需人员接入&#xff0c;完全的自动化交付了。那么你肯定不禁想问&#xff0c;如题的需求不是点击一…

Web 前端—HTML+CSS系列

HTML、CSS 一、HTMLCSS1.1什么是HTML、CSS1.2宇宙第一编辑器VS Code1.3Chrome浏览器1.4、深入了解网站开发 一、HTML基本操作1.web前端三大核心技术2.HTML初始代码3.HTML注释4.HTML语义化5.标题与段落6.文本修饰标签7.图片标签与图片属性8.引入文件的地址路径9.跳转链接10.跳转…

Leetcode—75.颜色分类【中等】

2023每日刷题&#xff08;六十五&#xff09; Leetcode—75.颜色分类 实现代码 class Solution { public:void sortColors(vector<int>& nums) {int red 0, white 0, blue 0;for(auto num: nums) {if(num 0) {red;} else if(num 1) {white;} else {blue;}}for…

机械、电气、自动化与人工智能融合:发展历程、问题与前景

导言 机械、电气、自动化行业与人工智能的结合&#xff0c;推动了工业革命的新浪潮。本文将深入研究这一融合的发展历程、遇到的问题、解决过程&#xff0c;以及未来的可用范围&#xff0c;着重分析在各国的应用现状和未来的研究趋势。同时&#xff0c;探讨在哪些方面能够取得胜…

环境搭建及源码运行_java环境搭建_idea版本下载及安装

1、介绍 Idea是一款被广泛使用的Java集成开发环境&#xff0c;它提供了丰富的功能和工具来帮助开发人员更高效地编写和调试代码。作为一款开源软件&#xff0c;Idea不仅提供了基本的代码编辑、自动完成和调试功能&#xff0c;还支持大量的插件和扩展&#xff0c;可为开发人员提…

MySQL基本操作 DDL DML DQL三大操作介绍

DDL 数据(结构)定义 创建表DML 数据操作 增删改DQL 查询语句 DDL 数据(结构)定义 创建表 创建 删除数据 注释 --空格内容 创建数据库 CREATE DATABASE [if not exists] 数据库名 [ CHARSET utf8]eg:CREATE DATABASE IF NOT EXISTS school CHARSET utf8如果对应school不存在,…

【NextJS】API请求执行两次的原因及解决方法

实验环境 next&#xff1a; 14.0.4react&#xff1a; ^18 实验代码 // file: app\page.tsx use client;export default function Home() {console.log(test)return (<></>) }原因 测试发现创建默认工程上面代码会输出两次test&#xff0c;其原因是为了模拟立即卸…

智慧安防视频监控EasyCVR如何通过回调接口向第三方平台推送RTSP视频通道离线通知

安防视频监控系统EasyCVR能在局域网、公网、专网等复杂的网络环境中部署&#xff0c;可支持4G、5G、WiFi、有线等方式进行视频的接入与传输、处理和分发。平台能将接入的视频流进行汇聚、转码、多格式输出和分发&#xff0c;具体包括&#xff1a;RTMP、RTSP、HTTP-FLV、WebSock…

文件传输软件SecureFX mac支持多种协议

SecureFX mac是一款文件传输客户端&#xff0c;可在 Mac 操作系统上使用。它由 VanDyke Software 公司开发&#xff0c;旨在为用户提供安全、可靠、高效的文件传输服务。 SecureFX 支持多种协议&#xff0c;包括 SFTP、SCP、FTP、FTP over SSL/TLS 和 HTTP/S。它使用强大的加密…

GEM5 Garent CPU cache消息传递路径:1. NI部分

简介 我们仔细分析下图怎么连的&#xff0c;以及消息传递路径。 图来自https://www.gem5.org/documentation/general_docs/ruby/ 代码的连接 fs.py->ruby.py-> gem5/configs/ruby/MESI_Two_Level.py 中的 create_system( options, full_system, system, dma_ports, b…

uniapp运行到手机模拟器

第一步&#xff0c;下载MUMU模拟器 下载地址&#xff1a;MuMu模拟器官网_安卓12模拟器_网易手游模拟器 (163.com) 第二步&#xff0c;运行mumu模拟器 第三步&#xff0c;运行mumu多开器 第三步&#xff0c;查看abs 端口 第四步&#xff0c;打开HBuilder,如下图&#xff0c;将…

使用TikTok云手机轻松拓展全球市场

TikTok作为一款风靡全球的短视频应用&#xff0c;全球影响力不断扩大。越来越多的商家开始借助TikTok分享作品、在海外市场上获取商业机会。要想更好地借助TikTok扩大海外市场&#xff0c;使用TikTok云手机是一个好选择。本文将介绍TikTok云手机的几大作用&#xff0c;以助您更…

AI创作系统ChatGPT商业运营网站系统源码,支持AI绘画,GPT语音对话+DALL-E3文生图

一、前言 SparkAi创作系统是基于ChatGPT进行开发的Ai智能问答系统和Midjourney绘画系统&#xff0c;支持OpenAI-GPT全模型国内AI全模型。本期针对源码系统整体测试下来非常完美&#xff0c;可以说SparkAi是目前国内一款的ChatGPT对接OpenAI软件系统。那么如何搭建部署AI创作Ch…

Keil编译STM32工程,提示__align(4)处语法错误

好久没有用Keil编程&#xff0c;因为别人的代码是用Keil写的&#xff0c;所以又得安装起来&#xff0c;编译时遇到__align(4)的错误提示。 这个问题主要是编译器版本的问题&#xff0c;默认使用的是v6.19版本的编译器&#xff0c;而工程原来使用的是v5版本的&#xff0c;两个编…

探索 Vuex 的世界:状态管理的新视角(上)

&#x1f90d; 前端开发工程师&#xff08;主业&#xff09;、技术博主&#xff08;副业&#xff09;、已过CET6 &#x1f368; 阿珊和她的猫_CSDN个人主页 &#x1f560; 牛客高级专题作者、在牛客打造高质量专栏《前端面试必备》 &#x1f35a; 蓝桥云课签约作者、已在蓝桥云…