基于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.rhkb.cn/news/221367.html

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

相关文章

【K8S基础】-k8s的核心概念pod

一、Pod 是什么 1.1 Pod 的定义和概念 在Kubernetes中&#xff0c;Pod是创建或部署的最小/最简单的基本单位。一个Pod代表着集群上正在运行的一个进程&#xff0c;它封装了一个或多个应用容器&#xff0c;并且提供了一些共享资源&#xff0c;如网络和存储&#xff0c;每个Pod…

nbcio-boot的flowable流程模型查询修正为按发布时间倒序

更多nbcio-boot功能请看演示系统 gitee源代码地址 后端代码&#xff1a; https://gitee.com/nbacheng/nbcio-boot 前端代码&#xff1a;https://gitee.com/nbacheng/nbcio-vue.git 在线演示&#xff08;包括H5&#xff09; &#xff1a; http://122.227.135.243:9888 之前…

部署智能合约以及 javascript 调用合约函数(Web3项目二实战之三)

在上一篇 智能合约是Web3项目的核心要务(Web3项目二实战之二) ,我们已然为项目编写了智能合约,在攥写完智能合约后,该项目将完成了一大部分,剩下无非就是用户界面交互的内容。 然而,在码完了智能合约代码后,起着承前启后关键性的便是,前端界面与智能合约的交互。 智能…

机器学习---聚类(原型聚类、密度聚类、层次聚类)

1. 原型聚类 原型聚类也称为“基于原型的聚类” (prototype-based clustering)&#xff0c;此类算法假设聚类结构能通过一 组原型刻画。算法过程&#xff1a;通常情况下&#xff0c;算法先对原型进行初始化&#xff0c;再对原型进行迭代更新求解。著 名的原型聚类算法&#…

服务器数据恢复-EMC存储raid5磁盘物理故障离线的数据恢复案例

服务器数据恢复环境&故障&#xff1a; 一台emc某型号存储服务器&#xff0c;存储服务器上组建了一组raid5磁盘阵列&#xff0c;阵列中有两块磁盘作为热备盘使用。存储服务器在运行过程中有两块磁盘出现故障离线&#xff0c;但是只有一块热备盘激活&#xff0c;最终导致该ra…

安卓小练习-校园闲置交易APP(SQLite+SimpleCursorAdapter适配器)

环境&#xff1a; SDK&#xff1a;34 JDK&#xff1a;20.0.2 编写工具&#xff1a;Android Studio 2022.3.1 整体效果&#xff08;视频演示&#xff09;&#xff1a; 小练习-闲置社区APP演示视频-CSDN直播 部分效果截图&#xff1a; 整体工作流程&#xff1a; 1.用户登录&…

【计算机网络】TCP协议——2.连接管理(三次握手,四次挥手)

目录 前言 一. 建立连接——三次握手 1. 三次握手过程描述 2. TCP连接建立相关问题 二. 释放连接——四次挥手 1. 四次挥手过程描述 2. TCP连接释放相关问题 三. TCP状态转换 结束语 前言 TCP——传输控制协议(Transmission Control Protocol)。是一种面向连接的传…

web前端游戏项目-雷霆战机飞机大战游戏【附源码】

文章目录 一&#xff1a;雷霆战机HTML源码&#xff1a;JS文件&#xff1a;&#xff08;1&#xff09;function.js&#xff08;2&#xff09;impact.js&#xff08;3&#xff09;move.1.1.js&#xff08;4&#xff09;script.js 二&#xff1a;飞机大战HTML源码&#xff1a;CSS源…

MySQL——表的增删查改

目录 一.Create&#xff08;创建&#xff09; 1.单行数据 全列插入 2.多行数据 指定列插入 3.插入否则更新 4. 替换 二.Retrieve&#xff08;读取&#xff09; 1. select 列 查询 2.where 条件 3.结果排序 4.筛选分页结果 三.Update &#xff08;修改&#xff09;…

【改进YOLOv8】磁瓦缺陷分类系统:改进LSKNet骨干网络的YOLOv8

1.研究背景与意义 项目参考AAAI Association for the Advancement of Artificial Intelligence 研究背景与意义 近年来&#xff0c;随着智能制造产业的不断发展&#xff0c;基于人工智能与机器视觉的自动化产品缺陷检测技术在各行各业中得到了广泛应用。磁瓦作为永磁电机的主…

4.3 C++对象模型和this指针

4.3 C对象模型和this指针 4.3.1 成员变量和成员函数分开存储 在C中&#xff0c;类内的成员变量和成员函数分开存储 只有非静态成员变量才属于类的对象上 #include <iostream>class Person { public:Person() {mA 0;} //非静态成员变量占对象空间int mA;//静态成员变量…

AXure的情景交互

目录 导语&#xff1a; 1.erp多样性登录界面 2.主页跳转 3.省级联动​编辑 4. 下拉加载 导语&#xff1a; Axure是一种流行的原型设计工具&#xff0c;可以用来创建网站和应用程序的交互原型。通过Axure&#xff0c;设计师可以创建情景交互&#xff0c;以展示用户与系统的交…

JavaAwtSwing的JFrame的pack()方法,容器适配子组件大小,笔记231220

pack()是extends自Window类的方法 使此窗口的大小适合其子组件的首选大小和布局。如果其中一个尺寸小于上一次调用setMinimumSize方法指定的最小尺寸&#xff0c;则会自动放大窗口的宽度和高度。 如果窗口和/或其所有者还不可显示&#xff0c;则在计算首选大小之前&#xff0…

大数据机器学习:从理论到实战,探索学习率的调整策略

大数据机器学习&#xff1a;从理论到实战&#xff0c;探索学习率的调整策略 全文目录 大数据机器学习&#xff1a;从理论到实战&#xff0c;探索学习率的调整策略一、引言二、学习率基础定义与解释学习率与梯度下降学习率对模型性能的影响 三、学习率调整策略常量学习率时间衰减…

如何入门 GPT 并快速跟上当前的大语言模型 LLM 进展?

入门GPT 首先说第一个问题&#xff1a;如何入门GPT模型&#xff1f; 最直接的方式当然是去阅读官方的论文。GPT模型从2018年的GPT-1到现在的GPT-4已经迭代了好几个版本&#xff0c;通过官方团队发表的论文是最能准确理清其发展脉络的途径&#xff0c;其中包括GPT模型本身和一…

迪文屏开发保姆级教程——页面键盘

迪文屏页面键盘保姆级教程。 本篇文章主要介绍了在DGBUS平台上使用页面键盘的步骤。 迪文屏官方开发指南PDF&#xff1a;&#xff08;不方便下载的私聊我发给你&#xff09; https://download.csdn.net/download/qq_21370051/88647174?spm1001.2014.3001.5503https://downloa…

浅析RoPE旋转位置编码的远程衰减特性

为什么 θ i \theta_i θi​的取值会造成远程衰减性 旋转位置编码的出发点为&#xff1a;通过绝对位置编码的方式实现相对位置编码。 对词向量 q \boldsymbol{q} q添加绝对位置信息 m m m&#xff0c;希望找到一种函数 f f f&#xff0c;使得&#xff1a; < f ( q , m ) …

深度学习中的张量维度

1 深度学习中的张量 在深度学习框架中&#xff0c;Tensor&#xff08;张量&#xff09;是一种数据结构&#xff0c;用于存储和操作多维数组。张量可以被视为一种扩展的矩阵&#xff0c;它可以具有任意数量的维度。 在深度学习中&#xff0c;张量通常被用来表示神经网络的输入…

Java 栈和队列的交互实现

文章目录 队列和栈的区别一.用队列模拟实现栈1.1入栈1.2出栈1.3返回栈顶元素1.4判断栈是否为空 二.用栈模拟实现队列2.1 入队2.2出队2.3peek2.4判断队列是否为空 三.完整代码3.1 队列模拟实现栈3.2栈模拟实现队列 队列和栈的区别 栈和队列都是常用的数据结构&#xff0c;它们的…

解读远程工作设计师之未来与发展

引言 在数字化的浪潮下&#xff0c;“远程工作”已经成为现代职场的一个重要趋势。对于设计师来说&#xff0c;这不仅是一种工作方式的转变&#xff0c;更是职业发展的新机遇。在这篇文章中&#xff0c;我将从以下9个方面&#xff0c;深入探讨远程工作设计师的机会、市场和职位…