文档章节

Java实现几种简单的重试机制

小灰灰Blog
 小灰灰Blog
发布于 2017/08/30 22:33
字数 1815
阅读 225
收藏 5
点赞 0
评论 0

背景

当业务执行失败之后,进行重试是一个非常常见的场景,那么如何在业务代码中优雅的实现重试机制呢?

设计

我们的目标是实现一个优雅的重试机制,那么先来看下怎么样才算是优雅

  • 无侵入:这个好理解,不改动当前的业务逻辑,对于需要重试的地方,可以很简单的实现
  • 可配置:包括重试次数,重试的间隔时间,是否使用异步方式等
  • 通用性:最好是无改动(或者很小改动)的支持绝大部分的场景,拿过来直接可用

针对上面的几点,分别看下右什么好的解决方案

几种解决思路

要想做到无侵入或者很小的改动,一般来将比较好的方式就是切面或者消息总线模式;可配置和通用性则比较清晰了,基本上开始做就表示这两点都是基础要求了,唯一的要求就是不要硬编码,不要写死,基本上就能达到这个基础要求,当然要优秀的话,要做的事情并不少

切面方式

这个思路比较清晰,在需要添加重试的方法上添加一个用于重试的自定义注解,然后在切面中实现重试的逻辑,主要的配置参数则根据注解中的选项来初始化

优点:

  • 真正的无侵入

缺点:

  • 某些方法无法被切面拦截的场景无法覆盖(如spring-aop无法切私有方法,final方法)
  • 直接使用aspecj则有些小复杂;如果用spring-aop,则只能切被spring容器管理的bean

消息总线方式

这个也比较容易理解,在需要重试的方法中,发送一个消息,并将业务逻辑作为回调方法传入;由一个订阅了重试消息的consumer来执行重试的业务逻辑

优点:

  • 重试机制不受任何限制,即在任何地方你都可以使用
  • 利用EventBus框架,可以非常容易把框架搭起来

缺点:

  • 业务侵入,需要在重试的业务处,主动发起一条重试消息
  • 调试理解复杂(消息总线方式的最大优点和缺点,就是过于灵活了,你可能都不知道什么地方处理这个消息,特别是新的童鞋来维护这段代码时)
  • 如果要获取返回结果,不太好处理, 上下文参数不好处理

模板方式

把这个单独捞出来,主要是某些时候我就一两个地方要用到重试,简单的实现下就好了,也没有必用用到上面这么重的方式;而且我希望可以针对代码快进行重试

这个的设计还是非常简单的,基本上代码都可以直接贴出来,一目了然:

public abstract class RetryTemplate {

    private static final int DEFAULT_RETRY_TIME = 1;

    private int retryTime = DEFAULT_RETRY_TIME;

    // 重试的睡眠时间
    private int sleepTime = 0;

    public int getSleepTime() {
        return sleepTime;
    }

    public RetryTemplate setSleepTime(int sleepTime) {
        if(sleepTime < 0) {
            throw new IllegalArgumentException("sleepTime should equal or bigger than 0");
        }

        this.sleepTime = sleepTime;
        return this;
    }

    public int getRetryTime() {
        return retryTime;
    }

    public RetryTemplate setRetryTime(int retryTime) {
        if (retryTime <= 0) {
            throw new IllegalArgumentException("retryTime should bigger than 0");
        }

        this.retryTime = retryTime;
        return this;
    }

    /**
     * 重试的业务执行代码
     * 失败时请抛出一个异常
     *
     * todo 确定返回的封装类,根据返回结果的状态来判定是否需要重试
     *
     * @return
     */
    protected abstract Object doBiz() throws Exception;


    public Object execute() throws InterruptedException {
        for (int i = 0; i < retryTime; i++) {
            try {
                return doBiz();
            } catch (Exception e) {
                log.error("业务执行出现异常,e: {}", e);
                Thread.sleep(sleepTime);
            }
        }

        return null;
    }


    public Object submit(ExecutorService executorService) {
        if (executorService == null) {
            throw new IllegalArgumentException("please choose executorService!");
        }

        return executorService.submit((Callable) () -> execute());
    }

}

预留一个doBiz方法由业务方来实现,在其中书写需要重试的业务代码,然后执行即可

使用case也比较简单

public void retryDemo() throws InterruptedException {
    Object ans = new RetryTemplate() {
        @Override
        protected Object doBiz() throws Exception {
            int temp = (int) (Math.random() * 10);
            System.out.println(temp);
  
            if (temp > 3) {
                throw new Exception("generate value bigger then 3! need retry");
            }
  
            return temp;
        }
    }.setRetryTime(10).setSleepTime(10).execute();
    System.out.println(ans);
}

优点:

  • 简单(依赖简单:引入一个类就可以了; 使用简单:实现抽象类,讲业务逻辑填充即可;)
  • 灵活(这个是真正的灵活了,你想怎么干都可以,完全由你控制)

缺点:

  • 强侵入
  • 代码臃肿

实现

上面的模板方式基本上就那样了,接下来谈到的实现,毫无疑问将是切面和消息总线的方式

1. 切面方式

实现依然是基于前面的模板方式做的,简单来看就是添加一个切面,内部实现模版类即可

注解定义如下

@Documented
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RetryDot {
    /**
     * 重试次数
     * @return
     */
    int count() default 0;


    /**
     * 重试的间隔时间
     * @return
     */
    int sleep() default 0;


    /**
     * 是否支持异步重试方式
     * @return
     */
    boolean asyn() default false;
}

切面逻辑如下

@Aspect
@Component
@Slf4j
public class RetryAspect {

    ExecutorService executorService = new ThreadPoolExecutor(3, 5,
            1, TimeUnit.MINUTES,
            new LinkedBlockingQueue<Runnable>());


    @Around(value = "@annotation(retryDot)")
    public Object execute(ProceedingJoinPoint joinPoint, RetryDot retryDot) throws Exception {
        RetryTemplate retryTemplate = new RetryTemplate() {
            @Override
            protected Object doBiz() throws Throwable {
                return joinPoint.proceed();
            }
        };

        retryTemplate.setRetryCount(retryDot.count())
                .setSleepTime(retryDot.sleep());


        if (retryDot.asyn()) {
            return retryTemplate.submit(executorService);
        } else {
            return retryTemplate.execute();
        }
    }
}

2. 消息方式

依然是在EventBus的基础上进行开发,结果写到一半,发现这种方式局限性还蛮大,基本上不太适合实际使用,下面依然给出实现逻辑

定义的重试事件RetryEvent

@Data
public class RetryEvent {

    /**
     * 重试间隔时间, ms为单位
     */
    private int sleep;


    /**
     * 重试次数
     */
    private int count;


    /**
     * 是否异步重试
     */
    private boolean asyn;


    /**
     * 回调方法
     */
    private Supplier<Object> callback;
}

消息处理类

@Component
public class RetryProcess {

    ExecutorService executorService = new ThreadPoolExecutor(3, 5,
            1, TimeUnit.MINUTES,
            new LinkedBlockingQueue<Runnable>());

    private  static EventBus eventBus = new EventBus("retry");
    
    public static void post(RetryEvent event) {
        eventBus.post(event);
    }

    public static void register(Object handler) {
        eventBus.register(handler);
    }

    public static void unregister(Object handler) {
        eventBus.unregister(handler);
    }

    @PostConstruct
    public void init() {
        register(this);
    }

    @Subscribe
    public void process(RetryEvent event) throws InterruptedException {

        RetryTemplate retryTemplate = new RetryTemplate() {
            @Override
            protected Object doBiz() throws Throwable {
                return event.getCallback().get();
            }
        };


        retryTemplate.setSleepTime(event.getSleep())
                .setRetryCount(event.getCount());

        if(event.isAsyn()) {
            retryTemplate.submit(executorService);
        } else {
            retryTemplate.execute();
        }
    }
}

问题比较明显,返回值以及输入参数的传入,比较不好处理

测试

测试下上面两种使用方式, 定义一个实例Service,分别采用注解和消息两种方式

@Service
public class RetryDemoService {


    private int genNum() {
        return (int) (Math.random() * 10);
    }


    @RetryDot(count = 5, sleep = 10)
    public int genBigNum() throws Exception {
        int a = genNum();
        System.out.println("genBigNum " + a);
        if (a < 3) {
            throw new Exception("num less than 3");
        }

        return a;
    }

    public void genSmallNum() throws Exception {
        RetryEvent retryEvent = new RetryEvent();
        retryEvent.setSleep(10);
        retryEvent.setCount(5);
        retryEvent.setAsyn(false);
        retryEvent.setCallback(() -> {
            int a = genNum();
            System.out.println("now num: " + a);
            if (a > 3) {
                throw new RuntimeException("num bigger than 3");
            }

            return a;
        });

        RetryProcess.post(retryEvent);
    }
}

因为使用了切面,在spring的基础上进行开发的,所以需要加上对应的配置信息 aop.xml

<context:component-scan base-package="com.hui.quickretry"/>

<context:annotation-config/>
<aop:aspectj-autoproxy/>

Test代码

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration({"classpath:aop.xml"})
public class AspectRetryTest {

    @Autowired
    private RetryDemoService retryDemoService;

    @Test
    public void testRetry() throws Exception {
        for (int i = 0; i < 3; i++) {
            int ans = retryDemoService.genBigNum();

            System.out.println("----" + ans + "----");

            retryDemoService.genSmallNum();

            System.out.println("------------------");
        }
    }
}

输出

genBigNum 9
----9----
now num: 1
------------------
genBigNum 9
----9----
now num: 4
now num: 1
------------------
genBigNum 5
----5----
now num: 6
now num: 6
now num: 0
------------------

其他

guava-retryingspring-retry 实际上是更好的选择,设计与实现都非常优雅,实际的项目中完全可以直接使用

相关代码:

https://github.com/liuyueyi/quick-retry

个人博客:一灰的个人博客

公众号获取更多:

个人信息

参考

© 著作权归作者所有

共有 人打赏支持
小灰灰Blog
粉丝 160
博文 150
码字总数 258693
作品 0
武汉
程序员
java面试必备之ThreadLocal

按照传统的经验,如果某个对象是非线程安全的,在多线程环境下对象的访问需要采用synchronized进行同步。但是模板类并未采用线程同步机制,因为线程同步会降低系统的并发性能,此外代码同步解...

编程老司机 ⋅ 05/16 ⋅ 0

《Spring技术内幕》学习笔记18——Spring使用Hessian实现远程调用

Spring目前提供了对RMI、 HttpInvoker、Hessian、Burlap及WebService等Remoting技术的集成。Spring屏蔽了这些实现技术的差异,用户只需开发简单的Java对象(Plain Old Java Objects,POJO)然后...

谜男amu ⋅ 05/16 ⋅ 0

利用“进程注入”实现无文件复活 WebShell

  * 本文作者:rebeyond,本文属FreeBuf原创奖励计划,未经许可禁止转载   引子   上周末,一个好兄弟找我说一个很重要的目标shell丢了,这个shell之前是通过一个S2代码执行的漏洞拿到...

FreeBuf ⋅ 05/30 ⋅ 0

Java就业变难了?你需要对自己有点信心

伴随着IT的火热,越来越多的人进入了IT领域,这在进一步推动着IT发展的同时也极大增加了就业压力。伴随着激烈的岗位竞争,越来越多的人开始感叹工作难找,越火的行业越是如此,Java自是首当其...

糖宝_d864 ⋅ 06/08 ⋅ 0

聊聊Dubbo-Dubbo可扩展机制实战

1. Dubbo的扩展机制 在Dubbo的官网上,Dubbo描述自己是一个高性能的RPC框架。今天我想聊聊Dubbo的另一个很棒的特性, 就是它的可扩展性。 如同罗马不是一天建成的,任何系统都一定是从小系统不...

中间件小哥 ⋅ 05/29 ⋅ 0

聊聊Dubbo - Dubbo可扩展机制实战

摘要: 在Dubbo的官网上,Dubbo描述自己是一个高性能的RPC框架。今天我想聊聊Dubbo的另一个很棒的特性, 就是它的可扩展性。 1. Dubbo的扩展机制 在Dubbo的官网上,Dubbo描述自己是一个高性能...

阿里云云栖社区 ⋅ 06/04 ⋅ 0

Java学习---Java简单认识

前言 小编在学习Java方面的基础知识,发现里面有很多是结合之前的语言的特点发展过来的,不同的地方是,Java有它自己的发展和特点。下面小编先简单地做一下总结,结合看过的1-2章的J2SE视频,...

m18633778874 ⋅ 04/01 ⋅ 0

JAVA程序员面试题整理(较全面)

以下是在面试中可能会遇到的问题,话不多说,往下看 1、面向对象的特征有哪些方面? 2、访问修饰符public,private,protected,以及不写(默认)时的区别? 3、String 是最基本的数据类型吗? ...

编程大侠 ⋅ 04/09 ⋅ 0

使用ConcurrentMap实现高效可靠的原子操作

问题:服务器S1从远程获取多个文件到本地处理。这些文件的数据会被Processor转换成不同类型的数据模型存放至S1的数据库。每个Processor处理逻辑是相互独立的,但是同一个文件的数据可能会被多...

屈想顺 ⋅ 06/14 ⋅ 0

Spring boot 2.0 之优雅停机

spring boot 框架在生产环境使用的有一段时间了,它“约定大于配置”的特性,体现了优雅流畅的开发过程,它的部署启动方式()也很优雅。但是我使用的停止应用的方式是 ,即使写了脚本,还是...

rabbitGYK ⋅ 05/20 ⋅ 0

没有更多内容

加载失败,请刷新页面

加载更多

下一页

LVM

LVM: 硬盘划分分区成物理卷->物理卷组成卷组->卷组划分逻辑分区。 1.磁盘分区: fdisk /dev/sdb 划分几个主分区 输入t更改每个分区类型为8e(LVM) 使用partprobe生成分区的文件:如/dev/sd...

ZHENG-JY ⋅ 35分钟前 ⋅ 0

彻底删除Microsoft Office的方法

参照此链接彻底删除Office https://support.office.com/zh-cn/article/%e4%bb%8e-pc-%e5%8d%b8%e8%bd%bd-office-9dd49b83-264a-477a-8fcc-2fdf5dbf61d8?ui=zh-CN&rs=zh-CN&ad=CN......

Kampfer ⋅ 50分钟前 ⋅ 0

大盘与个股之间关系

大盘走多:积极出手 顺势加码 大盘走空: 少量出手 退场观望 大盘做头:逆势减码 少量操作 大盘做底 : 小量建仓 小量试单

guozenhua ⋅ 52分钟前 ⋅ 0

Day16 LVM(逻辑卷管理)与磁盘故障小案例

lvm详解 简述 LVM的产生是因为传统的分区一旦分区好后就无法在线扩充空间,也存在一些工具能实现在线扩充空间但是还是会面临数据损坏的风险;传统的分区当分区空间不足时,一般的解决办法是再...

杉下 ⋅ 58分钟前 ⋅ 0

rsync实现多台linux服务器的文件同步

一、首先安装rsync,怎样安装都行,rpm,yum,还是你用源码安装都可以。因为我用的是阿里云的ESC,yum install rsync就ok了。 二、配置rsync服务 1.先建立个同步数据的帐号 123 groupadd r...

在下头真的很硬 ⋅ 今天 ⋅ 0

前端基础(三):函数

字数:1685 阅读时间:5分钟 函数定义 在最新的ES规范中,声明函数有4中方法: -函数声明 -函数表达式 -构造函数Function -生成器函数 1.函数声明 语法: function name([param[, param2 [....

老司机带你撸代码 ⋅ 今天 ⋅ 0

Java虚拟机的Heap监狱

在Java虚拟机中,我是一个位高权重的大管家,他们都很怕我,尤其是那些Java 对象,我把他们圈到一个叫做Heap的“监狱”里,严格管理,生杀大权尽在掌握。 中国人把Stack翻译成“栈”,把Hea...

java高级架构牛人 ⋅ 今天 ⋅ 0

Spring MVC基本概念

只写Controller

颖伙虫 ⋅ 今天 ⋅ 0

微软重金收购GitHub的背后逻辑原来是这样的

全球最大的开发者社区GitHub网站花落谁家的问题已经敲定,微软最终以75亿美元迎娶了这位在外界看来无比“神秘”的小家碧玉。尽管此事已过去一些时日,但整个开发者世界,包括全球各地的开源社...

linux-tao ⋅ 今天 ⋅ 0

磁盘管理—逻辑卷lvm

4.10-4.12 lvm 操作流程: 磁盘分区-->创建物理卷-->划分为卷组-->划分成逻辑卷-->格式化、挂载-->扩容。 磁盘分区 注: 创建分区时需要更改其文件类型为lvm(代码8e) 分区 3 已设置为 Linu...

弓正 ⋅ 今天 ⋅ 0

没有更多内容

加载失败,请刷新页面

加载更多

下一页

返回顶部
顶部