重构是什么

重构是什么

一般意义上来说,程序的重构和文章的修辞变更有着相同点

  1. 都是对同一件事情的描述
  2. 表达方式不尽相同

比如,购买这个操作:

伪代码可以写成:

Def buy:

{

Do transaction:

Service.Good -1

Customer.Good+1

Service.money+

Customer.money-1

Transaction end

}

也可以写成:

Def buy{

Do transaction

Service.Good-1

Customer.Good+1

End transaction

Defer:

Transfer money

}

两段伪代码都描述了这么一件事情

顾客购买商品的操作抽象为:

顾客拥有+1

商户拥有-1

顾客金钱-1

商户金钱+1

第二种写法在一般的做法上会比第一种写法更加灵活,因为我们将不同阶段的事情分开了,转账和库存变成了两个事情,可以异步进行

但是如果我们要求不高,第一种写法也是绝对可以满足需要的

这就是重构。

所谓代码的重构无非也就是将一开始语义不显著的代码,变为语义显著的代码,减少代码之间的不确定性,优化代码描述事件的模型。所谓clean code更多的是表现在能够准确的反映业务模型的操作。

实现hashMap

第一版:

package com.michaelssss;

import java.util.ArrayList;
import java.util.List;

/**
 * @author michaelssss
 * @since 2017/12/7
 */
public class SimpleMap<T, P> {
    private List<Pair<T, P>>[] buckets;

    public SimpleMap() {
        buckets = new ArrayList[64];
    }

    public void put(T key, P value) {
        int hashCode = key.hashCode();
        if (buckets[(hashCode % 64)] == null || buckets[(hashCode % 64)].isEmpty()) {
            buckets[(hashCode % 64)] = new ArrayList<>();
            buckets[(hashCode % 64)].add(new Pair<>(key, value));
        } else {
            boolean found = false;
            for (Pair<T, P> pair : buckets[(hashCode % 64)]) {
                if (pair.key.equals(key)) {
                    pair.value = value;
                    found = true;
                }
            }
            if (!found) {
                buckets[(hashCode % 64)].add(new Pair<>(key, value));
            }
        }
    }

    public P get(T t) {
        P value = null;
        int hashCode = t.hashCode();
        if (null == buckets[(hashCode % 64)] || buckets[(hashCode % 64)].isEmpty()) {
            return null;
        } else {
            for (Pair<T, P> pair : buckets[(hashCode % 64)]) {
                if (pair.key.equals(t)) {
                    value = pair.value;
                    break;
                }
            }
        }
        return value;
    }

    private static class Pair<T, P> {
        T key;
        P value;

        Pair(T key, P value) {
            this.key = key;
            this.value = value;
        }
    }
}

 

 

第二版(加入resize操作):

package com.michaelssss;

import java.util.ArrayList;
import java.util.List;

/**
 * @author michaelssss
 * @since 2017/12/7
 */
public class SimpleMap<T, P> {
    private List<Pair<T, P>>[] buckets;
    private float loadfactory = 0.75f;
    private int bucketSize;
    private int count;

    public SimpleMap() {
        bucketSize = 64;
        buckets = new ArrayList[64];
    }

    public SimpleMap(int initSize) {
        bucketSize = initSize;
        buckets = new ArrayList[initSize];
    }

    public SimpleMap(int initSize, float loadfactory) {
        this(initSize);
        this.loadfactory = loadfactory;
    }

    public void put(T key, P value) {
        int hashCode = key.hashCode();
        if (buckets[(hashCode % bucketSize)] == null || buckets[(hashCode % bucketSize)].isEmpty()) {
            buckets[(hashCode % bucketSize)] = new ArrayList<>();
            buckets[(hashCode % bucketSize)].add(new Pair<>(key, value));
            count++;
        } else {
            boolean found = false;
            for (Pair<T, P> pair : buckets[(hashCode % bucketSize)]) {
                if (pair.key.equals(key)) {
                    pair.value = value;
                    found = true;
                }
            }
            if (!found) {
                buckets[(hashCode % bucketSize)].add(new Pair<>(key, value));
                count++;
            }
        }
        if (loadfactory * count > bucketSize) {
            resize();
        }
    }

    private void resize() {
        int newBucketSeize = bucketSize * 2;
        List<Pair<T, P>>[] buckets = new ArrayList[newBucketSeize];
        for (List<Pair<T, P>> bucket : this.buckets) {
            if (null != bucket) {
                for (Pair<T, P> pair : bucket) {
                    int hashCode = pair.key.hashCode();
                    if (null == buckets[hashCode % newBucketSeize]) {
                        buckets[hashCode % newBucketSeize] = new ArrayList<>();
                    }
                    buckets[hashCode % newBucketSeize].add(pair);
                }
            }
        }
        this.buckets = buckets;
        this.bucketSize = newBucketSeize;
    }

    public P get(T t) {
        P value = null;
        int hashCode = t.hashCode();
        if (null == buckets[(hashCode % bucketSize)] || buckets[(hashCode % bucketSize)].isEmpty()) {
            return null;
        } else {
            for (Pair<T, P> pair : buckets[(hashCode % bucketSize)]) {
                if (pair.key.equals(t)) {
                    value = pair.value;
                    break;
                }
            }
        }
        return value;
    }

    private final static class Pair<T, P> {
        T key;
        P value;

        Pair(T key, P value) {
            this.key = key;
            this.value = value;
        }
    }
}

 

Dubbo中遇到的一点坑

1.无论何种方式部署,一定要设置delay=-1

2.dubbo调用的序列化有可能不是同步的,即当你远程获取一些信息时候,如果反序列化时间较长,你读取对象信息的时候会直接报空指针异常

3.dubbo是网络连接,无论如何都应该降低调用次数,设计接口的时候最好是batchget或者是做Client部分做Proxy缓存

4.最好将Dubbo的重试机制干掉。。。快速失败,并在Client处做记录,已经遇到过因为Dubbo接口响应慢,多次重试导致直接Provider打爆

几种数据结构的使用场景

Map/HashMap/TreeMap/LinkedHashMap

Map是一种抽象,维护了一组<key,value>的集合,可以通过map.get(key)访问value

HashMap是Map的完整实现。

TreeMap同样是Map的实现,同时还是SortedMap接口的实现,使用场景包括有序访问Map中的元素其顺序由Key中的实现的Comparable接口实现有序

LinkHashMap同样实现了元素有序,其中有序是按照元素进入Map的顺序

以上数据结构没有对多线程做保护,需要额外的代码才能维护在多线程下的正确性

ConcurrentHashMap

是HashMap的多线程版本,能够在多线程下保证Map本身的正确性,同时通过减少锁范围提高了多线程访问的性能

使用场景

用作集中式缓存,或者配置文件映射

Atomic处理系列

AtomicLong,AtomicInteger……等

通过硬件cas(compare and swap)来完成在多线程下的原子操作

这类对象的特性是,因为采用硬件级别的处理,故效率极高,且每个操作的原子性都有保证。

但是cas天生的特性,在竞争非常激烈的时候会导致所有人都瘫痪

AtomicLong的弱化版本LongAdder(JDK1.8加入)

为了解决CAS的缺陷,我们将放弃一部分原子性

具体到LongAdder是放弃了sum的原子性,故而这个类不能适用于比如分发uuid,限流计数等必须对sum有原子性的操作

使用场景

Atomic系列完全可以当作任何基本类型需要用到的且是多线程环境下的需要保证线程安全的地方比如:计数场景,UUID分发场景,边界值控制场景

LongAdder:适用于对long的当前准确值没有需求的场景,比如说阿里双十一大屏的那些数据,这些场景下,对与数据的延迟是可容忍的

Redis不正确使用姿势指南

Redis当对象存储系统

先来展示一段我司日常常见代码:

……………………

Abc = redisTemplate.opsValues().get(“******”);

if(null==Abc){

abc=………………一堆逻辑

redisTemplate.opsValues().set(“******”,Abc);

}

Return Abc;

 

从逻辑上来说这段代码并没有错误

但这样最终导致的结果是把所有外部的压力都转移到了redis上来了,极有可能导致把redis打爆,从而击穿数据库导致整个系统崩溃

Redis当集群内存用

另一种情况是,redis当共享内存用:

比如我们的zuul部件,每次都把用户的请求完整的set进redis,然后当下一个同session的转发进来后去redis去get,具体表现是redis看到日志是每当一个请求进来会出发两位数到三位数的redis请求(流量极端放大),尽管是zuul部件本身做到了无状态,但其实是用了redis做了共享状态。

这样依旧是把所有压力都放在了redis上面。引发问题更糟糕的是,我们的session内容存了太大的对象redis的并发能力遭到了限制,在少量用户下就已经会引发内部系统的崩溃。

总结一下:

正确的姿势是redis类似与一个单机系统里面不同进程的buffer/channel,在考虑上网络IO的重量级后,我们应该尽可能减少进程之间的通信,在这里的语境下应该是减少集群内之间的通信,从而避免把buffer/channel本身给弄挂了。

在第一种用法下,其实应该是将Abc的状态保存在redis但是Abc本身应该要存在当前结点上,这样就改变了集群内部通过redis传递对象,转而是传递信息了,比如我用时间作为Abc的特征,那么集群内部只需要传递特征时间就能够保持集群内部操作的幂等性,从原有的可能几kb的通讯变为几bit的通讯

第二种用法下,优化的手段就要更多了,首先第一点是减少session保存的数据,将不同业务之间的session分割开来减少session的大小。接着将请求按照一致性hash原理,同一个请求只从zuul集群的同一台服务器走,将保存的信息转换成本地内存,或者将用户-》集群的映射关系存入redis从而减少redis的使用量。

如何写socket通信

为了抛弃花生壳就自己写了个动态IP发现动态IP发现

不同于应用层,在Socket需要自己封装自己的协议,要自己决定如何传输数据

因为在整个网络的层次比较中间的位置,能给予开发者很大的灵活性

比如:

我需要用socket维持一个心跳链接

我的数据封装为如下格式

type lease struct{
  lenght int
  content []byte
}

lenght代表整个传输的长度,可以用于校验传输正确性

content就是整个需要传输的心跳内容

有时候需要更多的校验可以在lease的前后添加不同的结束符或者连接符,用于处理粘包等状态

相比较于Http层协议socket比较不爽的在于会造成业务和发送的逻辑交织

记一次远程调用优化

整个系统分为三个部分:

原始数据

包装成商品后数据

客户持有列表数据

原有设计是每一次客户获取客户持有列表数据都会从包装成商品之后的数据获取数据(包装成商品之后原有的原始数据的属性会有可能被商城变更)

因为Dubbo的方便性让人不自觉的就忽略掉了远程调用的IO是很重的

原有设计时,每个客户第一次访问会从原始数据+包装后商品数据进行遍历,然后存入redis,接着以后调用直接走redis。

当天上线后,发现原始数据的系统直接出现了百万次调用且因为代码笔误全部打到了数据库上,灰度环境紧急回滚

事后分析可能有以下几个原因:

1、不知道因何系统无法从redis上读取数据,都直接开始了调用

2、因为原有的设计可能流量只会放大N倍,但因为配合其他系统上线,增加了两段逻辑,流量放大为3N倍

3、因为dubbo的超时重试机制,流量再次放大三倍,直接导致整个系统的崩溃

优化方案:

将远程调用转为本地调用

1、给dubbo接口增加Proxy,作为本地缓存

2、定期从原始数据平台拉去数据,保证数据的一致性

3、其他逻辑保持不变

这样将网络请求变为本地内存调用,比较好的解决了因系统流量放大导致的系统雪崩情况

并发冲突处理

昨天晚上,遇上了阿里HR打电话来面试就聊了一下高并发情况下的冲突解决,结果我自己被绕晕了

原问题是:用CAS维持原子操作具体是如何操作的->这样操作有什么样的劣势->展示了一个实际的劣势场景:如何针对AtomicLong做优化

事实上并发问题是有一般解的,即:避免锁,减少锁范围,完全异步化(和我之前说高性能服务器的一般解是一样的)

CAS操作就不说了,一个自旋操作,高并发情况下可能会出现ABA问题,这些都是常见的面试题

那CAS实际的劣势呢,是在高并发操作下,写性能会急剧的降低(多个线程疯狂自旋,每个自旋周期都刚好被他人更新了值)

那为啥我会被绕晕了,其实他的真正问题是,在自旋锁的效应下,如何解决竟态写的性能下降问题,因此该问题不涉及读,而我脑子里面一直想的是AtomicLong.increaseAndGet()。。。。

这是有个解的,即是分线程加,类似于ConcurrentHashMap的处理方法,同理也可以预见到会发生和ConcurrentHashMap一样的sum操作会出现问题。当业务代码要马上获取自增的值时候,要么上锁,要么就得从业务上将读和写分开否则会使得业务代码在某次自增操作后获取到同一个值。

另,hashmap的负载因子的概念是:当hash发生时候,发生冲突的概率是多大

技术栈的选型

技术栈的选型:

  1. 人员成本
  2. 使用成本
  3. 改造成本

第一点指的是在市场上找一个熟悉当前技术栈的人是否困难, 一般情况下开源软件的人员比较容易找, 但也仅限于初级人员, 若是需要到较深层次的改写或者应用, 无论任何技术栈都非常困难(多数情况下情景退化成自研)

第二点是用起来这个组件好不好用,简不简单,比如EJB这种在绑定WebSphere战车上,用起来非常舒服.或者你整体都是Spring,沿用了Spring技术套件也会很舒服

第三点指当前选型与原有选型发生冲突要进行改造改动的返回 ,当然如果一个项目真心是完全敏捷开发这个完全不需要考虑.大部分情况下,比如从Dubbo切换至SpringCloud,从SOAP到Probuf(比如当前我司….),冲突厉害了还要做技术演进方案…如果无法演进就更痛苦成本就变高

私货:

个人偏好充血模型,但是以Spring开发的WEB应用都是贫血模型, 所以技术栈还可能影响开发习惯和偏好