spring-retry的简单模型整理

在业务系统中,经常会遇到进行业务重试的场景,当一个执行方法失败时,可能需要进行几次重试,如果重试时成功了,仍然认为业务是正确的。否则则throw相应的异常。这种通用的重试场景则可以使用标准的第三方重试库来完成,以避免自己在业务代码中写类似while try 的代码。同时,因为重试的条件以及处理的逻辑均不太一样,通过一个标准的重试库来完成此操作,也是很有必要的。

本文描述了spring提供的重试库spring-retry,通过提取相应的概念模型,了解其工作原理以及执行机制.
版本:1.2.2.RELEASE

会使用到的注释

Retryable
用于标识一个可以进行重试的方法,即一个方法如果需要在throw异常之后进行重试,则可以使用此注解进行标记.同时,此注解上标识了可以进行重试的一些条件以及次数信息。

Backoff
此标识一个方法在进行下一次重试时,需要暂停的一些手法以及参数信息,比如sleep多少秒,或者是是在一个区间范围内sleep。

Recover
此标识当一个方法执行多次都失败之后,进行的一个failback处理。

主要的业务接口

RetryContext
重试上下文,即维护了整个重试周期中相应调用对象,调用次数,以及在调用过程中的异常对象等,其它接口均从此对象中获取相应的数据.

RetryOperations
封装了一个主要可重试操作,可以理解为进行主要逻辑操作的template对象。

RetryPolicy
与@Backoff相对应的一个接口,用于在整个重试逻辑中,处理何时该重试,以及控制相应的重试上下文信息。

RetryListener
监听器,用于在重试过程中,对开始,重试出错,结束时进行拦截操作。默认情况下,此类为null,即无作用。

RetryCallback
简单的重试回调对象,用于封装原来的可重试方法,将整个操作统一使用callback来表示,以隔离相应的实现。

RecoveryCallback
与@Recover相对应,即最终进行failback的回调对象.

继续阅读“spring-retry的简单模型整理”

ZooKeeper Watcher 机制源码解释(转)

本文转自 https://www.ibm.com/developerworks/cn/opensource/os-cn-apache-zookeeper-watcher/index.html

ZooKeeper 允许客户端向服务端注册一个 Watcher 监听,当服务端的一些指定事件触发了这个 Watcher,那么就会向指定客户端发送一个事件通知来实现分布式的通知功能。

ZooKeeper 的 Watcher 机制主要包括客户端线程、客户端 WatchManager 和 ZooKeeper 服务器三部分。在具体工作流程上,简单地讲,客户端在向 ZooKeeper 服务器注册 Watcher 的同时,会将 Watcher 对象存储在客户端的 WatchManager 中。当 ZooKeeper 服务器端触发 Watcher 事件后,会向客户端发送通知,客户端线程从 WatchManager 中取出对应的 Watcher 对象来执行回调逻辑。

    private final HashMap<String, HashSet<Watcher>> watchTable =new HashMap<String, HashSet<Watcher>>();

    public synchronized void addWatch(String path, Watcher watcher) {
        HashSet<Watcher> list = watchTable.get(path);
        if(list == null) {
            list = new HashSet<Watcher>(4);
            watchTable.put(path, list);
        }
        list.add(watcher);

        HashSet<String> paths = watch2Paths.get(watcher);
        if(paths == null) {
            // cnxns typically have many watches, so use default cap here
            paths = new HashSet<String>();
            watch2Paths.put(watcher, paths);
        }
        paths.add(path);
    }

继续阅读“ZooKeeper Watcher 机制源码解释(转)”

使用greys找到泄漏的本地线程变量值

在程序代码中,出现过这样一种情况,在一个新的线程组中,尝试去获取一个threadLocal的值,按照相应的程序代码,应该是不能获取到的。但是在实际的运行过程中,却发现能够获取到相应的变量值。
在我们的程序代码中,使用了shiro来存储相应的用户信息,即一个简化版的的权限管理程序。其中,在标准的web程序中,shiro拦截器会把相应的会话信息存储在session中,并且在应用代码中,可以通过一个threadLocal的ThreadContext来获取相应的subject,进而拿到相应的会话值.相应的代码如下所示:

    public static UserId getUserId() {
        Optional<Subject> optional = Optional.ofNullable(ThreadContext.getSubject());
        return optional.map(t -> t.getSession(false))
                .map(t -> t.getAttribute("userId"))
                .orElse(null);
    }

因为相应的数据为web调用时才会注入到sessionId,而如果是一个定时类的任务,那么理论上应该是不会有相应的session对象存在,那么即不会有相应的subject存在,在调用此方法时即会返回在调用getSubject时失败。但是在实际上时,却发生调用subject不为null,进而在调用getSession方法时出现了错误信息。

相应的调用链看起来应该像是这样

  1. 线程1调用了 ThreadContext.setSubject 方法,设置了session信息
  2. 线程1完成整个业务方法的运行,相应的session信息被销毁,但相应的removeSubject方法并没有被调用
  3. 线程1重新以任务的形式执行任务代码,其中调用了getSubject,但不能拿到相应的数据

在这种调用链中,我们只看到了最终的现场,但原始的设置值的现场即不能复现,即不清楚数据是什么时候,哪个调用任务进行处理的。整个问题称之为数据泄漏,即数据在不应该出现的地方出现了。
处理这种问题,一种作法即是异常标记法。详细的步骤如下所示:

  1. 在调用 ThreadContext.setSubject 时,设置一个异常线程栈(Exception),此里面封装了当前调用的整个路径
  2. 将异常线程栈(字符串形式)存储在一个threadLocal变量中,方便后面获取
  3. 在调用 getSubject 时,如果发生了相应的错误信息,即表示复现了相应的错误场景
  4. 这时候即将之前存储到threadLocal中的字符串获取出来,打印出原始的setSubject调用路径

整个作法参考于alibaba druid的数据库连接泄漏检测。

本文介绍了在不修改源代码的情况下,并且在线上环境,使用greys,来监控相应的调用,当出现了满足条件的场景时,自动打印出相应的调用路径。

继续阅读“使用greys找到泄漏的本地线程变量值”

基于jgroups分布式缓存实现redis服务的fallback处理

现在的系统当中,实现缓存均是采用redis来完成,一个典型的场景即是将用户的会话信息存放于redis当中,以实现分布式缓存以及服务的高可用。当nginx在进行请求分发时,不需要采用ip hash,随机分发请求,后端直接读取中央缓存即可完成相应的业务处理。但这种场景同样存在一个问题,即是缓存的高可用问题。特别是相应的redis服务由外部提供时,这个问题更加明显。初期redis服务为一个单点时,当服务挂掉时,会导致整个服务将不再可用。

解决此问题有2个方案,一个是再使用同样的策略保证redis的高可用,如部署redis集群或者是sentinel模式,另外一种是直接放弃对redis的高依赖性,在应用层直接进行缓存的fallback支持。在本场景中,我们仅将redis作为一个缓存层使用,它的一些其它特性如发布订阅暂不考虑。

在本文中,我们使用了hystrix+caffeine+jgroups的方案,来完成整个redis的fallback化处理,通过hystrix实现对redis调用的断路及fallback处理,使用caffine提供高效本地缓存,使用jgroups完成多系统间的数据复制分发。 主要实现的目的在于,当redis挂掉之后,能够切换到本地分布式缓存,保证系统后续可用(不过之前存放在redis中的数据完全不再可见,会导致一部分的问题,如会话信息会丢失。可考虑仅将会话信息直接复制在本地缓存当中)

本方案适用于redis短时间由于网络或其它原因连接不上,但在相对短的时间内(如几分钟,不超过1个小时等)即会恢复的情况,这样本地缓存不会存储很多东西的情况.

继续阅读“基于jgroups分布式缓存实现redis服务的fallback处理”

greys在线诊断工具的主要实现

greys是一个使用java management tool进程注入javaagent实现在线系统的诊断一个工具。原github为(https://github.com/oldmanpushcart/greys-anatomy),其主要的功能在于系统不停机的情况下。可以查看系统中的线程信息,cpu使用情况,jmx信息,以及某个方法在运行时的调用栈,调用参数等。

一个典型的场景就是线上某个功能出bug,但是系统中并没有记录参数信息,这时候即可通过这个功能注入agent,临时地打印出这个调用方法的参数,以方便定位相应的问题。如无此工具,则只能改代码,然后重新上线。在这种情况下,可能出错的场景就不能再复现。(当然也有其它工具(如log monitor)作到在线系统参数记录开关的目的,这里不作描述)

本文主要描述greys是如何工作的,包括如何注入到在线系统,然后脚本client与注入后server端的交互,以及如何实现一个简单的参数拦截记录功能。从整个实现机制层面描述其工作原理。

继续阅读“greys在线诊断工具的主要实现”

使用spring boot和embedded tomcat开发时出现404找不到jsp问题的处理

初使用spring boot时,出现程序中配置了相应的指向地址,但前端界面始终报找不到jsp界面的404错误.但相同的程序在另一位同事时却正常工作.因此,有必要从源码角度分析一下,spring boot如何和embedded tomcat进行相应的整合.

本文开发时使用的为idea,相应的jsp放置在标准的src/main/webapp目录下,并且spring mvc也配置了 spring.mvc.view.prefix各项网上都能找到的配置信息.

首先是先将一个最简单的jsp文件放置在src/main/webapp,然后启动程序,直接访问此jsp(不通过mvc跳转), 仍然返回404.这就表示实际上这个src/main/webapp并没有被spring boot中tomcat所识别. 一般认为这个目录就是一个放置资源信息的地址目录,那么这里404表示在启动过程中,这个目录并没有被正常找到.

因为spring boot并没有一个显示配置项来配置此位置(从hello的demo来看),因此此地址的查找为一个简单的逻辑过程.从tomcat的角度来看,这属于一个documentRoot,即项目文件根目录,从尝试从整个源码中搜索有关于documentRoot,或者说对于tomcat一个上下文来讲为docBase的概念.最终找到的位置为 TomcatEmbeddedServletContainerFactory#prepareContext 方法.

简单的代码来看,主要的代码如下所示:

		File docBase = getValidDocumentRoot();
		docBase = (docBase != null ? docBase : createTempDir("tomcat-docbase"));
		context.setDocBase(docBase.getAbsolutePath());

继续阅读“使用spring boot和embedded tomcat开发时出现404找不到jsp问题的处理”