分布式与中间件
NIO
NIO 即 Nonblocking IO,基于事件驱动思想,采用的 Reactor 模式。先相对于 BIO,NIO 的一个明显好处是不需要为每个Socket 套接字分配一个线程,而可以在一个线程中处理多个 Socket 套接字相关的工作
Reactor原理
基于事件的响应式模型
Reactor 模式中的角色
Reactor
事件分离器,其核心是 Selector,负责响应 IO 事件,一旦发生,广播发送给响应的 Handler 去处理
具体为一个 Selector 和一个 ServerSocketChannel。ServerSocketChannel 注册到 Selector 中,获取 SelectionKey 绑定一个 Acceptor
Acceptor
负责创建具体处理 IO 请求的 Handler
如果 Reactor 广播时 SelectionKey 创建一个 Handler 负责绑定响应的 SocketChannel 到 Selector 中。下次有 IO 事件时会调用对应的Handler 去处理
Handler
具体 IO 事件的处理者
例如 ReadHandler、SendHandler。ReadHandler 负责读取缓冲中的数据,然后再调用一个工作处理线程去处理读取到的数据
数据读压力越来越大
使用缓存
采用数据库作为读库
采用搜索引擎–其实也是一个读库
读写分离后又遇到瓶颈
专库专用,数据垂直拆分
垂直拆分后的单机遇到瓶颈,数据水平拆分
步骤:缓存
->读写分离(搜索引擎)
->垂直拆分(专库专用)
->水平拆分(单机还是遇到瓶颈)
数据库分库分表中间件:
MyCat – 基于 Proxy
Shark – 应用集成
基本的套路都是:定义路由策略,配置多数据源,在执行 SQL 之前进行 SQL 的改写,最终执行 SQL
Java 中间件的定义
中间件不是最上层的应用,也不是最底层的支撑系统,是处于“中间”位置的组件。中间件起到的桥梁作用,是应用与应用之间的桥梁,也是应用与服务之间的桥梁
Java 代理
具体实现上可以分为静态代理和动态代理
静态代理
是为每个被代理的对象构造对应的代理类
动态代理
动态代理是动态地生成具体委托类的代理类实现对象
package me.codeflight.springboot;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
public class DynamicProxyTest {
public static void main(String[] args) {
Calculator calculator = new CalculatorImpl();
LogHandler lh = new LogHandler(calculator);
Calculator proxy = (Calculator) Proxy.newProxyInstance(calculator.getClass().getClassLoader(), calculator.getClass().getInterfaces(), lh);
proxy.add(1, 1);
}
}
interface Calculator {
int add(int a, int b);
}
class CalculatorImpl implements Calculator {
@Override
public int add(int a, int b) {
return a + b;
}
}
class LogHandler implements InvocationHandler {
Object obj;
public LogHandler(Object obj) {
super();
this.obj = obj;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
this.doBefore();
Object o = method.invoke(obj, args);
this.doAfter();
return o;
}
public void doBefore() {
System.out.println("do this before.");
}
public void doAfter() {
System.out.println("do this after.");
}
}
数据拆分方式
数据拆分有两种方式,一个是垂直拆分,一个水平拆分
垂直拆分
就是把一个数据库中不同业务单元的数据分到不同的数据库里面
水平拆分
是根据一定的规则把同一业务单元的数据拆分到多个数据库中
不论是垂直拆分还是水平拆分,最后的结果都是将在一个数据库中的数据拆分到了不同的数据库中
垂直拆分带来如下影响
- 单机的 ACID 保证被打破了。数据到了多机后,原来在单机通过事务来进行的处理逻辑会受到很大的影响。我们面临的选择是,要么放弃单机事务,修改实现,要么引入分布式事务
- 一些 Join 操作会变得比较困难,因为数据可能已经在两个数据库中了,所以不能很方便地利用数据自身的 Join 了,需要应用或者其他方式来解决
- 靠外键去进行约束的场景会受影响
水平拆分会带来如下影响
- 同样有可能有 ACID 被打破的情况
- 同样有可能有 Join 操作被影响的情况
- 靠外键去进行约束的场景会有影响
- 依赖单库的自增序列生成唯一 ID 会受到影响
- 针对单个逻辑意义上的表的查询要跨库了
数据访问层的设计
数据访问层就是方便应用进行数据读/写访问的抽象层,我们在这个层上解决各个应用通用的访问数据库的问题。在分布式系统中,我们也把数据访问层称为分布式数据访问层,有时简称数据层
如何对外提供数据访问层的功能
对外提供给数据访问层的方式
1.为用户提供专有 API
2.数据层自身可以作为一个 JDBC 的实现
3.基于 ORM 或类 ORM 接口的方式
我们可以在自己应用使用的 ORM 框架上在包装一层,用来实现数据层功能,对外暴露的任然是原来框架的接口。这样的做法对于某些功能来说实现成本比较低,并且在兼容性方面有一定的优势,例如原来系统都使用 iBatis 的话,对于应用来说,iBatis之上的封装就比较透明了
通过 JDBC 方式使用的数据层是兼容性和扩展性最好的,实现成本上也是相对最高的。底层封装了某个 ORM 或者类 ORM 框架的方式具备一定的通用性(不能提供给另外的 ORM /类ORM 框架用),实现成本相对 JDBC 接口方式的要低。而采用专有API的方式是在特定场景下的选择
不同提供方式之间在合并查询场景下的对比
分库分表后的排序分页场景,我们需要从多个数据源取足够的数据,然后才能在应用中进行归并排序
相对于在 ORM/类ORM 框架之上的实现,专有 API 方式和 JDBC 方式都要与数据库的 JDBC 驱动直接打交道,而且为了得到正确的排序分页结果也需要获取足够的数据,但是和使用 ORM/类ORM框架不同的是,这两种方式并不是一定要把所有数据都获取到应用端并生成对应的Java对象
直接使用 JDBC,可以对多个数据源使用数据结构中对两个有序的表进行合并排序的方式,而且无论数据如何分布,我们最多只会浪费一个生成的对象。
使用 ORM/类ORM 框架可能会有一些框架自身的限制带来困难,使用iBatis的同时想去动态改动SQL就会比较困难,而这在直接局域 JDBC 驱动方式的实现中就没有那么困难
按照数据层流程的数据看数据设计
SQL解析 -> 规则处理 -> SQL改写 -> 数据源选择 -> SQL执行 -> 结果集返回合并处理
SQL解析阶段的处理
SQL解析主要考虑的问题有两个: 一是对SQL支持的程度,而是支持多少SQL方言
具体解析时使用antlr、javacc还是其他工具
在进行SQL解析时,对于解析的缓存可以提升解析速度。当然需要注意缓存的容量限制,一般系统中执行的SQL数量相对可控,不过为了安全,解析的缓存需要加上数量上限
通过SQL解析可以得到SQL中的关键信息,例如表名、字段、where条件等。而在数据层中,一个很重要的事情是根据执行的SQL得到被操作的表,根据参数及规则来确定目标数据源连接
规则处理阶段
1.采用固定哈希算法作为规则
固定哈希的方式为,更具某个字段取模,然后将数据分散到不同的数据库和表中。
根据时间取模多用在日志类或者其他与时间维度密切相关的场景。通常将周期性的数据放在一起,这样进行数据备份、迁移或现有数据的清空都会很方便
扩容是个比较大的麻烦
2.一致性哈希算法带来的好处
新增一个节点时,出了新增的节点外,只有一个节点受影响,这个新增节点和受影响的节点的负载是明显比其它节点低的;减少一个节点时,出了减去的节点外,只有一个节点受影响
3.虚拟节点对一致性哈希的改进
把多个物理节点,虚拟化为更多的虚拟节点,这些新的虚拟节点时相对均匀地插入到整个哈希环上的,这样,就可以很好地分担现有物理节点的压力;如果减少一个物理节点,对应的很多虚拟节点就会失效,这样,就会有很多神域的虚拟节点来承担之前虚拟节点的工作,但是对于物理节点来说,增加的负载相对是均衡的
4.映射表与规则自定义计算方式
改写SQL
数据表从原来的单库单表变为了多库多表,这些分布在不同数据库中的表的结构一样,但是表名未必一模一样
选择数据源
执行SQL和结果处理阶段
实战经验
复杂的连接管理
重新实现数据源管理
通过虚拟的连接实现,这个连接其实不和数据直接连接,但是它掌管着配置的分库的的连接,根据规则进行组合使用
具体的策略:
多数据源分组配置,封装iBatis,进行规则处理
MultipleDataSource extends AbstractRoutingDataSource
独立部署的数据访问层实现方式
Jar包方式
Proxy方式
两种协议:数据库协议和私有协议
读写分离的挑战和应对
主库从库非对称的场景
1.数据结构相同,多从库对应以主库的场景
2.主/备库分库方式不同的数据复制
数据库的平滑迁移
对数据库做平滑迁移的最大挑战是,在迁移的过程中又会有数据的变化。可以考虑的方案是,在开始进行数据迁移时,记录增量的日志,在迁移结束后,再对增量的变化进行处理。在最后,可以把要被迁移的数据的写进行暂停,保证增量日志都处理完毕后,再切换规则,放开所有的写,完成迁移工作