基于RocketMQ实现分布式事务
前言
在上一篇文章Spring Boot自动装配原理以及实践我们完成了服务通用日志监控组件的开发,确保每个服务都可以基于一个注解实现业务功能的监控。
而本文我们尝试基于RocketMQ
实现下单的分布式的事务。可能会有读者会有疑问,之前我们不是基于Seata
完成了分布式事务,为什么我们还要用到RocketMQ
呢?
我们的再来回顾一下我们下单功能大抵是做以下三件事情:
- 创建订单,将订单记录存到数据库中。
- 扣款,记录用户扣款后钱包所剩下的额度。
- 扣除商品库存,并发放商品。
我们将该场景放到高并发场景下,这个功能势必要考虑性能和可靠性问题,所以我们在业务需求清楚明了的情况下,就希望能有一种方式确保下单功能在高并发场景保证性能、可靠性。
而Seata
的AT
模式确实可以保证最终一致性,但由于需要用到undo_log
和lock_table
等涉及数据持久化以及锁相关的操作,可能存在一定的性能问题。而且Seata
一旦报错会直接回滚事务,不存在任何重试机制,对于我们这种付款下单的场景是非常不可取的。
而RocketMQ
实现分布式的方式是基于消息通信的,既确保了业务功能解耦保证了并发场景的性能,而且RocketMQ
还对消息消费可靠性做了许多不错的优化,例如:失败重试、死信队列等,所以我们还是尝试使用RocketMQ
来改良我们的下单分布式事务问题。
需求介绍以及实现思路
用户下单大抵需要在三个服务中完成:订单创建、钱包扣款、库存扣减等业务逻辑。这其中会跨域三个服务,分别是订单服务创建订单、账户服务扣款、商品服务扣减库存。
以我们业务为最终目标,RocketMQ
实现分布式事务的原理是基于2PC
的,流程大抵如下:
- 订单服务发送一个事务消息到消息队列,消息内容就是我们的订单信息,这里面包含用户账号、购买的产品代码、购买产品数量等数据。
MQ
收到half
消息,并回复确认。- 生产者
(订单服务order-service)
得知我们发送的消息已被收到,订单服务则执行本地事务并提交事务,即将订单数据插入数据库中。 - 生产者
(订单服务order-service)
完成本地事务的提交,告知MQ
将事务消息commit
,此时消费者就可以消费这条消息了,注意若生产者消费失败,则将消息rollback
,一切就当没有发生过。 - 如果上述的消息是
commit
则将消息持久化到commitLog
中,以便后续MQ
宕机或者服务宕机后依然可以继续消费这条没有被消费的消息。 (非必要步骤)
若MQ长时间没有收到生产者的commit
或者rollback
的信号,则会主动找生产者索要当前消息状态。- 消费者即我们的用户服务或者库存服务收到消息则执行本地事务并提交,若失败则会不断重试,直到达到上限则将消息存到死信队列中。
常见问题
什么是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
,所以我们将这个路径配置到环境变量中。
完成环境变量配置后,我们到达mq
的bin
目录先键入这条命令,启动nameserver
start mqnamesrv.cmd
如果弹窗输出下面这条结果,则说明mq
的NameServer
启动成功。
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>
然后我们分别对order
、account
、product
三个服务中引入依赖
<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
服务消费。
对此我们实现思路有两种:
- 将服务都放到同一个消费者组,消费模式改为广播模式。
- 将
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消息发送、监听、回查回调逻辑
我们的订单服务需要做以下三件事:
- 发送half消息给MQ。
- half消息发送成功执行本地事务并记录日志。
- 告知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是否会调用回查逻辑。
为了完成这一点我们必须按照以下两个步骤执行:
- 在订单服务提交事务消息处打个断点。
- 发起请求,当代码执行到这里的时候通过
jps
定位到进程号,将其强制杀死。如下所示,我们的代码执行到了提交事务消息这一步:
我们通过jps
定位并将其杀死
- 完成这些步骤后,我们再次将服务启动,等待片刻之后可以发现,
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事务消息如何保证数据的最终一致性
相关文章:

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

TikTok社会学:短视频如何塑造社会认知?
TikTok,作为一款全球性的短视频平台,正在深刻地影响着用户的社会认知。在这个数字时代,短视频不仅仅是娱乐的载体,更是塑造和反映社会认知的一面镜子。本文将深入探讨TikTok是如何通过短视频影响社会认知,以及这种影响…...
小秋SLAM入门实战深度学习所有文章汇总
如何用python代码实现虚拟拖拽 MediaPipe Losses 损失函数 深度学习激活函数Activation Functions 【深度学习Regularization正则化】 深度学习: 数据扩充 (Data Augmentation) 【keras-yolo3】 【YOLO源码解读】 caffe源码解读系列 Python中的异常处理 精确率、精度ÿ…...
linux搭建git仓库
git安装与配置 # git安装 yum install -y git# git配置(以下为root用户下配置) # 添加git组 groupadd git# 添加账号、密码(账号zdtest可根据自己需求修改) useradd zdtest -g git passwd zdtest创建远程仓库(linux端) 创建个人文件夹 mkdir -p /home/data/zdtestcd /home/d…...
19. Mysql 循环语句
文章目录 概念循环语句while 循环语句repeat 循环语句loop 循环语句iterate 和 leave 语句 精选示例总结参考资料 概念 循环结构是编程中常见的控制结构,它允许我们重复执行一段代码,直到满足特定条件为止。 在 Mysql 中,常用来实现各种复杂…...

【qt】解决qt里编辑qss后失效问题(qt编码问题)
1、先创建qss文本stylesheet.qss 以按钮为例 QPushButton {background-color:rgb(240,255,255);color: rgb(0, 0, 2);border-style: outset;border-color: beige;border-radius: 10px; }/* hover按钮悬浮,鼠标悬浮在按钮上的状态,按钮颜色 */QPushButto…...

MySQL数据库高级SQL语句及存储过程
目录 一、高级SQL语句 (一)case语句 1.语法定义 2.示例 (二)空值(NULL) 和 无值( ) 1.区别 2.示例 (1)字符长度 (2)判断方法 ① 空值(NULL) ② 无值( ) (3…...

使用idea构建父子类springboot项目教程
第一步创建一个父类java项目(最外层java项目) 1.点击File 然后点击new 再点击Project 2.点击Maven 配置Java版本 再点击next 3.GroupId:包结构,ArtifactId:项目名称,填写完,点击next 4.点击…...

TCP_可靠数据传输原理
引言 在网络通信中,TCP是确保数据可靠传输的关键协议。但在我们深入研究TCP拥塞控制技术之前,让我们先探索可靠数据传输的原理,特别是TCP头部中一些重要字段的作用。 网络层提供了点对点的通信服务,努力交付数据报,但…...
Python随机点名
python随机点名 # 生成 0 ~ 9 之间的随机数 # 导入 random(随机数) 模块 import random print(random.randint(0,9)) 执行以上代码输出结果为: 4 尝试一下 以上实例我们使用了 random 模块的 randint() 函数来生成随机数,你每次执行后都返回不同的数字&a…...

HarmonyOS4.0系统性深入开发07创建一个ArkTS卡片
创建一个ArkTS卡片 在已有的应用工程中,创建ArkTS卡片,具体操作方式如下。 创建卡片。 根据实际业务场景,选择一个卡片模板。 在选择卡片的开发语言类型(Language)时,选择ArkTS选项,然后单…...

胡润研究院发布《2023胡润中国最具历史文化底蕴品牌榜》
胡润研究院发布《2023胡润中国最具历史文化底蕴品牌榜》,前十名分别是片仔癀、同仁堂、贵州茅台、五粮液、中国银行、中华、黄山、农业银行、建设银行、汾酒。 榜单调研范围涵盖中国内地具有60年以上历史的为消费者提供产品或服务的品牌,综合考察品牌历史…...

MFC编程技巧与范例详解01
目录 1、MFC概述 (1)、MFC为什么不用C语言使用C (2)、MFC的开发模型文档-视图模型 (3)、一个完善的MFC程序应该包括 (4)、MFC常用的类 2、MFC的特性 (1)…...

TPS5430正负电源模块
TPS5430正负电源模块 Chapter1 TPS5430正负电源模块一、芯片重要参数二、tps5430参考电路讲解以及PCB布局1.正压降压(15V转12V)2.正压降负压(15V转-12V) Chapter2 使用tps5430制作正负DC-DC降压电源,tps7a47和tps7a33制…...
【LeetCode 面试经典150题】45. Jump Game II 跳跃游戏II
45. Jump Game II 题目大意 You are given a 0-indexed array of integers nums of length n. You are initially positioned at nums[0]. Each element nums[i] represents the maximum length of a forward jump from index i. In other words, if you are at nums[i], yo…...

RustDesk连接客户端提示key不匹配 Key Mismatch无法连接(已解决)
环境: RustDesk1.1.9 服务端docker部署 问题描述: RustDesk连接客户端提示key不匹配 Key Mismatch无法连接 解决方案: 1.docker部署RustDesk服务检查配置 networks:rustdesk-net:external: falsevolumes:hbbr:hbbs:services:hbbs:container_name: rustdesk-hbbsport…...
puppeteer入门指南
一、简介 Puppeteer 是一个 Node 库,它提供了一个高级 API 来通过 DevTools 协议控制 Chromium 或 Chrome。 二、使用 1、安装nodejs最新版 2、安装puppeteer-core npm install puppeteer-core 3、编写main.js const puppeteer require(puppeteer-core);(as…...

vue3按钮点击频率控制
现有一个按钮,如下图 点击时 再次点击 刷新窗口再次点击 刷新窗口依然可以实现点击频率控制。 代码实现: <template><!--<el-config-provider :locale"locale"><router-view/></el-config-provider>--><el…...
(一)Matlab数值计算基础
目录 1.2Matlab中的数据类型 1.2Matlab中的数据类型 逻辑型 逻辑型变量值为1或0字符型 MATLAB的字符型输入使用单引号括起来,字符串存储为字符数组,每个元素占一个ASCII字符数值型 数值型分为整型(int)、单精度浮点型࿰…...

《MySQL系列-InnoDB引擎02》InnoDB存储引擎介绍
文章目录 第二章 InnoDB存储引擎1 InnoDB存储引擎概述2 InnoDB存储引擎的版本3 InnoDB体系架构3.1 后台线程3.2 内存 4 Checkpoint技术5 Master Thread 工作方式5.1 InnoDB 1.0.x版本之前的Master Thread5.2 InnoDB 1.2.x版本之前的Master Thread5.3 InnoDB 1.2.x版本的Master …...
java 实现excel文件转pdf | 无水印 | 无限制
文章目录 目录 文章目录 前言 1.项目远程仓库配置 2.pom文件引入相关依赖 3.代码破解 二、Excel转PDF 1.代码实现 2.Aspose.License.xml 授权文件 总结 前言 java处理excel转pdf一直没找到什么好用的免费jar包工具,自己手写的难度,恐怕高级程序员花费一年的事件,也…...

视频字幕质量评估的大规模细粒度基准
大家读完觉得有帮助记得关注和点赞!!! 摘要 视频字幕在文本到视频生成任务中起着至关重要的作用,因为它们的质量直接影响所生成视频的语义连贯性和视觉保真度。尽管大型视觉-语言模型(VLMs)在字幕生成方面…...
土地利用/土地覆盖遥感解译与基于CLUE模型未来变化情景预测;从基础到高级,涵盖ArcGIS数据处理、ENVI遥感解译与CLUE模型情景模拟等
🔍 土地利用/土地覆盖数据是生态、环境和气象等诸多领域模型的关键输入参数。通过遥感影像解译技术,可以精准获取历史或当前任何一个区域的土地利用/土地覆盖情况。这些数据不仅能够用于评估区域生态环境的变化趋势,还能有效评价重大生态工程…...
AI编程--插件对比分析:CodeRider、GitHub Copilot及其他
AI编程插件对比分析:CodeRider、GitHub Copilot及其他 随着人工智能技术的快速发展,AI编程插件已成为提升开发者生产力的重要工具。CodeRider和GitHub Copilot作为市场上的领先者,分别以其独特的特性和生态系统吸引了大量开发者。本文将从功…...
Unit 1 深度强化学习简介
Deep RL Course ——Unit 1 Introduction 从理论和实践层面深入学习深度强化学习。学会使用知名的深度强化学习库,例如 Stable Baselines3、RL Baselines3 Zoo、Sample Factory 和 CleanRL。在独特的环境中训练智能体,比如 SnowballFight、Huggy the Do…...
OpenPrompt 和直接对提示词的嵌入向量进行训练有什么区别
OpenPrompt 和直接对提示词的嵌入向量进行训练有什么区别 直接训练提示词嵌入向量的核心区别 您提到的代码: prompt_embedding = initial_embedding.clone().requires_grad_(True) optimizer = torch.optim.Adam([prompt_embedding...
在鸿蒙HarmonyOS 5中使用DevEco Studio实现录音机应用
1. 项目配置与权限设置 1.1 配置module.json5 {"module": {"requestPermissions": [{"name": "ohos.permission.MICROPHONE","reason": "录音需要麦克风权限"},{"name": "ohos.permission.WRITE…...

【JavaWeb】Docker项目部署
引言 之前学习了Linux操作系统的常见命令,在Linux上安装软件,以及如何在Linux上部署一个单体项目,大多数同学都会有相同的感受,那就是麻烦。 核心体现在三点: 命令太多了,记不住 软件安装包名字复杂&…...
全面解析各类VPN技术:GRE、IPsec、L2TP、SSL与MPLS VPN对比
目录 引言 VPN技术概述 GRE VPN 3.1 GRE封装结构 3.2 GRE的应用场景 GRE over IPsec 4.1 GRE over IPsec封装结构 4.2 为什么使用GRE over IPsec? IPsec VPN 5.1 IPsec传输模式(Transport Mode) 5.2 IPsec隧道模式(Tunne…...

Spring Cloud Gateway 中自定义验证码接口返回 404 的排查与解决
Spring Cloud Gateway 中自定义验证码接口返回 404 的排查与解决 问题背景 在一个基于 Spring Cloud Gateway WebFlux 构建的微服务项目中,新增了一个本地验证码接口 /code,使用函数式路由(RouterFunction)和 Hutool 的 Circle…...