Skip to content

Latest commit

 

History

History
308 lines (274 loc) · 17.9 KB

[13]系统设计.md

File metadata and controls

308 lines (274 loc) · 17.9 KB

# 如何自己设计一个类似 Dubbo 的 RPC 框架?

RPC框架的组成:

  1. 服务提供者(Server):对外提供后台服务,将自己的服务信息,注册到注册中心。
  2. 注册中心(Registry):用于服务端注册远程服务以及客户端发现服务。目前主要的注册中心可以借由 zookeeper,eureka,consul,etcd 等开源框架实现。
  3. 服务消费者(Client):从注册中心获取远程服务的注册信息,然后进行远程过程调用。

RPC的工作流程:

  1. 服务调用方(client)调用以本地调用方式调用服务;
  2. client stub接收到调用后负责将方法、参数等组装成能够进行网络传输的消息体;在Java里就是序列化的过程
  3. client stub找到服务地址,并将消息通过网络发送到服务端;
  4. server stub收到消息后进行解码,在Java里就是反序列化的过程;
  5. server stub根据解码结果调用本地的服务;
  6. 本地服务执行处理逻辑;
  7. 本地服务将结果返回给server stub;
  8. server stub将返回结果打包成消息,Java里的序列化;
  9. server stub将打包后的消息通过网络并发送至消费方
  10. client stub接收到消息,并进行解码, Java里的反序列化;
  11. 服务调用方(client)得到最终结果。

Dubbo的组成:

  1. service 层,接口层,给服务提供者和消费者来实现的
  2. config 层,配置层,主要是对 dubbo 进行各种配置的
  3. proxy 层,服务代理层,无论是 consumer 还是 provider,dubbo 都会给你生成代理,代理之间进行网络通信
  4. registry 层,服务注册层,负责服务的注册与发现
  5. cluster 层,集群层,封装多个服务提供者的路由以及负载均衡,将多个实例组合成一个服务
  6. monitor 层,监控层,对 rpc 接口的调用次数和调用时间进行监控
  7. protocal 层,远程调用层,封装 rpc 调用
  8. exchange 层,信息交换层,封装请求响应模式,同步转异步
  9. transport 层,网络传输层,抽象 mina 和 netty 为统一接口
  10. serialize 层,数据序列化层

# 分布式服务接口的幂等性如何设计(比如不能重复扣款)?

  1. 对于每个请求必须有一个唯一的标识,举个栗子:订单支付请求,肯定得包含订单 id,一个订单 id 最多支付一次,对吧。
  2. 每次处理完请求之后,必须有一个记录标识这个请求处理过了。常见的方案是在 mysql 中记录个状态啥的,比如支付之前记录一条这个订单的支付流水。
  3. 每次接收请求需要进行判断,判断之前是否处理过。比如说,如果有一个订单已经支付了,就已经有了一条支付流水,那么如果重复发送这个请求,则此时先插入支付流水,orderId 已经存在了,唯一键约束生效,报错插入不进去的。然后你就不用再扣款了。

# 分布式服务接口请求的顺序性如何保证?

利用唯一ID进行hash负载均衡。

# 使用 Redis 如何设计分布式锁?使用 zk 来设计分布式锁可以吗?这两种分布式锁的实现方式哪种效率比较高?

Redis 分布式锁:

  1. 获取当前时间戳,单位是毫秒;
  2. 轮流尝试在每个 master 节点上创建锁,超时时间较短,一般就几十毫秒(客户端为了获取锁而使用的超时时间比自动释放锁的总时间要小。例如,如果自动释放时间是 10 秒,那么超时时间可能在 5~50 毫秒范围内);
  3. 尝试在大多数节点上建立一个锁,比如 5 个节点就要求是 3 个节点 n / 2 + 1 ;
  4. 客户端计算建立好锁的时间,如果建立锁的时间小于超时时间,就算建立成功了;
  5. 要是锁建立失败了,那么就依次之前建立过的锁删除;
  6. 只要别人建立了一把分布式锁,你就得不断轮询去尝试获取锁。

ZK分布式锁:

  1. 就是某个节点尝试创建临时 znode,此时创建成功了就获取了这个锁;这个时候别的客户端来创建锁会失败,只能注册个监听器监听这个锁。释放锁就是删除这个 znode,一旦释放掉就会通知客户端,然后有一个等待着的客户端就可以再次重新加锁。
  2. 创建临时顺序节点:如果有一把锁,被多个人给竞争,此时多个人会排队,第一个拿到锁的人会执行,然后释放锁;后面的每个人都会去监听排在自己前面的那个人创建的 node 上,一旦某个人释放了锁,排在自己后面的人就会被 ZooKeeper 给通知,一旦被通知了之后,就 ok 了,自己就获取到了锁,就可以执行代码了。

对比:

  • redis 分布式锁,其实需要自己不断去尝试获取锁,比较消耗性能。如果是 Redis 获取锁的那个客户端 出现 bug 挂了,那么只能等待超时时间之后才能释放锁
  • zk 分布式锁,获取不到锁,注册个监听器即可,不需要不断主动尝试获取锁,性能开销较小。zk 的话,因为创建的是临时 znode,只要客户端挂了,znode 就没了,此时就自动释放锁。

# 分布式事务了解吗?你们如何解决分布式事务问题的?

  1. XA 方案:两阶段提交,有一个事务管理器的概念,负责协调多个数据库(资源管理器)的事务,事务管理器先问问各个数据库你准备好了吗?如果每个数据库都回复 ok,那么就正式提交事务,在各个数据库上执行操作;如果任何其中一个数据库回答不 ok,那么就回滚事务。
  2. TCC 方案:TCC 的全称是: Try 、 Confirm 、 Cancel。严重依赖于你自己写代码来回滚和补偿了,会造成补偿代码巨大。
    1. Try 阶段:这个阶段说的是对各个服务的资源做检测以及对资源进行锁定或者预留。
    2. Confirm 阶段:这个阶段说的是在各个服务中执行实际的操作。
    3. Cancel 阶段:如果任何一个服务的业务方法执行出错,那么这里就需要进行补偿,就是执行已经执行成功的业务逻辑的回滚操作。(把那些执行成功的回滚)
  3. SAGA 方案:业务流程中每个参与者都提交本地事务,若某一个参与者失败,则补偿前面已经成功的参与者。比如当执行到 T3 时发生了错误,则开始执行右边的事务补偿流程,反向执行 T3、T2、T1 的补偿服务 C3、C2、C1,将 T3、T2、T1 已经修改的数据补偿掉。

# 设计微信抢红包

业务特点:

  1. 微信红包业务比普通商品“秒杀”有更海量的并发要求;
  2. 微信红包业务要求更严格的安全级别;

解决方案:

  1. 系统垂直SET化,分而治之:微信红包用户发一个红包时,微信红包系统生成一个ID作为这个红包的唯一标识。接下来这个红包的所有发红包、抢红包、拆红包、查询红包详情等操作,都根据这个ID关联。红包系统根据这个红包ID,按一定的规则(如按ID尾号取模等),垂直上下切分。切分后,一个垂直链条上的逻辑Server服务器、DB统称为一个SET。各个SET之间相互独立,互相解耦。并且同一个红包ID的所有请求,包括发红包、抢红包、拆红包、查详情详情等,垂直stick到同一个SET内处理,高度内聚。通过这样的方式,系统将所有红包请求这个巨大的洪流分散为多股小流,互不影响,分而治之;
  2. 逻辑Server层将请求排队,解决DB并发问题:红包系统是资金交易系统,DB操作的事务性无法避免,所以会存在“并发抢锁”问题。但是如果到达DB的事务操作(也即拆红包行为)不是并发的,而是串行的,就不会存在“并发抢锁”的问题了。按这个思路,为了使拆红包的事务操作串行地进入DB,只需要将请求在Server层以FIFO(先进先出)的方式排队,就可以达到这个效果。从而问题就集中到Server的FIFO队列设计上。首先,将同一个红包ID的所有请求stick到同一台Server。然后将stick到同一台Server上的所有请求在被接收进程接收后,按红包ID进行排队。然后串行地进入worker进程(执行业务逻辑)进行处理,从而达到排队的效果。
  3. 根据红包ID的hash值分为多库多表,冷热分离,将历史冷数据与当前热数据分开存储;
  4. 限流降级;

金额计算:微信金额是拆的时候实时算出来,不是预先分配的,采用的是纯内存计算,不需要预算空间存储。采取实时计算金额的考虑:预算需要占存储,实时效率很高,预算才效率低。金额额度在0.01和剩余平均值*2之间。 微信从财付通拉取金额数据过来,生成个数/红包类型/金额放到Redis集群里,app端将红包ID的请求放入请求队列中,如果发现超过红包的个数,直接返回。根据红包的逻辑处理成功得到令牌请求,则由财付通进行一致性调用,通过像比特币一样,两边保存交易记录,交易后交给第三方服务审计,如果交易过程中出现不一致就强制回归。

# 接口服务变慢,该如何查原因?

  1. 检查网络是否通畅;
  2. 检查内存使用率,GC情况;
  3. 检查CPU使用率;
  4. 检查调用链耗时分布;
  5. 业务逻辑;

# 10万张券,每个人只能领一张,一张券只能用一次,如何设计架构方案(包括数据库怎么设计);

  1. 功能设计:
    1. 券的总数量固定;
    2. 每个人只能领一张;
    3. 一张券只能领一次;
    4. 一张券只能用一次;
    5. 券安全;
  2. 架构图:
  3. 交互时序图:
  4. 数据库存储结构:
  5. 扩展方案:
  6. 可用性:

# 设计微博信息流

推模式 拉模式 推拉结合

  • 功能设计:
  • 架构图:
  • 交互时序图:
  • 数据库存储结构:
  • 扩展方案:
  • 可用性:
  • 如何解决热点博文的问题?如何发现热点博文?

# 设计一个系统,实现“附近的人”这一功能。

  1. 功能设计:
    1. 用户的上下线(前后台交互协议)
    2. 怎么计算附近的人基本算法: GeoHash+Redis
  2. 架构图:
  3. 交互时序图:
  4. 数据库存储结构(支持增删改查):
  5. 数据在一台机器存不下的时候,如果扩展到多台,给出扩展方案,并分析其优缺点:
  6. 如何提高系统的容灾性

# 你需要实现一个高效的缓存,它允许多个用户读,但只允许一个用户写,以此来保持它的完整性,你会怎样去实现它?

读写锁。

  1. 功能设计:
  2. 架构图:
  3. 交互时序图:
  4. 数据库存储结构:
  5. 扩展方案:
  6. 可用性:

# 设计一个长串和短串转换获取的系统,高并发怎么解决,长串和短串之间怎么转换,高并发下怎么防攻击,数据库表怎么设计?

  1. 功能设计:
    1. 返回一个比原始URL短的URL
    2. 必须存储原始URL
    3. 新生成的URL必须能够链接到存储的原始
    4. 缩短的URL应该允许重定向
    5. 必须同时支持多个请求
  2. 架构图:
    1. 长度不超过7的字符串,由大小写字母加数字共62个字母组成;
    2. 一个长网址,对应多个短网址,每个短网址可以关联一些用户信息;
    3. 使用302重定向,如果用了301, Google,百度等搜索引擎,搜索的时候会直接展示真实地址,那我们就无法统计到短地址被点击的次数了,也无法收集用户的Cookie, User Agent 等信息,这些信息可以用来做很多有意思的大数据分析,也是短网址服务商的主要盈利来源;
  3. 交互时序图:
  4. 数据库存储结构:
    1. 短网址为 primary key, 长网址为value;
  5. 扩展方案:
  6. 可用性:
  7. 安全性:
    1. 增加单IP访问频率和单IP访问总量的限制,超过阈值进行封禁;

# 写一个你认为最好的单例模式

// DCL(Double CheckLock)实现单例
public class Singleton {
    private static Singleton instance = null;

    private Singleton() {
    }

    public static Singleton getInstance() {
        // 两层判空,第一层是为了避免不必要的同步
        // 第二层是为了在null的情况下创建实例
        if (instance == null) {
            synchronized (Singleton.class) {
                if (instance == null) {
                    instance = new Singleton();
                }
            }


        }
        return instance;
    }
}

// 静态内部类
public class Singleton {
    private Singleton() {
    }

    public static Singleton getInstance() {
        return SingletonHolder.instance;
    }

    /**
     * 静态内部类
     */
    private static class SingletonHolder {
        private static Singleton instance = new Singleton();
    }
}

// 枚举单例
public enum SingletonEnum {
    INSTANCE;
    public void doSomething() {
        System.out.println("do something");
    }
}

# 写一个生产者消费者模式

  1. LinkedList + synchronized;
  2. ArrayBlockingQueue;
  3. LinkedBlockingDeque;
  4. LinkedList + Lock + Condition;

# 限流算法有哪些?

  1. 计数器算法
  2. 滑动窗口
  3. 漏桶算法:漏桶具有固定容量,出水速率是固定常量(流出请求),如果桶是空的,则不需流出水滴,可以以任意速率流入水滴到漏桶(流入请求),如果流入水滴超出了桶的容量,则流入的水滴溢出(新请求被拒绝);
public class LeakBucket {
    /**
     * 时间
     */
    private long time;
    /**
     * 总量
     */
    private Double total;
    /**
     * 水流出去的速度
     */
    private Double rate;
    /**
     * 当前总量
     */
    private Double nowSize;

    public boolean limit() {
        long now = System.currentTimeMillis();
        nowSize = Math.max(0, (nowSize - (now - time) * rate));
        time = now;
        if ((nowSize + 1) < total) {
            nowSize++;
            return true;
        } else {
            return false;
        }
    }
}
  1. 令牌桶算法:大小固定的令牌桶可自行以恒定的速率源源不断地产生令牌。如果令牌不被消耗,或者被消耗的速度小于产生的速度,令牌就会不断地增多,直到把桶填满。后面再产生的令牌就会从桶中溢出。最后桶中可以保存的最大令牌数永远不会超过桶的大小;
public class TokenBucket {
    /**
     * 时间
     */
    private long time;
    /**
     * 总量
     */
    private Double total;
    /**
     * token 放入速度
     */
    private Double rate;
    /**
     * 当前总量
     */
    private Double nowSize;

    public boolean limit() {
        long now = System.currentTimeMillis();
        nowSize = Math.min(total, nowSize + (now - time) * rate);
        time = now;
        if (nowSize < 1) {
            // 桶里没有token
            return false;
        } else {
            // 存在token
            nowSize -= 1;
            return true;
        }
    }
}

# 监控系统设计?

功能设计:

  1. 一段代码的执行时间,一段代码可以是URL执行耗时,也可以是SQL的执行耗时
  2. 一段代码的执行次数,比如程序抛出异常记录次数,或者一段逻辑的执行次数
  3. 定期执行某段代码,比如定期上报一些核心指标,jvm内存、gc等指标
  4. 关键的业务监控指标,比如监控订单数、交易额、支付成功率等

# 日志系统设计?

ELK 方案(Elasticsearch+Logstash+Kibana)

  • Elasticsearch 是一个搜索和分析引擎。
  • Logstash 是服务器端数据处理管道,能够同时从多个来源采集数据,转换数据,然后将数据发送到诸如 Elasticsearch 等“存储库”中。
  • Kibana 则可以让用户在 Elasticsearch 中使用图形和图表对数据进行可视化。