【重试】Java 中的 7 种重试机制

news2024/11/15 23:30:23

随着互联网的发展项目中的业务功能越来越复杂,有一些基础服务我们不可避免的会去调用一些第三方的接口或者公司内其他项目中提供的服务,但是远程服务的健壮性和网络稳定性都是不可控因素。在测试阶段可能没有什么异常情况,但上线后可能会出现调用的接口因为内部错误或者网络波动而出错或返回系统异常,因此我们必须考虑加上重试机制

重试机制 可以提高系统的健壮性,并且减少因网络波动依赖服务临时不可用带来的影响,让系统能更稳定的运行

1. 手动重试

手动重试:使用 while 语句进行重试:

@Service
public class OrderServiceImpl implements OrderService {
	public void addOrder() {
	    int times = 1;
	    while (times <= 5) {
	        try {
	            // 故意抛异常
	            int i = 3 / 0;
	            // addOrder
	        } catch (Exception e) {
	            System.out.println("重试" + times + "次");
	            Thread.sleep(2000);
	            times++;
	            if (times > 5) {
	                throw new RuntimeException("不再重试!");
	            }
	        }
	    }
	}
}

运行上述代码:

在这里插入图片描述

上述代码看上去可以解决重试问题,但实际上存在一些弊端:

  1. 由于没有重试间隔,很可能远程调用的服务还没有从网络异常中恢复,所以有可能接下来的几次调用都会失败
  2. 代码侵入式太高,调用方代码不够优雅
  3. 项目中远程调用的服务可能有很多,每个都去添加重试会出现大量的重复代码

2. 静态代理

上面的处理方式由于需要对业务代码进行大量修改,虽然实现了功能,但是对原有代码的侵入性太强,可维护性差。所以需要使用一种更优雅一点的方式,不直接修改业务代码,那要怎么做呢?

其实很简单,直接在业务代码的外面再包一层就行了,代理模式在这里就有用武之地了。

@Service
public class OrderServiceProxyImpl implements OrderService {
    
    @Autowired
    private OrderServiceImpl orderService;

    @Override
    public void addOrder() {
        int times = 1;
        while (times <= 5) {
            try {
                // 故意抛异常
                int i = 3 / 0;
                orderService.addOrder();
            } catch (Exception e) {
                System.out.println("重试" + times + "次");
                try {
                    Thread.sleep(2000);
                } catch (InterruptedException ex) {
                    ex.printStackTrace();
                }
                times++;
                if (times > 5) {
                    throw new RuntimeException("不再重试!");
                }
            }
        }
        
    }
}

这样,重试逻辑就都由代理类来完成,原业务类的逻辑就不需要修改了,以后想修改重试逻辑也只需要修改这个类就行了

代理模式虽然要更加优雅,但是如果依赖的服务很多的时候,要为每个服务都创建一个代理类,显然过于麻烦,而且其实重试的逻辑都大同小异,无非就是重试的次数和延时不一样而已。如果每个类都写这么一长串类似的代码,显然,不优雅!

3. JDK 动态代理

这时候,动态代理就闪亮登场了。只需要写一个代理处理类就 ok 了

public class RetryInvocationHandler implements InvocationHandler {

    private final Object subject;

    public RetryInvocationHandler(Object subject) {
        this.subject = subject;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        int times = 1;
        while (times <= 5) {
            try {
                // 故意抛异常
                int i = 3 / 0;
                return method.invoke(subject, args);
            } catch (Exception e) {
                System.out.println("重试【" + times + "】次");
                try {
                    Thread.sleep(2000);
                } catch (InterruptedException ex) {
                    ex.printStackTrace();
                }
                times++;
                if (times > 5) {
                    throw new RuntimeException("不再重试!");
                }
            }
        }
        return null;
    }

    public static Object getProxy(Object realSubject) {
        InvocationHandler handler = new RetryInvocationHandler(realSubject);
        return Proxy.newProxyInstance(handler.getClass().getClassLoader(), realSubject.getClass().getInterfaces(), handler);
    }

}

测试:

@RestController
@RequestMapping("/order")
public class OrderController {

    @Qualifier("orderServiceImpl")
    @Autowired
    private OrderService orderService;

    @GetMapping("/addOrder")
    public String addOrder() {
        OrderService orderServiceProxy = (OrderService)RetryInvocationHandler.getProxy(orderService);
        orderServiceProxy.addOrder();
        return "addOrder";
    }
    
}

动态代理可以将重试逻辑都放到一块,显然比直接使用代理类要方便很多,也更加优雅。

这里使用的是JDK动态代理,因此就存在一个天然的缺陷,如果想要被代理的类,没有实现任何接口,那么就无法为其创建代理对象,这种方式就行不通了

4. CGLib 动态代理

既然已经说到了 JDK 动态代理,那就不得不提 CGLib 动态代理了。使用 JDK 动态代理对被代理的类有要求,不是所有的类都能被代理,而 CGLib 动态代理则刚好解决了这个问题

@Component
public class CGLibRetryProxyHandler implements MethodInterceptor {

    private Object target;

    @Override
    public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
        int times = 1;
        while (times <= 5) {
            try {
                // 故意抛异常
                int i = 3 / 0;
                return method.invoke(target, objects);
            } catch (Exception e) {
                System.out.println("重试【" + times + "】次");
                try {
                    Thread.sleep(2000);
                } catch (InterruptedException ex) {
                    ex.printStackTrace();
                }
                times++;
                if (times > 5) {
                    throw new RuntimeException("不再重试!");
                }
            }
        }
        return null;
    }

    public Object getCglibProxy(Object objectTarget){
        this.target = objectTarget;
        Enhancer enhancer = new Enhancer();
        enhancer.setSuperclass(objectTarget.getClass());
        enhancer.setCallback(this);
        Object result = enhancer.create();
        return result;
    }

}

测试:

@GetMapping("/addOrder")
public String addOrder() {
    OrderService orderServiceProxy = (OrderService) cgLibRetryProxyHandler.getCglibProxy(orderService);
    orderServiceProxy.addOrder();
    return "addOrder";
}

这样就很棒了,完美的解决了 JDK 动态代理带来的缺陷。优雅指数上涨了不少。

但这个方案仍旧存在一个问题,那就是需要对原来的逻辑进行侵入式修改,在每个被代理实例被调用的地方都需要进行调整,这样仍然会对原有代码带来较多修改

5. 手动 Aop

考虑到以后可能会有很多的方法也需要重试功能,咱们可以将重试这个共性功能通过 AOP 来实现:使用 AOP 来为目标调用设置切面,即可在目标方法调用前后添加一些重试的逻辑

<dependency>
    <groupId>org.aspectj</groupId>
    <artifactId>aspectjweaver</artifactId>
</dependency>

自定义注解:

@Documented
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface MyRetryable {
    
    // 最大重试次数
    int retryTimes() default 3;
    // 重试间隔
    int retryInterval() default 1;

}
@Slf4j
@Aspect
@Component
public class RetryAspect {

    @Pointcut("@annotation(com.hcr.sbes.retry.annotation.MyRetryable)")
    private void retryMethodCall(){}

    @Around("retryMethodCall()")
    public Object retry(ProceedingJoinPoint joinPoint) throws InterruptedException {
        // 获取重试次数和重试间隔
        MyRetryable retry = ((MethodSignature)joinPoint.getSignature()).getMethod().getAnnotation(MyRetryable.class);
        int maxRetryTimes = retry.retryTimes();
        int retryInterval = retry.retryInterval();

        Throwable error = new RuntimeException();
        for (int retryTimes = 1; retryTimes <= maxRetryTimes; retryTimes++){
            try {
                Object result = joinPoint.proceed();
                return result;
            } catch (Throwable throwable) {
                error = throwable;
                log.warn("调用发生异常,开始重试,retryTimes:{}", retryTimes);
            }
            Thread.sleep(retryInterval * 1000L);
        }
        throw new RuntimeException("重试次数耗尽", error);
    }

}

给需要重试的方法添加注解 @MyRetryable

@Service
public class OrderServiceImpl implements OrderService {

    @Override
    @MyRetryable(retryTimes = 5, retryInterval = 2)
    public void addOrder() {
        int i = 3 / 0;
        // addOrder
    }
    
}

这样即不用编写重复代码,实现上也比较优雅了:一个注解就实现重试。

6. spring-retry

<dependency>
    <groupId>org.springframework.retry</groupId>
    <artifactId>spring-retry</artifactId>
</dependency>

开启重试功能:在启动类或者配置类上添加 @EnableRetry 注解

在需要重试的方法上添加 @Retryable 注解

@Slf4j
@Service
public class OrderServiceImpl implements OrderService {

    @Override
    @Retryable(maxAttempts = 3, backoff = @Backoff(delay = 2000, multiplier = 2))
    public void addOrder() {
        System.out.println("重试...");
        int i = 3 / 0;
        // addOrder
    }

    @Recover
    public void recover(RuntimeException e) {
        log.error("达到最大重试次数", e);
    }
    
}

该方法调用后会进行重试,最大重试次数为 3,第一次重试间隔为 2s,之后以 2 倍大小进行递增,第二次重试间隔为 4 s,第三次为 8s

Spring 的重试机制还支持很多很有用的特性,由三个注解完成:

  • @Retryable
  • @Backoff
  • @Recover

查看 @Retryable 注解源码:指定异常重试、次数

public @interface Retryable {

	// 设置重试拦截器的 bean 名称
    String interceptor() default "";
	
	// 只对特定类型的异常进行重试。默认:所有异常
    Class<? extends Throwable>[] value() default {};
	
	// 包含或者排除哪些异常进行重试
    Class<? extends Throwable>[] include() default {};
    Class<? extends Throwable>[] exclude() default {};
	
	// l设置该重试的唯一标志,用于统计输出
    String label() default "";

    boolean stateful() default false;
	
	// 最大重试次数,默认为 3 次
    int maxAttempts() default 3;
	
	
    String maxAttemptsExpression() default "";
	
	// 设置重试补偿机制,可以设置重试间隔,并且支持设置重试延迟倍数
    Backoff backoff() default @Backoff;
	
	// 异常表达式,在抛出异常后执行,以判断后续是否进行重试
    String exceptionExpression() default "";

    String[] listeners() default {};
}

@Backoff 注解:指定重试回退策略(如果因为网络波动导致调用失败,立即重试可能还是会失败,最优选择是等待一小会儿再重试。决定等待多久之后再重试的方法。通俗的说,就是每次重试是立即重试还是等待一段时间后重试)

@Recover 注解:进行善后工作:当重试达到指定次数之后,会调用指定的方法来进行日志记录等操作

注意:

  1. @Recover 注解标记的方法必须和被 @Retryable 标记的方法在同一个类中
  2. 重试方法抛出的异常类型需要与 recover() 方法参数类型保持一致
  3. recover() 方法返回值需要与重试方法返回值保证一致
  4. recover() 方法中不能再抛出 Exception,否则会报无法识别该异常的错误

这里还需要再提醒的一点是,由于 Spring Retry 用到了 Aspect 增强,所以就会有使用 Aspect 不可避免的坑——方法内部调用,如果被 @Retryable 注解的方法的调用方和被调用方处于同一个类中,那么重试将会失效

通过以上几个简单的配置,可以看到 Spring Retry 重试机制考虑的比较完善,比自己写AOP实现要强大很多

弊端:

但也还是存在一定的不足,Spring的重试机制只支持对 异常 进行捕获,而无法对返回值进行校验

@Retryable
public String hello() {
    long current = count.incrementAndGet();
    System.out.println("第" + current +"次被调用");
    if (current % 3 != 0) {
        log.warn("调用失败");
        return "error";
    }
    return "success";
}

因此就算在方法上添加 @Retryable,也无法实现失败重试

除了使用注解外,Spring Retry 也支持直接在调用时使用代码进行重试:

@Test
public void normalSpringRetry() {
    // 表示哪些异常需要重试,key表示异常的字节码,value为true表示需要重试
    Map<Class<? extends Throwable>, Boolean> exceptionMap = new HashMap<>();
    exceptionMap.put(HelloRetryException.class, true);
 
    // 构建重试模板实例
    RetryTemplate retryTemplate = new RetryTemplate();
 
    // 设置重试回退操作策略,主要设置重试间隔时间
    FixedBackOffPolicy backOffPolicy = new FixedBackOffPolicy();
    long fixedPeriodTime = 1000L;
    backOffPolicy.setBackOffPeriod(fixedPeriodTime);
 
    // 设置重试策略,主要设置重试次数
    int maxRetryTimes = 3;
    SimpleRetryPolicy retryPolicy = new SimpleRetryPolicy(maxRetryTimes, exceptionMap);
 
    retryTemplate.setRetryPolicy(retryPolicy);
    retryTemplate.setBackOffPolicy(backOffPolicy);
 
    Boolean execute = retryTemplate.execute(
        //RetryCallback
        retryContext -> {
            String hello = helloService.hello();
            log.info("调用的结果:{}", hello);
            return true;
        },
        // RecoverCallBack
        retryContext -> {
            //RecoveryCallback
            log.info("已达到最大重试次数");
            return false;
        }
    );
}

此时唯一的好处是可以设置多种重试策略:

  1. NeverRetryPolicy:只允许调用RetryCallback一次,不允许重试
  2. AlwaysRetryPolicy:允许无限重试,直到成功,此方式逻辑不当会导致死循环
  3. SimpleRetryPolicy:固定次数重试策略,默认重试最大次数为3次,RetryTemplate默认使用的策略
  4. TimeoutRetryPolicy:超时时间重试策略,默认超时时间为1秒,在指定的超时时间内允许重试
  5. ExceptionClassifierRetryPolicy:设置不同异常的重试策略,类似组合重试策略,区别在于这里只区分不同异常的重试
  6. CircuitBreakerRetryPolicy:有熔断功能的重试策略,需设置3个参数openTimeout、resetTimeout和delegate
  7. CompositeRetryPolicy:组合重试策略,有两种组合方式,乐观组合重试策略是指只要有一个策略允许即可以重试,
    悲观组合重试策略是指只要有一个策略不允许即可以重试,但不管哪种组合方式,组合中的每一个策略都会执行

7. guava-retry

和 Spring Retry 相比,Guava Retry 具有更强的灵活性,并且能够根据 返回值 来判断是否需要重试

<dependency>
    <groupId>com.github.rholder</groupId>
    <artifactId>guava-retrying</artifactId>
    <version>2.0.0</version>
</dependency>
@Override
public String guavaRetry(Integer num) {
    Retryer<String> retryer = RetryerBuilder.<String>newBuilder()
            //无论出现什么异常,都进行重试
            .retryIfException()
            //返回结果为 error时,进行重试
            .retryIfResult(result -> Objects.equals(result, "error"))
            //重试等待策略:等待 2s 后再进行重试
            .withWaitStrategy(WaitStrategies.fixedWait(2, TimeUnit.SECONDS))
            //重试停止策略:重试达到 3 次
            .withStopStrategy(StopStrategies.stopAfterAttempt(3))
            .withRetryListener(new RetryListener() {
                @Override
                public <V> void onRetry(Attempt<V> attempt) {
                    System.out.println("RetryListener: 第" + attempt.getAttemptNumber() + "次调用");
                }
            })
            .build();
    try {
        retryer.call(() -> testGuavaRetry(num));
    } catch (Exception e) {
        e.printStackTrace();
    }
    return "test";
}

先创建一个Retryer实例,然后使用这个实例对需要重试的方法进行调用,可以通过很多方法来设置重试机制:

  • retryIfException():对所有异常进行重试
  • retryIfRuntimeException():设置对指定异常进行重试
  • retryIfExceptionOfType():对所有 RuntimeException 进行重试
  • retryIfResult():对不符合预期的返回结果进行重试

还有五个以 withXxx 开头的方法,用来对重试策略/等待策略/阻塞策略/单次任务执行时间限制/自定义监听器进行设置,以实现更加强大的异常处理:

  • withRetryListener():设置重试监听器,用来执行额外的处理工作
  • withWaitStrategy():重试等待策略
  • withStopStrategy():停止重试策略
  • withAttemptTimeLimiter:设置任务单次执行的时间限制,如果超时则抛出异常
  • withBlockStrategy():设置任务阻塞策略,即可以设置当前重试完成,下次重试开始前的这段时间做什么事情

8. 总结

从手动重试,到使用 Spring AOP 自己动手实现,再到站在巨人肩上使用特别优秀的开源实现 Spring Retry 和 Google guava-retrying,经过对各种重试实现方式的介绍,可以看到以上几种方式基本上已经满足大部分场景的需要:

  1. 如果是基于 Spring 的项目,使用 Spring Retry 的注解方式已经可以解决大部分问题
  2. 如果项目没有使用 Spring 相关框架,则适合使用 Google guava-retrying:自成体系,使用起来更加灵活强大

【转载】Java重试机制

Java教程:重试实现高可用一览

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

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

相关文章

漏洞分析|死磕Jenkins漏洞回显与利用效果

0x01 背景 近期我们发起了一个Goby漏洞挑战赛的活动&#xff0c;在活动期间收到了大量的反馈信息&#xff0c;延伸出一系列在编写POC漏洞检测与利用中考虑场景不全的问题&#xff0c;我们针对发现的各种场景用市面上常见的工具进行了一些列的对比工作&#xff0c;发现市面上工…

前端-vue-对打包的静态文件进行压缩

通过 npm run build 打包时候&#xff0c;会发现静态文件很大&#xff0c;放在nginx服务中访问页面会很慢&#xff0c;所以可在打包过程中对静态文件再压缩。 对js、css、html等文件进行压缩&#xff1a; 安装插件 “compression-webpack-plugin” 。(我本地版本&#xff1a;…

leecode-字符串解码-栈

题目 题目链接 分析 用两个栈存储数字、字符串的情况 如果遇到[&#xff0c;就入栈&#xff0c;遇到]就按照数字把字符串出栈&#xff0c;并构造出新的字符串赋给res。 一步步内部解码。 代码 class Solution { public:string decodeString(string s) {stack<int> …

【自监督论文阅读 4】BYOL

文章目录 一、摘要二、引言三、相关工作四、方法4.1 BYOL的描述4.2 Intuitions on BYOL’s behavior&#xff08;BYOL行为的直觉&#xff09;4.3 实验细节 五、实验评估5.1 Linear evaluation on ImageNet&#xff08;ImageNet上的线性评估&#xff09;5.2 Semi-supervised tra…

宝塔面板 - 如何通过宝塔面板设置服务器禁止 Ping?

问题描述 在介绍如何通过宝塔面板设置服务器禁止ping之前&#xff0c;我们先来简单了解一下什么是ping。 Ping 可用于测试网络上IP系统的可用性&#xff0c;也可用于通过监控往返时间和数据包丢失&#xff0c;来测试网络连接的质量。ping 也可以用于识别网络子网&#xff0c;…

数字孪生和GIS结合将为智慧园区带来什么样的改变?

数字孪生和地理信息系统&#xff08;GIS&#xff09;是当今智慧园区发展中的两个重要技术&#xff0c;它们的结合将为智慧园区带来根本性的改变和巨大的发展机遇。这种结合将深刻影响园区的规划、建设、运营和管理&#xff0c;为人们创造更智能、高效、可持续的生活环境。 首先…

真的不想知道文字转语音真人发声免费有哪些软件吗

曾经有一个盲人作家&#xff0c;他热爱写作却无法看到自己的文字成品。直到有一天&#xff0c;他听说了一款文字转语音的软件&#xff0c;可以将他的文字转化为声音&#xff0c;让他能够听到自己的作品。这则故事令我们体会到文字转语音这项技术的重要性。那么&#xff0c;你知…

【⑤MySQL函数】:让你的数据库操作更高效(二)

前言 ✨欢迎来到小K的MySQL专栏&#xff0c;本节将为大家带来MySQL日期时间、条件判断、系统信息、加密、进制转换和IP地址转换函数的讲解✨ 目录 前言一、日期和时间函数二、条件判断函数三、系统信息函数四、加密函数五、进制转换函数六、IP地址转换函数七、总结 一、日期和时…

韦纳VeighNa(VNPY)新手入门到写策略

韦纳VeighNa&#xff08;VNPY&#xff09;新手入门到写策略 1.VeighNa终端如何登录交易&#xff1f;勾选好相应接口和功能启动后在哪输入账号&#xff1f;2.在哪开发策略&#xff1f;策略文件夹在哪&#xff1f;3.如何写策略&#xff1f;如何开发策略&#xff1f;4.有策略模板吗…

离线语音识别芯片IC方案,打造智能眼部按摩仪WTK6900H-C-24SS

随着科技的不断进步&#xff0c;智能化产品已经成为人们日常生活中的一部分。眼部按摩仪作为舒缓眼部疲劳的利器&#xff0c;如今更是加入了离线语音识别芯片IC方案&#xff0c;让使用体验更加智能化、便捷化&#xff0c;WTK6900H-C-24SS离线语音识别芯片IC方案&#xff0c;将为…

Tomcat 8.5 环境搭建指南

文章目录 导言环境搭建总结 导言 欢迎阅读本篇博客&#xff0c;本文将为您提供关于如何搭建Tomcat 8.5环境的详细指南。Tomcat是一个流行的开源Java Servlet容器&#xff0c;它提供了一个运行Java Web应用程序的平台。无论您是新手还是有经验的开发人员&#xff0c;本文都将帮…

Selenium教程__使用switch_to.alert处理弹窗(14)

与switch_to.window 和 switch_to.frame 相比&#xff0c;switch_to.alert的alert方法使用了property 装饰器&#xff0c;所以在使用时alert被当成属性调用。 演示如下 from selenium import webdriverdriver webdriver.Chrome() driver.maximize_window() driver.get("…

推荐一个好用的Python词云展示库-wordcloud

目录标题 前言1.介绍2.使用说明尾语 前言 嗨喽~大家好呀&#xff0c;这里是魔王呐 ❤ ~! 1.介绍 wordcloud是优秀的词云展示第三方库&#xff0c;wordcloud能够将一段文本变成一个词云。 词云就是将词语通过图形可视化的方式&#xff0c;直观和艺术的展示出来。词云在我们的…

Word模板替换,并转PDF格式输出

Poi-tl参考文档地址&#xff1a;http://deepoove.com/poi-tl/1.8.x/#hack-loop-table word模板替换&#xff0c;转pdf 1. 依赖引入&#xff1a;2. word模板配置&#xff1a;3. 示例demo:4 . 效果图 1. 依赖引入&#xff1a; <dependency><groupId>org.apache.poi&…

Unity3D:在 Inspector 中操作

推荐&#xff1a;将 NSDT场景编辑器 加入你的3D工具链 3D工具集&#xff1a; NSDT简石数字孪生 在 Inspector 中操作 本页面描述了一些用于控制 Inspector 窗口本身的选项。有关设置游戏对象、脚本和资源的属性的信息&#xff0c;请参阅编辑属性。 Lock the Inspector 通常&…

网络安全零基础学习路线

一&#xff0c;怎么入门&#xff1f; 1、Web 安全相关概念&#xff08;2 周&#xff09; 了解网络安全相关法律法规 熟悉基本概念&#xff08;SQL 注入、上传、XSS、CSRF、一句话木马等&#xff09;。 通过关键字&#xff08;SQL 注入、上传、XSS、CSRF、一句话木马等&#…

C. Robot Collisions(暑期集训)

原题链接 题意&#xff1a; 有n个机器人在OX轴上运动&#xff0c;运动范围为[0,m]。 第i个机器人开始位于xi的位置&#xff0c;然后以1单位距离每秒的速度向左或向右运动&#xff0c;当其运动到0点或m点时会调转方向。 如果任意时刻多于一个机器人在同一整数位置&#xff0c;…

【硬件自动化测试--测试软件的设计及实现】测试框架设计及实现!

如何设计并实现自动化框架? 首先,需要了解我们的产品,需要做哪些测试,如有很多硬件产品的测试中,需要用到示波器、频谱仪、信号源等等一系列的仪器,仪器的型号也各有不同,所以在设计框架之前,一定得了解我们的产品。 那么,就以一台服务器为例子,基带部分的测试,包括…

酷开系统 |酷开科技持续创造大屏生命力

在这个信息爆炸的时代&#xff0c;人们对于互联网和智能生活的需求&#xff0c;不再局限于碎片化或工作场景&#xff0c;更多的是延伸至连续化的家庭化使用环境。而智能大屏作为互联网时代下的产物&#xff0c;融合了互联网流媒体的数据处理方式&#xff0c;深受年轻人的青睐&a…

牛客·矩阵取数游戏

矩阵取数游戏 这里我们推一下这个题的精髓&#xff1a; dp[i][j]max(int(b[c][i])dp[i1][j],dp[i][j-1]int(b[c][j])) dp[i][j]2*dp[i][j]假定某一行有两个数x,y; 那么我们有两种选择&#xff1a; 先拿x&#xff0c;再拿y&#xff1a;x*2y*4&#xff1b; 先拿y&#xff0c;再…