RocketMQ事务消息在订单创建和库存扣减的使用
前言
下单的过程包括订单创建,还有库存的扣减,为提高系统的性能,将库存放在redis扣减,则会涉及到Mysql和redis之间的数据同步,其中,这个过程还涉及到,必须是订单创建成功才进行库存的扣减操作。其次,还涉及到库存的同步,需要保证订单创建成功和redis里的库存都扣减成功,再将库存数据同步到Mysql,为了实现上述这里情况,可以借助RocketMQ的事务型消息来实现。
流程图
流程图如下,这里引入了stocklog,即订单流水表,通过判断stocklog的状态来决定是否commite消息去同步mysql,这里stocklog状态为成功的前提是订单入库和redis库存扣减成功。
RocketMQ事务消息
在第五步执行成功返回可能因为网络状况卡住,但是stocklog状态已经得到修改
如果返回成功 MQ事务就会commit这条消息
如果没有返回成功 MQ事务会去轮询stocklog有没有被修改
一直五次轮询发现没有被修改就会回滚这条消息,这个消息相当于被删掉,不会让消费系统消费到
这条消息commit后,就会被MQ的消费者消费,对MySQL的实际库存进行更新
stock_log的意义
这里是为了保证订单的插入和redis库存扣减都成功,才进行后续异步操作MySQL,本身的存在就是为了辅助这个本地事务的成功执行再进行后续的操作,保证一致性。
这里再说一下我之前面试遇到的一个问题:既然先对redis扣减库存再MQ异步是去操作MySQL数据库扣减库存,这样子是为了提高性能,那么这套流程一开始就操作MySQL,性能会有提升吗?答案肯定有的,这里操作数据库是将订单流水入库,并没有涉及到锁,并发下不会因为行锁而影响性能。而针对某个产品的库存扣减,直接操作MySQL进行Update操作,会对这一行加上行锁,其他请求都需要阻塞等待行锁的释放。
需要的SQL表
这里简化一下下单的流程,不涉及用户表,只涉及到库存表,库存流水表,订单表。
order表
CREATE TABLE `order` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '订单id',
`product_id` int(11) DEFAULT NULL COMMENT '产品id',
`product_num` int(11) DEFAULT NULL COMMENT '产品数量',
PRIMARY KEY (`id`),
KEY `product_id_index` (`product_id`) USING BTREE COMMENT '产品id索引'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
stock表
CREATE TABLE `stock` (
`id` int(10) NOT NULL AUTO_INCREMENT COMMENT '库存id',
`product_id` int(11) DEFAULT NULL COMMENT '产品id',
`product_name` varchar(255) DEFAULT NULL COMMENT '产品名字',
`stock_num` int(11) DEFAULT NULL COMMENT '产品库存',
PRIMARY KEY (`id`),
UNIQUE KEY `product_id_index` (`product_id`) USING BTREE COMMENT '产品Id唯一索引'
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4;
stock_log表
CREATE TABLE `stock_log` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '库存id',
`product_id` int(11) DEFAULT NULL COMMENT '产品id',
`amount` int(11) DEFAULT NULL COMMENT '库存变化数量',
`status` int(11) DEFAULT NULL COMMENT '状态0->初始化,1->成功,2->回滚',
PRIMARY KEY (`id`),
KEY `product_id_index` (`product_id`) USING BTREE COMMENT '产品id索引'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
关键代码
OrderController类
@Controller
@RequestMapping("/order")
@RequiredArgsConstructor
@Slf4j
public class OrderController {
private final OrderService orderService;
private final StockLogService stockLogService;
private final DecreaseStockProducer decreaseStockProducer;
private final StockService stockService;
private final RedisTemplate redisTemplate;
@PostMapping(value = "/create/{id}")
public ResponseEntity<Object> create(@PathVariable("id") Integer productId) {
// 检查redis是否有库存0的标识
if (redisTemplate.hasKey("product_stock_invalid_" + productId)) {
return new ResponseEntity<>("库存不足", HttpStatus.OK);
}
// 先创建库存流水 这里默认一次只能扣减数量1的库存
StockLog stockLog = StockLog.builder()
.amount(1)
.productId(productId)
.status(0)
.build();
stockLogService.save(stockLog);
// 发送事务消息
try {
DecreaseStockEvent decreaseStockEvent = DecreaseStockEvent.builder()
.productId(productId)
.stockLogId(stockLog.getId())
.build();
SendResult sendResult = decreaseStockProducer.sendMessageInTransaction(decreaseStockEvent);
if (!Objects.equals(sendResult.getSendStatus(), SendStatus.SEND_OK)) {
log.error("事务消息发送错误,请求参数productId:{}", productId);
}
} catch (Exception e) {
log.error("消息发送错误,请求参数:{}", productId, e);
}
return new ResponseEntity<>("created successfully", HttpStatus.OK);
}
StockStatusCheckerListener类,执行本地事务和检查事务
@Slf4j
@RocketMQTransactionListener
@RequiredArgsConstructor
public class StockStatusCheckerListener implements RocketMQLocalTransactionListener {
private final OrderService orderService;
private final StockLogService stockLogService;
private final TransactionTemplate transactionTemplate;
@Override
public RocketMQLocalTransactionState executeLocalTransaction(Message message, Object arg) {
log.info("message: {}, args: {}", message, arg);
TypeReference<MessageWrapper<DecreaseStockEvent>> typeReference = new TypeReference<MessageWrapper<DecreaseStockEvent>>() {};
MessageWrapper<DecreaseStockEvent> messageWrapper = JSON.parseObject(new String((byte[]) message.getPayload()), typeReference);
DecreaseStockEvent decreaseStockEvent = messageWrapper.getMessage();
log.info("decreaseStockEvent info : {}", decreaseStockEvent);
try {
orderService.createOrder(decreaseStockEvent.getProductId(), decreaseStockEvent.getStockLogId());
} catch (Exception e) {
log.error("插入订单失败, decreaseStockEvent info : {}", decreaseStockEvent, e);
// 触发回查
//设置对应的stockLog为回滚状态
StockLog stockLog = stockLogService.getOne(new QueryWrapper<StockLog>().eq("id", decreaseStockEvent.getStockLogId()));
stockLog.setStatus(2);
stockLogService.updateById(stockLog);
return RocketMQLocalTransactionState.ROLLBACK;
}
return RocketMQLocalTransactionState.COMMIT;
}
@Override
public RocketMQLocalTransactionState checkLocalTransaction(Message message) {
log.info("message: {}, args: {}", message);
MessageWrapper<DecreaseStockEvent> messageWrapper = (MessageWrapper) message.getPayload();
DecreaseStockEvent decreaseStockEvent = messageWrapper.getMessage();
StockLog stockLog = stockLogService.getOne(new QueryWrapper<StockLog>().eq("id", decreaseStockEvent.getStockLogId()));
if (stockLog == null) {
return RocketMQLocalTransactionState.UNKNOWN;
}
// 已经被扣减了库存
if (stockLog.getStatus().intValue() == 1) {
return RocketMQLocalTransactionState.COMMIT;
// 初始化状态
} else if (stockLog.getStatus().intValue() == 0) {
return RocketMQLocalTransactionState.UNKNOWN;
}
return RocketMQLocalTransactionState.ROLLBACK;
}
}
MQ相关代码,使用模板方法
DecreaseStockProducer,消息生产者,实现了一些指定方法
@Slf4j
@Component
public class DecreaseStockProducer extends AbstractCommonSendProduceTemplate<DecreaseStockEvent> {
private final ConfigurableEnvironment environment;
public DecreaseStockProducer(@Autowired RocketMQTemplate rocketMQTemplate, @Autowired ConfigurableEnvironment environment) {
super(rocketMQTemplate);
this.environment = environment;
}
@Override
protected BaseSendExtendDTO buildBaseSendExtendParam(DecreaseStockEvent messageSendEvent) {
return BaseSendExtendDTO.builder()
.eventName("库存同步到mysql")
.keys(String.valueOf(messageSendEvent.getProductId()))
.topic(environment.resolvePlaceholders(StockMQConstant.STOCK_TOPIC_KEY))
.tag(environment.resolvePlaceholders(StockMQConstant.STOCK_DEREASE_STOCK_TAG_KEY))
.sentTimeout(2000L)
.build();
}
@Override
protected Message<?> buildMessage(DecreaseStockEvent messageSendEvent, BaseSendExtendDTO requestParam) {
String keys = StrUtil.isEmpty(requestParam.getKeys()) ? UUID.randomUUID().toString() : requestParam.getKeys();
return MessageBuilder
.withPayload(new MessageWrapper(requestParam.getKeys(), messageSendEvent))
.setHeader(MessageConst.PROPERTY_KEYS, keys)
.setHeader(MessageConst.PROPERTY_TAGS, requestParam.getTag())
.build();
}
}
AbstractCommonSendProduceTemplate,发送消息的类
@Slf4j
@RequiredArgsConstructor
public abstract class AbstractCommonSendProduceTemplate<T> {
private final RocketMQTemplate rocketMQTemplate;
/**
* 构建消息发送事件基础扩充属性实体
*
* @param messageSendEvent 消息发送事件
* @return 扩充属性实体
*/
protected abstract BaseSendExtendDTO buildBaseSendExtendParam(T messageSendEvent);
/**
* 构建消息基本参数,请求头、Keys...
*
* @param messageSendEvent 消息发送事件
* @param requestParam 扩充属性实体
* @return 消息基本参数
*/
protected abstract Message<?> buildMessage(T messageSendEvent, BaseSendExtendDTO requestParam);
/**
* 事务消息事件通用发送
*
* @param messageSendEvent 事务消息发送事件
* @return 消息发送返回结果
*/
public SendResult sendMessageInTransaction(T messageSendEvent) {
BaseSendExtendDTO baseSendExtendDTO = buildBaseSendExtendParam(messageSendEvent);
SendResult sendResult;
try {
StringBuilder destinationBuilder = StrUtil.builder().append(baseSendExtendDTO.getTopic());
if (StrUtil.isNotBlank(baseSendExtendDTO.getTag())) {
destinationBuilder.append(":").append(baseSendExtendDTO.getTag());
}
sendResult = rocketMQTemplate.sendMessageInTransaction(
destinationBuilder.toString(),
buildMessage(messageSendEvent, baseSendExtendDTO),
null
);
log.info("[{}] 消息发送结果:{},消息ID:{},消息Keys:{}", baseSendExtendDTO.getEventName(), sendResult.getSendStatus(), sendResult.getMsgId(), baseSendExtendDTO.getKeys());
} catch (Throwable ex) {
log.error("[{}] 消息发送失败,消息体:{}", baseSendExtendDTO.getEventName(), JSON.toJSONString(messageSendEvent), ex);
throw ex;
}
return sendResult;
}
OrderService的createOrder方法:
@Service
@RequiredArgsConstructor
public class OrderServiceImpl extends ServiceImpl<OrderMapper, Order> implements OrderService {
private final OrderMapper orderMapper;
private final StockLogMapper stockLogMapper;
private final RedisTemplate redisTemplate;
private final TransactionTemplate transactionTemplate;
private static final String LUA_DECRESE_STOCK_PATH = "lua/decreseStock.lua";
@Override
public void createOrder(Integer productId, Integer stockLogId) {
// 减少Redis里面的库存
DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>();
redisScript.setScriptSource(new ResourceScriptSource(new ClassPathResource(LUA_DECRESE_STOCK_PATH)));
redisScript.setResultType(Long.class);
// 执行Lua脚本
Long redisResult = (Long) redisTemplate.execute(redisScript, Collections.singletonList(String.valueOf(productId)));
if (redisResult < 1L) {
throw new RuntimeException("库存售罄");
}
// 编程式事务
transactionTemplate.executeWithoutResult(status -> {
try {
// 事务性操作
Order order = Order.builder()
.productId(productId)
.productNum(1)
.build();
orderMapper.insert(order);
// 改stockLog
StockLog stockLog = stockLogMapper.selectOne(new QueryWrapper<StockLog>().eq("id", stockLogId));
if (stockLog == null) {
throw new RuntimeException("该库存流水不存在");
}
stockLog.setStatus(1);
stockLogMapper.updateById(stockLog);
// 如果操作成功,不抛出异常,事务将提交
} catch (Exception e) {
// 如果操作失败,抛出异常,事务将回滚 并且需要补偿redis的库存
redisTemplate.opsForValue().increment(String.valueOf(productId));
status.setRollbackOnly();
}
});
}
}
redis的lua脚本代码如下,这里只会在库存大于0的时候进行扣减,先检查库存,再扣减。如果库存为0,在redis里面setIfAbsent该商品售罄的标识,这样子在controller查询到售罄就直接return
local key = KEYS[1]
-- 检查键是否存在
local exists = redis.call('EXISTS', key)
if exists == 1 then
-- 键存在,获取值
local value = redis.call('GET', key)
if tonumber(value) > 0 then
-- 如果值大于0,则递减
redis.call('DECR', key)
return 1 -- 表示递减成功
else
local prefix = "product_stock_invalid_"
local stock_invalid_tag = prefix .. KEYS[1]
local exists_tag = redis.call('EXISTS', stock_invalid_tag)
if exists_tag == 0 then
-- 键不存在,设置键的值
redis.call('SET', stock_invalid_tag, "true")
return 0 -- 表示递减失败,值不大于0
end
end
else
return -1 -- 表示递减失败,键不存在
end
MQ的consumer:
@Slf4j
@Component
@RequiredArgsConstructor
@RocketMQMessageListener(
topic = StockMQConstant.STOCK_TOPIC_KEY,
selectorExpression = StockMQConstant.STOCK_DEREASE_STOCK_TAG_KEY,
consumerGroup = StockMQConstant.STOCK_DEREASE_STOCK_CG_KEY
)
public class DecreaseStockConsumer implements RocketMQListener<MessageWrapper<DecreaseStockEvent>> {
private final StockService stockService;
@Transactional(rollbackFor = Exception.class)
@Override
public void onMessage(MessageWrapper<DecreaseStockEvent> message) {
DecreaseStockEvent decreaseStockEvent = message.getMessage();
Integer productId = decreaseStockEvent.getProductId();
try {
stockService.decreaseStock(productId);
} catch (Exception e) {
log.error("库存同步到mysql失败,productId:{}", productId, e);
throw e;
}
}
}
stockService.decreaseStock()方法如下
public int decreaseStock(Integer productId) {
return stockMapper.decreaseStock(productId);
}
相关的SQL语句
<update id="decreaseStock">
UPDATE stock
SET stock_num = stock_num - 1
WHERE id = #{id} AND stock_num >= 1
</update>
消息重复消费问题
我们知道,MQ可能会存在重复消费的问题,包括我在压测的时候,就存在了重复消费,导致MySQL的库存最终比redis库存要少,重复扣减了MySQL的库存,针对这种情况,应该解决幂等性问题。
在前面我们用MessageWrapper来包装消息体的时候,每次new一个MessageWrapper都会生成新的UUID,我们将这UUID存到Redis里面来保证幂等性
/**
* 消息体包装器
*/
@Data
@Builder
@NoArgsConstructor(force = true)
@AllArgsConstructor
@RequiredArgsConstructor
public final class MessageWrapper<T> implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 消息发送 Keys
*/
@NonNull
private String keys;
/**
* 消息体
*/
@NonNull
private T message;
/**
* 唯一标识,用于客户端幂等验证
*/
private String uuid = UUID.randomUUID().toString();
/**
* 消息发送时间
*/
private Long timestamp = System.currentTimeMillis();
}
修改后的扣减库存方法,先判断redis里面有没有存在已经扣除了库存的标识,有就直接返回
@Service
@RequiredArgsConstructor
public class StockServiceImpl extends ServiceImpl<StockMapper, Stock> implements StockService {
private final StockMapper stockMapper;
private final RedisTemplate redisTemplate;
@Override
public int decreaseStock(Integer productId, String UUID) {
if(redisTemplate.hasKey("decrease_mark_" + UUID)) {
return 0;
}
redisTemplate.opsForValue().set("decrease_mark_" + UUID, "true", 24, TimeUnit.HOURS);
return stockMapper.decreaseStock(productId);
}
}
下面是上述demo的代码地址,修改数据库和mysql地址即可使用
相关文章:

Jetson AGX Orin安装archiconda、Pytorch
Jetson AGX Orin安装archiconda、Pytorch

深入解析JavaScript中new Function语法
Function是JavaScript中非常重要的内置构造函数,可以用来动态创建函数。new Function语法就是其中一种函数创建方式。但是new Function也有一定的缺点需要注意。本文将带您深入解析new Function语法,了解其应用场景以及需要注意的问题。new Function是动态创建函数的一种方式,但也有缺点。为了更好的代码质量和性能,应该慎用或避免使用。对JavaScript函数和作用域有深入理解,可以编写出更简洁、高效、稳定的代码。。

RTSP协议播放不兼容TPLINK摄像头的处理办法
报错的内容是Number of element invalid in origin string.两个数字中间多了一个空格,导致判断数据不等于6。所以数据输入的时候把中间的空格去掉一个即可。

苹果手机死机怎么重启?iPhone各机型强制重启方法来了!
iPhone手机莫名其妙死机怎么办?苹果手机死机怎么重启?看这里!小编针对不同iPhone机型,为大家提供了苹果手机死机后强制重启的方法。操作都很简单,有需要的朋友快来看看吧!

编码技巧:如何在Golang中高效解析和生成XML
在本文中,我们详细探讨了在Golang中高效处理XML的各个方面。从基础的XML概念到解析和生成XML文件的具体步骤,再到错误处理、调试技巧以及一些高级技巧和最佳实践,我们提供了一个全面的指南,旨在帮助读者掌握在Golang中处理XML的技能。理解Golang中XML处理的基本概念和方法。使用包来解析和生成XML文件。有效地处理常见的XML解析和生成中的错误。应用最佳实践和高级技巧来优化XML处理的性能和安全性。

鸿蒙HarmonyOS实战-工具安装和Helloworld案例
🚀前言 HarmonyOS是华为自主开发的操作系统,它在2020年9月正式发布。它最初被称为鸿蒙OS,后来更名为HarmonyOS。HarmonyOS旨在提供一种可在各种设备上无缝运行的统一操作系统,包括智能手机、平板电脑、智能穿戴设备、智能音箱、车载系统、智能家居设备等等。相比于其

TCP怎么保证传输过程的可靠性?
校验和发送方在发送数据之前计算校验和,接收方收到数据后同样计算,如果不一致,那么传输有误确认应答,序列号TCP进行传输时数据都进行了编号,每次接收方返回ACK都有确认序列号。超时重试这里是引用连接管理流量控制阻塞控制..._tcp传输过程可靠性

15.鸿蒙HarmonyOS App(JAVA)进度条与圆形进度条
/设置无限模式,运行查看动态效果。15.鸿蒙HarmonyOS App(JAVA)进度条与圆形进度条。//创建并设置无限模式元素。

javascript的变量存储机制和原理
js的变量存储机制

tomcat缺少awt支持的解决&java.lang.NoClassDefFoundError: Could not initialize class sun.awt.X11GraphicsEnvir
这几天,线上的项目出现了一个问题,就是二维码图片没有出来,考虑到图像都是用到awt库,可能是tomcat没有图像库的问题,给tomcat加上awt的支持就解决了。加在catalina.sh的开头的JAVA_OPTS环境变量中加入,然后重启搞定。

Java中 && 和|| 在同一个 if 里面使用 会出现啥问题&Java中运算符“|”和“||”以及“&”和“&&”区别
如果在同一个 if 语句中同时使用 && 和 || 运算符,可能会导致逻辑错误。这样就会导致逻辑错误,当 a 为 false,b 为 true 时,输出结果会是 “Goodbye”,而不是我们期望的 “Hello World”。假设有两个布尔类型的变量 a 和 b,我们要判断当 a 为 true 或者 b 为 true 时输出 “Hello World”,否则输出 “Goodbye”。:不论运算符左侧为true还是false,右侧语句都会进行判断,下面代码。

pandas进行数据计算时如何处理空值的问题?
我们在处理数据时经常会遇到空值的问题,比如有个学生某科弃考但是其他科有成绩的话,计算总分时便需要解决空值计算的问题

Windows系统搭建WebDAV服务并结合内网穿透实现公网访问本地文件
在Windows上如何搭建WebDav,并且结合cpolar的内网穿透工具实现在公网访问。WebDav是一种基于http的协议,允许用户在服务器上创建、修改、删除和移动文件,它的优点是可以方便的通过网络访问和管理文件,并且支持多用户协作,提供安全的加密机制。使用WebDav协议,用户可以将网盘挂载到本地电脑或手机上,可以直接操作网盘上的文件了。

鸿蒙APP闪退的问题
以下是一些建议的步骤,可以帮助你定位和解决鸿蒙应用闪退的原因,希望对大家有所帮助。使用鸿蒙开发者工具中的调试功能,尝试在应用发生闪退的情况下进行调试。这可以帮助你实时观察应用的状态,查看变量的值,并找到可能的问题。如果你的应用使用版本控制工具(如Git),回退到之前的稳定版本,看看问题是否仍然存在。有时,一个小的更改可能导致应用闪退,因此查看最近的代码修改是很重要的。如果应用使用第三方库或依赖,确保它们的版本与应用的其他部分兼容。使用鸿蒙开发者工具提供的崩溃分析服务,分析应用的崩溃报告。

如何用pthon连接mysql和mongodb数据库【极简版】
发现宝藏 前言 1. 连接mysql 1.1 安装 PyMySQL 1.2 导入 PyMySQL 1.3 建立连接 1.4 创建游标对象 1.5 执行查询 1.6 关闭连接 1.7 完整示例 2. 连接mongodb 2.1 安装 PyMongo 2.2 导入 PyMongo 2.3 建立连接 2.4

如何快速部署本地训练的 Bert-VITS2 语音模型到 Hugging Face
Hugging Face是一个机器学习(ML)和数据科学平台和社区,帮助用户构建、部署和训练机器学习模型。它提供基础设施,用于在实时应用中演示、运行和部署人工智能(AI)。用户还可以浏览其他用户上传的模型和数据集。Hugging Face通常被称为机器学习界的GitHub,因为它让开发人员公开分享和

uniap vue3 组件使用uni.createSelectorQuery() 获取dom报错
批量查询时,结果是按照查询的顺序返回的。由于vue3中没有this,所以使用。

linux环境中一次启动多个jar包,并且设置脚本开机自启
我们在通过jar启动项目时,有时候会比较多,启动会比较麻烦,需要编写shell脚本启动,将启动脚本存放在需要启动的jar包路径下。(文档存放在 /home/process_parent )PORTS 端口号,多个用空格隔开MODULES 模块,多个用空格隔开MODULE_NAMES 模块名称,多个用空格隔开。

将 RGB 转换为十六进制、生成随机十六进制
RGB是一种加法混色模式,它通过调节红、绿、蓝三个颜色通道的亮度来混合出各种颜色。对于每个颜色通道,取值范围是0到255,0表示该通道对应的颜色分量没有亮度,255表示达到最大亮度。

用python实现实现手势音量控制
要实现手势音量控制,您可以使用Python中的PyAutoGUI和pynput库。PyAutoGUI可以模拟鼠标和键盘操作,而pynput可以检测用户的输入事件。,用于检测键盘事件。如果用户按下ESC键,则停止监听鼠标和键盘事件并退出程序。最后,我们创建了鼠标和键盘监听器对象,并调用它们的。,用于模拟按下音量增加和音量减少键的操作。然后,我们定义了一个鼠标手势检测函数。,用于检测鼠标左键的点击事件。在程序的主循环中,我们使用。在这个示例代码中,我们定义了两个函数。函数等待用户按下ESC键退出程序。

STM32F103_ESP8266基于RTOS移植MQTT
基于STM32F103C8T6单片机FreeRTOS系统ESP8266 WIFI模组移植MQTTClient,成功连接MQTT服务器,实现订阅和发布消息!

nodemon(自动重启 node 应用程序)的安装与使用
(2).键入命令 set-ExecutionPolicy remoteSigned。windows 默认不允许 npm 全局命令执行脚本文件,所以需要修改执行策略。全局安装完成之后就可以在命令行的任何位置运行 nodemon 命令。(4)、再运行 nodemon app.js ok。我们可以执行安装选项 -g 进行全局安装。1、安装,在随意一个命令窗口都可以。自动重启 node 应用程序。(3)、输入Y 或者 A。

【msvcr120.dll】修复电脑出现msvcr120.dll找不到的详细方法
当我们在使用某些程序或游戏时,需要调用这个动态链接库文件中的函数或资源,如果这个文件丢失或损坏,就会导致程序无法正常运行,从而出现“msvcr120.dll丢失”的错误提示。了解了msvcr120.dll丢失的原因后,我们再来看一下msvcr120.dll的作用。我们可以下载一个dll修复工具,使用dll修复工具进行修复操作非常简单(亲测可以修复),它可以自动检测电脑缺失或者损坏的dll文件,如果msvcr120.dll缺失,dll修复工具检测到以后,便会自动安装msvcr120.dll文件。

鸿蒙Harmony-层叠布局(Stack)详解
我们总是为了太多遥不可及的东西去拼命,却忘了人生真正的幸福不过是灯火阑珊处的温暖,柴米油盐的充实,人生无论你赚的钱,是多还是少,经历的事情是好还是坏,都不如过好当下的每一天!

如何优化Uniapp应用程序的性能?
避免频繁的重绘和重排:频繁的DOM操作会导致浏览器频繁的重绘和重排,影响性能。使用v-for中的key属性:在使用v-for渲染列表时,为每个列表项添加唯一的key属性,这样可以减少渲染的次数,提高渲染的效率。减少页面加载时间:避免页面过多和过大的组件,减少不必要的资源加载。使用图片懒加载:对于图片较多的场景,可以使用图片懒加载的方式,当图片进入用户可视范围时再进行加载,减少初始页面加载的时间。节流和防抖:对于频繁触发事件的场景,可以使用节流和防抖的方法来减少事件处理的频率,从而提高性能。

【MATLAB】 SSA奇异谱分析信号分解算法
SSA奇异谱分析(Singular Spectrum Analysis)是一种处理非线性时间序列数据的方法,可以对时间序列进行分析和预测。它基于构造在时间序列上的特定矩阵的奇异值分解(SVD),可以从一个时间序列中分解出趋势、振荡分量和噪声。具体流程如下:根据原始时间序列构建轨迹矩阵X XX。对矩阵X进行奇异值分解:X = ∑ i = 1 r σ i U i V i T X=\sum_{i=1}^{r} \sigma_i U_i V_{i}^TX=∑i=1rσiUiViT。

自定义白平衡调节的步骤 白平衡怎么设置好 白平衡和色温的关系 用什么软件调节白平衡
也就是说,当相机的白平衡数值和色温数值一样时,相机呈现的光源就是纯白光源,拍摄出来的画面就是白色,这就是白平衡的主要作用,通过调整色温和白平衡数值,能改变画面色彩,色温和白平衡数值越接近,画面就越白。白平衡的k值越高,拍摄的画面颜色就越暖/偏黄(它与色温相反,色温是k值越高,画面越冷),k值越低,画面颜色越冷/偏蓝,自定义白平衡能让画面色温校正的很精准,但是操作很复杂。先来说下色温,色温是指光线在不同的能量下,眼睛感受到的颜色变化,简单理解就是光线的颜色,色温计算单位是开尔文(k)。

windows安装conda环境,开发openai应用准备,运行第一个ai程序
作者开发第一个openai应用的环境准备、第一个openai程序调用成功,做个记录,希望帮助新来的你。第一次能成功运行的openai程序,狠开心。

【总结】SpringBoot 中过滤器、拦截器、监听器的基本使用
拦截器是在面向切面编程中应用的,就是在你的service或者一个方法前调用一个方法,或者在方法后调用一个方法。是基于JAVA的反射机制。1)预处理preHandle()方法用户发送请求时,先执行preHandle()方法。会先按照顺序执行所有拦截器的preHandle方法,一直遇到return false为止,比如第二个preHandle方法是return false,则第三个以及以后所有拦截器都不会执行。若都是return true,则执行用户请求的url方法。2)后处理postHandle()方法。
SpringBoot 使用过滤器、拦截器、切面(AOP),及其之间的区别和执行顺序
Servlet(Server Applet),全称是Java Servlet,是提供基于协议请求/响应服务的Java类。在JavaEE中是Servlet规范,即是指Java语言实现的一个接口,广义的Servlet是指任何实现了这个Servlet接口的Java类,一般人们理解是后者。是什么。