一、初识分布式系统
分布式系统含义:多个结点组成;互相连通;相互协作完成任务;
多线程工作的模式:
- 互不通信的方式
- 基于共享容器的协同方式:比如消息队列。这种情况下需要容器是线程安全的,可以通过加锁等方式解决。
- 基于事件协同的多线程方式:一个线程会依赖别的线程的状态或者结果。这种情况下会出现死锁。死锁的四个条件:互斥、不剥夺、请求保持、循环等待。预防死锁可以破坏后面三个条件。避免死锁可以用银行家算法。
IO方式:
- BIO:阻塞的方式,一个socket对应一个线程,服务端需要采用多线程来管理多个连接。
- NIO:基于Reactor模式,不用为每个socket分配一个线程,而是用一个selector管理多个连接,当某个连接可用时才去处理。
- AIO:AIO在进行读写时只需要调用相应的read/write方法,并且传入动作完成的处理器就能返回,处理完成功后会回调处理器。AIO和NIO最大的区别是NIO在有通知时可以进行相关操作,AIO有通知时表明操作完成了。
二、大型网站架构演进
架构演进:
- 单体应用
- db和app分离
- db做集群化。伴随出现的问题是session管理的问题,因为session是保存在单机上的,集群化之后用户的请求每次都会落在不同的机器上会出现session失效的问题。
- session sticky:保证集群化之后同个用户的请求还是在同一个机器上面就还是可以服务器本地保存session。这样做需要LB组件能根据sessionID转发请求到对应的机器上面,缺点是session是http层的信息,要做高层的解析才能转发,会比较慢;其次是LB组件会变成有状态的组件。
- Session Replication:所有机器之间吧session都同步了,就不会出现session失效的问题,缺点是带宽消耗和空间占用。
- Session集中存储:把Session集中存储起来,不同机器都能进行访问。缺点是多一次网络访问,而且session服务区会有单点问题。
- Cookie Based:通过cookie来传递session,把session放在cookie中,然后再服务端从cookie中取session,缺点是cookie长度问题,而且安全性差,也消耗了带宽,最后每次请求和响应都要带上session数据,效率差。
- db做读写分离。读写分离带来的问题:数据复制问题,数据源选择问题。
- 数据如何复制到读库:数据库自带的功能,需要考虑时延问题和短期数据不一致。
- 数据源选择:应用需要根据不同情况选择数据源(可以交给中间件比如MyCat之类的做),比如写操作要走主库,事务中的读也要走主库。
- 增加搜索引擎作为读库:比如站内搜索等功能,可以使用solr或者es等搜索引擎建立倒排索引。
- 使用cache加速读操作,常用的有数据缓存和页面缓存。
- db读写分离也不行了,需要垂直拆分和水平拆分
- 垂直拆分:不同模块的数据拆分到不同的数据库,增加读写性能,这样的话会出现跨库的问题。
- 水平拆分:把同一个表的数据分散存储在不同的数据库中。读写分离是解决读压力大的问题,对于数据量太大或者更新量大的问题病不起作用。水平切分带来的问题是sql路由的问题,其次主键的生成也有问题,不能用数据库的自增主键,还有就是可能会出现跨库问题,如果出现分页就更难处理了。
- 数据库上面没得做了,可以应用拆分,比如走服务化。
消息中间件:异步和解耦。
三、构建java中间件
JVM:内存回收,内存区域,GC
多线程:线程池、synchronized、ReentrantLock、volatile、Atomcis、[wait/notify/notifyall]
CountDownLatch:提供的机制是当多个线程都到达了预期状态或者完成预期工作时,触发事件,其他多个线程可以等待这个事件来触发后续的工作。到达自己预期状态的线程会调用countDown
方法,而等待的线程会调用await
方法。具体场景实例:多线程排序,多个线程对每个部分的数据单独进行排序,排完序之后调用countDown
,然后合并的线程先调用await
方法,之后进行合并工作。
CyclicBarrier:可以协作多个线程,让多个线程在这个屏障之前等待,知道所有的线程都到达了这个屏障时,再开始一起执行后面的工作。任何一个线程调用await
方法都会进入阻塞状态,直到所有的线程都到达。
CountDownLatch和CyclicBarrier的区别是:CountDownLatch在多个线程都进行了countDown
之后才会触发事件,唤醒await
在latch上面的线程,然后继续执行countDown
之后的自己的工作;CyclicBarrier要等到所有的线程都到了await
方法时才会一起开始之后各自的工作(CyclicBarrier可能会导致死锁);CountDownLatch不能循环使用,CyclicBarrier可以。
动态代理:InvocationHandler
、Proxy.newProxyInstance()
、invoke()
。
反射:在运行时判断任意一个对象所属的类;在运行时构造任意一个类的对象;在运行时判断任意一个类所具有的的成员变量和方法;在运行时调用任意一个对象的方法;生成动态代理。
四、服务框架
单机单进程的方法调用其实就是把PC指向相应的入口地址,而在多机之间,我们需要对调用的请求信息进行编码,然后传给远程的结点,解码之后在进行真正的调用。
服务框架的设计与实现:
- 集中式到分布式遇到的问题:接口调用、寻址路由、编码/解码、网络通信等。
- 根据调用的服务名称获取可用的服务机器列表,然后选择一台机器作为目标。可以使用LVS的负载均衡方案,或者名称服务方案。名称服务方案的话一般采用接口名称+参数等作为命名方式。
- 构造请求数据包其实就是序列化的过程,把对象序列化成二进制数据。之后可以使用socket通信。
- 调用者和服务提供者之间通讯的问题:如何确定和哪一台服务提供者通信。可以通过中间代理来解决;也可以使用直连方式,直连方式需要引入一个服务注册查找中心的机器。
- 异步调用服务的方式:Oneway方式,只关心发送请求而不关心结果,不可靠;Callback方式,发送请求之后就继续处理自己的操作,等对方有响应之后进行一个回调操作;Future方式,把数据放入队列之后就处理别的工作,然后通过Future来获取结果并进行超时处理;可靠异步执行,使用消息中间件来实现。
- 服务端的工作:对本地服务的注册管理+根据请求定位服务并执行。服务端需要把服务注册到服务注册查找中心才能被调用者发现。
五、数据访问层
垂直拆分+水平拆分
垂直拆分带来的问题:
- ACID打破,需要分布式事务
- join操作困难,需要应用层解决,或者引入中间件
- 外键失效
水平拆分带来的问题:
- ACID,join,外键同样是问题
- 自增主键唯一性id
- 以前单表查询现在也会跨库
分布式事务的解决方案:2pc,3pc
CAP/BASE理论:
CAP=consistency+available+partition-tolerance。三者只能满足两个,但是分布式系统p是肯定要满足的,实际上就是cp或者ap。选择ap就是放弃一致性,大部分分布式系统比如nosql都是采取这种方案;选择cp就是放弃可用性,出问题就比较严重。
BASE=basically available+soft state+eventually consistent。当CAP中选择了ap其实就是保证了最终一致性,允许存在中间状态的不一致,但是最终是一致的。
多机的Sequence问题:水平分库之后就不能使用自增id了。实现方案有
- mysql使用auto increment设置初始值和步长,则能分布式生成全局递增id,缺点是扩展性差,可能会有并发问题。
- 使用单独的id生成器,问题是性能问题,每次都要去取id,且id生成器是单点,存在单点问题。
- 直接在应用逻辑中分配id。缺点是高耦合,系统会复杂,不能保证id是按照插入的顺序。
多机数据查询的问题:
- 跨库join:在应用层进行拆分,进行多次读库;数据冗余,经常需要的数据冗余多分,就不用每次join跨库了;使用外部系统,比如搜索引擎。
- 外键约束:应用层处理。
- 跨库查询(水平切分带来的问题):查询结果合并。负责情况下的操作有排序,函数处理,分页等,这些都要在应用层处理了,或者用中间件。
数据访问层中间件的整理流程:sql解析-规则处理-sql改写-数据源选择-sql执行-结果集处理返回。
sql解析:是否支持所有sql?是否支持多少的方言。
规则处理:如何定位到目标库。
- hash取模。扩展性差。
- 一致性哈希。负载不均衡,可以使用虚拟节点改进。
sql改写:表名后缀改写,数据库后缀等。
数据源选择:确定了分片之后还得确定是该分片的那个从库。
执行sql和结果集返回:结果处理和异常判断。
读写分离的挑战与应对:
- 多从一主,可以通过消息解决数据同步的问题。
- 主从结构不同的分库,比如主库按照卖家id取模,从库按照买家id取。
- 引入数据分发平台
数据平滑迁移:迁移过程中数据发生变化怎么办。可以使用增量日志,在数据迁移开始时记录,完成后同步日志进行处理。处理增量日志的收可能还会有新的增量日志,这是一个逐渐收敛的过程。