缓存和数据库一致性

前言:

项目的难点是如何保证缓存和数据库的一致性。无论我们是先更新数据库,后更新缓存还是先更新数据库,然后删除缓存,在并发场景之下,仍然会存在数据不一致的情况(也存在删除失败的情况,删除失败可以使用异步重试解决)。有一种解决方法是延迟双删的策略,先删除缓存,再更新数据库,然后休眠一会儿,再删除一次缓存,这样做可以提高提高数据的一致性,但是,延迟的时间是要根据业务需求决定的,需要谨慎设置,同时由于删除了两次缓存,导致性能下降。这个项目中选择的是借助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, 1); // 初始重试次数为 1ack.acknowledge();}@Retryable(maxAttempts = 3, backoff = @Backoff(delay = 1000))private void retryDeleteCache(CanalMessage canalMessage, int retryCount) {try {// 删除缓存的操作deleteCache(canalMessage);log.info("删除缓存成功");} catch (Exception e) {// 处理删除缓存失败的异常log.error("删除缓存失败:{}", e.getMessage());if (retryCount < 3) {// 如果未达到最大重试次数,继续重试retryDeleteCache(canalMessage, retryCount + 1);} else {// 达到最大重试次数,抛出异常触发 @Recover 方法throw new RuntimeException("删除缓存达到最大重试次数");}}}private void deleteCache(CanalMessage canalMessage) {// 实际的删除缓存的操作// ...// 这里可以模拟删除失败的情况if (shouldFailToDeleteCache()) {throw new RuntimeException("模拟删除缓存失败");}}private boolean shouldFailToDeleteCache() {// 模拟删除缓存失败的条件,你可以根据实际情况进行修改return Math.random() < 0.5;}@Recoverprivate void recoverAfterMaxAttempts(Exception e, CanalMessage canalMessage) {// 在达到最大重试次数后执行的备用方法log.error("删除缓存达到最大重试次数,执行备用方法:{}", e.getMessage());// 可以在这里处理达到最大重试次数后的逻辑,例如记录日志、发送通知等}
}

先更新数据库,后更新缓存

假设我们采用「先更新数据库,再更新缓存」的方案,并且两步都可以「成功执行」的前提下,如果存在并发,情况会是怎样的呢?

有线程 A 和线程 B 两个线程,需要更新「同一条」数据,会发生这样的场景:

  1. 线程 A 更新数据库(X = 1)

  2. 线程 B 更新数据库(X = 2)

  3. 线程 B 更新缓存(X = 2)

  4. 线程 A 更新缓存(X = 1)

最终 X 的值在缓存中是 1,在数据库中是 2,发生不一致。A 虽然先于 B 发生,但 B 操作数据库和缓存的时间,却要比 A 的时间短,执行时序发生「错乱」,最终这条数据结果是不符合预期的。

先更新数据库,后删除缓存

依旧是 2 个线程并发「读写」数据:

  1. 缓存中 X 不存在(数据库 X = 1)

  2. 线程 A 读取数据库,得到旧值(X = 1)

  3. 线程 B 更新数据库(X = 2)

  4. 线程 B 删除缓存

  5. 线程 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)       //删除某个分区下的所有数据

延迟双删:

  1. 首先,删除了 Redis 中的缓存数据,以确保接下来的读取操作会从数据库中读取最新的数据。
  2. 接着,更新数据库中的数据,将数据更新为最新的值。
  3. 在此之后,代码让当前线程休眠一段时间N,这个时间段是为了给数据库操作足够的时间来完成,确保数据已经持久化到数据库中。
  4. 最后,代码再次删除 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工作原理

  1. Canal将自己伪装为MysQL slave(从库),向MysQL master(住库)发送dump协议
  2. MysQLmaster(主库)收到dump请求,开始推送binarylog给slave(即canal).
  3. 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已经成为了各种数据处理任务的重要工具之一。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/239620.html

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

相关文章

软件测试|解决Github port 443 : Timed out连接超时的问题

前言 GitHub是全球最大的开源代码托管平台之一&#xff0c;许多开发者和团队使用它来管理和协作开源项目。但在当下&#xff0c;我们在clone或者提交代码时会经常遇到"GitHub Port 443: Timed Out"错误&#xff0c;这意味着我们的电脑无法建立与GitHub服务器的安全连…

鸿蒙Harmony--AppStorage--应用全局的UI状态存储详解

无所求必满载而归&#xff0c;当你降低期待&#xff0c;降低欲望&#xff0c;往往会得到比较好的结果&#xff0c;把行动交给现在&#xff0c;用心甘情愿的态度&#xff0c;过随遇而安的生活&#xff0c;无论结果如何&#xff0c;都是一场惊喜的获得! 目录 一&#xff0c;定义 …

MySQL单表查询练习题

一、创建表的素材 表名&#xff1a;worker——表中字段均为中文&#xff0c;比如&#xff1a;部门号、工资、职工号、参加工作等 CREATE TABLE worker ( 部门号 int(11) NOT NULL, 职工号 int(11) NOT NULL, 工作时间 date NOT NULL, 工资 float(8,2) NOT NULL, 政治面貌 …

MySQL进阶45讲【1】基础架构:一条SQL查询语句是如何执行的?

1 前言 我们经常说&#xff0c;看一个事儿千万不要直接陷入细节里&#xff0c;应该先鸟瞰其全貌&#xff0c;这样能够帮助你从高维度理解问题。同样&#xff0c;对于MySQL的学习也是这样。平时我们使用数据库&#xff0c;看到的通常都是一个整体。比如&#xff0c;有个最简单的…

uniapp 字母索引列表插件(组件版) Ba-SortList

简介&#xff08;下载地址&#xff09; Ba-SortList 是一款字母索引列表组件版插件&#xff0c;可自定义样式&#xff0c;支持首字母字母检索、首字检索、搜索等等&#xff1b;支持点击事件。 支持首字母字母检索支持首字检索支持搜索支持点击事件支持长按事件支持在uniapp界…

八:分布式锁

1、为什么要使用分布式锁 锁是多线程代码中的概念&#xff0c;只有多任务访问同一个互斥的共享资源时才需要锁。单机应用开发时一般使用synchronized或lock。多线程的运行都是在同一个JVM之下。应用是分布式集群&#xff0c;属于多JVM的工作环境&#xff0c;JVM之间已经无法通过…

STM32 定时器输入捕获2——捕获高电平时长

由上图我们可以知道&#xff0c;高电平时间t2-t1。在代码中&#xff0c;可以记录此时t1的时间然后再记录t2的时间&#xff0c;t2-t1&#xff0c;就是我们所想要的答案。 但是&#xff0c;还有更简单一点点的&#xff0c;当到达t1的时候&#xff0c;我们把定时器清零&#xff0c…

MIT_线性代数笔记:第 26 讲 复矩阵;快速傅里叶变换

目录 复向量 Complex vectors复矩阵 Complex matrices傅里叶变换 Fourier transform快速傅里叶变换 Fast Fourier transform 实矩阵也可能有复特征值&#xff0c;因此无法避免在矩阵运算中碰到复数&#xff0c;本讲学习处理复数矩阵和复向量。 最重要的复矩阵是傅里叶矩阵&…

Linux CentOS 7.6安装JDK详细保姆级教程

一、检查系统是否自带jdk java --version 如果有的话&#xff0c;找到对应的文件删除 第一步&#xff1a;先查看Linux自带的JDK有几个&#xff0c;用命令&#xff1a; rpm -qa | grep -i java第二步:删除JDK&#xff0c;执行命令&#xff1a; rpm -qa | grep -i java | xarg…

【Nuxt3】Nuxt3脚手架nuxi安装项目和项目目录介绍

简言 最近学了Nuxt3,并使用它创建了自己的小网站。记录下学习到的nuxt3内容。 Nuxt3官网 Nuxt 是一个免费的开源框架&#xff0c;可通过直观、可扩展的方式使用 Vue.js 创建类型安全、高性能、生产级的全栈 Web 应用程序和网站。 支持SSR、SPA、建立静态网站&#xff0c;也可以…

【大数据】Flink 详解(九):SQL 篇 Ⅱ

《Flink 详解》系列&#xff08;已完结&#xff09;&#xff0c;共包含以下 10 10 10 篇文章&#xff1a; 【大数据】Flink 详解&#xff08;一&#xff09;&#xff1a;基础篇【大数据】Flink 详解&#xff08;二&#xff09;&#xff1a;核心篇 Ⅰ【大数据】Flink 详解&…

若依在表格中如何将字典的键值转为中文

文章目录 一、需求&#xff1a;二、问题解决步骤1、给需要转换的列绑定formatter属性2、获取字典项3、编写formatter属性绑定的方法 一、需求&#xff1a; 后端有时候返回的是字典的键值&#xff0c;在前端展示时需要转成中文值 后端返回的是dictValue&#xff0c;现在要转换…

QT -狗狗管理工具

QT -狗狗管理工具 一、演示效果二、UML三、关键代码四、程序链接 一、演示效果 二、UML 三、关键代码 #include <QFrame> #include <QHBoxLayout> #include <QVBoxLayout> #include <QLabel> #include <QSizePolicy> #include <QDialog> …

谷歌aab包在Android 14闪退而apk没问题(targetsdk 34)

问题原因 Unity应用(target SDK 34)上线到GooglePlay&#xff0c;有用户反馈fold5设备上&#xff08;Android14系统&#xff09;疯狂闪退&#xff0c;经测试&#xff0c;在小米手机Android14系统的版本复现成功了&#xff0c;奇怪的是apk直接安装没问题&#xff0c;而打包成aa…

为什么使用双token实现无感刷新用户认证?

单token机制 认证机制&#xff1a;对与单token的认证机制在我们项目中仅使用一个Access Token的访问令牌进行用户身份认证和授权的方案处理。 不足之处&#xff1a; 安全性较低(因为只有一个token在客户端和服务器端之间进行传递&#xff0c;一目Acess Token被截获或者被泄露…

MetaGPT前期准备与快速上手

大家好&#xff0c;MetaGPT 是基于大型语言模型&#xff08;LLMs&#xff09;的多智能体协作框架&#xff0c;GitHub star数量已经达到31.3k。 接下来我们聊一下快速上手 这里写目录标题 一、环境搭建1.python 环境2. MetaGpt 下载 二、MetaGPT配置1.调用 ChatGPT API 服务2.简…

Python武器库开发-武器库篇之Whois信息收集模块化(四十五)

Python武器库开发-武器库篇之Whois信息收集模块化(四十五) 我们在进行渗透的时候&#xff0c;需要进行全面的信息收集&#xff0c;除了主动信息收集之外&#xff0c;我们还经常会进行被动信息收集&#xff0c;Whois信息收集就是其中的一种,我们可以利用一些网站进行Whois信息收…

k8s 存储卷和pvc,pv

存储卷---数据卷 容器内的目录和宿主机的目录进行挂载。 容器在系统上的生命周期是短暂的&#xff0c;deletek8s用控制器创建的pod&#xff0c;delete相当于重启&#xff0c;容器的状态也会回复到初始状态。 一旦回到初始状态&#xff0c;所有的后天编辑的文件的都会消失。 …

rime中州韵小狼毫 LaTex输入法配置

教程目录&#xff1a;rime中州韵小狼毫须鼠管安装配置教程 保姆级教程 100增强功能配置教程 本文的分享一种在rime中州韵小狼毫须鼠管输入法中配置LaTex输入方案的方法&#xff0c;并不完美&#xff0c;仅供参考。 先睹为快 LaTex输入方案可以快捷的在公式模式下输入一些基础…

C#编程-使用事件

使用事件 事件是一个动作或发生的事情,例如:鼠标点击、按键、鼠标移动或系统产生的通知。应用程序可以在事件发生的时候做出响应。通知的一个示例是中断。事件是对象发生的消息以表示事件的发生。事件是进程内通信的有效方法。它们对对象时有用的,因为它们标识了单个状态改…