缓存和数据库一致性
前言:
项目的难点是如何保证缓存和数据库的一致性。无论我们是先更新数据库,后更新缓存还是先更新数据库,然后删除缓存,在并发场景之下,仍然会存在数据不一致的情况(也存在删除失败的情况,删除失败可以使用异步重试解决)。有一种解决方法是延迟双删的策略,先删除缓存,再更新数据库,然后休眠一会儿,再删除一次缓存,这样做可以提高提高数据的一致性,但是,延迟的时间是要根据业务需求决定的,需要谨慎设置,同时由于删除了两次缓存,导致性能下降。这个项目中选择的是借助canal,订阅binlog的方式来实现同步。当我们对数据进行修改的时候,数据库就有一个binlog,用canal订阅这个binlog,并将消息投放到kafka中,获取到变更数据的id,删除缓存,如果失败,则重新从消息队列中获得该数据进行重试,超过次数后提示错误信息。重试使用的是@Retryable注解,有最大重试次数、重试延迟时间等属性,如果三次重试后仍然失败,会抛出 RuntimeException
,然后触发@recover注解标记的备用方法。
(把canal.serverMode = kafka,canal.destinations = example (对应一个数据库实例),
在example下修改MySQL实例配置文件,设置用户名、登陆密码、需要同步的表,topic等等)
然后创建消费者,接收topic传过来的消息,进行解析,获取id,删除缓存。
canal本质(工作原理)
- canal 模拟 MySQL slave 的交互协议,伪装自己为 MySQL slave ,向 MySQL master 发送 dump 协议;
- MySQL master 收到 dump 请求,开始推送 binary log 给 slave (即 canal );
- canal 解析 binary log 对象(原始为 byte 流);
- 并将其发送给下游消息队列
/*** canal 发送过来的消息** @author huan.fu 2021/9/2 - 下午4:06*/
@Getter
@Setter
@ToString
public class CanalMessage {/*** 测试得出 同一个事物下产生多个修改,这个id的值是一样的。*/private Integer id;/*** 数据库或schema*/private String database;/*** 表名*/private String table;/*** 主键字段名*/private List<String> pkNames;/*** 是否是ddl语句*/private Boolean isDdl;/*** 类型:INSERT/UPDATE/DELETE*/private String type;/*** binlog executeTime, 执行耗时*/private Long es;/*** dml build timeStamp, 同步时间*/private Long ts;/*** 执行的sql,dml sql为空*/private String sql;/*** 数据列表*/private List<Map<String, Object>> data;/*** 旧数据列表,用于update,size和data的size一一对应*/private List<Map<String, Object>> old;
}
import org.springframework.kafka.annotation.KafkaListener;
import org.springframework.retry.annotation.Backoff;
import org.springframework.retry.annotation.Recover;
import org.springframework.retry.annotation.Retryable;
import org.springframework.stereotype.Component;@Component
@Slf4j
public class KafkaConsumer {@KafkaListener(topics = "customer", groupId = "canal-kafka-springboot-001", concurrency = "5")@Retryable(maxAttempts = 3, backoff = @Backoff(delay = 1000))public void consumer(ConsumerRecord<String, String> record, Acknowledgment ack) throws InterruptedException {log.info(Thread.currentThread().getName() + ":" + LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")) + "接收到kafka消息,partition:" + record.partition() + ",offset:" + record.offset() + "value:" + record.value());CanalMessage canalMessage = JSON.parseObject(record.value(), CanalMessage.class);log.info("\r=================================");log.info("接收到的原始 canal message为: {}", record.value());log.info("转换成Java对象后转换成Json为 : {}", JSON.toJSONString(canalMessage));// 重试删除缓存的操作retryDeleteCache(canalMessage);ack.acknowledge();}@Retryable(maxAttempts = 3, backoff = @Backoff(delay = 1000))private void retryDeleteCache(CanalMessage canalMessage) {// 实现删除缓存的逻辑,如果失败将会重试,最多三次try {// 删除缓存的操作deleteCache(canalMessage);} catch (Exception e) {// 处理删除缓存失败的异常log.error("删除缓存失败:{}", e.getMessage());throw new RuntimeException("删除缓存失败");}}private void deleteCache(CanalMessage canalMessage) {// 实际的删除缓存的操作// ...log.info("删除缓存成功");}@Recoverprivate void recoverAfterMaxAttempts(Exception e, CanalMessage canalMessage) {// 在达到最大重试次数后执行的备用方法log.error("删除缓存达到最大重试次数,执行备用方法:{}", e.getMessage());// 可以在这里处理达到最大重试次数后的逻辑,例如记录日志、发送通知等}
}
先更新数据库,后更新缓存
假设我们采用「先更新数据库,再更新缓存」的方案,并且两步都可以「成功执行」的前提下,如果存在并发,情况会是怎样的呢?
有线程 A 和线程 B 两个线程,需要更新「同一条」数据,会发生这样的场景:
-
线程 A 更新数据库(X = 1)
-
线程 B 更新数据库(X = 2)
-
线程 B 更新缓存(X = 2)
-
线程 A 更新缓存(X = 1)
最终 X 的值在缓存中是 1,在数据库中是 2,发生不一致。A 虽然先于 B 发生,但 B 操作数据库和缓存的时间,却要比 A 的时间短,执行时序发生「错乱」,最终这条数据结果是不符合预期的。
先更新数据库,后删除缓存
依旧是 2 个线程并发「读写」数据:
-
缓存中 X 不存在(数据库 X = 1)
-
线程 A 读取数据库,得到旧值(X = 1)
-
线程 B 更新数据库(X = 2)
-
线程 B 删除缓存
-
线程 A 将旧值写入缓存(X = 1)
最终 X 的值在缓存中是 1(旧值),在数据库中是 2(新值),也发生不一致。
相关代码:
public R save(UserVO userVO) {User user = new User();BeanUtils.copyProperties(userVO, user);saveUser(user);//删除缓存redisTemplate.delete("userInfo:" + user.getUserName());return R.success("操作成功");}
在查询数据时,先从缓存获取,如果缓存没有,就从数据库查询,并同时存放到缓存上,这样保证了下次访问时数据能直接从缓存获取,减少了数据库压力
public User getByUserName(String userName) {User user = (User) redisTemplate.opsForValue().get(userName);if (user != null) {return user;}user = this.getOne(Wrappers.<User>lambdaQuery().eq(User::getUserName, userName));redisTemplate.opsForValue().set("userInfo:" + userName, user);return user;}
但是执行redis的删除操作时,比如因为网络问题,或者redis本身服务问题,就会失败,而且多线程并发访问时,也会出现数据不一致的情况。
//使用注解,直接实现先更新数据库,后删除缓存的操作@CacheEvict(value = "category",allEntries = true) //删除某个分区下的所有数据
延迟双删:
- 首先,删除了 Redis 中的缓存数据,以确保接下来的读取操作会从数据库中读取最新的数据。
- 接着,更新数据库中的数据,将数据更新为最新的值。
- 在此之后,代码让当前线程休眠一段时间N,这个时间段是为了给数据库操作足够的时间来完成,确保数据已经持久化到数据库中。
- 最后,代码再次删除 Redis 中的缓存数据。这里是延迟双删的关键步骤。由于之前已经删除了缓存数据,再次删除的目的是为了防止在休眠的时间内有其他线程读取到旧的数据,加载到缓存中。
休眠时间的控制
-
延迟时间要大于「主从复制」的延迟时间
-
延迟时间要大于线程 B 读取数据库 + 写入缓存的时间
方案的选择:
- 延时双删适用于对数据一致性要求较高的场景。它能够保证在数据库更新期间,读取请求不会读取到已经失效的缓存数据,从而保证数据的一致性。但是它需要进行两次缓存删除操作,可能会增加一定的资源开销;
- 先更新数据库后删除缓存对性能要求较高的场景。它能够减少一次缓存删除的开销,但是在数据库更新期间,读取请求可能会读取到已经失效的缓存数据,从而导致数据不一致。
RedisUtils.del(key);// 先删除缓存
updateDB(user);// 更新db中的数据
Thread.sleep(N);// 延迟一段时间,在删除该缓存key
RedisUtils.del(key);// 先删除缓存
最好的方法是开设一个线程池,在线程中删除key,而不是使用Thread.sleep进行等待,这样会阻塞用户的请求。
代码:
OrderController中新增接口:
/*** 下单接口:先更新数据库,再删缓存* @param sid* @return*/
@RequestMapping("/createOrderWithCacheV2/{sid}")
@ResponseBody
public String createOrderWithCacheV2(@PathVariable int sid) {int count = 0;try {// 完成扣库存下单事务orderService.createPessimisticOrder(sid);// 删除库存缓存stockService.delStockCountCache(sid);} catch (Exception e) {LOGGER.error("购买失败:[{}]", e.getMessage());return "购买失败,库存不足";}LOGGER.info("购买成功,剩余库存为: [{}]", count);return String.format("购买成功,剩余库存为:%d", count);
}
新增线程池接口
// 延时双删线程池
private static ExecutorService cachedThreadPool = new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60L, TimeUnit.SECONDS,new SynchronousQueue<Runnable>());/*** 缓存再删除线程*/
private class delCacheByThread implements Runnable {private int sid;public delCacheByThread(int sid) {this.sid = sid;}public void run() {try {LOGGER.info("异步执行缓存再删除,商品id:[{}], 首先休眠:[{}] 毫秒", sid, DELAY_MILLSECONDS);Thread.sleep(DELAY_MILLSECONDS);stockService.delStockCountCache(sid);LOGGER.info("再次删除商品id:[{}] 缓存", sid);} catch (Exception e) {LOGGER.error("delCacheByThread执行出错", e);}}
}
异步重试:
将删除缓存的请求写到消息队列中,如果删除成功,则去除消息;如果删除失败,执行失败策略,重试服务从消息队列中重新读取这些值,然后再次进行删除重试,重试超过的一定次数,向业务层发送报错信息。但是这在一定程度上也会增加代码的耦合度和维护成本。
高内聚,低耦合:耦合指模块与模块之间的关系,依赖程度,尽量减少一个模块过度依赖另一个模块的情况(我们在A元素去调用B元素,当B元素有问题或者不存在的时候,A元素就不能正常的工作,那么就说元素A和元素B耦合)。内聚模块内部的功能职责的相关性,如果元素有高度的相关职责,除了这些职责在没有其他的工作,那么该元素就有高内聚。这样做是为了可读性,复用性,可维护性和易变更性。
代码实现(使用rocketmq):
pom.xml新增rocketmq依赖:
<rocketmq-spring-boot-starter-version>2.0.3</rocketmq-spring-boot-starter-version><dependency><groupId>org.apache.rocketmq</groupId><artifactId>rocketmq-client</artifactId><version>4.9.3</version>
</dependency>
<dependency><groupId>org.apache.rocketmq</groupId><artifactId>rocketmq-spring-boot-starter</artifactId><version>${rocketmq-spring-boot-starter-version}</version>
</dependency>
yaml配置
rocketmq:name-server: xxx.xxx.xxx.174:9876;xxx.xxx.xxx.246:9876producer:group: shopDataGroup
创建服务:
@Override
@Transactional
public Result updateShopById(Shop shop) {Long id = shop.getId();if(ObjectUtil.isNull(id)){return Result.fail("====>店铺ID不能为空");}log.info("====》开始更新数据库");//更新数据库updateById(shop);String shopRedisKey = SHOP_CACHE_KEY + id;Message message = new Message(TOPIC_SHOP,"shopRe",shopRedisKey.getBytes());//异步发送MQtry {rocketMQTemplate.getProducer().send(message);} catch (Exception e) {log.info("=========>发送异步消息失败:{}",e.getMessage());}//stringRedisTemplate.delete(SHOP_CACHE_KEY + id);//int i = 1/0; 验证异常流程后,return Result.ok();
}
创建消费者:
package com.hmdp.mq;
/*** @author xbhog* @describe:* @date 2022/12/21*/
@Slf4j
@Component
@RocketMQMessageListener(topic = TOPIC_SHOP,consumerGroup = "shopRe",messageModel = MessageModel.CLUSTERING)
public class RocketMqNessageListener implements RocketMQListener<MessageExt> {@Resourceprivate StringRedisTemplate stringRedisTemplate;@SneakyThrows@Overridepublic void onMessage(MessageExt message) {log.info("========>异步消费开始");String body = null;body = new String(message.getBody(), "UTF-8");stringRedisTemplate.delete(body);int reconsumeTimes = message.getReconsumeTimes();log.info("======>重试次数{}",reconsumeTimes);if(reconsumeTimes > 3){log.info("消费失败:{}",body);return;}throw new RuntimeException("模拟异常抛出");}}
kafuka的删除重试机制:Kafka 生产者负责将删除缓存的请求发送到指定主题,而 Kafka 消费者则监听该主题,处理删除缓存的逻辑。在处理失败时,通过不提交偏移量来实现消息的重试。
import org.apache.kafka.clients.consumer.*;
import org.apache.kafka.common.TopicPartition;import java.time.Duration;
import java.util.Collections;
import java.util.Properties;public class CacheDeletionConsumer {private static final String TOPIC = "cache-deletion-topic";private static final String GROUP_ID = "cache-deletion-group";private static final String BOOTSTRAP_SERVERS = "your_bootstrap_servers";public static void main(String[] args) {Properties props = new Properties();props.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, BOOTSTRAP_SERVERS);props.put(ConsumerConfig.GROUP_ID_CONFIG, GROUP_ID);props.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, "org.apache.kafka.common.serialization.StringDeserializer");props.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, "org.apache.kafka.common.serialization.StringDeserializer");try (KafkaConsumer<String, String> consumer = new KafkaConsumer<>(props)) {consumer.subscribe(Collections.singletonList(TOPIC));while (true) {ConsumerRecords<String, String> records = consumer.poll(Duration.ofMillis(100));for (ConsumerRecord<String, String> record : records) {try {// 处理删除缓存的业务逻辑processCacheDeletion(record);// 如果删除成功,手动提交偏移量consumer.commitSync();} catch (Exception e) {// 处理删除缓存失败,不提交偏移量,消息将在下次拉取时重新获取handleCacheDeletionFailure(record, e);}}}}}private static void processCacheDeletion(ConsumerRecord<String, String> record) {// 实际的删除缓存逻辑String cacheKey = record.value().substring("DELETE_CACHE:".length());System.out.println("Deleting cache for key: " + cacheKey);}private static void handleCacheDeletionFailure(ConsumerRecord<String, String> record, Exception e) {// 处理删除缓存失败的逻辑,可以记录日志、进行重试等System.err.println("Error deleting cache for key: " + record.value() + ". Exception: " + e.getMessage());}
}
参考:两难!先更新数据库再删缓存?还是先删缓存再更新数据库?-CSDN博客
https://www.cnblogs.com/xbhog/p/17004151.html
订阅binlog异步删除,使用canal
流程如下图所示:
(1)更新数据库数据
(2)数据库会将操作信息写入binlog日志当中
(3)canal订阅程序提取出所需要的数据以及key
(4)另起一段非业务代码,获得该信息
(5)尝试删除缓存操作,发现删除失败
(6)将这些信息发送至消息队列
(7)重新从消息队列中获得该数据,重试操作
canal基础知识:
Canal组件是一个基于MysQL数据库增量日志解析,提供增量数据订阅和消费,支持将增量数据投递到下游消费者(如Kafka、RocketMQ等)或者存储(如Elasticsearch、HBase等)的组件。
binlog的格式:
canal工作原理
- Canal将自己伪装为MysQL slave(从库),向MysQL master(住库)发送dump协议
- MysQLmaster(主库)收到dump请求,开始推送binarylog给slave(即canal).
- Canal接收并解析Binlog日志,得到变更的数据,执行后续逻辑
修改配置支持binlog:
修改canal.properties配置:
修改mysql文件的instance.properties配置:
代码:
//1.获取canal连接对象
CanalConnector canalConnector=CanalConnectors.newSingleConnector(new InetSocketAddress( hostname:"localhost",port:11111),destination:"example",username:"",password:"");while(true){//2.获取连接canalConnector.connect(;//3.指定要监控的数据库canalConnector.subscribe( s:"canal-demo.*");//4.获取MessageMessage message =canalConnector.get(100);List<CanalEntry.Entry>entries =message.getEntries();if(entries.size()<=0){System.out.println("没有数据,休息一会");try {Thread.sleep(millis:1000);}catch(InterruptedException e){e.printStackTrace();}}else{for(CanalEntry.Entry entry:entries){//获取表名String tableName =entry.getHeader().getTableName();//Entry类型CanalEntry.EntryType entryType =entry.getEntryType();//判断entryType 是否为ROWDATAif (CanalEntry.EntryType.ROWDATA.equals(entryType)){ //序列化数据ByteString storeValue =entry.getStoreValue();// 反序列化CanalEntry.RowChange rowChange =CanalEntry.RowChange.parseForm(storeValue);//获取事件类型CanalEntry.EventType eventType =rowChange.getEventType();//获取具体的数据List<CanalEntry.RowData>rowDatasList =rowChange.getRowDatasList();//遍历并打卬for (CanalEntry.RowData rowData : rowDatasList){//获取变更前的列List<CanalEntry.Column> beforeColumnsList = rowData.getBeforeColumnsList();Map<String,0bject>bMap=new HashMap<>();for (CanalEntry.CoLumn coLumn : beforeCoLumnsList)//列名和对应的值Map.put(column.getName(O),column.getValue(0);Map<String,0bject> afMap =new HashMap>();//变更后的List<CanalEntry.ColumnafterColumnsList=rowData.getAfterColumnsList();
问:Canal是什么?有哪些特性?
答:Canal是阿里巴巴开源的一款基于Netty实现的分布式、高性能、可靠的消息队列,在实时数据同步和数据分发场景下有着广泛的应用。Canal具有以下特性:支持MysQL、Oracle等数据库的日志解析和订阅;支持多种数据输出方式,如Kafka、RocketMQ、ActiveMQ等;支持支持数据过滤和格式转换;拥有低延迟和高可靠性等优秀的性能指标。
问:Canal的工作原理是什么?
答:Canal主要通过解析数据库的binlog日志来获取到数据库的增、删、改操作,然后将这些变更事件发送给下游的消费者。Canal核心组件包括Client和Server两部分,Client负责连接数据库,并启动日志解析工作,将解析出来的数据发送给Server;Server则负责接收Client发送的数据,并进行数据过滤和分发。Canal还支持多种数据输出器,如Kafka、RocketMQ、ActiveMQ等,可以将解析出来的数据发送到不同的消息队列中,以便进行进一步的处理和分析。
问:Canal的优缺点是什么?
答:Canal的优点主要包括:高性能、分布式、可靠性好、支持数据过滤和转换、跨数据库类型(如MysQL、Oracle等)等。缺点包括:使用难度较大、对数据库的日志产生一定的影响、不支持数据的回溯(即无法获取历史数据)等。
问:Canal在业务中有哪些应用场景?
答:Canal主要用于实时数据同步和数据分发场景,常见的应用场景包括:数据备份与灾备、增量数据抽取和同步、数据实时分析、在线数据迁移等。特别是在互联网大数据场景下,Canal已经成为了各种数据处理任务的重要工具之一。
相关文章:

缓存和数据库一致性
前言: 项目的难点是如何保证缓存和数据库的一致性。无论我们是先更新数据库,后更新缓存还是先更新数据库,然后删除缓存,在并发场景之下,仍然会存在数据不一致的情况(也存在删除失败的情况,删除…...

iOS UI掉帧和卡顿优化解决方案记录
UI卡顿原理 在 VSync 信号到来后,系统图形服务会通过 CADisplayLink 等机制通知 App,App 主线程开始在 CPU 中计算显示内容,比如视图的创建、布局计算、图片解码、文本绘制等。随后 CPU 会将计算好的内容提交到 GPU 去,由 GPU 进行…...

transbigdata 笔记: 轨迹密集化/稀疏化 轨迹平滑
1 密集化 transbigdata.traj_densify(data, col[Vehicleid, Time, Lng, Lat], timegap15) 轨迹致密化,保证至多每隔timegap秒都有一个轨迹点 这边插补使用的是pandas的interpolate,method设置的是index 1.1 举例 transbigdata 笔记: 官方…...

反向代理的本质是什么?
反向代理是一种网络架构模式,通常用于提供静态内容、处理安全、负载均衡和缓存等任务。在这种架构中,客户端发送的请求首先到达反向代理服务器,然后由反向代理服务器将请求转发给后端的实际服务器。反向代理服务器可以处理和修改请求和响应&a…...

Kali Linux保姆级教程|零基础从入门到精通,看完这一篇就够了!(附工具包)
作为一名从事网络安全的技术人员,不懂Kali Linux的话,连脚本小子都算不上。 Kali Linux预装了数百种享誉盛名的渗透工具,使你可以更轻松地测试、破解以及进行与数字取证相关的任何其他工作。 今天给大家分享一套Kali Linux资料合集…...

UML-用例图
提示:用例图是软件建模的开始,软件建模中的其他图形都将以用例图为依据。用例图列举了系统所需要实现的所有功能,除了用于软件开发的需求分析阶段,也可用于软件的系统测试阶段。 UML-用例图 一、用例图的基础知识1.用例图的构成元…...

jmeter--8.加密传输
目录 1. Base64加密 2. MD5加密 3. SHA加密(sha1\sha\sha224\sha256\sha384\sha512) 4. RSA加密-公钥加密,私钥解密 1. Base64加密 1.1 在需要加密传输的接口下新增BeanShell 预处理程序,${username}可替换成value值ÿ…...
微信小程序canvas画布转图片转pdf文件
关键步骤介绍 步骤一:将canvas页面保存为图片 for(var a=0;a<this.data.page_canvas.length;++a){ var t_page_img = await this.canvas_to_image(this.data.page_canvas[a]) t_img.push(t_page_img) } this.data.page_canvas是保存的canvas界面,this.c…...

【Linux操作】国产Linux服务管理操作
【Linux操作】国产Linux服务管理操作 前言SAMBA配置服务器端1. 安装相关包2. 配置/etc/samba/smb.conf,在此文件末尾添加如下内容,并保存退出。3. 创建/home/share并更改权限4. 启动samba服务 客户端• Windows客户端• 麒麟客户端 Telnet1、telnet语法2…...

大语言模型系列-word2vec
文章目录 前言一、word2vec的网络结构和流程1.Skip-Gram模型2.CBOW模型 二、word2vec的训练机制1. Hierarchical softmax2. Negative Sampling 总结 前言 在前文大语言模型系列-总述已经提到传统NLP的一般流程: 创建语料库 > 数据预处理 > 分词向量化 > …...

vue项目运行报错this[kHandle] = new _Hash(algorithm, xofLen)
自从昨天分盘重装了最新版本的Node之后,项目是一启一个报错 出现这个报错时,需要在package.json文件中 dev命令行 增加:set NODE_OPTIONS–openssl-legacy-provider 出现该问题的原因: node.js V17开始版本中发布的是OpenSSL3.0,…...

APP兼容性测试,这几个面试硬技能,包教包会
兼容性测试主要通过人工或自动化的方式,在需要覆盖的终端设备上进行功能用例执行,查看软件性能、稳定性等是否正常。 对于需要覆盖的终端设备,大型互联网公司,像 BAT,基本都有自己的测试实验室,拥有大量终…...

【学习iOS高质量开发】——熟悉Objective-C
文章目录 一、Objective-C的起源1.OC和其它面向对象语言2.OC和C语言3.要点 二、在类的头文件中尽量少引用其他头文件1.OC的文件2.向前声明的好处3.如何正确引入头文件4.要点 三、多用字面量语法,少用与之等价的方法1.何为字面量语法2.字面数值3.字面量数组4.字面量字…...

Qt/QML编程之路:Grid、GridLayout、GridView、Repeater(33)
GRID网格用处非常大,不仅在excel中,在GUI中,也是非常重要的一种控件。 Grid 网格是一种以网格形式定位其子项的类型。网格创建一个足够大的单元格网格,以容纳其所有子项,并将这些项从左到右、从上到下放置在单元格中。每个项目都位于其单元格的左上角,位置为(0,0)。…...

mac pro “RESP.app”意外退出 redis desktop manager
文章目录 redis desktop manager下载地址提示程序含有恶意代码“RESP.app”意外退出解决办法:下载python3.10.并安装重新打开RESP如果还是不行,那么需要替换错误路径(我的没用)外传 最近在研究redis的消息,看到了strea…...

VirtualBox 如何让虚拟机和主机互相通信
首先建立一张虚拟网卡 在这里进行网络设置 设置成固定ip,这张网卡专门用来通信,上面的网卡用来上网的...

【Java】源码文件开头添加注释
需求 应公司质量部要求,需要对代码做静态检查。质量部要求,源码文件必须在起始行起设置一些注释,然而项目已经开发了一年之久,且没有维护这个注释。 此时,面对好几千个源码文件,我们如何快速添加相应的注…...
GitHub 异常 - 无法连接22端口 Connection timed out
GitHub 异常 - 无法连接22端口 Connection timed out 问题描述 错误信息: 今天突然用ssh方式 pull GitHub的项目报:ssh: connect to host xx.xx.xx.xx port 22: Connection timed out 表明 SSH 连接在尝试通过 22 端口连接到远程服务器时超时。这可能是由于网络环…...

python基础学习
缩⼩图像(或称为下采样(subsampled)或降采样(downsampled))的主要⽬的有两个:1、使得图像符合显⽰区域的⼤⼩;2、⽣成对应图像的缩略图。 放⼤图像(或称为上采样…...

Python密码本连接wifi
有时候我们会忘记自己的Wi-Fi密码,或者需要连接某个Wi-Fi网络以满足合法需求。本文将介绍如何使用Python编程语言编写一个简单的连接Wi-Fi的程序。 一、密码本准备 在进行wifi猜测时,其实就是列出各种可能的密码,用来尝试去访问目标wifi&…...

2025 后端自学UNIAPP【项目实战:旅游项目】6、我的收藏页面
代码框架视图 1、先添加一个获取收藏景点的列表请求 【在文件my_api.js文件中添加】 // 引入公共的请求封装 import http from ./my_http.js// 登录接口(适配服务端返回 Token) export const login async (code, avatar) > {const res await http…...
CRMEB 框架中 PHP 上传扩展开发:涵盖本地上传及阿里云 OSS、腾讯云 COS、七牛云
目前已有本地上传、阿里云OSS上传、腾讯云COS上传、七牛云上传扩展 扩展入口文件 文件目录 crmeb\services\upload\Upload.php namespace crmeb\services\upload;use crmeb\basic\BaseManager; use think\facade\Config;/*** Class Upload* package crmeb\services\upload* …...

QT: `long long` 类型转换为 `QString` 2025.6.5
在 Qt 中,将 long long 类型转换为 QString 可以通过以下两种常用方法实现: 方法 1:使用 QString::number() 直接调用 QString 的静态方法 number(),将数值转换为字符串: long long value 1234567890123456789LL; …...
大语言模型(LLM)中的KV缓存压缩与动态稀疏注意力机制设计
随着大语言模型(LLM)参数规模的增长,推理阶段的内存占用和计算复杂度成为核心挑战。传统注意力机制的计算复杂度随序列长度呈二次方增长,而KV缓存的内存消耗可能高达数十GB(例如Llama2-7B处理100K token时需50GB内存&a…...

华硕a豆14 Air香氛版,美学与科技的馨香融合
在快节奏的现代生活中,我们渴望一个能激发创想、愉悦感官的工作与生活伙伴,它不仅是冰冷的科技工具,更能触动我们内心深处的细腻情感。正是在这样的期许下,华硕a豆14 Air香氛版翩然而至,它以一种前所未有的方式&#x…...

html css js网页制作成品——HTML+CSS榴莲商城网页设计(4页)附源码
目录 一、👨🎓网站题目 二、✍️网站描述 三、📚网站介绍 四、🌐网站效果 五、🪓 代码实现 🧱HTML 六、🥇 如何让学习不再盲目 七、🎁更多干货 一、👨…...

R语言速释制剂QBD解决方案之三
本文是《Quality by Design for ANDAs: An Example for Immediate-Release Dosage Forms》第一个处方的R语言解决方案。 第一个处方研究评估原料药粒径分布、MCC/Lactose比例、崩解剂用量对制剂CQAs的影响。 第二处方研究用于理解颗粒外加硬脂酸镁和滑石粉对片剂质量和可生产…...

七、数据库的完整性
七、数据库的完整性 主要内容 7.1 数据库的完整性概述 7.2 实体完整性 7.3 参照完整性 7.4 用户定义的完整性 7.5 触发器 7.6 SQL Server中数据库完整性的实现 7.7 小结 7.1 数据库的完整性概述 数据库完整性的含义 正确性 指数据的合法性 有效性 指数据是否属于所定…...

push [特殊字符] present
push 🆚 present 前言present和dismiss特点代码演示 push和pop特点代码演示 前言 在 iOS 开发中,push 和 present 是两种不同的视图控制器切换方式,它们有着显著的区别。 present和dismiss 特点 在当前控制器上方新建视图层级需要手动调用…...

零知开源——STM32F103RBT6驱动 ICM20948 九轴传感器及 vofa + 上位机可视化教程
STM32F1 本教程使用零知标准板(STM32F103RBT6)通过I2C驱动ICM20948九轴传感器,实现姿态解算,并通过串口将数据实时发送至VOFA上位机进行3D可视化。代码基于开源库修改优化,适合嵌入式及物联网开发者。在基础驱动上新增…...