冗余双写方案下数据一致性问题解决及延申问题处理方案

主要整理了采用冗余双写方案后的问题解决方案。

1、问题:冗余双写场景下,如何解决数据一致性问题?

方案一:

        直接RPC调用+Seata分布式事务框架,采用该方式实现了事务的强一致性,代码逻辑简单的同时业务侵入性比较小。Seata支持AT、TCC、Soga三种模式:AT:隔离性好和低改造成本,但性能低;TCC:性能和隔离性好,但改造成本大;Soga:性能和低改造成本,但隔离性不好。先调用写入普通用户的短链表Rpc方法,后调用写入商家短链表的Rpc方法,通过@GlobalTransaction方式注解主方法。

        缺点:采用事务一致性后,高并发场景下性能下降严重,且本身Seata自身也存在一定的性能损耗。Seata更适用于后台管理系统等并发量不高的应用,并不适用C端应用。

        

方案二:

        使用消息队列(MQ)作为通信中介是一种高效的方式,其中生产者仅需确认消息发送成功即可,而订阅的消费者(B端/C端)则负责消费消息。对于商户(生产者)而言,一旦消息成功发送,便可立即返回成功标识,使得响应效率最大化。具体的业务逻辑,如创建短链码等任务,则由相应的消费者负责处理。这种方式不再依赖于强一致性(全局锁),从而提升了系统的请求并发量。然而,为确保最终一致性,必须加强消息处理的幂等性和异常处理能力。

        缺点:弱一致性,不适用于需要强一致的场景,当消费者消费失败时,需要额外写接口回滚生产者业务逻辑。

2、问题:市面上有很多MQ产品,例如Kafka或RabbitMq,两者各有千秋,Mq产品如何选择?

方案一:

     Kafka采用发布/订阅模式,消息被分区并分发给订阅者,每个消费者可以独立地消费特定分区的消息。通过消费者组使得消息能够分组消费,一个topic可以有多个partition,一个partition leader可以由一个消费者组中的一个消费者进行消费。

        缺点:Kafka的消息存储是基于日志的,主要用于实时数据处理场景,如日志收集、事件流处理、指标监控等。对于消息传递的可靠性和灵活性,如延迟消费能则缺乏对应支撑,需要业务处理。

方案二:

        RabbitMq则是将消息投递到交换机,通过匹配规则投递消息到队列,再由队列对应的消费者进行消费,它更强调消息传递的可靠性,更符合业务场景的功能开发,自带了延迟队列和异常消息处理。其次在rabbitmq的社区更加活跃,配套文档和案例十分完善,降低了使用者的学习成本,并在团队中较多人会使用。

       1)Rabbitmq可配置异常超时配置,当队列消费异常时且超出重试次数时,自动将消息投递到异常交换机中,由匹配机制传递到队列,最后再交由异常消费者订阅,短信还是邮件通知告警也是在此处解决。RepublishMessageRecoverer。

#消息确认方式,manual(手动ack) 和auto(自动ack);消息消费重试到达指定次数进到异常交换机和异常队列,需要改为自动ack确认消息
spring.rabbitmq.listener.simple.acknowledge-mode=auto
#开启重试,消费者代码不能添加try catch捕获不往外抛异常
spring.rabbitmq.listener.simple.retry.enabled=true
#最大重试次数
spring.rabbitmq.listener.simple.retry.max-attempts=4
# 重试消息的时间间隔,5秒
spring.rabbitmq.listener.simple.retry.initial-interval=5000

import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.Binding;
import org.springframework.amqp.core.BindingBuilder;
import org.springframework.amqp.core.Queue;
import org.springframework.amqp.core.TopicExchange;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.amqp.rabbit.retry.MessageRecoverer;
import org.springframework.amqp.rabbit.retry.RepublishMessageRecoverer;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;@Configuration
@Slf4j
public class RabbitMQErrorConfig {/*** 异常交换机*/private String shortLinkErrorExchange = "short_link.error.exchange";/*** 异常队列*/private String shortLinkErrorQueue = "short_link.error.queue";/*** 异常routing key*/private String shortLinkErrorRoutingKey = "short_link.error.routing.key";@Autowiredprivate RabbitTemplate rabbitTemplate;/*** 建立异常交换机** @return*/@Beanpublic TopicExchange errorTopicExchange() {return new TopicExchange(shortLinkErrorExchange, true, false);}/*** 建立队列** @return*/@Beanpublic Queue errorQueue() {return new Queue(shortLinkErrorQueue, true);}/*** 建立绑定关系** @return*/@Beanpublic Binding bingdingErrorQueueAndExchange() {return BindingBuilder.bind(errorQueue()).to(errorTopicExchange()).with(shortLinkErrorRoutingKey);}/*** 配置RepublishMessageRecoverer* 消息重试一定次数后,用特定的routingKey转发到指定的交换机中,方便后续排查和告警*/@Beanpublic MessageRecoverer messageRecoverer() {return new RepublishMessageRecoverer(rabbitTemplate, shortLinkErrorExchange, shortLinkErrorRoutingKey);}   
}

注意:消息消费确认使用自动确认方式(acknowledge-mode=auto)

 2)Rabbitmq的延迟队列采用死信队列方式解决,即被投递的队列无消费者订阅,所进入该队列的消息超时未消费时,会重新投递到另外的队列,超时时间则就是延迟时间。

@Configuration
@Data
public class RabbitMQConfig {/*** 交换机*/private String orderEventExchange="order.event.exchange";/*** 延迟队列, 不能被监听消费*/private String orderCloseDelayQueue="order.close.delay.queue";/*** 关单队列, 延迟队列的消息过期后转发的队列,被消费者监听*/private String orderCloseQueue="order.close.queue";/*** 进入延迟队列的路由key*/private String orderCloseDelayRoutingKey="order.close.delay.routing.key";/*** 进入死信队列的路由key,消息过期进入死信队列的key*/private String orderCloseRoutingKey="order.close.routing.key";/*** 过期时间 毫秒,临时改为1分钟定时关单*/private Integer ttl=1000*60;/*** 消息转换器* @return*/@Beanpublic MessageConverter messageConverter(){return new Jackson2JsonMessageConverter();}/*** 创建交换机 Topic类型,也可以用dirct路由* 一般一个微服务一个交换机* @return*/@Beanpublic Exchange orderEventExchange(){return new TopicExchange(orderEventExchange,true,false);}/*** 延迟队列*/@Beanpublic Queue orderCloseDelayQueue(){Map<String,Object> args = new HashMap<>(3);args.put("x-dead-letter-exchange",orderEventExchange);args.put("x-dead-letter-routing-key",orderCloseRoutingKey);args.put("x-message-ttl",ttl);return new Queue(orderCloseDelayQueue,true,false,false,args);}/*** 死信队列,普通队列,用于被监听*/@Beanpublic Queue orderCloseQueue(){return new Queue(orderCloseQueue,true,false,false);}/*** 第一个队列,即延迟队列的绑定关系建立* @return*/@Beanpublic Binding orderCloseDelayBinding(){return new Binding(orderCloseDelayQueue,Binding.DestinationType.QUEUE,orderEventExchange,orderCloseDelayRoutingKey,null);}/*** 死信队列绑定关系建立* @return*/@Beanpublic Binding orderCloseBinding(){return new Binding(orderCloseQueue,Binding.DestinationType.QUEUE,orderEventExchange,orderCloseRoutingKey,null);}}

@Component
@Slf4j
@RabbitListener(queuesToDeclare = {@Queue("order.close.queue"),@Queue("order.update.queue")})
public class ProductOrderMQListener {@Autowiredprivate ProductOrderService productOrderService;@RabbitHandlerpublic void productOrderHandler(EventMessage eventMessage, Message message, Channel channel){log.info("监听到消息ProductrOrderMQListener message:{}", eventMessage);try {productOrderService.handleProductOrderMessage(eventMessage);} catch (Exception e) {log.error("消费失败:{}", eventMessage);throw new BizException(BizCodeEnum.MQ_CONSUME_EXCEPTION);}log.info("消费成功");}}

3、问题:海量数据场景下冗余双写唯一码生成方式探讨,以短链码(订单号)讲解。

        方案1:

        生产者端生成短链码。先在数据库查询短链码是否存在,不存在的话通过redis设计一个分布式锁key=code并配置过期时间(加锁失败则重新生成),存在则重新生成,重复以上操作。随后再C端消费者和B端消费者均写入成功后再进行解锁。

        缺点:性能比较低,用户需要等待所生成的短链上锁,最终入库解锁才能返回给用户,等待时间较长。

        方案2:

        消费者(C端/B端)生成短链码。用户请求后立即返回消息给客户,随后消费者(C端/B端)各自进行加锁写入数据库,确保两者冲突时能遵照统一ver版本自增机制,重新生成短链码。

        

 /*** 如果短链码重复,则调用这个方法* url前缀的编号递增1* 如果还是用雪花算法,则容易C端和B端不一致,所以才用编号递增+1的方式* 123456789&http://baidu.com/download.html** @param url* @return*/public static String addUrlPrefixVersion(String url) {String urlPrefix = url.substring(0, url.indexOf("&"));String originalUrl = url.substring(url.indexOf("&") + 1);Long newUrlPrefix = Long.parseLong(urlPrefix) + 1;String newUrl = newUrlPrefix + "&" + originalUrl;return newUrl;}
/*** 判断短链域名是否合法* 判断组名是否合法* 生成长链摘要* 生成短链码* 加锁* 查询短链码是否存在* 构建短链对象* 保存数据库** @param eventMessage* @return*/@Overridepublic boolean handlerAddShortLink(EventMessage eventMessage) {Long accountNo = eventMessage.getAccountNo();String messageType = eventMessage.getEventMessageType();String content = eventMessage.getContent();ShortLinkAddRequest addRequest = JsonUtil.json2Obj(content, ShortLinkAddRequest.class);//短链域名校验DomainDO domainDO = checkDomain(addRequest.getDomainType(), addRequest.getDomainId(), accountNo);LinkGroupDO linkGroupDO = checkLinkGroup(addRequest.getGroupId(), accountNo);//长链摘要生成String originalUrlDigest = CommonUtil.MD5(addRequest.getOriginalUrl());//短链码重复标记boolean duplicateCodeFlag = false;//生成短链码String shortLinkCode = shortLinkComponent.createShortLinkCode(addRequest.getOriginalUrl());String script ="if redis.call('EXISTS',KEYS[1])==0 then " +"redis.call('set',KEYS[1],ARGV[1]); " +"redis.call('expire',KEYS[1],ARGV[2]); " +"return 1;" +" elseif redis.call('get',KEYS[1]) == ARGV[1] then " +"return 2;" +" else return 0; " +"end;";Long result = redisTemplate.execute(newDefaultRedisScript<>(script, Long.class), Arrays.asList(shortLinkCode), accountNo, 100);//加锁成功if (result > 0) {//C端处理if (EventMessageTypeEnum.SHORT_LINK_ADD_LINK.name().equalsIgnoreCase(messageType)) {//先判断短链码是否被占用ShortLinkDO shortLinkDOInDB = shortLinkManager.findByShortLinkCode(shortLinkCode);if (shortLinkDOInDB == null) {//扣减流量包boolean reduceFlag = reduceTraffic(eventMessage,shortLinkCode);//扣减成功才创建流量包if(reduceFlag){//链式调用ShortLinkDO shortLinkDO = ShortLinkDO.builder().accountNo(accountNo).code(shortLinkCode).title(addRequest.getTitle()).originalUrl(addRequest.getOriginalUrl()).domain(domainDO.getValue()).groupId(linkGroupDO.getId()).expired(addRequest.getExpired()).sign(originalUrlDigest).state(ShortLinkStateEnum.ACTIVE.name()).del(0).build();shortLinkManager.addShortLink(shortLinkDO);//校验组是否合法return true;}} else {log.error("C端短链码重复:{}", eventMessage);duplicateCodeFlag = true;}} else if (EventMessageTypeEnum.SHORT_LINK_ADD_MAPPING.name().equalsIgnoreCase(messageType)) {//先判断短链码是否被占用GroupCodeMappingDO groupCodeMappingDOInDB = groupCodeMappingManager.findByCodeAndGroupId(shortLinkCode, linkGroupDO.getId(), accountNo);if (groupCodeMappingDOInDB == null) {//B端处理GroupCodeMappingDO groupCodeMappingDO = GroupCodeMappingDO.builder().accountNo(accountNo).code(shortLinkCode).title(addRequest.getTitle()).originalUrl(addRequest.getOriginalUrl()).domain(domainDO.getValue()).groupId(linkGroupDO.getId()).expired(addRequest.getExpired()).sign(originalUrlDigest).state(ShortLinkStateEnum.ACTIVE.name()).del(0).build();groupCodeMappingManager.add(groupCodeMappingDO);return true;} else {log.error("B端短链码重复:{}", eventMessage);duplicateCodeFlag = true;}}} else {//加锁失败,自旋100毫秒,再调用;失败的可能是短链码已经被占用了,需要重新生成log.error("加锁失败:{}", eventMessage);try {TimeUnit.MILLISECONDS.sleep(100);} catch (InterruptedException e) {}duplicateCodeFlag = true;}if (duplicateCodeFlag) {String newOriginalUrl = CommonUtil.addUrlPrefixVersion(addRequest.getOriginalUrl());addRequest.setOriginalUrl(newOriginalUrl);eventMessage.setContent(JsonUtil.obj2Json(addRequest));log.warn("短链码保存失败,重新生成:{}", eventMessage);handlerAddShortLink(eventMessage);}return false;}

4、问题:海量数据高并发场景下冗余双写消费端生成唯一码错乱问题处理,以短链码(订单号)讲解。冲突详细如下:

        1)用户A生成短链码AABBCC ,C端先插入,B端还没插入
        2)用户B也生成短链码AABBCC ,B端先插入,C端还没插入
        3)用户A生成短链码AABBCC ,B端插入 (死锁,相互等待)
        4)用户B生成短链码AABBCC ,C端插入(死锁,相互等待)

        那么如何让1、3可以成功, 2、4可以成功呢?

        方案1:

        添加本地锁,synchronize、lock等,再锁内处理事务。JDK指的是以线程为单位,当一个线程获取对象锁之后,这个线程可以再次获取本对象上的锁,而其他的线程是不可以的,synchronized 和  ReentrantLock 都是可重入锁

        缺点:锁在当前进程内,集群部署下依旧存在问题。

        方案2:

        添加分布式锁,redis、zookeeper等实现,虽然还是锁,但是多个进程共用的锁标记,可以用Redis、Zookeeper、Mysql。当一个线程获取对象锁之后,其他节点的同个业务线程可以再次获取本对象上的锁。

 设计分布式锁应该考虑:

1)排他性。在分布式应用集群中,同一个方法在同一时间只能被一台机器上的一个线程执行;
2)容错性。分布式锁一定能得到释放,比如客户端奔溃或者网络中断;
3)满足可重入、高性能、高可用;
4)注意分布式锁的开销、锁粒度;

分布式锁设计(redis):

        key 是锁的唯一标识,一般按业务来决定命名,比如想要给一种商品的秒杀活动加锁,key 命名为 “seckill_商品ID” 。value就可以使用固定值,比如设置成1。短链码可以:short_link:code:xxxx,基于redis实现分布式锁,文档http://www.redis.cn/commands.html#string

methodA(){String key = "short_link:code:abcdef"if(setnx(key,1) == 1){expire(key,30,TimeUnit.MILLISECONDS)try {//做对应的业务逻辑} finally {del(key)}}else{//睡眠100毫秒,然后自旋调用本方法methodA()}
}

        问题:多个命令之间不是原子性操作,如setnxexpire之间,如果setnx成功,但是expire失败,且宕机了,则这个资源就是死锁

        核心是保证多个指令原子性,加锁使用setnx setex 可以保证原子性,那解锁使用判断和设置等怎么保证原子性。

         多个命令的原子性:采用 lua脚本+redis, 由于【判断和删除】是lua脚本执行,所以要么全成功,要么全失败

使用原子命令:设置和配置过期时间  setnx / setex
如: set key 1 ex 30 nx
java代码里面 String key = "short_link:code:abcdef"
redisTemplate.opsForValue().setIfAbsent(key,1,30,TimeUnit.MILLISECONDS)

//key1是短链码,ARGV[1]是accountNo,ARGV[2]是过期时间
String script = "if redis.call('EXISTS',KEYS[1])==0 then redis.call('set',KEYS[1],ARGV[1]); redis.call('expire',KEYS[1],ARGV[2]); return 1;" +" elseif redis.call('get',KEYS[1]) == ARGV[1] then return 2;" +" else return 0; end;";Long result = redisTemplate.execute(newDefaultRedisScript<>(script, Long.class), Arrays.asList(code), value,100);

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

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

相关文章

【题解】—— LeetCode一周小结12

【题解】—— 每日一道题目栏 上接&#xff1a;【题解】—— LeetCode一周小结11 18.区域和检索 - 数组不可变 题目链接&#xff1a;303. 区域和检索 - 数组不可变 1.计算索引 left 和 right &#xff08;包含 left 和 right&#xff09;之间的 nums 元素的 和 &#xff0c;其…

嵌入式学习46——硬件相关2串口通信

串口&#xff1a; 端口&#xff1a; COM 波特率&#xff1a; 9600 115200 &#xff08;bps&#xff09; 每秒传输的数据…

Multimodal Chain-of-Thought Reasoning in Language Models阅读笔记

论文&#xff08;2023年&#xff09;链接&#xff1a;https://arxiv.org/pdf/2302.00923.pdf GitHub项目链接&#xff1a;GitHub - amazon-science/mm-cot: Official implementation for "Multimodal Chain-of-Thought Reasoning in Language Models" (stay tuned a…

14:有效的符号

给定一个只包括 (&#xff0c;)&#xff0c;{&#xff0c;}&#xff0c;[&#xff0c;] 的字符串 s &#xff0c;判断字符串是否有效。 有效字符串需满足&#xff1a; 左括号必须用相同类型的右括号闭合。左括号必须以正确的顺序闭合。每个右括号都有一个对应的相同类型的左括…

前端-html-02

1.列表 标签名功能和语义属性单标签还是双标签ul无序列表包裹元素双标签 ol 有序列表包裹元素双标签li列表项双标签dl定义列表包裹元素双标签dt定义列表项标题双标签dd定义列表项描述双标签 li必须由Ul或者ol包裹 <!DOCTYPE html> <html><head><…

腾讯云4核8G12M轻量服务器性能测评,支持多少人同时在线?

腾讯云4核8G服务器价格&#xff1a;轻量4核8G12M优惠价格646元15个月、CVM S5服务器4核8G配置1437元买1年送3个月。腾讯云4核8G服务器支持多少人同时在线&#xff1f;支持30个并发数&#xff0c;可容纳日均1万IP人数访问。腾讯云百科txybk.com整理4核8G服务器支持多少人同时在线…

7.JDK下载和安装

文章目录 一、下载二、安装三、JDK的安装目录介绍 写JAVA代码不是随随便便能写的&#xff0c;我们得先做一点准备工作。例如&#xff0c;我们平时想要玩一把游戏&#xff0c;就需要先下载、安装才能玩游戏。JAVA也是一样的&#xff0c;也是需要下载并安装相关的软件&#xff0c…

Avalonia笔记2 -数据集合类控件

学习笔记&#xff1a; 1. DataGrid 笔记1中已经记录&#xff1b; 2. ItemsControl 属性&#xff1a; ItemsSource&#xff1a;数据源 ItemsControl.ItemTemplate&#xff1a;单项数据模板&#xff0c;内部使用<DataTemplate> 示例&#xff1a; <ItemsContr…

【启发式算法】同核分子优化算法 Homonuclear Molecules Optimization HMO算法【Matlab代码#70】

文章目录 【获取资源请见文章第4节&#xff1a;资源获取】1. 算法简介2. 部分代码展示3. 仿真结果展示4. 资源获取 【获取资源请见文章第4节&#xff1a;资源获取】 1. 算法简介 同核分子优化算法&#xff08;Homonuclear Molecules Optimization&#xff0c;HMO&#xff09;是…

【源码】I.MX6ULL移植OpenCV

编译完成的源码&#xff1a; git clone https://gitee.com/wangyoujie11/atkboard_-linux_-driver.git 1.下载源码放在自己的opecv源码目录下 2.QTOpenCV工程代码放置的位置 3.更改.pro工程文件的opencv地址 4.使用命令行编译 前提是自己环境中已经配置好arm-qt的交叉编译…

Springboot整合Redis报错:Unable to connection Redis

今天在做Springboot整合Redis中碰到下列错误&#xff1a; 基于以上的错误首先在Xshell或者其他远程操控虚拟机的软件上看能不能连接到Redis: [zzllocalhost ~]$ redis-cli -h 192.168.136.132 -p 6379 -a ****** Warning: Using a password with -a or -u option on the comma…

xxl-job 适配人大金仓数据库 V8R6

前言 由于一些众所周知的原因&#xff0c;项目需要需要进行改造使其适配人大金仓的数据库。 xxl-job适配人大金仓 特此说明&#xff1a; 当前修改的xxl-job版本 为 2.4.1-SNAPSHOT mysql上的xxl-job库 迁移到 人大金仓数据库上pom中新增依赖 kingbase8 驱动 注意版本<!-…

连接数据库(MySQL)的JDBC

目录 JDBC简介快速入门API详解DriverManager&#xff08;驱动管理类&#xff09;注册驱动&#xff1a;获取数据库连接(对象)&#xff1a; Connection&#xff08;数据库连接对象&#xff09;获取执行SQL的对象管理事务 Statement(执行SQL语句)执行DML、DDL语句执行DQL语句 Resu…

Sora那么牛,他的模型的成本会有多少呢?

Sora的训练需要大量的计算资源&#xff0c;估计需要4211-10528个 Nvidia H100 GPUs运行一个月。推理成本&#xff1a;一个Nvidia H100 GPU大约每小时能生成5分钟的视频。初期的Sora成本将非常高&#xff0c;肯定是不适合普通人来使用&#xff0c;所以目前OpenAI都是先找一些艺术…

STM32串口收发单字节数据原理及程序实现

线路连接&#xff1a; 显示屏的SCA接在B11&#xff0c;SCL接在B10&#xff0c;串口的RX连接A9&#xff0c;TX连接A10。 程序编写&#xff1a; 在上一个博客中实现了串口的发送代码&#xff0c;这里实现串口的接收代码&#xff0c;在上一个代码的基础上增加程序功能。 Seiral.…

创建AI智能体

前言 灵境矩阵是百度推出的基于文心大模型的智能体&#xff08;Agent&#xff09;平台&#xff0c;支持广大开发者根据自身行业领域、应用场景&#xff0c;选取不同类型的开发方式&#xff0c;打造大模型时代的产品能力。开发者可以通过 prompt 编排的方式低成本开发智能体&am…

VMware和Xshell连接

1.开启虚拟机 2.使用管理员账户&#xff0c;点击未列出 3.输入用户名密码 4.点击编辑虚拟网络编辑器 5.记住自己的网关和IP地址 6.打开终端 7.输入命令&#xff0c;vim / etc / sysconfig / network -scripts / ifcfg-ens33 回车 8.修改图中两处按“ I ”键进入编辑 d…

计算机组成原理-6-计算机的运算方法

6. 计算机的运算方法 文章目录 6. 计算机的运算方法6.1 机器数的表示6.1.1 无符号数和有符号数6.1.2 有符号数-原码6.1.3 有符号数-补码6.1.4 有符号数-反码6.1.5 有符号数-移码6.1.6 原码、补码、反码的比较 6.2 数的定点表示和浮点表示6.2.1 定点表示6.2.2 浮点表示6.2.3 ΔI…

C语言程序编译和链接

翻译环境和运行环境 我们程序员天天要写代码&#xff0c;那我们天天写的代码是什么呢&#xff1f;我们写的其实莫过于是一些test.c文件和test.h这样的文件。都是一些文本信息&#xff0c;这些如果直接交给机器去处理机器是看不懂的&#xff0c;就像我们和外国人语言不通一样&…

如何使用ChatGPT准备即将到来的面试How to Use ChatGPT to Prepare for an Upcoming Interview

使用ChatGPT来准备即将到来的面试可以非常有帮助&#xff0c;因为它可以模拟真实的面试场景并提供反馈。以下是一些步骤和提示&#xff0c;说明如何利用ChatGPT进行面试准备&#xff1a; 研究职位和公司&#xff1a;在与ChatGPT对话之前&#xff0c;先对你申请的职位和公司进行…