缓存和数据库一致性
前言:
项目的难点是如何保证缓存和数据库的一致性。无论我们是先更新数据库,后更新缓存还是先更新数据库,然后删除缓存,在并发场景之下,仍然会存在数据不一致的情况(也存在删除失败的情况,删除失败可以使用异步重试解决)。有一种解决方法是延迟双删的策略,先删除缓存,再更新数据库,然后休眠一会儿,再删除一次缓存,这样做可以提高提高数据的一致性,但是,延迟的时间是要根据业务需求决定的,需要谨慎设置,同时由于删除了两次缓存,导致性能下降。这个项目中选择的是借助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&…...
vscode里如何用git
打开vs终端执行如下: 1 初始化 Git 仓库(如果尚未初始化) git init 2 添加文件到 Git 仓库 git add . 3 使用 git commit 命令来提交你的更改。确保在提交时加上一个有用的消息。 git commit -m "备注信息" 4 …...
使用VSCode开发Django指南
使用VSCode开发Django指南 一、概述 Django 是一个高级 Python 框架,专为快速、安全和可扩展的 Web 开发而设计。Django 包含对 URL 路由、页面模板和数据处理的丰富支持。 本文将创建一个简单的 Django 应用,其中包含三个使用通用基本模板的页面。在此…...
Debian系统简介
目录 Debian系统介绍 Debian版本介绍 Debian软件源介绍 软件包管理工具dpkg dpkg核心指令详解 安装软件包 卸载软件包 查询软件包状态 验证软件包完整性 手动处理依赖关系 dpkg vs apt Debian系统介绍 Debian 和 Ubuntu 都是基于 Debian内核 的 Linux 发行版ÿ…...
Device Mapper 机制
Device Mapper 机制详解 Device Mapper(简称 DM)是 Linux 内核中的一套通用块设备映射框架,为 LVM、加密磁盘、RAID 等提供底层支持。本文将详细介绍 Device Mapper 的原理、实现、内核配置、常用工具、操作测试流程,并配以详细的…...
让回归模型不再被异常值“带跑偏“,MSE和Cauchy损失函数在噪声数据环境下的实战对比
在机器学习的回归分析中,损失函数的选择对模型性能具有决定性影响。均方误差(MSE)作为经典的损失函数,在处理干净数据时表现优异,但在面对包含异常值的噪声数据时,其对大误差的二次惩罚机制往往导致模型参数…...
快刀集(1): 一刀斩断视频片头广告
一刀流:用一个简单脚本,秒杀视频片头广告,还你清爽观影体验。 1. 引子 作为一个爱生活、爱学习、爱收藏高清资源的老码农,平时写代码之余看看电影、补补片,是再正常不过的事。 电影嘛,要沉浸,…...
Webpack性能优化:构建速度与体积优化策略
一、构建速度优化 1、升级Webpack和Node.js 优化效果:Webpack 4比Webpack 3构建时间降低60%-98%。原因: V8引擎优化(for of替代forEach、Map/Set替代Object)。默认使用更快的md4哈希算法。AST直接从Loa…...
Unity中的transform.up
2025年6月8日,周日下午 在Unity中,transform.up是Transform组件的一个属性,表示游戏对象在世界空间中的“上”方向(Y轴正方向),且会随对象旋转动态变化。以下是关键点解析: 基本定义 transfor…...
LLaMA-Factory 微调 Qwen2-VL 进行人脸情感识别(二)
在上一篇文章中,我们详细介绍了如何使用LLaMA-Factory框架对Qwen2-VL大模型进行微调,以实现人脸情感识别的功能。本篇文章将聚焦于微调完成后,如何调用这个模型进行人脸情感识别的具体代码实现,包括详细的步骤和注释。 模型调用步骤 环境准备:确保安装了必要的Python库。…...
加密通信 + 行为分析:运营商行业安全防御体系重构
在数字经济蓬勃发展的时代,运营商作为信息通信网络的核心枢纽,承载着海量用户数据与关键业务传输,其安全防御体系的可靠性直接关乎国家安全、社会稳定与企业发展。随着网络攻击手段的不断升级,传统安全防护体系逐渐暴露出局限性&a…...
