2018 年终总结

一、生活

最大的变化应该是变身房奴了,租房时房租每年涨10%,租金加上公积金再加小几千的现金支出够供房,所以也还好吧。

选择在南山买个老破小、而不去关外买个大点的,主要是考虑上下班方便,一般情况下坐公交半个钟能到公司,走路回来也只要50分钟。

国庆后老妈也出来深圳,回家吃晚饭的次数就多了,吃过晚饭去中山公园溜溜老婆也挺好。

二、工作

1. 拆服务

年初开始,大部门划分为多个小组,每个小组负责的模块都从原来的系统里拆分出来做服务,所有新项目基于 Spring Cloud 1.X 技术栈搭建。

我小组拆服务比较晚,正好可以看看其他组踩的坑,观察到的问题主要有:

  • Ribbon、Feign 的自动重试问题;对于没有做幂等处理的逻辑,就可能出现重复执行、数据重复等问题。

  • SpringBoot 默认的数据源属性前缀是 spring.datasource,Druid 连接池的 SpringBoot 的组件的数据源属性前缀默认是 spring.datasource.druid,这样可能导致配置不生效。还必须指定 Druid 获取连接的超时时间,否则可能都进入一直等待状态,应该是有活锁的bug。

"http-nio-8080-exec-100" #7057 daemon prio=5 os_prio=0 tid=0x00007fc83c0a3800 nid=0x1b99 waiting on condition [0x00007fd7c9a57001]
java.lang.Thread.State: WAITING (parking)
at sun.misc.Unsafe.park(Native Method)
- parking to wait for  <0x00000000c2923bd8> (a java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject)
at java.util.concurrent.locks.LockSupport.park(LockSupport.java:175)
at java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.await(AbstractQueuedSynchronizer.java:2039)
at com.alibaba.druid.pool.DruidDataSource.takeLast(DruidDataSource.java:1444)
at com.alibaba.druid.pool.DruidDataSource.getConnectionInternal(DruidDataSource.java:1088)
at com.alibaba.druid.pool.DruidDataSource.getConnectionDirect(DruidDataSource.java:953)
  • 用 Apollo 做配置管理,要考虑 线程池、连接池的实时刷新问题。Apollo 一般只支持刷新 Spring 容器管理的 @Value 注解的属性,这些池的属性一般都是没有这样的注解的。

  • Hystrix 用线程异步执行调用时,导致调用链跟踪中断。这个是小问题,不影响业务。

别人踩的坑也是可以学习的;技术还是要自己去验证、不能网上直接抄段代码就用。

2. 项目质量

小组负责的模块是纯后台服务,业务逻辑比较复杂,偏偏又是跟钱相关,不能出错。代码质量、逻辑的正确就非常重要。

趁着这次拆服务,把单元测试做起来,觉得效果还是不错的。让一个实习生给一个逻辑写单元测试时,发现了一个之前没注意到的数据问题;自己给一个模块补充测试用例时,也发现在某个场景下的逻辑问题。

19 年准备采取如下的步骤来做单元测试:

  1. 与具体的开发同事一起做需求分析;
  2. 让开发同事根据需求做单元测试用例设计:把场景、边界值等列出来;
  3. 复核测试用例及实现,所有对结果的校验必须用断言来实现。

欢迎关注我的微信公众号: coderbee笔记,可以更及时回复你的讨论。

《从0开始学架构》–笔记

第2篇 架构是什么

系统 泛指由一群有关联的个体组成,根据某种规则运作,能完成个别元件不能单独完成的工作的群体(能力)。

从逻辑的角度拆分系统后,得到的单元就是“模块”,从物理的角度拆分系统后,得到的单元就是“组件”。划分模块的主要目的是职责分离,划分组件的主要目的是单元复用。

框架是组件规范,提供基础功能的产品。软件架构指软件系统的“基础结构”,创造这些基础结构的准则,以及这些结构的描述。框架关注的是“规范”,架构关注的是“结构”。

软件架构指软件系统的顶层结构:
* 架构需要明确系统包含哪些“个体”,个体可以是 子系统、模块、组件等。
* 架构需要明确个体的运作和协作的规则。
* 顶层结构可以更好地区分系统和子系统。

自话:软件架构确定了系统中应该包含哪些个体,以及个体之间应该如何协作,以提供某种能力,从而实现系统的价值。

第2篇 架构设计的历史背景

“模块”“对象”“组件”本质上都是对达到一定规模的软件进行拆分,区别只是在于随着软件的复杂度不断增加,拆分的粒度越来越粗,拆分的层次越来越高。

第3篇 架构设计的目的

架构设计的主要目的是为了解决软件系统复杂度带来的问题。

架构设计首先要分析系统的复杂度所在,然后针对这些复杂度进行设计、制定方案。

第4篇 复杂度来源:高性能

软件系统中高性能带来的复杂度主要体现在两方面,一方面是单台计算机内部为了提高性能带来的复杂度;另一方面是多态计算机集群为了高性能带来的复杂度。

第5篇 复杂度来源:高可用

系统的高可用本质都是通过“冗余”来实现的,方法是增加机器。

高性能增加机器的目的在于“提升”处理性能,高可用增加机器的目的在于“冗余”处理单元。

存储高可用的难点不在于如何备份数据,而在于如何减少或者规避数据不一致对业务造成的影响。

高可用状态决策:无论是计算高可用还是存储高可用,其基础都是“状态决策”,即系统能够判断当前状态是正常还是异常,如果出现异常就要采取行动来保证高可用。

决策方式:
* 独裁式:所有冗余的个体向一个独立的决策主体上报状态信息,决策者进行决策。问题在于决策者本身是个单点。
* 协商式:两个独立的个体通过交流信息,然后根据规则进行决策,最常用的协商式决策就是主备决策。难点在于两者的信息交换出现问题时如何决策。
* 民主式:指多个独立的个体通过投票的方式来进行决策。缺陷是脑裂:原来统一的集群因为连接中断,造成两个独立分隔的子集群,每个子集群单独进行选举,选出两个决策者。解决方法是要求“投票节点数必须超过系统总结点数一半”。

第6篇 复杂度来源:可扩展性

可扩展性是指为了应对将来需求变化而提供的一种扩展能力。

设计具备良好可扩展性的系统,有两个基本条件:正确预测变化、完美封装变化。

预测变化的复杂性在于:不能每个设计点都考虑可扩展性、不能完全不考虑可扩展性、所有的预测都存在出错的可能性。

设计具备良好可扩展性的系统,有两个思考角度:从业务维度,对业务深入理解,对可预计的业务变化进行预测;从技术维度,利用扩展性好的技术,实现对变化的封装。

应对变化的常见方案一是将“变化”封装在一个“变化层”,将不变的部分封装在一个独立的“稳定层”。
方案二是提炼出一个“抽象层”和一个“实现层”,抽象是文档的,实现可根据具体业务需要定制开发,加入新的功能时,只需要增加新的实现,无需修改抽象层。

举例:设计一个支付网关对接不同的支付机构,为业务系统提供支付能力。每家支付机构的通信方式、请求/响应格式都是不一样的,但基本参数都是四要素信息(银行卡号、预留手机号、姓名、身份证号)、扣款金额等,扣款结果一般就是成功/失败/等通知,因此可以抽象出统一的接口,对接不同支付机构的具体实现类实现这个接口、完成具体的调用逻辑,当要对接新的支付机构时只需要添加一个实现类;业务系统只需访问这个统一的接口。

继续阅读

设计模式之工厂家族

《冒号课堂编程范式与OOP思想》 13.1 创建模式笔记

工厂家族

构造器的弊端:名字必须与类名一致,缺乏表现力;每次调用都会创建新对象;无法多态,new 必须使用具体类型,没法使用抽象的超类型。

抽象工厂模式:
1. 把静态工厂拆分成了一个接口和若干个实现类;
2. 把工厂方法模式中的主题类中的抽象工厂方法提炼为一个接口,用对象合成取代了类继承。

代码说明示例

1. 静态工厂模式:

public class StaticFactory {
    public enum Type {
        AWT, SWING
    };

    public static Container createFrame(Type type, String title) {
        switch (type) {
            case AWT:
                return new Frame(title);
            case SWING:
                return new JFrame(title);
            default:
                return null;
        }
    }

    public static Component createLabel(Type type, String text) {
        switch (type) {
            case AWT:
                return new Label(text);
            case SWING:
                return new JLabel(text);
            default:
                return null;
        }
    }
}

// 使用静态公共方法的类
class LoginForm {
    public Container createLoginWindow() {
        // 使用前要指定具体的类型
        StaticFactory.Type type = StaticFactory.Type.AWT;
        Container frame = StaticFactory.createFrame(type, "标题");
        Component label = StaticFactory.createLabel(type, "文本");
        // 组装组件
        return null;
    }
}

每个创建对象的方法都通过参数来指定要创建的具体对象类型,调用方必须指定具体的类型。

继续阅读

RxJava 线程模型

本文基于 RxJava 2.1.2 。根据代码和输出日志会更容易理解。

RxJava 的线程模型如下:

1. 不指定线程的情况

  • 不指定线程也就是不使用 observeOnsubscribeOn,所有操作在调用 subscribe 的线程执行。
@Test
public void noThread() {
    buildObservable().subscribe();
}

上面代码的输出为:

Thread[main]   execute   Action start emmit
Thread[main]   execute   Operation-1, event: 1
Thread[main]   execute   Operation-2, event: 1

2. subscribeOn

  • subscribeOn 不管调用多少次,只以第一次为准。如果只使用了 subscribeOn、没有使用 observeOn,则所有操作在第一次调用生成的线程里执行。
@Test
public void subscribeOn() throws InterruptedException {
    CountDownLatch latch = new CountDownLatch(1);

    Observable<Integer> observable = buildObservable();
    observable
        .subscribeOn(scheduler("subscribeOn-1"))
        .subscribeOn(scheduler("subscribeOn-2"))
        .subscribe(i -> {
            showMessageWithThreadName("Action subscribe");
            latch.countDown();
        });

    latch.await();
}

上面代码的输出为:

create scheduler subscribeOn-2
create scheduler subscribeOn-1
Thread[subscribeOn-1]   execute   Action start emmit
Thread[subscribeOn-1]   execute   Operation-1, event: 1
Thread[subscribeOn-1]   execute   Operation-2, event: 1
Thread[subscribeOn-1]   execute   Action subscribe

3. observeOn

  • observeOn 必须跟 subscribeOn 一起使用,单独使用会抛出空引用异常。
  • observeOn 应在 subscribeOn 的后面调用,否则会出现死锁的情况。
  • observeOn 操作会更改后续操作的执行线程,直至下一个 observeOn 调用之前的操作或 subscribe 操作。

继续阅读

《冒号课堂:编程范式与OOP思想》之编程范式笔记

《冒号课堂:编程范式与OOP思想》 写得非常有趣,对了解编程语言的发展、不同的编程范式有非常大的帮助。

1.4 编程范式

编程范式是计算机编程中的基本风格和典范模式,引导人们带着其特有的倾向和思路去分析和解决问题。

抽象的编程范式必须通过具体的编程语言来体现。范式的世界观体现在语言的核心概念中,范式的方法论体现在其表达机制中。一种语言的语法和风格与其所支持的编程范式密切相关。

1.5 开发技术

在宏观上选取一些框架以控制整体的结构和流程,在微观上利用库和工具包来解决具体的细节问题。

  • 框架:使设计者在特定领域的整体设计上不必重新发明轮子,是一种设计重用。利用控制反转机制实现对模块的统一调度,给程序员带来约束。

  • 库/工具包:使开发者摆脱底层编码,专注特定问题和业务逻辑;是代码重用。为程序员带来自由。

  • 设计模式(design pattern)和架构(architecture)不是软件产品,而是软件思想。设计模式是战术思想,架构是战略思想。

  • 设计模式:是针对某些经常出现的问题而提出的行之有效的设计解决方案,侧重思想重用,因此比框架更抽象、更普适,但多局限于局部解决方案,没有框架的整体性。

  • 惯用法(idiom):也是针对常发问题的解决方案,偏重实现而非设计,与实现语言密切相关,是一种更底层更具体的编程技巧。

  • 架构:一般指一个软件系统的最高层次的整体结构和规划,一个架构可能包含多个框架,而一个框架可能包含多个设计模式。

继续阅读

RxJava

ReactiveX

ReactiveX 是一个用于异步编程的 API 规范。 ReactiveX 结合了 Observer 模式、Iterator 模式和函数式编程的最佳理念。

ReactiveX 带来了更好的代码基础:

  • Functional, 函数式:避免了复杂的有状态的程序,在可观察流上使用干净(无副作用)的 输入/输出 函数。
  • Less is more, 少即是多:ReactiveX 的操作子通常把精心制作的修改简化为几行代码。
  • Async error handling, 异步错误处理:传统的 try/catch 对于异步计算的错误非常乏力,但 ReactiveX 具有恰当的机制来处理错误。
  • Concurrency made easy, 更容易的并发:ReactiveX 的 Observables 和 Schedulers 允许程序员从底层的线程、同步和并发问题中抽象出来。

RxJava

RxJava 是 ReactiveX 在 Java 编程语言里的一个实现。

基本概念:

  • 事件:主题生成的、订阅者感兴趣的东西。
  • 订阅者:Observer,抽象基类是 Subscriber
  • 主题:被观察的对象,抽象基类是 Observable。每个主题都有一个 OnSubscribe 的实例,OnSubscribe 从类名看是对订阅行为的反应,其 call(Subscriber subscriber) 方法封装了事件发生、通知的逻辑,供每次订阅时调用。
  • 订阅:subscribe,是一种动作,RxJava 在订阅时建立主题与监听者的关系,每次订阅,主题都会调用其内部 OnSubscribe.call(Subscriber subscriber) 方法。

  • 对于 Observable.doOnNext/doOnCompleted/doOnError/doOnEach/map 这类中间操作,生成一个新的订阅者 Subscriber,封装了相关行为,用于添加新的逻辑,并代理了对之前订阅者的调用;用新的订阅者和当前主题创建新的主题并返回。(采用的是包装器模式)
    继续阅读

应用事务管理混乱导致的一个坑

Spring 的事务传播属性

org.springframework.transaction.annotation.Propagation 定义了 Spring 的事务传播属性:

  • REQUIRED: 支持当前事务,如果不存在则新建一个。

  • REQUIRES_NEW: 创建新的事务,如果当前存在一个则 suppend 当前的。

  • SUPPORTS: 支持当前事务,如果不存在则以非事务方式执行。

  • MANDATORY: 支持当前事务,如果不存在则抛出异常。

  • NOT_SUPPORTED: 以非事务方式执行,如果当前存在一个事务则 suppend 当前的。

  • NEVER: 以非事务方式执行,如果存在事务则抛出异常。

  • NESTED: 如果当前存在一个事务则以嵌套事务的方式执行。

一个生产问题

一开始是 DBA 反馈数据库出现两种现象:

  1. 出现一些操作做完但会话一直还在等待客户端的提交动作。
  2. 偶尔出现大量的行锁,导致 JVM 线程互相等待而假死。

有个获取流水号的方法 systemService.getSerialNo 的事务传播属性是 NOT_SUPPORTED 的,这个方法通过类似这样的 update t_serialno set serial_no = :newSerialNo where serial_key = :key and serial_no = :oldSerialNo SQL 语句进行更新,更新返回的受影响行数等于 1 认为新的流水号是不重复的,更新不成功则重试。

行锁就出现在这些流水号的更新上。

继续阅读

微热山丘,探索 IoC、AOP 实现原理(二) AOP 实现原理

AOP 实现原理

一、简介

AOP 是 Aspect Oriented Programming 的简写,面向切面编程。主要的作用是以一种统一的方式对程序的逻辑进行修改、增强处理。可以在编译时也可以在运行时实现。编译时处理一般是通过字节码处理技术,运行时进行的一般是通过动态代理技术实现。

二、AOP 核心概念

  • Concerns, 关注:有两类,核心关注–是关于业务逻辑的;横切关注–是一些通用的逻辑,比如日志、缓存。
  • Joinpoint, 连接点:是执行时的切入点。可以是字段访问也可以是被调用方法。基于动态代理技术实现的一般只支持方法调用的连接点。
  • Target, 目标:是一个被切入的地方,一般是一个业务逻辑。比如是对一个业务逻辑的方法的调用。
  • Pointcut, 切入点:并不是所有的连接点都需要切入,切入点用于指定哪些连接点需要切入。
  • Advice, 建议:定义了 Aspect 的任务和什么时候执行它,是在核心关注之前还是之后。
  • Aspect, 方面:Advice 和 pointcut 定义一个方面。Advice 定义 Aspect 的任务和什么时候执行它,而切入点 pointcut 定义在哪里具体地方切入。就是说 Aspect 定义了它是什么东西、什么时候切入和在哪里切入。
  • Weaving, 织入:织入是一个把横切方面混合到业务目标对象的过程。可以是在编译时间,也可以是在运行时使用类加载机制,Spring AOP 是在运行时生成 bean 时处理。

下面是在 Sping 的 XML 里定义一个 AOP 的配置,注意其中个元素间的关系:

<!-- 一个 weaving 的定义 -->
<aop:config>
    <!-- 定义 aspect, ref 指向 任务的定义 -->
    <aop:aspect id="aspectCommonLogHandler" ref="commonLogHandler">
        <!-- 定义 pointcut -->
        <aop:pointcut id="commonLogPointcut" expression="execution( * net.coderbee.*.controller..*.*(..))" />

        <!-- 定义 advice, around 表示在目标的前/后执行 -->
        <aop:around method="inceptor" pointcut-ref="commonLogPointcut" />
    </aop:aspect>
</aop:config>

三、AOP 实现

1. 各组件定义配置

warnhill 的 AOP 实现没有采用 Spring 那样的配置方式,而是采用 bean 定义的形式组织起来:

<!-- 定义织入逻辑的实现 -->
<bean id="aspectJAutoProxyCreator" class="net.coderbee.warmhill.aop.AspectJAutoProxyCreator" />

<!-- 定义 pointcut -->
<bean id="pointcut" class="net.coderbee.warmhill.aop.AspectJExpressionPointcut" >
    <property name="expression" value="execution(* net.coderbee..*.*(..))" />
</bean>

<!-- 定义 任务 -->
<bean id="timerInterceptor" class="net.coderbee.warmhill.aop.TimerInterceptor" />
<bean id="anotherInterceptor" class="net.coderbee.warmhill.aop.AnotherInterceptor" />

<!-- 定义 aspect -->
<bean id="testAdvisor" class="net.coderbee.warmhill.aop.AspectJExpressionPointcutAdvisor">
    <property name="advice" ref="timerInterceptor" />
    <property name="pointcut" ref="pointcut" />
</bean>
<bean id="testAdvisor2" class="net.coderbee.warmhill.aop.AspectJExpressionPointcutAdvisor">
    <property name="advice" ref="anotherInterceptor" />
    <property name="pointcut" ref="pointcut" />
</bean>

继续阅读

微热山丘,探索 IoC、AOP 实现原理(一) IoC 实现原理

一. 简介及项目设定

1.1 微热山丘 介绍

warmhill(微热山丘)是一个参考 Spring 实现 IoC、AOP 特性的小项目。

相比于 Spring 庞杂的分类、层层继承、抽象,warmhill(微热山丘) 里都是简单直接的类、方法调用,核心在于简洁地展现实现原理。

1.2 项目设定及限制

有如下的限定:
1. 所有的 bean 都是单例。
2. bean 只有一个唯一的标识符 id,没有名字、别名。
3. 所有的 bean 都是立即初始化的,不支持延迟初始化。
4. 对于 BeanPostProcessor 的应用是基于声明的先后顺序。
5. 对于 AOP 的切入点,只支持对方法调用的拦截,不细分 Before/After/Around/Throw 等。
6. 对于 AOP 的配置也是基于 bean 定义的,不支持 <aop:config> 标签。
7. 目前只支持从 XML 方式配置 bean 。
8. bean 的属性目前只支持 String 类型和对其他 bean 的引用,只支持 setter 方法的依赖注入。

二. IoC、AOP 基本概念介绍

  • Resource: 资源。可存放在任意位置,只有一个方法: InputStream getStream();

  • BeanDefinition: bean 的定义信息。

  • BeanDefitionLoader: bean 定义加载类,只有一个方法: List<BeanDefinition> load();

  • BeanFactory: bean 工厂,负责根据 bean 定义创建 bean 的实例。

继续阅读

Spring-MVC 文件上传优化

Spring-MVC 文件上传优化

  1. 配置 CommonsMultipartResolver 时把 maxInMemorySize 配置为合适的大小,让小文件可以缓存在内存中,对磁盘只需一次写操作;

  2. 在 Controller 里调用 multipartFile.transferTo(file); 把文件保存到目标路径。该方法首先进行重命名,如果不成功则进行流拷贝,如果成功则可以省下一次读、写操作。对于 org.apache.commons.fileupload.disk.DiskFileItem 类,调用的方法是下面这个:

public void write(File file) throws Exception {
    if (isInMemory()) {
        FileOutputStream fout = null;
        try {
            fout = new FileOutputStream(file);
            fout.write(get());
        } finally {
            if (fout != null) {
                fout.close();
            }
        }
    } else {
        File outputFile = getStoreLocation();
        if (outputFile != null) {
            // Save the length of the file
            size = outputFile.length();
            /*
             * The uploaded file is being stored on disk
             * in a temporary location so move it to the
             * desired file.
             */
            if (!outputFile.renameTo(file)) {
                BufferedInputStream in = null;
                BufferedOutputStream out = null;
                try {
                    in = new BufferedInputStream(
                        new FileInputStream(outputFile));
                    out = new BufferedOutputStream(
                            new FileOutputStream(file));
                    IOUtils.copy(in, out);
                } finally {
                    if (in != null) {
                        try {
                            in.close();
                        } catch (IOException e) {
                            // ignore
                        }
                    }
                    if (out != null) {
                        try {
                            out.close();
                        } catch (IOException e) {
                            // ignore
                        }
                    }
                }
            }
        } else {
            /*
             * For whatever reason we cannot write the
             * file to disk.
             */
            throw new FileUploadException(
                "Cannot write uploaded file to disk!");
        }
    }
}

欢迎关注我的微信公众号: coderbee笔记,可以更及时回复你的讨论。