优雅缩扩容

前言

最近接到一个需求,因为我们所处的业务领域流量具有非常强的周期性,因此对于微服务周期性的缩扩容是在所难免的,毕竟服务器资源都是耗钱的主。缩扩容会引发一系列的问题,例如访问中断,必然会产生错误的数据,虽然不在核心服务时段,但是也不可避免的引发一些问题。对于已经接收的流量,我们可以使用“优雅关机”来处理,先在服务注册中心注销我们的服务,停止接收新的请求,然后在真正停机前休眠一段时间让系统将残余的请求消化掉。
但是对于一些比较后端,或者异步的任务,或者是持续的流数据,又或者是消息处理等等,这类任务可能耗时比较久就不能这么处理了。

思路

我们要能够在不影响任务执行的情况下,对http请求能力进行缩扩容。但是任务基本上就是不能停机的。所以很自然的想到把http请求处理与task分开,因此借助配置我们可以选择在实例上觉得是否加载这些任务来做到分离。例如把它配置在jvm的环境变量中,然后在通过@Conditional结合@Configuration来动态加载,这样就可以让运行task的实例不停机,而随意对service进行缩扩容操作了。

show me the code

1
#控制service中的task任务是否加载 这个属性会配置在jvm环境变量中 默认值为true 加载
2
service:
3
  task:
4
    enable: ${service.task.enable:true}
1
/**
2
 * @Description 根据配置,动态注入service后台定时调度的任务和kafka消息处理Bean,部署时task与用户请求处理实例分开【同一工程】,减少缩扩容时对任务调度的影响
3
 * @date 2020.12.10 13:59
4
 */
5
@ConditionalOnProperty(name = "service.task.enable", havingValue = "true")
6
@Configuration
7
public class ServiceTaskConfiguration {
8
@Bean
9
    public Task task(){
10
        return new Task();
11
    }
12
}
1
@ConditionalOnProperty注解源码 其实和实现Conditional接口是一样的效果
2
可以接收value和name参数进行条件匹配,来决定是否加载被修饰的Bean
3
4
@Retention(RetentionPolicy.RUNTIME)
5
@Target({ElementType.TYPE, ElementType.METHOD})
6
@Documented
7
@Conditional({OnPropertyCondition.class})
8
public @interface ConditionalOnProperty {
9
    String[] value() default {};
10
11
    String prefix() default "";
12
13
    String[] name() default {};
14
15
    String havingValue() default "";
16
17
    boolean matchIfMissing() default false;
18
19
    boolean relaxedNames() default true;
20
}

接口统一脱敏处理

#接口统一脱敏处理

前言

最近 在做一个关于数据脱敏的需求,就是将接口返回的一些敏感信息用*代替,对用户的敏感数据予以保护,因为线上出现了用户账号申诉被盗的情况
其实这样一个需求,做起来是是蛮简单的,在前端交互支持的情况下,我们在接口数据序列化之前把它替换掉就可以了。但是因为它是一个相对通用的业务,所以考虑将它做到基础框架里面去。

思路

基本思路:拿到原始待序列化的对象,通过自定义注解的方式,对不同的字段进行相应的脱敏处理
https://github.com/DannyHoo/desensitized 这里前人有一个不错的思路,基本就是这样实现的,但是他使用的是ResponseBodyAdvice结合注解来处理接口和对象,所有的接口(包括非序列化的接口)都要走到这个逻辑里面,性能必定会受到一些影响,而且为了不对原始对象处理,采用了对象反射和深拷贝使得性能会大大降低。但是在并发不高的情况下作为一个全局的脱敏处理框架还是可行的。

扩展

但是我要应对的场景,并发还是挺高的,峰值tps目前在150左右,虽然不是每个接口的调用都会有大并发,但是既然要做成框架性的东西,就不能确定使用方的调用频率了,因此需要换一种更简单的思路来实现以提高性能。
对于springmvc来说,其实已经帮我们做了一次序列化操作,因此我们可以利用这个序列化过程,直接定制处理,不用手动再去处理一次,因此我使用com.fasterxml.jackson序列化来实现这个功能。
另外对于方法的控制,我使用拦截器和线程上下文来处理,被拦截要加密的接口会设置脱敏上下文,包括国际化信息等等,在处理结束后记得清理,以免造成线程污染。

show me the code

1
/**
2
 * @Description 脱敏注解
3
 * @Author Storm
4
 * @date 2020.11.25 17:00
5
 */
6
@Target({ElementType.FIELD})
7
@Retention(RetentionPolicy.RUNTIME)
8
@JacksonAnnotationsInside
9
@JsonSerialize(using = DesensitizedSerializer.class)
10
public @interface Desensitized {
11
12
    /*脱敏类型(规则)枚举*/
13
    SensitiveTypeEnum type();
14
}
1
/**
2
 * @Description 脱敏序列化器,根据属性上的注解和上下文信息进行脱敏,使用时注意上下文的设置和清理,避免线程污染!!
3
 * @Author Storm
4
 * @date 2020.11.25 17:10
5
 */
6
public class DesensitizedSerializer extends StdSerializer<String> implements ContextualSerializer {
7
8
    private static final long serialVersionUID = 1L;
9
10
    private SensitiveTypeEnum type;
11
12
    protected DesensitizedSerializer(Class<String> t) {
13
        super(t);
14
    }
15
16
    protected DesensitizedSerializer() {
17
        super(String.class);
18
    }
19
20
    protected DesensitizedSerializer(SensitiveTypeEnum type) {
21
        super(String.class);
22
        this.type = type;
23
    }
24
25
    @Override
26
    public JsonSerializer<?> createContextual(SerializerProvider serializerProvider, BeanProperty beanProperty) {
27
    //获取注解上下文,拿到配置的脱敏类型
28
        Desensitized annotation = beanProperty.getAnnotation(Desensitized.class);
29
        if (annotation == null) {
30
            return new DesensitizedSerializer();
31
        }
32
        SensitiveTypeEnum type = annotation.type();
33
        return new DesensitizedSerializer(type);
34
    }
35
36
    @Override
37
    public void serialize(String s, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException {
38
        DesensitizeContext context = DesensitizeContext.getContext();
39
        if (context == null || !context.getNeedDesensitize()) {
40
            //不需要进行脱敏处理
41
            jsonGenerator.writeString(s);
42
            return;
43
        }
44
        String language = UserContext.get().getHl();
45
        jsonGenerator.writeString(DesensitizedUtils.desensitize(type, s, language));//根据类型进行具体的脱敏处理,回写到对象中
46
    }
1
/**
2
 * @Description 脱敏上下文信息
3
 * @Author Storm
4
 * @date 2020.11.25 17:48
5
 */
6
public class DesensitizeContext {
7
    public static final ThreadLocal<DesensitizeContextObj> context = new ThreadLocal<>();
8
9
    @Data
10
    public static class DesensitizeContextObj {
11
        /**
12
         * 语言 zh en
13
         */
14
        private String language = Locale.ENGLISH.getLanguage();
15
16
        /**
17
         * 是否需要脱敏
18
         */
19
        private boolean needDesensitize = false;
20
21
        public DesensitizeContextObj() {
22
        }
23
24
        public DesensitizeContextObj(String language, boolean needDesensitize) {
25
            this.language = language;
26
            this.needDesensitize = needDesensitize;
27
        }
28
    }
29
30
31
    public static DesensitizeContextObj getContext() {
32
        return context.get();
33
    }
34
35
    public static void setContext(DesensitizeContextObj obj) {
36
        context.set(obj);
37
    }
38
39
    public static void removeContext() {
40
        if (context.get() == null) {
41
            return;
42
        }
43
        context.remove();
44
    }
45
}
1
public class DesensitizeInterceptor extends HandlerInterceptorAdapter {
2
3
    @Override
4
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
5
        //设置脱敏上下文
6
        DesensitizeContext.setContext(true);
7
        return true;
8
    }
9
10
    @Override
11
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
12
        //清除脱敏上下文
13
        DesensitizeContext.removeContext();
14
    }
15
}
1
    @Configuration
2
    class MvcConfiguration extends WebMvcConfiguration {
3
        @Override
4
        protected void addInterceptors(InterceptorRegistry registry) {
5
            super.addInterceptors(registry);
6
registry.addInterceptor(desensitizeInterceptor()).addPathPatterns("/**");
7
        }
8
9
    }
1
//使用
2
 @Desensitized(type = SensitiveTypeEnum.EMAIL)
3
  private String email;

装饰器模式

装饰器模式

前言

我们之前谈到代理模式可以用来增强与业务无关的功能,装饰器呢则可以解决与业务相关的功能增强问题。其解决的思路就是:组合。当然也是可以通过继承来解决的,但是继承可能会导致因装饰的功能过多而造成继承链爆炸的问题,维护困难。绝大多数情况之下组合还是优于继承的(箴言)

正文

装饰器模式的另外一个特征:为了解决多重装饰的问题。装饰类和原始类都继承自同一抽象父类或者接口。

Java I/O中类纷繁复杂,大多都是使用装饰器模式进行设计的以达到复用和扩展。有兴趣的朋友可以看看源码,加深理解

1
2
// 代理模式的代码结构(下面的接口也可以替换成抽象类)
3
public interface IA {
4
  void f();
5
}
6
public class A impelements IA {
7
  public void f() { //... }
8
}
9
public class AProxy impements IA {
10
  private IA a;
11
  public AProxy(IA a) {
12
    this.a = a;
13
  }
14
  
15
  public void f() {
16
    // 新添加的代理逻辑
17
    a.f();
18
    // 新添加的代理逻辑
19
  }
20
}
21
22
// 装饰器模式的代码结构(下面的接口也可以替换成抽象类)
23
public interface IA {
24
  void f();
25
}
26
public class A impelements IA {
27
  public void f() { //... }
28
}
29
public class ADecorator impements IA {
30
  private IA a;
31
  public ADecorator(IA a) {
32
    this.a = a;
33
  }
34
  
35
  public void f() {
36
    // 功能增强代码
37
    a.f();
38
    // 功能增强代码
39
  }
40
}

小结

因为装饰器模式主要解决业务增强的问题,所以在日常业务代码的开发过程当中还是挺常用的,可以方便扩展和服用原有代码。主要还是要理解组合优于继承的思想。

桥接模式

桥接模式

前言

设计初衷:将抽象和实现解耦,让它们可以独立变化。
更加贴切的理解:一个类存在两个(或多个)独立变化的维度,我们通过组合的方式,让这两个(或多个)维度可以独立进行扩展。
这里所说的抽象和实现,我们不能简单的理解为是Java中的接口和实现类,而是一个更加高维度的一个描述,或者可以说是抽象和具体两者的关系。

正文

我们使用JDBC来操作数据库时会有很多模板式的代码。例如加载驱动,获取连接,执行sql等等。其实大部分的操作最终都是委托给具体的Driver类来执行的,因此我们完全可以直接替换驱动类来决定使用Mysql还是Oracle数据库,而不需要改动或者只需要改动很少的代码。

对于JDBC这个模块来说,它不是一个Java定义的interface,但是它从语义上来说是抽象的,它是Java规范定义的操作数据库的规范,但是它并不是一个简单的接口,但是可以理解为一个更加广义的“接口”。

从以上的角度来说,具体数据库厂商的驱动类就是这个更为广义的“接口”的实现类了,而这个实现类也更为复杂,它必须实现整个JDBC的规范,一个完整的操作数据库的规范。在JDBC中是通过组合DriverManager委派来实现的(与具体的驱动一样实现了Driver接口)。

小结

桥接模式,其实代码结构是比较简单的。通过组合的方式来引入实现模块,使得抽象和具体解耦。其难点还是在于理解抽象和具体的关系,区别于Java中的interface和实现类。

代理模式

前言

作为结构型设计模式的一种,代理模式解决的是关于扩展与原有业务不太相关的功能的问题。当我们需要对一些功能进行统一增强(一些公关的功能),但是又不侵入原有代码的时候,代理模式就能发挥大作用。

例如我们熟知的日志功能,关于Spring AOP 的前置,环绕,后置通知等等。大多是基于代理模式来实现的,所谓代理就是代替被代理人(对象)来执行一些事情。

最简单的实现,即在代理类中注入被代理的对象,实现与代理对象同样的接口,在实现方法当中增加目标逻辑,再调用被代理类的目标方法。使用时,我们直接使用代理对象代替目标对象来完成原有的逻辑处理。

动态代理

简单的代理模式虽然解决了代码侵入的问题,但是因为设计模式的引入使得复杂度也有所上升。因为在代理类中我们又把接口所有的方法又重新实现了一遍,基本上没有太大的变动,这造成了很多重复的工作。另外假如我们要增强的功能不止一个的话,我们就需要创建多个代理类来达到目的了。这样的话类结构就会膨胀很多。

为了解决以上的问题,我们引入动态代理的概念来解决这样的问题。即,我们不在事先不需要为每个目标类编写代理类。而是在程序运行时才动态的去组装代理类,然后替换原始类。整个过程基于Java的反射机制来完成。在Java中常使用原始的JDK动态代理和第三方的CGlib动态代理来实现上述过程。

1
2
public class MetricsCollectorProxy {
3
  private MetricsCollector metricsCollector;
4
5
  public MetricsCollectorProxy() {
6
    this.metricsCollector = new MetricsCollector();
7
  }
8
9
  public Object createProxy(Object proxiedObject) {
10
    Class<?>[] interfaces = proxiedObject.getClass().getInterfaces();
11
    DynamicProxyHandler handler = new DynamicProxyHandler(proxiedObject);
12
    return Proxy.newProxyInstance(proxiedObject.getClass().getClassLoader(), interfaces, handler);
13
  }
14
15
  private class DynamicProxyHandler implements InvocationHandler {
16
    private Object proxiedObject;
17
18
    public DynamicProxyHandler(Object proxiedObject) {
19
      this.proxiedObject = proxiedObject;
20
    }
21
22
    @Override
23
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
24
      long startTimestamp = System.currentTimeMillis();
25
      Object result = method.invoke(proxiedObject, args);
26
      long endTimeStamp = System.currentTimeMillis();
27
      long responseTime = endTimeStamp - startTimestamp;
28
      String apiName = proxiedObject.getClass().getName() + ":" + method.getName();
29
      RequestInfo requestInfo = new RequestInfo(apiName, responseTime, startTimestamp);
30
      metricsCollector.recordRequest(requestInfo);
31
      return result;
32
    }
33
  }
34
}
35
36
//MetricsCollectorProxy使用举例
37
MetricsCollectorProxy proxy = new MetricsCollectorProxy();
38
IUserController userController = (IUserController) proxy.createProxy(new UserController());

总结

代理模式最常用的应用场景就是在业务系统中开发一些非功能性需求,比如:监控、统计、鉴权、限流、事务、幂等、日志。我们将这些附加功能与业务功能解耦,放到代理类中统一处理,让目标类只需要关注业务方面的逻辑即可。

使用Jmeter进行压力测试

前言

作为一个服务端开发,个人认为掌握各种测试方法和软件是一项必备的技能。小到单元测试,mock测试,大到集成测试,性能测试,压力测试等等。当然这项能力也是不断在工作当中积累经验才能娴熟驾驭的。关于mock测试之前也写了一篇文章,最近因为工作项目上面刚好碰到了压力测试的相关场景,所以也稍微记录一下。

JMeter

(关于介绍还是可以看看,了解它是什么,解决什么问题)Apache JMeter是Apache组织开发的基于Java的压力测试工具。用于对软件做压力测试,它最初被设计用于Web应用测试,但后来扩展到其他测试领域。 它可以用于测试静态和动态资源,例如静态文件、Java 小服务程序、CGI 脚本、Java 对象、数据库、FTP 服务器, 等等。JMeter 可以用于对服务器、网络或对象模拟巨大的负载,来自不同压力类别下测试它们的强度和分析整体性能。另外,JMeter能够对应用程序做功能/回归测试,通过创建带有断言的脚本来验证你的程序返回了你期望的结果。为了最大限度的灵活性,JMeter允许使用正则表达式创建断言。

简单来说,JMeter就是用java写的一个客户端程序,用它可以在java环境中使用线程来模拟用户,构造各种协议请求,在较小的资源消耗的情况下,伪造大量请求从而对目标服务器发起攻击,形成压力测试的场景。常用测试指标包括qps(每秒钟处理完请求的次数)、tps(每秒钟处理完的事务次数)、rt(响应时间,处理一次请求所需要的平均处理时间)、并发数(系统同时处理的request/事务数)、吞吐量(每秒钟最大能接受的用户访问量,或者每秒钟最大能处理的请求数)等等。

实践

以压测web接口为例,我们使用Jmeter创建一个测试计划,这个类似于工程。JMeter也给我们提供了常用的测试模板,覆盖了大量的测试场景。我们选择Web Test Plan,JMeter会自动为我们创建好HTTP协议模板,包括基本设置(请求地址,端口等等),Cookie管理器,请求头管理器,请求消息体设置,还有返回结果过滤器等等,另外还包括统计数据管理器。

接下来,我们根据接口的定义,在对应的管理器中设置好值,针对具体的url可以自定义请求体,用于在同一基础路径下测试不同的api接口。

关于结果过滤,JMeter也提供了基于正则的匹配过滤机制,支持JSON断言,可以通过简单的配置过滤出想要的返回结果。

设置好这些后,我们可以在线程组中设置我们需要模拟的用户数量。以线程的形式来模拟用户,可以设置用户的数量(统一发起请求),间隔时间,以及循环执行次数等等。

为了调试方便,我们可以先使用一个用户来模拟请求,待所有的参数都设置好,请求可以走通的时候我们在放开用户限制,开启压测。在结果统计报表中,我们可以看到常用的统计数据,包括:并发数,平均响应时间,最大响应时间,最小响应时间,错误率,吞吐量,数据发送速率等等。

接下来,就是不断调整用户数,还有时间间隔等等参数来模拟真实的应用场景,找到系统的最大性能支撑数据。当然是一个参考数据,因为我们很少在真实生产环境来压测,但是以测试环境来估计也是一个较稳妥的办法。

后记

这里只是对JMeter的简单实用做一个总结,关于它的高阶使用还在进一步的探索当中,但是当前也是能解决大部分的应用场景了,以后如果有新的认识在做进一步的总结。

创建型设计模式学习总结

前言(照常废话)

最近学习争哥的《设计模式之美》正式进入到23种常用设计模式的讲解阶段,这两个星期因为工作上面的原因也一直没有时间好好去沉淀一下学习的内容。今天周六,终于处理完杂七杂八的事情,逮着机会把可以比较简单但是非常常用的创建型设计模式做一个学习总结。

问题

顾名思义,创建型设计模式是为了解决程序设计过程中所遇到的创建问题。在java环境当中自然指的就是对象的创建。关于创建对象,面向对象编程(呸,哪来的对象)的我们再熟悉不过了,没有new一个不就完了吗。但是追求代码极致的我们,总是要多想一点,不能放过任何可以优化的点让代码变得跟精辟。

对象的创建过程复杂吗?需要的资源多不多,会不会影响程序执行效率?对象能不能重复利用呢?有没有线程安全问题?构建逻辑对代码的复用性,耦合度,扩展性等等有没有影响?其实仔细一想问题还是蛮多的。
创建型设计模式常用的主要有:单例,工厂,建造者,原型这四种。

单例

单例模式,即只需要保证环境中有且只有一个实例来提供服务(这是全局唯一的)。这意味着,当有多个实例同时提供服务时可能会造成资源冲突,同时这一个实例就可以完成工作,不需要浪费资源去重复创建。

单例模式有两种形式,俗称“饿汉”和“懒汉”模式。其区别在于,创建的时间不同。“饿汉”因为饿,所以资源加载的时候就赶紧去创建好目标对象,后面就不在创建了,调用时直接返回此对象来提供服务。

而“懒汉模式”则不会预创建,而是在第一次调用的时候才会去创建(这种加载模式也叫做懒加载或者延迟加载)。

以上两种模式,“饿汉模式”不支持懒加载,而“懒汉模式”直接加锁的设计导致并发度低。
但是我们可以使用双重检测来解决这两个问题。

1
2
public class IdGenerator { 
3
  private AtomicLong id = new AtomicLong(0);
4
  private static IdGenerator instance;
5
  private IdGenerator() {}
6
  public static IdGenerator getInstance() {
7
    if (instance == null) {
8
      synchronized(IdGenerator.class) { // 此处为类级别的锁
9
        if (instance == null) {
10
          instance = new IdGenerator();
11
        }
12
      }
13
    }
14
    return instance;
15
  }
16
  public long getId() { 
17
    return id.incrementAndGet();
18
  }
19
}

其他实现单例模式的方法还有:静态内部类方式

1
	
2
public class IdGenerator { 
3
  private AtomicLong id = new AtomicLong(0);
4
  private IdGenerator() {}
5
6
  private static class SingletonHolder{
7
    private static final IdGenerator instance = new IdGenerator();
8
  }
9
  
10
  public static IdGenerator getInstance() {
11
    return SingletonHolder.instance;
12
  }
13
 
14
  public long getId() { 
15
    return id.incrementAndGet();
16
  }
17
}

SingletonHolder 是一个静态内部类,当外部类 IdGenerator 被加载的时候,并不会创建 SingletonHolder 实例对象。只有当调用 getInstance() 方法时,SingletonHolder 才会被加载,这个时候才会创建 instance。

instance 的唯一性、创建过程的线程安全性,都由 JVM 来保证。所以,这种实现方法既保证了线程安全,又能做到延迟加载。

另外我们也可以通过枚举来实现,就不具体说明了。

工厂

对于工厂我的理解是用来定制化生产的地方,工厂模式解决的问题就是对象的定制化创建。而具体的创建过程对使用者隐藏(隔离复杂性),对外暴露创建接口。使用者只需要告诉工厂,需要生成什么样的产品即可使得调用代码职责更加单一,代码更加简洁(控制复杂度)。

根据创建逻辑的复杂度,我们可以分别用简单工厂和工厂方法两种形式来应对。
对于少量对象类型,较为简单的创建逻辑,可以直接把if else创建逻辑直接剥离到工厂当中,即简单工厂。

而对于较多类型(短期内未考虑到的类型,后期会做扩展),以及单个对象创建具有较为复杂的创建逻辑,或者依赖其他比较多的资源。我们可以把工厂进行抽象。暴露创建接口,让具体的工厂类实现此接口来生成不同的复杂对象。

但此时,虽然解决了对象的扩展问题,却引入的新的复杂性,即多个工厂类。这些工厂类的创建又成了一个根据类型创建对象的大规模if else问题。对此我们可以用“超级工厂”来解决这样的问题–即生产工厂的工厂。

因为工厂只需要是单个对象即可提供服务,因此结合单例模式,我们可以把工厂单例对象缓存到超级工厂当中。当需要调用具体的工厂来提供生产服务的时候直接返回次对象提供服务即可。

1
2
public interface IConfigParserFactory {
3
  IRuleConfigParser createRuleParser();
4
  ISystemConfigParser createSystemParser();
5
  //此处可以扩展新的parser类型,比如IBizConfigParser
6
}
7
8
public class JsonConfigParserFactory implements IConfigParserFactory {
9
  @Override
10
  public IRuleConfigParser createRuleParser() {
11
    return new JsonRuleConfigParser();
12
  }
13
14
  @Override
15
  public ISystemConfigParser createSystemParser() {
16
    return new JsonSystemConfigParser();
17
  }
18
}
19
20
public class XmlConfigParserFactory implements IConfigParserFactory {
21
  @Override
22
  public IRuleConfigParser createRuleParser() {
23
    return new XmlRuleConfigParser();
24
  }
25
26
  @Override
27
  public ISystemConfigParser createSystemParser() {
28
    return new XmlSystemConfigParser();
29
  }
30
}
31
32
// 省略YamlConfigParserFactory和PropertiesConfigParserFactory代码
33
34
35
public class RuleConfigSource {
36
  public RuleConfig load(String ruleConfigFilePath) {
37
    String ruleConfigFileExtension = getFileExtension(ruleConfigFilePath);
38
39
    IRuleConfigParserFactory parserFactory = RuleConfigParserFactoryMap.getParserFactory(ruleConfigFileExtension);
40
    if (parserFactory == null) {
41
      throw new InvalidRuleConfigException("Rule config file format is not supported: " + ruleConfigFilePath);
42
    }
43
    IRuleConfigParser parser = parserFactory.createParser();
44
45
    String configText = "";
46
    //从ruleConfigFilePath文件中读取配置文本到configText中
47
    RuleConfig ruleConfig = parser.parse(configText);
48
    return ruleConfig;
49
  }
50
51
  private String getFileExtension(String filePath) {
52
    //...解析文件名获取扩展名,比如rule.json,返回json
53
    return "json";
54
  }
55
}
56
57
//因为工厂类只包含方法,不包含成员变量,完全可以复用,
58
//不需要每次都创建新的工厂类对象,所以,简单工厂模式的第二种实现思路更加合适。
59
public class RuleConfigParserFactoryMap { //工厂的工厂
60
  private static final Map<String, IRuleConfigParserFactory> cachedFactories = new HashMap<>();
61
62
  static {
63
    cachedFactories.put("json", new JsonRuleConfigParserFactory());
64
    cachedFactories.put("xml", new XmlRuleConfigParserFactory());
65
    cachedFactories.put("yaml", new YamlRuleConfigParserFactory());
66
    cachedFactories.put("properties", new PropertiesRuleConfigParserFactory());
67
  }
68
69
  public static IRuleConfigParserFactory getParserFactory(String type) {
70
    if (type == null || type.isEmpty()) {
71
      return null;
72
    }
73
    IRuleConfigParserFactory parserFactory = cachedFactories.get(type.toLowerCase());
74
    return parserFactory;
75
  }
76
}

建造者

如果说工厂模式是注重创建不同类型但是又有关联的对象,那么建造者模式就是专注于建造对象本身的设计模式了。解决的是对象本身建造过程的复杂度隐藏,而对调用者透明。将建造的前期准备工作都委托给“建造者”去做,万事俱备了才交给目标对象构造器去执行。

关于使用的场景,我总结了几个:

  • 构造的对象属性较多,我们手动构造时需要大量的set操作(代码冗余,而且排版不好看)
  • 存在某些属性,生命周期内有且仅需赋值一次,为了控制权限和安全,不能暴露set方法(例如某些资源类对象的构造,线程池等等)
  • 对于对象本身的属性,赋值需要校验,属性间存在关联的情况,直接在set方法中校验会违反单一职责,而且校验规则耦合,做不到统一校验。利用建造模式,可以进行统一校验,校验完毕再调用属性的set私有方法来构建最终对象。
    1
    2
    public class ResourcePoolConfig {
    3
      private String name;
    4
      private int maxTotal;
    5
      private int maxIdle;
    6
      private int minIdle;
    7
    8
      private ResourcePoolConfig(Builder builder) {
    9
        this.name = builder.name;
    10
        this.maxTotal = builder.maxTotal;
    11
        this.maxIdle = builder.maxIdle;
    12
        this.minIdle = builder.minIdle;
    13
      }
    14
      //...省略getter方法...
    15
    16
      //我们将Builder类设计成了ResourcePoolConfig的内部类。
    17
      //我们也可以将Builder类设计成独立的非内部类ResourcePoolConfigBuilder。
    18
      public static class Builder {
    19
        private static final int DEFAULT_MAX_TOTAL = 8;
    20
        private static final int DEFAULT_MAX_IDLE = 8;
    21
        private static final int DEFAULT_MIN_IDLE = 0;
    22
    23
        private String name;
    24
        private int maxTotal = DEFAULT_MAX_TOTAL;
    25
        private int maxIdle = DEFAULT_MAX_IDLE;
    26
        private int minIdle = DEFAULT_MIN_IDLE;
    27
    28
        public ResourcePoolConfig build() {
    29
          // 校验逻辑放到这里来做,包括必填项校验、依赖关系校验、约束条件校验等
    30
          if (StringUtils.isBlank(name)) {
    31
            throw new IllegalArgumentException("...");
    32
          }
    33
          if (maxIdle > maxTotal) {
    34
            throw new IllegalArgumentException("...");
    35
          }
    36
          if (minIdle > maxTotal || minIdle > maxIdle) {
    37
            throw new IllegalArgumentException("...");
    38
          }
    39
    40
          return new ResourcePoolConfig(this);
    41
        }
    42
    43
        public Builder setName(String name) {
    44
          if (StringUtils.isBlank(name)) {
    45
            throw new IllegalArgumentException("...");
    46
          }
    47
          this.name = name;
    48
          return this;
    49
        }
    50
    51
        public Builder setMaxTotal(int maxTotal) {
    52
          if (maxTotal <= 0) {
    53
            throw new IllegalArgumentException("...");
    54
          }
    55
          this.maxTotal = maxTotal;
    56
          return this;
    57
        }
    58
    59
        public Builder setMaxIdle(int maxIdle) {
    60
          if (maxIdle < 0) {
    61
            throw new IllegalArgumentException("...");
    62
          }
    63
          this.maxIdle = maxIdle;
    64
          return this;
    65
        }
    66
    67
        public Builder setMinIdle(int minIdle) {
    68
          if (minIdle < 0) {
    69
            throw new IllegalArgumentException("...");
    70
          }
    71
          this.minIdle = minIdle;
    72
          return this;
    73
        }
    74
      }
    75
    }
    76
    77
    // 这段代码会抛出IllegalArgumentException,因为minIdle>maxIdle
    78
    ResourcePoolConfig config = new ResourcePoolConfig.Builder()
    79
            .setName("dbconnectionpool")
    80
            .setMaxTotal(16)
    81
            .setMaxIdle(10)
    82
            .setMinIdle(12)
    83
            .build();

    原型

    关于原型模式,使用比较少,大致的思想就是利用已经存在的对象,使用拷贝的方式来创建新的对象,达到快速创建的目的。(java语言环境中分为深拷贝和浅拷贝的模式,有兴趣的小伙伴可以自行了解,这里就不展开讨论啦)

后记

第一次尝试以这种方式来总结设计模式,参考了争哥的课程。对于理解帮助很大,因为设计模式的应用,有时候你看原理觉得很简单,但是实际开发过程当中能熟练运用还是需要能深入理解它的思想,知道它解决什么问题,才能做到恰到好处,而不是过度设计,或者胡乱设计。

关于CAS思想的一些思考

前言

最近在复习java的一些基础知识,重新对于一些比较模糊的概念进行了一番梳理。用一些实际的案例来加深自己的理解证实是一个行之有效的好办法。
关于CAS其实早有耳闻,在刚刚开始接触java的时候遍已经听闻过它的大名,然而所学尚欠火候,始终无法得其真谛,现今虽不能自信地说完全懂得,但好歹有一些自己总结的东西可以聊聊。

乐观锁

首先在谈及CAS之前,还是得先聊聊乐观锁这个概念。顾名思义,乐观乐观即总是倾向于把事情往好的方面去想。 锁呢,大家应该都不陌生了。
在并发环境中,为了保证数据的安全性而设立的一种保障机制。字大家都认识,但有时候组合起来吧就懵圈了。咳咳,回到正题上来。
乐观锁呢,实际属于一种概念,我们可以理解为接口定义,定义它是干什么用的,而CAS呢其实是它的一种实现。可以试着这么去理解,
即在对目标数据进行修改前,我总是倾向于相信这个数据当前没有人修改,只有我自己。那我就直接读取数据,然后修改成我想要的值,但是为了确保万无一失(想着没人改不代表就没人改)
在准备提交的时候呢再校验一下是不是有人修改了。因为我只是倾向于相信没有人修改,但是实际有没有呢这个是只有验证才知道的。
(我们暂时先不去管怎么个验证法)因为它还不是具体实现机制。如果没人修改就直接提交,
如果有人修改就说明自己的倾向判断错了,需要重新读取最新数据,回到这个验证的过程,一直重复这个过程直到确定没人修改再提交。

CAS

CAS(Compare And Set)是乐观锁的一种具体实现机制,从名字上就能看出来是基于比较的一种机制。上文的介绍中我没有详细进行说明的验证方法,在CAS中其实是这么执行的。读取目标数据后,缓存起来。
然后带着想要设置的值进入准备提交的阶段,这时再读取一次目标数据,比较缓存值和当前目标值是否一致(目标值必须是内存可见的,在java中使用volatile修饰)。如果数据一致则代表当前没有其他人(线程)操作目标。
这时就可以放心提交啦,但是如果不相等就需要再次回到读取验证和预提交的环节。这个环节可以称之为“自旋”。(因为如果线程竞争比较激烈的时候,它就一直在循环读取比较啦,宏观表象就是转圈)。

ABA问题

关于上述基本的CAS原理,大家应该都看明白了,因为还是比较简单的。但是这个机制真的没问题吗,其实暴露了一个称之为“ABA”的问题。细心的小伙伴想必应该已经发现了,
就是我们验证缓存值和当前目标值相不相等,以此来作为是否有线程修改的依据其实是有问题的。如果我们缓存值为0,我们的线程想把它更改为1,这个时候我们还没有读取值,但是有另一个线程把它改为了1随后马上又改成了0,
这时我们的线程读取目标值也为0。虽然结果一样,但是中间过程却隐藏了,程序无法感知,这就是“ABA”问题。

解决ABA

那如何解决ABA问题呢,我们引入一个版本号的概念,目标值初始化时给定一个版本,例如1.0,在修改目标值的时候我们就加一个版本。以后每次判断目标值与缓存值相不相等的时候必须校验值和版本是否一致,
其中有一个不一致的时候必须重新从内存读写最新的值。这样就不存在“ABA”问题了。

悲观锁

以上关于乐观锁和CAS的介绍就差不多了,相对乐观锁其实就有悲观锁的说法。概念上也是很好理解的,在操作目标数据时,总是倾向于认为有人也在修改数据,为了保证数据安全,我必须先锁住当前目标才敢操作(排他性)。
在java当中synchronized就是悲观锁的一种实现,是不是很好理解呢,被它修饰的方法都是同步执行的。

效率比较

从实现上来讲,其实很直观的能看出乐观锁比悲观锁的效率要更高。因为悲观锁是宁可错杀一千,不可放过一个,一律同步执行。就synchronized这种JVM层级的锁来说早期实现是比较重的,执行效率很慢,
但是经过这么多年的版本迭代更新,它的效率已经不可同日而语了。但是具体的效率还是得看线程并发环境,如果竞争非常激烈,乐观锁因为长时间的自旋其实会拖慢效率甚至比使用悲观锁还要慢。

应用

  • jdk1.8当中ConcurrentHashMap是直接使用的 CAS + Synchronized来保证线程安全的。(1.7当中使用的是分段可重入锁ReentrantLock)相反的HashTable和Collections.synchronizedMap()则直接使用的是synchronized这种悲观锁来保证线程安全,
    在一般情况下前者的效率还是大大比后者好的。
  • CAS在自旋锁当中也有运用,只是把资源换成了锁,有兴趣的小伙伴可以自行查阅,是很好理解的
  • java轻量级锁属于乐观锁,重量级锁的实现属于悲观锁
  • 基于自旋锁可以动态根据竞争压力调整锁的策略,从偏向锁转化为轻量级或者重量级锁(锁状态:无锁状态、偏向锁、轻量级锁和重量级锁,单向升级从低到高)JDK 1.6中默认是开启偏向锁和轻量级锁的。

后记

掌握CAS的思想,对于创建复杂多变的多线程应用是很有帮助的,借此可以实现轻量级的线程安全机制,对于理解很多复杂的程序设计思想(流行的redis事务等等)也有很好的果效。这期的分享完啦,前路慢慢,以此为记。

关于单元测试的一点思考

前言

Hello,我是Storm。今天呢很想就单元测试这个点整理一下。在平时的coding当中,其实单元测试应该是用的比较多的,但是不知道大家有没有跟我有一样的感受。在测试自己的工具类或者比较简单的模块时,用junit等这样的测试框架就很方便的解决了问题。但是一旦测试的代码依赖于太多第三方的模块,或者自己写的其他模块代码,测试就变得非常麻烦,因为你需要启动这些三方服务或者模块。特别是分布式环境下,自己开发机器又不是性能优越的情况下那简直就是噩梦。

思考

那有没有方便的测试思想或者方法甚至工具可以帮我们摆脱这种困扰呢,答案是肯定的。但是还是想自己来想一想,这个背后到底是怎么个原理。针对我们遇到的问题,无非就是“解耦”,如何在不影响现有测试流程的情况下,把测试需要的环境给准备好。

例如需要某个接口服务,这个接口服务我们不关心它是分布式的还是单例的,网络状况如何,我只关注它给我返回的数据。在java中我们就可以借助依赖注入的方式来解决,自己实现一个接口,或者直接new一个服务,覆盖其方法,返回值直接设置成我们需要的数据,以此来模拟实例化一个服务。甚至我们可以自定义一个流程,在调用某些方法的前后来定制这个入参和返回值,以此达到测试的目的。

同时我也发现一个问题,对于服务、接口我们可以依赖注入的方式来规避,但是静态方法呢就不太好处理了。简单的方法还好,对于复杂的静态方法引入了过多外部资源其实很影响代码的可测试性,因此提醒自己在写码的时候要注意规避这种问题,也算是测试驱动开发(TDD)的一种应用吧。

mock

mock即模拟,在测试领域指的是模拟与测试相关的数据和环境,达到解耦第三方依赖的目的而只关注测试目标本体的思想方法。在java生态中比较常用的mock工具有EasyMock(早起流行)、Mockito(改进了EasyMock,主流)、PowerMock(主流)、Jmockit(轻量)等等。这里我简单介绍一下Mockito的用法。

Mockito可以让你轻松模拟任何Java类行为和数据,并且可以跟踪执行流程,自定义测试节点的参数和方法返回值,从而解耦第三方依赖,简化单元测试。
PUZZLE1min 1.png

Mockito基本使用

1
   @Test
2
   public void createMockObject() {
3
       // 使用 mock 静态方法创建 Mock 对象.
4
       List mockedList = mock(List.class);
5
       Assert.assertTrue(mockedList instanceof List);
6
7
       // mock 方法不仅可以 Mock 接口类, 还可以 Mock 具体的类型.
8
       ArrayList mockedArrayList = mock(ArrayList.class);
9
       Assert.assertTrue(mockedArrayList instanceof List);
10
       Assert.assertTrue(mockedArrayList instanceof ArrayList);
11
   }
12
13
   @Test
14
   public void configMockObject() {
15
//定制类行为
16
       List mockedList = mock(List.class);
17
18
       // 我们定制了当调用 mockedList.add("one") 时, 返回 true
19
       when(mockedList.add("one")).thenReturn(true);
20
       // 当调用 mockedList.size() 时, 返回 1
21
       when(mockedList.size()).thenReturn(1);
22
23
       Assert.assertTrue(mockedList.add("one"));
24
       // 因为我们没有定制 add("two"), 因此返回默认值, 即 false.
25
       Assert.assertFalse(mockedList.add("two"));
26
       Assert.assertEquals(mockedList.size(), 1);
27
28
       Iterator i = mock(Iterator.class);
29
       when(i.next()).thenReturn("Hello,").thenReturn("Mockito!");
30
       String result = i.next() + " " + i.next();
31
       //assert
32
       Assert.assertEquals("Hello, Mockito!", result);
33
   }
34
35
   @Test(expected = NoSuchElementException.class)
36
   public void testForIOException() throws Exception {
37
//模拟异常
38
       Iterator i = mock(Iterator.class);
39
       when(i.next()).thenReturn("Hello,").thenReturn("Mockito!"); // 1
40
       String result = i.next() + " " + i.next(); // 2
41
       Assert.assertEquals("Hello, Mockito!", result);
42
43
       doThrow(new NoSuchElementException()).when(i).next(); // 3
44
       i.next(); // 4
45
   }
46
47
   @Test
48
   public void testVerify() {
49
//验证方法调用情况
50
       List mockedList = mock(List.class);
51
       mockedList.add("one");
52
       mockedList.add("two");
53
       mockedList.add("three times");
54
       mockedList.add("three times");
55
       mockedList.add("three times");
56
       when(mockedList.size()).thenReturn(5);
57
       Assert.assertEquals(mockedList.size(), 5);
58
59
       verify(mockedList, atLeastOnce()).add("one");
60
       verify(mockedList, times(1)).add("two");
61
       verify(mockedList, times(3)).add("three times");
62
       verify(mockedList, never()).isEmpty();
63
   }
64
65
   @Test
66
   public void testSpy() {
67
       //Mockito 提供的 spy 方法可以包装一个真实的 Java 对象, 并返回一个包装后的新对象. 
68
//若没有特别配置的话, 对这个新对象的所有方法调用, 都会委派给实际的 Java 对象.
69
       List list = new LinkedList();
70
       List spy = spy(list);
71
72
       // 对 spy.size() 进行定制.
73
       when(spy.size()).thenReturn(100);
74
75
       spy.add("one");
76
       spy.add("two");
77
78
       // 因为我们没有对 get(0), get(1) 方法进行定制,
79
       // 因此这些调用其实是调用的真实对象的方法.
80
       Assert.assertEquals(spy.get(0), "one");
81
       Assert.assertEquals(spy.get(1), "two");
82
83
       Assert.assertEquals(spy.size(), 100);
84
   }
85
86
   @Test
87
   public void testCaptureArgument() {
88
//参数模拟
89
       List<String> list = Arrays.asList("1", "2");
90
       List mockedList = mock(List.class);
91
       ArgumentCaptor<List> argument = ArgumentCaptor.forClass(List.class);
92
       mockedList.addAll(list);
93
       verify(mockedList).addAll(argument.capture());
94
95
       Assert.assertEquals(2, argument.getValue().size());
96
       Assert.assertEquals(list, argument.getValue());
97
   }

总结

利用现代化的测试工具和框架能够大大简化日常开发当中涉及到的一些测试工作量,但是也要保持一颗求真的心,知道其背后的思想,才能沉淀成自己的东西。也会加深自己对于框架和工具的理解,更好的使用它。

mysql慢查询分析

前言

最近线上一个报表的查询接口出现慢查询的情况,是比较重要的一个报表。虽然查询sql有些复杂但是针对现有的查询条件和数据库结构做了相关优化,之前查询一年数据在分钟内,现在查一个月数据就需要3分钟,按道理是不应该出现这个问题的。
但是本着透过现象看本质的心,还是放下手中刚摸到的鱼开始bug时间。

基础分析

首先还是本能的定位到目标sql位置,show history了解到这个sql已经半年没有更新了。接着就explain分析,sql该走索引的地方都走了索引。
运维那边反馈最近除了把一个分区的数据从固态迁移到机械盘之外,其他的数据并没有动,mysql内存也是足够的。但是为了排除这个因素的影响,还是测试了一下目标分区和其他分区数据的查询速度差别,结果并没有差很多。
没办法,只能去追踪详细的sql执行计划了。

正经分析

首先在从库上执行目标sql,免得加重主库的负担。(这个查询本就是做了读写分离的,查询走的是从库,手动滑稽)
但是结果诡异了,竟然只花了10s左右。说好的3分钟呢,感紧催运维查了一下,果然结果是出乎意料的。之前调整,竟然把从库域名配置干掉了,所有查询都打到主库上。赶紧让运维切换到从库去,虽然大石头放下了,但是查询还是没有之前的效果,于是继续跟踪。

  • 查看正在执行的 SQL 语句

    1
    -- 查询当前执行sql的id 执行用户 数据库 操作类型 耗时 状态 等等
    2
    select * from information_schema.`PROCESSLIST` where info is not null
  • 查看 SQL 查询耗时

    1
    -- 查看 profiling 功能是否已打开
    2
    select @@profiling
    3
    -- 打开 profiling
    4
    set global profiling=1
    5
    -- 查看 profiling
    6
    show profiles
    7
    -- 查看某个 query 的耗时情况
    8
    show profile for query query_id
    9
    -- 查看锁
    10
    show global status like '%lock%';

markdown放图片还是不方便呀。从结果上显示,耗时主要发生在 Creating sort index 严重的时候耗费将近90%的时间,mysql的耗时情况分析涉及到很多的子项(mysql状态)具体可以看文末详情。
借助万能的Google了解到,这主要是order by 和limit引发的。当涉及到排序和分页的数据过多时性能会急剧下降。因为limit10000,20的意思扫描满足条件的10020行,扔掉前面的10000行,返回最后的20行,问题就在这里。

解决

对于默认查询,不再使用order by。对于一定要使用的排序字段 建立复合索引 排序字段,主键
对于分页解决方案是获取到一批数据之后拿到最大的ID,加入到where条件中加入>该ID条件过滤掉前面的数据,在使用子查询获取数据即可。

附录 sql执行状态

Checking table 正在检查数据表(这是自动的)。
Closing tables 正在将表中修改的数据刷新到磁盘中,同时正在关闭已经用完的表。这是一个很快的操作,如果不是这样的话,就应该确认磁盘空间是否已经满了或者磁盘是否正处于重负中。
Connect Out 复制从服务器正在连接主服务器。
Copying to tmp table on disk 由于临时结果集大于tmp_table_size,正在将临时表从内存存储转为磁盘存储以此节省内存。
Creating tmp table 正在创建临时表以存放部分查询结果。
deleting from main table 服务器正在执行多表删除中的第一部分,刚删除第一个表。
deleting from reference tables 服务器正在执行多表删除中的第二部分,正在删除其他表的记录。
Flushing tables 正在执行FLUSH TABLES,等待其他线程关闭数据表。
Killed 发送了一个kill请求给某线程,那么这个线程将会检查kill标志位,同时会放弃下一个kill请求。MySQL会在每次的主循环中检查kill标志位,不过有些情况下该线程可能会过一小段才能死掉。如果该线程程被其他线程锁住了,那么kill请求会在锁释放时马上生效。
Locked 被其他查询锁住了。
Sending data 正在处理SELECT查询的记录,同时正在把结果发送给客户端。
Sorting for group 正在为GROUP BY做排序。
Sorting for order 正在为ORDER BY做排序。
Opening tables 这个过程应该会很快,除非受到其他因素的干扰。例如,在执ALTER TABLE或LOCK TABLE语句行完以前,数据表无法被其他线程打开。正尝试打开一个表。
Removing duplicates 正在执行一个SELECT DISTINCT方式的查询,但是MySQL无法在前一个阶段优化掉那些重复的记录。因此,MySQL需要再次去掉重复的记录,然后再把结果发送给客户端。
Reopen table 获得了对一个表的锁,但是必须在表结构修改之后才能获得这个锁。已经释放锁,关闭数据表,正尝试重新打开数据表。
Repair by sorting 修复指令正在排序以创建索引。
Repair with keycache 修复指令正在利用索引缓存一个一个地创建新索引。它会比Repair by sorting慢些。
Searching rows for update 正在讲符合条件的记录找出来以备更新。它必须在UPDATE要修改相关的记录之前就完成了。
Sleeping 正在等待客户端发送新请求.
System lock 正在等待取得一个外部的系统锁。如果当前没有运行多个mysqld服务器同时请求同一个表,那么可以通过增加–skip-external-locking参数来禁止外部系统锁。
Upgrading lock INSERT DELAYED正在尝试取得一个锁表以插入新记录。
Updating 正在搜索匹配的记录,并且修改它们。
User Lock 正在等待GET_LOCK()。
Waiting for tables 该线程得到通知,数据表结构已经被修改了,需要重新打开数据表以取得新的结构。然后,为了能的重新打开数据表,必须等到所有其他线程关闭这个表。以下几种情况下会产生这个通知:FLUSH TABLES tbl_name, ALTER TABLE, RENAME TABLE, REPAIR TABLE, ANALYZE TABLE 或 OPTIMIZE TABLE
waiting for handler insert INSERT DELAYED已经处理完了所有待处理的插入操作,正在等待新的请求。
after create 线程创建一个表或临时表的最后会进入该状态
Analyzing 线程正在分析一个 MyISAM 表或索引描述(例如 ANALYZE TABLE)
checking permissions 线程在查看是否具有权限
Checking table 表检查操作
cleaning up 线程已处理了一个命令,正在准备释放内存和资源
closing tables 线程将更改的表数据刷新到磁盘并关闭使用的表
converting HEAP to MyISAM 线程正在将内存表中的内部临时表转换为磁盘上的 MyISAM 表
copy to tmp table 线程正在执行一条 alter table 语句,已创建新结构的表,正在将数据复制到新结构的表中
Copying to group table 一条语句的ORDER BY和GROUP BY条件不同时,将数据行按组排序并复制到临时表中
Copying to tmp table 复制数据到内存中的一张临时表中
Copying to tmp table on disk 由于临时结果集大于 tmp_table_size,所以线程正在将临时表从内存中更改为基于磁盘的格式保存
Creating index 线程正在对一个 MyISAM 表执行 ALTER TABLE … ENABLE KEYS
Creating sort index 正在执行一个使用内部临时表的查询
creating table 正在创建一个表(包括临时表)
Creating tmp table 线程正在内存或磁盘上创建一个临时表。如果表是在内存中创建的,但稍后被转换为磁盘上的表,则该操作期间的状态将复制到磁盘上的tmp表