# 一、用 Java 自己实现一个 LRU

LRU(Least Recently Used:最近最少使用):简单的说,就是保证基本的 Cache容量,如果超过容量则必须丢掉最不常用的缓存数据,再添加最新的缓存。每次读取缓存都会改变缓存的使用时间,将缓存的存在时间重新刷新。其实,就是清理缓冲的一种策略。 我们可以通过双向链表的数据结构实现 LRU Cache,链表头(head)保存最新获取和存储的数据值,链表尾(tail)既为最不常使用的值,当需要清理时,清理链表的 tail 即可,并将前一个元素设置为tail。结构图如下:

架构

Java 代码实现: 1)、通过原理的分析,我们可以维护一个双向链表结构,如下:LRUNode 实体类,主要理解 prev 和 next 属性,结合上面的链表结构图,就可以理解为,当此对象为 3 时 , prev 应指向 2 , next 应指向 4 。此类为下面类的类种类。

/**
 * @ClassName:       LRUNode
 * @Description:    定义一个链表类,包含head(指针的上一个对象),tail(指针的下一个对象)类种类
 * @author:          zzx
 * @date:            2019年3月4日        下午10:32:58
 */
 class LRUNode {
    private String key;
    private Object value;
    //当前对象的上一个对象
    LRUNode prev;
    //当前对象的下一个对象
    LRUNode next;

    /**
     * @param key  value
     * @desc 带有参数的构造器
     */
    public LRUNode(String key,Object value) {
        this.key=key;
        this.value=value;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

2)、我们通过 HashMap 实现一个缓存类 LRUCache ,我们通过逻辑处理,对最少使用的数据进行删除。代码如下:

/**
 * @ClassName:       LRUCache
 * @Description:    实现 LRU策略 清除缓存(当缓存大小>=指定大小时)
 * @author:          zzx
 * @date:            2019年3月4日        下午11:42:11
 */
public class LRUCache {
    private int capacity;
    /**
     * @desc 用来存储 key 和 LRUNode对象 充当缓存,且 hashmap 中的数据不会自动清理,需要我们手动清理
     */
    private HashMap<String, LRUNode> hashMap;

    //数据链表 第一个对象
    private LRUNode head;
    //数据链表 最后一个对象
    private LRUNode tail;

    //带参数 capacity 缓存打下的构造器
    public LRUCache(int capacity) {
        this.capacity=capacity;
        //创建对象时,创建一个hashmap 充当缓存
        hashMap = new HashMap<String,LRUNode>();
    }
    /**
     * @Description:    向链表中插入数据
     * @return:         void
     */
    public void set(String key , Object value) {
        //通过 key 查询链表中是否存在此对象
         LRUNode lruNode = hashMap.get(key);
         //缓存中有值时
         if(lruNode != null) {
             //为key 附新值,替换就值
             lruNode.value = value;
             //更新链表指针
             appendTail(lruNode,false);
         //缓存中无值时
         }else {
             //新对象
             lruNode = new LRUNode(key, value);
             //判断 hashMap 是否大于缓存大小
             if(hashMap.size() >= capacity) {
                 appendTail(tail,true);
             }
             //存取新值
             hashMap.put(key, lruNode);
         }

         //将新值设置为 head
         setHead(lruNode);
    }

    /**
     * @Title:             appendTail
     * @Description:     更新链表的前后指针,新增数据公用
     */
    public void appendTail(LRUNode lNode,boolean flag) {
        //存在上一个对象 prev
        if(lNode.prev != null) {
            //当前对象的上一个对象指向,当前对象的下一个对象。例如:2<-3(当前对象)->4 更新后: 2——>4
            lNode.prev.next = lNode.next;
        //如果不存在,下一个对象为 head 对象(暂不考虑更改对象)
        }else {
            head=lNode.next;
        }

        //存在下一个对象 next
        if(lNode.next != null) {
            // 4——>2
            lNode.next.prev=lNode.prev;
        }else {
            //如果当前对象是最后一个对象,则上一个对象就为最后一个对象
            tail=lNode.prev;
        }

        lNode.prev=null;
        lNode.next=null;
        //如果内存不足,需要删除tail
        if(flag) {
            hashMap.remove(lNode.key);
        }
    }

    /**
     * @Title:             setHead
     * @Description:     设置链表的 头 节点
     * @param:           node
     * @return:         void
     */
    public void setHead(LRUNode node) {
        if(head != null) {
            node.next=head;
            head.prev=node;
        }
        head=node;
        if(tail == null) {
            tail=node;
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101

# 二、分布式集群下如何做到唯一序列号

分布式架构下,生成唯一序列号是设计系统常常会遇到的一个问题。例如,数据库使用分库分表的时候,当分成若干个 sharding 表后,如何能够快速拿到一个唯一序列号,是经常遇到的问题。实现思路如下:

【1】数据库自增长序列或字段:全数据库唯一。
【优点】:简单,代码方便,性能可以接受。数字ID天然排序,对分页后者需要排序的结果很有帮组。适合小应用,无需分表,么有高并发性能要求。
【缺点】:不同数据库实现不同,在水平分表时,使用自增ID时可能会出现ID冲突。同时在高并发的情况下需要使用事务。在性能达不到要求的情况下,比较难于扩展。如果多个系统需要合并或者设计到数据迁移会相当痛苦。
【优化】:针对主库单点,如果有多个Master库,则每个Master库设置的起始数字不一样,步长一样,可以是Master的个数。比如:Master1 生成的是 1,4,7,10,Master2生成的是2,5,8,11 Master3生成的是 3,6,9,12。这样就可以有效生成集群中的唯一ID,也可以大大降低ID生成数据库操作的负载。

【2】UUID:常见的方式。可以利用数据库也可以利用程序生成32位的16进制格式的字符串,唯一性很高。
【优点】:简单,方便,生产ID性能非常好且全球基本唯一,在数据迁移和系统后期合并,或数据库变更等情况下都可应对。
【缺点】:没有排序,无法保证趋势递增。UUID使用字符串存储,查询效率低。存储空间较大,如果数据海量就绪考虑存储量问题,传输数据量大。
【3】Redis生成ID:当使用数据库来生成ID性能不够要求的时候,我们可以尝试使用 Redis来生成ID。这主要依赖于Redis是单线程的,所以也可以用生成全局唯一的ID。可以用 Redis的原子操作 INCR和INCRBY来实现。可以使用Redis集群来获取更高的吞吐量。假如一个集群中有5台Redis。可以初始化每台 Redis的值分别是1,2,3,4,5,然后步长都是5。各个Redis生成的ID为:
A:1,6,11,16,21
B:2,7,12,17,22
C:3,8,13,18,23
D:4,9,14,19,24
E:5,10,15,20,25
这个,随便负载到哪个机器确定好,未来很难做修改。但是3-5台服务器基本能够满足器上,都可以获得不同的ID。但是步长和初始值一定需要事先需要了。使用 Redis集群也可以防止单点故障(系统中一点失效,就会让整个系统无法运作的部件)的问题。另外,比较适合使用 Redis来生成每天从0开始的流水号。比如订单号=日期+当日自增长号。可以每天在 Redis中生成一个 Key,使用 INCR进行累加。
【优点】:不依赖于数据库,灵活方便,且性能优于数据库。数字ID天然排序,对分页或者需要排序的结果很有帮助。
【缺点】:如果系统中没有Redis,还需要引入新的组件,增加系统复杂度。需要编码和配置的工作量比较大。

【4】Twitter(推特) 的 snowflake 算法:twitter 在把存储系统从 MySQL 迁移到 Cassandra(一套开源分布式NoSQL数据库系统)的过程中由于 Cassandra 没有顺序 ID 生成机制,于是自己开发了一套全局唯一 ID 生成服务:Snowflake。
 ● 41位的时间序列(精确到毫秒,41位的长度可以使用69年)
 ● 10位的机器标识(10位的长度最多支持部署1024个节点)
 ● 12位的计数顺序号(12位的计数顺序号支持每个节点每毫秒产生4096个ID序号) 最高位是符号位,始终为0

Snowflake 的结构如下(每部分用-分开): 一共加起来刚好64位,为一个Long型。(转换成字符串后长度最多19)
0 - 0000000000 0000000000 0000000000 0000000000 0 - 00000 - 00000 - 000000000000
snowflake 生成的ID整体上按照时间自增排序,并且整个分布式系统内不会产生ID碰撞(由datacenter和workerId作区分),并且效率较高。经测试 snowflake每秒能够产生26万个ID。
【优点】:高性能,低延迟;独立的应用;按时间有序。
【缺点】:需要独立的开发和部署。在单机上是递增的,但是由于涉及到分布式环境,每台机器上的时钟不可能完全同步,也许有时候也会出现不是全局递增的情况。

【5】MongoDB 的 ObjectId:MongoDB 的 ObjectId 和 snowflake 算法类似。它设计成轻量型的,不同的机器都能用全局唯一的同种方法方便地生成它。MongoDB 从一开始就设计用来作为分布式数据库,处理多个节点是一个核心要求。使其在分片环境中要容易生成得多。

架构

前4 个字节是从标准纪元开始的时间戳,单位为秒。时间戳,与随后的5 个字节组合起来,提供了秒级别的唯一性。由于时间戳在前,这意味着ObjectId 大致会按照插入的顺序排列。这对于某些方面很有用,如将其作为索引提高效率。这4 个字节也隐含了文档创建的时间。绝大多数客户端类库都会公开一个方法从ObjectId 获取这个信息。
接下来的3 字节是所在主机的唯一标识符。通常是机器主机名的散列值。这样就可以确保不同主机生成不同的ObjectId,不产生冲突。 为了确保在同一台机器上并发的多个进程产生的ObjectId 是唯一的,接下来的两字节来自产生ObjectId 的进程标识符(PID)。 前9 字节保证了同一秒钟不同机器不同进程产生的ObjectId 是唯一的。后3 字节就是一个自动增加的计数器,确保相同进程同一秒产生的ObjectId 也是不一样的。同一秒钟最多允许每个进程拥有2563(16 777 216)个不同的ObjectId。

【6】其他一些方案:比如京东淘宝等电商的订单号生成。因为订单号和用户id 在业务上的区别,订单号尽可能要多些冗余的业务信息,比如:滴滴:时间+起点编号+车牌号 淘宝订单:时间戳+用户ID 其他电商:时间戳+下单渠道+用户ID,有的会加上订单第一个商品的ID。而用户ID,则要求含义简单明了,包含注册渠道即可,尽量短。

# 三、设计一个秒杀系统,30分钟没付款就自动关闭交易

【秒杀架构设计理念】:
限流: 鉴于只有少部分用户能够秒杀成功,所以要限制大部分流量,只允许少部分流量进入服务后端。
削峰: 对于秒杀系统瞬时会有大量用户涌入,所以在抢购一开始会有很高的瞬间峰值。高峰值流量是压垮系统很重要的原因,所以如何把瞬间的高流量变成一段时间平稳的流量也是设计秒杀系统很重要的思路。实现削峰的常用的方法有利用缓存和消息中间件等技术。 异步处理:秒杀系统是一个高并发系统,采用异步处理模式可以极大地提高系统并发量,其实异步处理就是削峰的一种实现方式。 内存缓存: 秒杀系统最大的瓶颈一般都是数据库读写,由于数据库读写属于磁盘IO,性能很低,如果能够把部分数据或业务逻辑转移到内存缓存,效率会有极大地提升。
可拓展: 当然如果我们想支持更多用户,更大的并发,最好就将系统设计成弹性可拓展的,如果流量来了,拓展机器就好了。像淘宝、京东等双十一活动时会增加大量机器应对交易高峰。

【设计思路】:
将请求拦截在系统上游,降低下游压力:
秒杀系统特点是并发量极大,但实际秒杀成功的请求数量却很少,所以如果不在前端拦截很可能造成数据库读写锁冲突,甚至导致死锁,最终请求超时。
充分利用缓存: 利用缓存可极大提高系统读写速度。
消息队列: 消息队列可以削峰,将拦截大量并发请求,这也是一个异步处理过程,后台业务根据自己的处理能力,从消息队列中主动的拉取请求消息进行业务处理。

【前端方案】:
页面静态化: 将活动页面上的所有可以静态的元素全部静态化,并尽量减少动态元素。通过CDN来抗峰值。
禁止重复提交: 用户提交之后按钮置灰,禁止重复提交。
用户限流: 在某一时间段内只允许用户提交一次请求,比如可以采取IP限流。

【后端方案】:
服务端控制器层(网关层): 限制uid(UserID)访问频率:我们上面拦截了浏览器访问的请求,但针对某些恶意攻击或其它插件,在服务端控制层需要针对同一个访问uid,限制访问频率。

【服务层】: 上面只拦截了一部分访问请求,当秒杀的用户量很大时,即使每个用户只有一个请求,到服务层的请求数量还是很大。比如我们有100W用户同时抢100台手机,服务层并发请求压力至少为100W。
采用消息队列缓存请求:既然服务层知道库存只有100台手机,那完全没有必要把100W个请求都传递到数据库啊,那么可以先把这些请求都写到消息队列缓存一下,数据库层订阅消息减库存,减库存成功的请求返回秒杀成功,失败的返回秒杀结束。 利用缓存应对读请求:对类似于12306等购票业务,是典型的读多写少业务,大部分请求是查询请求,所以可以利用缓存分担数据库压力。
利用缓存应对写请求: 缓存也是可以应对写请求的,比如我们就可以把数据库中的库存数据转移到Redis缓存中,所有减库存操作都在Redis中进行,然后再通过后台进程把Redis中的用户秒杀请求同步到数据库中。

【数据库层】: 数据库层是最脆弱的一层,一般在应用设计时在上游就需要把请求拦截掉,数据库层只承担“能力范围内”的访问请求。所以,上面通过在服务层引入队列和缓存,让最底层的数据库高枕无忧。
30分钟没付款就自动关闭交易:要看你取消订单后需不需要恢复库存了,不需要的话可以插入个结束时间来做标识,如果需要恢复库存的话可使用 swoole 里面的毫秒定时器 swoole_timer_after 来实现,如果使用的是 laravel 框架的话也可以使用延时队列来实现。我们公司在页面上会有倒计时刷新,其实也是通过页面定时器(setTiemout、setInterval)定时向后台发送请求。当超时后会进行订单状态的修改,达到自动关闭。

# 四、如何使用 Redis 和 Zookeeper 实现分布式锁?有什么区别优缺点,会有什么问题,分别适用什么场景。(延伸:如果知道redlock,讲讲他的算法实现,争议在哪里)

Redis实现分布式锁思路:基于Redis实现分布式锁(setnx)setnx也可以存入key,如果存入 key成功返回1,如果存入的 key已经存在了,返回0。
Zookeeper实现分布式锁思路:基于Zookeeper实现分布式锁 Zookeeper是一个分布式协调工具,在分布式解决方案中。多个客户端(jvm),同时在 zookeeper 上创建相同的一个临时节点,因为临时节点路径是保证唯一,只要谁能够创建节点成功,谁就能够获取到锁,没有创建成功节点,就会进行等待,当释放锁的时候,采用事件通知给客户端重新获取锁的资源。
Redis实现分布式锁与Zookeeper实现分布式锁区别: 相同点:在集群环境下,保证只允许有一个 jvm进行执行。
不同点: Redis 是nosql数据,主要特点缓存; Zookeeper是分布式协调工具,主要用于分布式解决方案。

实现思路: 主要通过获取锁、释放锁、死锁三方面进行说明:
【1】获取锁: Zookeeper:多个客户端(jvm),会在 Zookeeper上创建同一个临时节点,因为 Zookeeper节点命名路径保证唯一,不允许出现重复,只要谁能够先创建成功,谁能够获取到锁。
Redis: 多个客户端(jvm),会在Redis使用setnx命令创建相同的一个key,因为Redis的key保证唯一,不允许出现重复,只要谁能够先创建成功,谁能够获取到锁。
【2】释放锁: Zookeeper:使用直接关闭临时节点 session 会话连接,因为临时节点生命周期与 session 会话绑定在一块,如果 session 会话连接关闭的话,该临时节点也会被删除。这时候客户端使用事件监听,如果该临时节点被删除的话,重新进入盗获取锁的步骤。
Redis:在释放锁的时候,为了确保是锁的一致性问题,在删除的 redis 的 key 时候,需要判断同一个锁的 id,才可以删除。 【3】共同特征: 如何解决死锁现象问题:Zookeeper:使用会话有效期方式解决死锁现象。Redis:是对 key 设置有效期解决死锁现象。
【4】性能角度考虑: 因为 Redis 是 NoSQL 数据库,相对比来说 Redis 比 Zookeeper 性能要好。
【5】可靠性: 从可靠性角度分析,Zookeeper可靠性比Redis更好。因为Redis有效期不是很好控制,可能会产生有效期延迟。Zookeeper 就不一样,因为 Zookeeper 临时节点先天性可控的有效期,所以相对来说 Zookeeper 比 Redis 更好。

# 五、如果有人恶意创建非法连接,怎么解决

通过Filter进行拦截处理。

# 六、分布式事务的原理,优缺点,如何使用分布式事务,2PC 3PC 的区别,解决了哪些问题,还有哪些问题没解决,如何解决,你自己项目里涉及到分布式事务是怎么处理的

分布式事物的原理: 从广义上来看,分布式事务其实也是事务,只是由于业务上的定义以及微服务架构设计的问题,所以需要在多个服务之间保证业务的事务性,也就是 ACID 四个特性;从单机的数据库事务变成分布式事务时,原有单机中相对可靠的方法调用以及进程间通信方式已经没有办法使用,同时由于网络通信经常是不稳定的,所以服务之间信息的传递会出现障碍。

架构

模块(或服务)之间通信方式的改变是造成分布式事务复杂的最主要原因,在同一个事务之间的执行多段代码会因为网络的不稳定造成各种奇怪的问题,当我们通过网络请求其他服务的接口时,往往会得到三种结果:正确、失败和超时,无论是成功还是失败,我们都能得到唯一确定的结果,超时代表请求的发起者不能确定接受者是否成功处理了请求,这也是造成诸多问题的诱因。

架构

系统之间的通信可靠性从单一系统中的可靠变成了微服务架构之间的不可靠,分布式事务其实就是在不可靠的通信下实现事务的特性。无论是事务还是分布式事务实现原子性都无法避免对持久存储的依赖,事务使用磁盘上的日志记录执行的过程以及上文,这样无论是需要回滚还是补偿都可以通过日志追溯,而分布式事务也会依赖数据库、Zookeeper 或者 ETCD 等服务追踪事务的执行过程,总而言之,各种形式的日志是保证事务几大特性的重要手段。 2PC与3PC:两阶段提交是一种使分布式系统中所有节点在进行事务提交时保持一致性而设计的一种协议;在一个分布式系统中,所有的节点虽然都可以知道自己执行操作后的状态,但是无法知道其他节点执行操作的状态,在一个事务跨越多个系统时,就需要引入一个作为协调者的组件来统一掌控全部的节点并指示这些节点是否把操作结果进行真正的提交,想要在分布式系统中实现一致性的其他协议都是在两阶段提交的基础上做的改进。

架构

两阶段提交的执行过程就跟它的名字一样分为两个阶段,投票阶段 和 提交阶段,在投票阶段中,协调者(Coordinator)会向事务的参与者(Cohort)询问是否可以执行操作的请求,并等待其他参与者的响应,参与者会执行相对应的事务操作并记录重做和回滚日志,所有执行成功的参与者会向协调者发送 AGREEMENT 或者 ABORT 表示执行操作的结果。

架构

当所有的参与者都返回了确定的结果(同意或者终止)时,两阶段提交就进入了提交阶段,协调者会根据投票阶段的返回情况向所有的参与者发送提交或者回滚的指令。

架构

当事务的所有参与者都决定提交事务时,协调者会向参与者发送 COMMIT 请求,参与者在完成操作并释放资源之后向协调者返回完成消息,协调者在收到所有参与者的完成消息时会结束整个事务;与之相反,当有参与者决定 ABORT 当前事务时,协调者会向事务的参与者发送回滚请求,参与者会根据之前执行操作时的回滚日志对操作进行回滚并向协调者发送完成的消息,在提交阶段,无论当前事务被提交还是回滚,所有的资源都会被释放并且事务也一定会结束。

两阶段提交协议是一个阻塞协议,也就是说在两阶段提交的执行过程中,除此之外,如果事务的执行过程中协调者永久宕机,事务的一部分参与者将永远无法完成事务,它们会等待协调者发送 COMMIT 或者 ROLLBACK 消息,甚至会出现多个参与者状态不一致的问题。

架构

3PC 为了解决两阶段提交在协议的一些问题,三阶段提交引入了 超时机制 和 准备阶段,如果协调者或者参与者在规定的时间内没有接受到来自其他节点的响应,就会根据当前的状态选择提交或者终止整个事务,准备阶段的引入其实让事务的参与者有了除回滚之外的其他选择。

架构

当参与者向协调者发送 ACK 后,如果长时间没有得到协调者的响应,在默认情况下,参与者会自动将超时的事务进行提交,不会像两阶段提交中被阻塞住;上述的图片非常清楚地说明了在不同阶段,协调者或者参与者的超时会造成什么样的行为。

# 七、什么是一致性 hash

一致性Hash算法也是使用取模的方法,只是,刚才描述的取模法是对服务器的数量进行取模,而一致性Hash算法是对2^32取模,什么意思呢?简单来说,一致性Hash算法将整个哈希值空间组织成一个虚拟的圆环,如假设某哈希函数H的值空间为0-2^32-1(即哈希值是一个32位无符号整形),整个哈希环如下:

架构

整个空间按顺时针方向组织,圆环的正上方的点代表0,0点右侧的第一个点代表1,以此类推,2、3、4、5、6……直到2^32-1,也就是说0点左侧的第一个点代表2^32-1, 0和2^32-1在零点中方向重合,我们把这个由2^32个点组成的圆环称为Hash环。

下一步将各个服务器使用Hash进行一个哈希,具体可以选择服务器的IP或主机名作为关键字进行哈希,这样每台机器就能确定其在哈希环上的位置,这里假设将上文中四台服务器使用IP地址哈希后在环空间的位置如下:

架构

接下来使用如下算法定位数据访问到相应服务器:将数据key使用相同的函数 Hash 计算出哈希值,并确定此数据在环上的位置,从此位置沿环顺时针“行走”,第一台遇到的服务器就是其应该定位到的服务器!

例如我们有Object A、Object B、Object C、Object D四个数据对象,经过哈希计算后,在环空间上的位置如下:

架构

根据一致性Hash算法,数据A会被定为到Node A上,B被定为到Node B上,C被定为到Node C上,D被定为到Node D上。

# 八、什么是 Restful,讲讲你理解的 Restful

REST(Representational State Transfer 表现层状态转化):互联网软件的架构原则,定名为REST。SpringCloud 微服务设计原则就遵循 RESTful。就是用 URL定位资源,用 HTTP描述操作(GET,POST,DELETE,DETC)组成,进行微服务之间的交互。

# 九、如何设计一个良好的 API接口

在设计接口时,有很多因素要考虑,如接口的业务定位,接口的安全性,接口的可扩展性、接口的稳定性、接口的跨域性、接口的协议规则、接口的路径规则、接口单一原则、接口过滤和接口组合等诸多因素。

职责原则: 在设计接口时,必须明确接口的职责,即接口类型,接口应解决什么业务问题等。
单一性原则: 在明确接口职责的条件下,尽量做到接口单一,即一个接口只做一件事,而非两件以上。
协议规范: 在设计接口时,应明确接口协议,是采用 HTTP协议,HTTPS协议还是 FTP协议,要根据具体情况来定。

TIP

FTP协议(File Transfer Protocol,简称FTP),是一套标准的文件传输协议,用于传输文件,如.txt,.csv等,一般文件传输,采用FTP协议;
HTTP协议,适用一般对安全性要求比较低或没要求的业务情景;
HTTPS=HTTP+SSL,适用于对安全性要求较高的业务情景;

路径规则: 由于 api获取的是一种资源,所以网址中尽量为名词,而非动词。
http请求方式: 接口基本访问协议:get(获取),post(新增),put(修改)和delete(删除)。
域名: 一般地,域名分为主域名和专有域名,主域名适合 api长期不变或变化较少的业务,专有域名是解决具体的专有业务的。
跨域考虑: 在明确域名的情况下,一定要考虑接口是否跨域,以及跨域应采用的技术手段等。
api版本: 对于接口的url,应加版本号http://api.demo.com/v{d}/,其中 d表示版本号,如v1.0,v2.0。
度过滤信息: 当记录数比较多时(如 SELECT * FROM TBName),因适当添加一些条件对数据进行过滤,如TOP,分页,分组,排序和 WHERE条件等。
返回数据格式: 返回数据格式,一般包括三个字段:
【1】失败情况(状态码、错误码和错误描述)

{
    "status":0,//状态码 0-表示失败,1-表示成功
    "error_code":"2003",//错误码,一般在设计时定义
    "error_des":"身份验证失败"//错误描述,一般在设计时定义
}
1
2
3
4
5

【2】成功情况(标识id,数据对象,状态码)

{
     "sid":"sh20190111",//token id
     "users":{
                  "id":"al201901111341",//用户id
                  "name":"Alan_beijing",//用户名
                  "addr":"用户地址"
	      },
     "status":1//状态码 0-表示失败,1-表示成功
}
1
2
3
4
5
6
7
8
9

安全性原则: 接口暴露的考虑,接口并发量的考虑(限流),接口防攻击的考虑(黑白名单),接口跨域的考虑等。
可扩展性原则: 在设计接口时,充分考虑接口的可扩展性。
定义api界限: 任何api,从权限上,可归结为匿名 api和非匿名 api,前者不需要验证,后者需要验证。
定义api返回码: 在 api设计时,要定好 api返回码,如:

TIP

1 --授权过期 404--未找到资源 500--内部服务器错误 600--账号被锁

# 十、一次RPC请求的流程是什么

【1】Socket 套接字: 网络上的两个程序通过一个双向的通信连接实现数据的交换,这个链接的一端称为 Socket。可以实现不同计算机之间的通信,是网络编程接口的具体实现。Socket 套接字是客户端/服务端网络结构程序的基本组成部分。
【2】RPC 的调用过程: 实现透明的远程过程调用的重点是创建客户存根(client stub),存根(stub)就像代理(agent)模式里的代理。在生成代理代码后,代理的代码就能与远程服务端通信了,通信的过程都是由 RPC 框架实现,而调用者就像调用本地代码一样方便。在客户端看来,存根函数就像普通的本地函数一样,但实际上包含了通过网络发送和接收消息的代码。

架构

● 第1步:客户端调用本地的客户端存根方法(client stub)。客户端存根的方法会将参数打包并封装成一个或多个网络消息体并发送到服务端。将参数封装到网络消息中的过程被称为编程(encode),它会将所有数据序列化为字节数组格式。
● 第2步:客户端存根(client stub)通过系统调用,使用操作系统内核提供的 Socket 套接字接口来向远程服务发送我们编码的网络消息。
● 第3步:网络消息由内核通过某种协议(无连接协议:UDP,面向连接协议:TCP)传输到远程服务端。
● 第4步:服务端存根(server stub)接受客户端发送的消息,并对参数消息进行解码(decode),通常它会将参数从标准的网络格式转化成特定的语言格式。
● 第5步:服务端存根调用服务端,并将从客户端接收的参数传递给该方法,它来运行具体的功能并返回,对客户端来说这部分代码的执行就是远程过程调用。
● 第6步:将返回值返回到服务端存根代码中。
● 第7步:服务端存根在将该返回值进行编码并序列化后,通过一个或多个网络消息发送给客户端。
● 第8步:消息通过网络发送到客户端存根中。
● 第9步:客户端存根从本地 Socket 接口中读取结果消息。
● 第10步:客户端存根再将结果返回给客户端函数,并且消息从网络二进制形式转化成本低语言格式,这样就完成了远程服务调用。

# 十一、什么是 MESI协议(缓存一致性)

现在的处理器都是多核处理器,并且每个核都带有多个缓存(指令缓存和数据缓存,见下图)。为什么需要缓存呢,这是因为 CPU访问内存的速度比较慢,所以在 CPU和内存之间加了个缓存以提高访问速度。既然每个核都有缓存,那么假设两个核或者多个核同时访问同一个变量时这些缓存是如何进行同步的呢(缓存细分为一个个缓存行),这就有了MESI协议。

架构

缓存行的四个状态:MESI中每个缓存行都有四个状态,分别是E(exclusive)、M(modified)、S(shared)、I(invalid)。下面我们介绍一下这四个状态分别代表什么意思。
M:代表该缓存行中的内容被修改了,并且该缓存行只被缓存在该 CPU中。这个状态的缓存行中的数据和内存中的不一样,在未来的某个时刻它会被写入到内存中(当其他 CPU要读取该缓存行的内容时。或者其他CPU要修改该缓存对应的内存中的内容时(个人理解 CPU要修改该内存时先要读取到缓存中再进行修改),这样的话和读取缓存中的内容其实是一个道理)。
E:代表该缓存行对应内存中的内容只被该 CPU缓存,其他 CPU没有缓存该缓存对应内存行中的内容。这个状态的缓存行中的内容和内存中的内容一致。该缓存可以在任何其他CPU读取该缓存对应内存中的内容时变成S状态。或者本地处理器写该缓存就会变成M状态。
S:该状态意味着数据不止存在本地 CPU缓存中,还存在别的 CPU的缓存中。这个状态的数据和内存中的数据是一致的。当有一个 CPU修改该缓存行对应的内存的内容时会使该缓存行变成 I 状态。
I:代表该缓存行中的内容是无效的。

EMSI状态转移图:

架构

local read 和 local write分别代表本地 CPU读写。remote read和 remote write分别代表其他 CPU读写。建议首次看 EMSI内容的可以自己把下面这个表格写下来(我自己就是这么做的),这样理解会深一点。

当前状态 事件 行为 下一个状态
I(invalid) local read 1.如果其他处理器中没有这份数据,本缓存从内存中取该数据,状态变为 E
2.如果其他处理器中有这份数据,且缓存行状态为 M,则先把缓存行中的内容写回到内存。本地 cache再从内存读取数据,这时两个 cache的状态都变为S
3.如果其他缓存行中有这份数据,并且其他缓存行的状态为 S或 E,则本地 cache从内存中取数据,并且这些缓存行的状态变为S
E或S
I(invalid) local write 1.先从内存中取数据,如果其他缓存中有这份数据,且状态为M,则先将数据更新到内存再读取(个人认为顺序是这样的,其他CPU的缓存内容更新到内存中并且被本地cache读取时,
两个cache状态都变为S,然后再写时把其他 CPU的状态变为I,自己的变为M)
2.如果其他缓存中有这份数据,且状态为E或S,那么其他缓存行的状态变为I
M
I(invalid) remote read remote read不影响本地 cache的状态 I
I(invalid) remote write remote read不影响本地 cache的状态 I
E(exclusive) local read 状态不变 E
E(exclusive) local write 状态变为M M
E(exclusive) remote read 数据和其他核共享,状态变为S S
E(exclusive) remote write 其他CPU修改了数据,状态变为I I
S(shared) local read 不影响状态 S
S(shared) local write 其他 CPU的 cache状态变为I,本地 cache状态变为M M
S(shared) remote read 不影响状态 S
S(shared) remote write 本地 cache状态变为I,修改内容的 CPU的 cache状态变为M I
M(modified) local read 状态不变 M
M(modified) local write 状态不变 M
M(modified) remote read 先把 cache中的数据写到内存中,其他 CPU的 cache再读取,状态都变为S S
M(modified) remote write 先把 cache中的数据写到内存中,其他 CPU的 cache再读取并修改后,本地 cache状态变为I。修改的那个 cache状态变为 M I

# 十二、说说你知道的几种 HASH算法

【1】MD5: 它对输入仍以512位分组,其输出是4个32位字的级联,与 MD4 同样。MD5比MD4来得复杂,而且速度较之要慢一点,但更安全,在抗分析和抗差分方面表现更好。

【2】SHA-1: SHA1是由NIST NSA设计为同DSA一起使用的,它对长度小于264的输入,产生长度为160bit的散列值,因此抗穷举(brute-force)性更好。SHA-1 设计时基于和MD4同样原理,而且模仿了该算法。

哈希表不可避免冲突(collision)现象:对不同的keyword可能得到同一哈希地址即key1≠key2,而hash(key1)=hash(key2)。因此,在建造哈希表时不仅要设定一个好的哈希函数,并且要设定一种处理冲突的方法。可例如以下描写叙述哈希表:依据设定的哈希函数H(key)和所选中的处理冲突的方法,将一组keyword映象到一个有限的、地址连续的地址集(区间)上并以keyword在地址集中的“象”作为对应记录在表中的存储位置,这样的表被称为哈希表。

# 十三、什么是paxos算法, 什么是zab协议

Zab(Zookeeper Atomic Broadcast)协议【链接

# 十四、一个在线文档系统,文档可以被编辑,如何防止多人同时对同一份文档进行编辑更新

# 十五、线上系统突然变得异常缓慢,你如何查找问题

# 十六、说说你平时用到的设计模式

23种设计模式【链接

# 十七、Dubbo的原理,有看过源码么,数据怎么流转的,怎么实现集群,负载均衡,服务注册和发现,重试转发,快速失败的策略是怎样的

# 十八、如何设计建立和保持100w的长连接

Netty 长连接服务【链接】
HTTP长连接200万尝试及调优方法【链接】

# 十九、自己实现过 Rpc么,原理可以简单讲讲。Rpc要解决什么问题

使用 Netty 实现简单的 RPC 框架【链接

# 二十、异步模式的用途和意义

# 二十一、编程中自己都怎么考虑一些设计原则的,比如开闭原则,以及在工作中的应用

# 二十二、设计一个社交网站中的“私信”功能,要求高并发、可扩展等等。 画一下架构图

# 二十三、MVC模式,即常见的MVC框架

# 二十四、聊下曾经参与设计的服务器架构并画图,谈谈遇到的问题,怎么解决的

# 二十五、应用服务器怎么监控性能,各种方式的区别

# 二十六、如何设计一套高并发支付方案,架构如何设计

# 二十七、如何实现负载均衡,有哪些算法可以实现

# 二十八、Zookeeper的用途,选举的原理是什么

Zookeeper Leader选举【链接

# 二十九、Zookeeper watch机制原理

# 三十、Mybatis的底层实现原理

MyBatis 源码【链接】

# 三十一、请思考一个方案,实现分布式环境下的countDownLatch

# 三十二、后台系统怎么防止请求重复提交

# 三十三、描述一个服务从发布到被消费的详细过程

# 三十四、讲讲你理解的服务治理

# 三十五、如何做到接口的幂等性

# 三十六、如何做限流策略,令牌桶和漏斗算法的使用场景

接口限流常见的四种算法【链接】

# 三十七、什么叫数据一致性,你怎么理解数据一致性

# 三十八、分布式服务调用方,不依赖服务提供方的话,怎么处理服务方挂掉后,大量无效资源请求的浪费,如果只是服务提供方吞吐不高的时候该怎么做,如果服务挂了,那么一会重启,该怎么做到最小的资源浪费,流量半开的实现机制是什么

# 三十九、dubbo的泛化调用怎么实现的,如果是你,你会怎么做

Dubbox泛化【链接

# 四十、远程调用会有超时现象,如果做到优雅的控制,JDK自带的超时机制有哪些,怎么实现的

(adsbygoogle = window.adsbygoogle || []).push({});