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

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

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";

    @Autowired
    private RabbitTemplate rabbitTemplate;

    /**
     * 建立异常交换机
     *
     * @return
     */
    @Bean
    public TopicExchange errorTopicExchange() {
        return new TopicExchange(shortLinkErrorExchange, true, false);
    }

    /**
     * 建立队列
     *
     * @return
     */
    @Bean
    public Queue errorQueue() {
        return new Queue(shortLinkErrorQueue, true);
    }

    /**
     * 建立绑定关系
     *
     * @return
     */
    @Bean
    public Binding bingdingErrorQueueAndExchange() {
        return BindingBuilder.bind(errorQueue()).to(errorTopicExchange()).with(shortLinkErrorRoutingKey);
    }

    /**
     * 配置RepublishMessageRecoverer
     * 消息重试一定次数后,用特定的routingKey转发到指定的交换机中,方便后续排查和告警
     */
    @Bean
    public 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
     */
    @Bean
    public MessageConverter messageConverter(){
        return new Jackson2JsonMessageConverter();
    }


    /**
     * 创建交换机 Topic类型,也可以用dirct路由
     * 一般一个微服务一个交换机
     * @return
     */
    @Bean
    public Exchange orderEventExchange(){
        return new TopicExchange(orderEventExchange,true,false);
    }


    /**
     * 延迟队列
     */
    @Bean
    public 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);
    }


    /**
     * 死信队列,普通队列,用于被监听
     */
    @Bean
    public Queue orderCloseQueue(){

        return new Queue(orderCloseQueue,true,false,false);

    }

    /**
     * 第一个队列,即延迟队列的绑定关系建立
     * @return
     */
    @Bean
    public Binding orderCloseDelayBinding(){

        return new Binding(orderCloseDelayQueue,Binding.DestinationType.QUEUE,orderEventExchange,orderCloseDelayRoutingKey,null);
    }

    /**
     * 死信队列绑定关系建立
     * @return
     */
    @Bean
    public 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 {


    @Autowired
    private ProductOrderService productOrderService;

    @RabbitHandler
    public 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
     */
    @Override
    public 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(new
                DefaultRedisScript<>(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(new
                DefaultRedisScript<>(script, Long.class), Arrays.asList(code), value,100);

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

【ORB-SLAM3】在 Ubuntu20.04 上编译 ORM-SLAM3 并使用 D435i、EuRoC 和 TUM-VI 运行测试

【ORB-SLAM3】在 Ubuntu20.04 上编译 ORM-SLAM3 并使用 D435i、EuRoC 和 TUM-VI 运行测试 1 Prerequisites1.1 C11 or C0x Compiler1.2 Pangolin1.3 OpenCV1.4 Eigen3 2 安装 Intel RealSense™ SDK 2.02.1 测试设备2.2 编译源码安装 (Recommend)2.3 预编译包安装 3 编译 ORB-S…

网络编程基本概念(一篇文章掌握基本内容的详细概念,IP,端口号,协议,协议分层,封装和分用,客户端和服务端,请求和回应,两台主机的通信)

IP地址 概念 IP地址主要⽤于标识⽹络主机、其他⽹络设备&#xff08;如路由器&#xff09;的⽹络地址。简单说&#xff0c;IP地址⽤于定位主机的⽹络地址。 就像我们发送快递⼀样&#xff0c;需要知道对⽅的收货地址&#xff0c;快递员才能将包裹送到⽬的地。 IP的格式 IP地址…

优化iproute2中的tc流控规则下发机制

设备基于IP对每个用户配置流量控制规则&#xff0c;规则如下&#xff1a; tc filter add dev eth0 protocol ip parent 1:0 prio 1 u32 match ip src 192.168.0.10 flowid 1:10 上面的tc 是一个配置工具&#xff0c;本身是一个应用程序&#xff0c;tc后面的参数通过应用程序参…

vue3+threejs新手从零开发卡牌游戏(十八):己方场上手牌添加画线

手牌上场后&#xff0c;点击己方怪兽区卡牌会跟随鼠标移动画出线条&#xff0c;之后可以通过判断鼠标移动到对方场地的某卡牌进行战斗操作&#xff0c;代码主要改动在game/index.vue文件。 1.添加鼠标移动监听事件&#xff08;移动端&#xff09;&#xff1a; window.addEven…

Scikit Learn中的概率校准曲线

概率校准是一种用于将二分类的输出分数转换为概率的技术&#xff0c;以与目标类的实际概率相关联。在本文中&#xff0c;我们将讨论概率校准曲线以及如何使用Scikit-learn绘制它们。 概率校准 概率校准曲线是二分类问题的正类的预测概率和实际观察频率之间的图。它用于检查分…

DLS-42/5-5双位置继电器 柜内安装板前接线 JOSEF约瑟

系列型号&#xff1a; DLS-41/10-2双位置继电器&#xff1b; DLS-41/9-3双位置继电器&#xff1b; DLS-41/8-4双位置继电器&#xff1b; DLS-41/6-6双位置继电器&#xff1b; DLS-42/9-1双位置继电器&#xff1b; DLS-42/8-2双位置继电器&#xff1b; DLS-42/7-3双位置继…

理解CPU与执行指令原理

本文侧重介绍cpu的工作任务&#xff0c;与cpu执行指令的过程是怎么样的&#xff1f; 目录 1.理解CPU 1.1.CPU的功能 1.2.CPU的逻辑构成 2.认识指令 2.1.什么是指令 2.2.CPU执行指令的准备工作(重点) 3.指令的执行过程 前景知识&#xff1a; 什么是计算机 就是遵循冯诺依…

微信小程序使用Vant组件库流程

目前 Vant 官方提供了 Vue 2 版本、Vue 3 版本和微信小程序版本&#xff0c;并由社区团队维护 React 版本和支付宝小程序版本。这样开发原生微信小程序的会方便很多。 官方网址&#xff1a;Vant Weapp - 轻量、可靠的小程序 UI 组件库 步骤一 通过 npm 安装 npm i vant/weap…

RXMA1 RK211 075 AC220V中间继电器 柜内安装板前接线 JOSEF约瑟

系列型号 RXMA1 RK 211 063中间继电器&#xff1b;RXMA1 RK 211 064中间继电器; RXMA1 RK 211 066中间继电器&#xff1b;RXMA1 RK 211 072中间继电器; RXMA1 RK 211 073中间继电器&#xff1b;RXMA1 RK 211 074中间继电器&#xff1b; RXMA1 RK 211 025中间继电器&#xff1b;…

k8s入门到实战(十二)—— pod的深入理解

pod 深入理解 pod 容器生命周期 pod 的几种状态 可以使用命令kubectl get pod -w实时监控查看 pod 的状态 running&#xff1a;正常运行状态Pending&#xff1a;资源分配不对的时候会挂起&#xff0c;出现此状态Terminating&#xff1a;某个节点突然关机&#xff0c;上面的 p…

uniapp微信小程序消息订阅详解

一、微信公众平台申请订阅模板 注意&#xff1a;订阅信息 这个事件 是 当用户 点击的时候触发 或者 是 支付成功后触发&#xff0c; 用户勾选 “总是保持以上选择&#xff0c;不再询问” 之后或长期订阅&#xff0c;下次订阅调用 wx.requestSubscribeMessage 不会弹窗&#xf…

浅析扩散模型与图像生成【应用篇】(十三)——PITI

13. Pretraining is All You Need for Image-to-Image Translation 该文提出一种基于预训练扩散模型的图像转换方法&#xff0c;称为PITI。其思想并不复杂&#xff0c;就是借鉴现有视觉和NLP领域中常见的预训练方法&#xff0c;考虑预先在一个大规模的任务无关数据集上对扩散模…

基于java+springboot+vue实现的校园二手交易系统(文末源码+Lw+ppt)23-336

摘 要 自从新冠疫情爆发以来&#xff0c;各个线下实体越来越难做&#xff0c;线下购物的人也越来越少&#xff0c;随之带来的是一些不必要的浪费&#xff0c;尤其是即将毕业的大学生&#xff0c;各种用品不方便携带走导致被遗弃&#xff0c;造成大量的浪费。本系统目的就是让…

浏览器如何渲染页面

浏览器渲染页面的过程是一个多步骤的复杂流程&#xff0c;下面我将通过一个简单的例子来逐步说明这一过程&#xff1a; 以下是浏览器渲染该页面的主要步骤&#xff1a; 请求HTML文档&#xff1a; 用户在浏览器中输入URL并回车&#xff0c;浏览器向服务器发送HTTP请求。 服务器…

【Java多线程】1——多线程知识回顾

1 多线程知识回顾 ⭐⭐⭐⭐⭐⭐ Github主页&#x1f449;https://github.com/A-BigTree 笔记仓库&#x1f449;https://github.com/A-BigTree/tree-learning-notes 个人主页&#x1f449;https://www.abigtree.top ⭐⭐⭐⭐⭐⭐ 如果可以&#xff0c;麻烦各位看官顺手点个star…

测试开发工程师(QA)职业到底需要干些什么?part6:数据测试工程师QA

常见任务和工作内容 作为数据测试工程师QA&#xff08;Quality Assurance&#xff09;&#xff0c;您的主要职责是确保数据的质量、准确性和一致性。以下是数据测试工程师QA的一些常见任务和工作内容&#xff1a; 数据验证和准确性测试&#xff1a;您将负责验证数据的准确性和…

数论问题代码模板

文章目录 一、质数1.1、质数筛&#xff08;筛1~n中的所有质数&#xff09;1.2、判断一个数是否为质数1.3、对一个数进行质因数分解 二、快速幂2.1、费马小定理——乘法逆元2.2、快速幂 三、约数3.1、N个数的正约数集合3.2、一个数的正约数集合 四、欧拉函数&#xff08;互质数数…

洗地机好用吗?哪款型号值得推荐?看完本文你就知道

在如今社会生活节奏不断加快的情况下&#xff0c;洗地机已经成为众多家庭的必备的清洁设备&#xff0c;面对市面上种类繁多的洗地机&#xff0c;我们常常会发出感叹“洗地机好用吗&#xff1f;洗地机哪个型好用&#xff1f;”等的疑问&#xff0c;今天&#xff0c;为了帮助大家…

一文搞定用python实现终身免费的听书工具

你好&#xff0c;我是 shengjk1&#xff0c;多年大厂经验&#xff0c;努力构建 通俗易懂的、好玩的编程语言教程。 欢迎关注&#xff01;你会有如下收益&#xff1a; 了解大厂经验拥有和大厂相匹配的技术等 希望看什么&#xff0c;评论或者私信告诉我&#xff01; 文章目录 一…

C语言程序练习——汉诺塔递归

1. 题目 在终端输入汉诺塔层数n&#xff0c;实现将n层汉诺塔通过三座塔座A、B、C进行排列 2. 代码 #include <stdio.h>int hannuota(int len, int str, int tmp, int dst) {if (1 len){printf("%c -> %c\n", str, dst);}else{hannuota(len-1, str, dst, …