线程
初始化-就绪--运行-等待(请求完后会让出位置,等待数据返回)
数据返回时,还是得先经过就绪状态,等cpu调度,到达运行状态,取到数据返回。
优先级高的情况下,可能一进入就绪状态就可以运行。
(1)运行(running)态:进程占有处理器正在运行。
(2)就绪(ready)态:进程具备运行条件,等待系统分配处理器以便运行。
(3)等待(wait)态:又称为阻塞(blocked)态或睡眠(sleep)态,指进程不具备运行条件,正在等待某个事件的完成。
通常,一个进程在创建后将处于就绪状态。每个进程在执行过程中,任意时刻当且仅当处于上述三种状态之一。同时,在一个进程执行过程中,它的状态将会发生改变。引起进程状态转换的具体原因如下:
(1)运行态一一等待态:等待使用资源或某事件发生,如等待外设传输;等待人工干预。
(2)等待态一一就绪态:资源得到满足或某事件己经发生,如外设传输结束;人工干预完成。
(3)运行态一一就绪态:运行时间片到,或出现有更高优先权进程。
(4)就绪态一一运行态:CPU空闲时被调度选中一个就绪进程执行。
线程池:线程运行完成后并不会销毁,会回到线程池中等待被下一次调用。
线程锁:为了保证多线程可以同时运行多个任务但是当多个线程同时访问共享数据时,保持数据同步。
死锁:是指两个或两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。比如转账过程:由A给B转账,同时B又给A转账,线程甲给A加锁,阻塞,等待cpu分配执行,线程乙给B加锁,阻塞,等待cpu分配执行,线程甲想获取B的锁,失败,继续等待,线程乙想获取A的锁,失败,继续等待,出现了死锁现象。
应按照资源“大小”的顺序来申请锁,比如按照操作系统的算法,从最大的开始加锁。
系统服务重启时,一切资源全部都销毁,包括线程池中的线程。
TCP/IP
中间节点不负责建立可靠的连接通道,物资完全可能失序、重复、丢失。TCP协议就是在这些不可控的情况下建立一种可靠的方法,基本上就是失败重发。
TCP连接:是虚拟的,连接的状态信息并不会在过程中保存,相反,是在连接的两端维持的。
三次握手:验证通信双方的发送请求和接收请求的能力。
第一次握手:路人甲发送请求,宋兵乙接收到了请求,宋兵乙就会明白路人甲的发送能力和自己的接收能力是没有问题的。
第二次握手:宋兵乙发送请求,路人甲接收到了请求,路人甲就会明白自己的发送和接收能力是没有问题的,宋兵乙的发送和接收能力是没有问题的,但是宋兵乙不知道自己的发送能力如何,所以产生第三次握手。
第三次握手:路人甲发送请求,宋兵乙接收到了请求,此次请求只是为了证明宋兵乙的发送能力和路人甲的接收能力是没有问题的。
程序的局部性原理,分为两种
- 时间局部性:如果程序中的某条指令一旦执行,则不久之后该指令可能再次被执行;如果某数据被访问,则不久之后该数据可能再次被访问。
- 空间局部性:是指一旦程序访问了某个存储单元,则不久之后,其附近的存储单元也将被访问。
线程和进程
什么情况下使用进程个线程:
1、需要频繁创建销毁的优先使用线程;因为对进程来说创建和销毁一个进程代价是很大的
2、线程的切换速度快,所以在需要大量计算,切换频繁时用线程,还有耗时的操作使用线程可提高应用程序的响应
3、因为对CPU系统的效率使用上线程更占优,所以可能要发展到多机分布的用进程,多核分布用线程
4、并行操作时使用线程,如C/S架构的服务器端并发线程响应用户的请求
5、需要更稳定安全时,适合选择进程;需要速度时,选择线程更好
进程和线程的关系:
1、一个线程只能属于一个进程,而一个进程可以有多个线程,但至少有一个线程。线程是操作系统可识别的最小执行和调度单位。
2、资源分配给进程,同一进程的所有线程共享该进程的所有资源。 同一进程中的多个线程共享代码段(代码和常量),数据段(全局变量和静态变量),扩展段(堆存储)。但是每个线程拥有自己的栈段,栈段又叫运行时段,用来存放所有局部变量和临时变量。
3、处理机分给线程,即真正在处理机上运行的是线程。
4、线程在执行过程中,需要协作同步。不同进程的线程间要利用消息通信的办法实现同步。
事务的原子性问题
当遇到系统崩溃,断电等极端情况下,如何保持数据的一致性?
可以采用数据的Undo日志文件的方式,将操作行为记录下来,比如
[开始事务]
[t1 A账户剩余金额100]
[t1 B账户剩余金额200]
[提交事务]
当恢复数据的时候,如何知道事务是否完成,如果在日志文件中有提交事务或回顾事务,就说明这个事务已经结束,那么就不需要去处理它,如果只有开始事务,没有提交或回滚,那么就得恢复,如果在恢复后,需要在日志文件上补上,回滚事务,下次就可以忽略这个事务。
事务日志也是一个文件,如果日志还没有写入文件就断电了,该如何?
遵循两个规则
- 在将数据写入硬盘的数据文件之前,一定要把对应的日志写入日志文件
- 在提交事务或回滚事务这样的日志记录,一定要在各自数据写入硬盘的数据文件之后,写入。
遵循第一条规则,如果日志都没有写入硬盘,那么数据自然不会写入,什么影响都没有
遵循第二天规则,提交事务这种操作在断电时,有可能没有写入硬盘,在系统恢复时认为事务没有提交,就会恢复余额。
幂等性:应用在软件系统中,简单定义为:某个函数或者某个接口使用相同参数调用一次或者无限次,其造成的后果是一样的。
CPU和内存
简化一下,内存就是一个个小格子,每个格子都有编号,编号被称为内存的地址,格子中的数据可以被cpu读写。
cpu中有两个比较重要的成员,运算器和寄存器,运算器可以进行运算,但是不能直接操作内存进行运算,需要使用寄存器。
cpu必须将数据装载到寄存器中才能进行运算
- 从内存中的某个小格子读取数据,放入某个寄存器中
- 把寄存器中的数据写入内存的某个小格子中(会覆盖原来的数据)
- 进行数据运算和逻辑运算
- 根据条件进行跳转
比如50+60的例子
- 将数字50放入到编号为a的内存小格子中
- 将数字60放入到编号为b的内存小格子中
- 把格子a中的数字取出来,暂时放到寄存器R1中
- 把格子b中的数字取出来,暂时放到寄存器R2中
- 把R1+R2的值相加,结果放到R1中
- 把R1结果放到编号为a的内存小格子中
指令也需要在内存中才能够被内存访问到,cpu从内存中读取到指令后,会进行译码,看看这条指令是做什么的,然后再进行运算。所以,内存小格子里存放的不仅仅是数据,还有程序指令,只需要告诉cpu第一条指令在哪里,然后cpu就可以运行处理了。那么内存的数据是哪里来的,肯定是来自于硬盘,将写好的程序放在硬盘中,执行时调入内存。
线程锁
没有锁,多个线程并发地读/写共享资源的时候就会出错,最常用的就是互斥锁。所谓互斥,就是同一时刻,只有获得锁的那个线程才有资格去操作共享资源,其他线程都被阻塞了。
自旋锁:是指当一个线程在获取锁的时候,如果锁已经被其它线程获取,那么该线程将循环等待,然后不断的判断锁是否能够被成功获取,直到获取到锁才会退出循环。
boolean lock = false;哪个线程抢先改为true,就意味着获取了这把锁,将功能执行完后,将变量改为false,让出锁。
当有两个线程,都读到了lock=false,都把lock改为了true,那么这个锁属于谁,Test and Set Clock机制会处理这种情况。
不可重入的自旋锁
foo();
while(true){
--获取自旋锁
get();
foo();
--释放自旋锁
push();
}
这种情况,就会造成死锁,第一次可以获取自旋锁,第二次递归调用的时候,还要获取的时候,就无法获取,第一次也没有释放。
可重入解决
每次成功申请锁后,要记录到底是谁申请的,还要用计数器记录重入的次数,持有锁的线程再次申请只是将计算器+1,释放锁的时候,计数器-1,等于0才代表真正释放了锁。
不加锁的方式,CAS算法
//加锁方式保证线程安全
public class Sequence{
private int value;
public synchronized int next(){
return value++;
}
}
1、从内存中读取一个值,假设为10,取名A
2、B=A+1;B=11
3、用A的值和内存的值相比,如果相等,代表这段时间没有线程对其,进行修改,那么就把B(值11)写入内存,如果不等,说明这段时间有人修改了值,那么放弃写入操作,返回第一步,重启获取内存中新的值
其中读内存,写入内存可以使用Compare and Swap这条硬件指令来让操作系统和硬件保持原子执行。
public int next(){ while(true){ int A = 读取内存的值 int B = A+1; if(compareAndSwap(内存的值,A,B)){ return B; } } }
当线程X和Y同时进入这段代码,都读取到内存的值10,然后这时候X线程时间片到了,退出了CPU,线程Y继续执行,对于线程Y来说,A=10,B=A+1=11,然后运行compareAndSwap,发现A的值和内存的值是相等的,于是新值B写入内存,结束程序执行。等到线程X重新获取运行的时候,线程X手中的初始值还是10,也得到了B=11,当他运行compareAndSwap时,发现A的值和内存的值不等了(因为被线程Y把内存修改成了11),说明被别的线程修改了,这是线程X只能继续循环,直到A的值和内存的值相等,说明无线程修改,才可以将新的B值写入内存。这里的compareAndSwap只是举个例子,并不是真的方法叫这个名字。
这既是非阻塞的线程,不是那么的悲观,互斥了,因为对于阻塞而言,激活是不小的开销。
由CAS而引出的ABA问题
假设有两个线程,线程A读到内存的值为10,然后时间片到期,撤出CPU。线程B运行,读到内存的值是10,并将其修改为11,然后又将其修改为10,简单的说,就是10->11->10。接着线程A开始执行,发现内存的值还是10,完全不知道内存被操作过。
如果只是操作的是简单数据类型,并不影响什么,但是如果操作的是复杂数据结构,就会出问题了。
假设有一个链表,可以从链表的头部删除元素,在多线程的并发下,有一个链表,由节点A,B,C组成,当线程X试图删除头部元素,当线程X时间片用完,退出CPU,线程Y执行,把节点A移除,节点B移除,再把节点A加回链表头部(注意:节点A已被复用),线程Y时间片用完,切换到线程X,线程X发现指向的节点没有变化,误以为链表没有变化(其实节点B已经被删除了),进行compareAndSwap操作,成功了。节点A再次被删除,指向了错误的节点C,而不是预期的节点B。
垃圾回收的可达性分析
由GC Root出发,找到被GC Root为起点的引用链,如果有对象不再这条链条上,就被清除掉。
Java的持久化
当断电来袭,java对象会瞬间消失,因为所有的java对象是依附于内存之中的,cpu调用内存,进行执行命令,而内存无法存储持久数据。
可以使用Java序列化,将内存中的重要对象转化成二进制文件存储在硬盘中,等电力恢复后,可以进行反序列化,由二进制文件变成Java对象,继续存在于内存。
简单工厂
public class Driver{
public static Connection getConnection(String dbType,Properties info){
if("mysql".equals(dbType)){
return new MySqlConnectionImpl(info);
}
if("oracle".equals(dbType)){
return new OracleConnectionImpl(info);
}
if("db2".equals(dbType)){
return new DB2ConnectionImpl(info);
}
throw new RuntimeException("dbType="+dbType);
}
}
分布式事务(不同数据库的统一事务处理)
强一致性:两阶段提交
由于涉及多个分布式数据库,特设一个全局的事务管理器,来负责协调各个数据库的事务提交。
- 全局的事务管理器向各个数据库发出准备消息,各个数据库需要在本地把一切都准备好,执行操作,锁住资源,记录redo/undo日志,但并不提交。总而言之,要进入一种时刻准备提交或回滚的状态,然后向全局的事务管理器报告是否准备完成。
- 如果所有数据库都已准备好,全局事务管理器就下命令,提交!这时候,各个数据库才真正提交。如果有一个数据库没准备好,全局事务管理器就下命令,回滚!这时候,各个数据库都要进行回滚。
但是,如果阶段2,全局事务管理器出现了问题怎么办?各个数据库还在等待着命令,阻塞住了,还锁住了资源。
如果阶段2中,事务管理器发送的提交命令由于网络问题,数据库1收到了,数据库2没收到,两个库处于不一致状态怎么办?
最终一致性
使用消息队列来实现
以转账为例
- 由A账户转100给B,需要在数据库发起一个事务,从A账户扣款100,还得向消息队列中插入一条给B添加100的消息,然后事务结束
- 当消息队列中给B添加100的消息被读取并成功执行,达到延迟执行,最终一致性的效果
但是,第一步,事务无法同时操作数据库和消息队列,可以改为
- 由A账户转100给B,需要在数据库发起一个事务,从A账户扣款100,同时在“事件表”插入一条记录:给B添加100,这两张表存于一个库,直接使用本地事务就可
- 再创建一个定时任务,从事件表读取记录,并向消息队列写入消息,然后将记录状态改成finish,下次定时任务就不会去执行该条记录了。
- 这时候,定时任务也可能出错:当向消息队列写入数据后,还没来得及把状态改为finish就崩溃了,这样会向消息队列多次写入同一消息,这时候就需要,在消息接收处理的时候,判断,是否这条消息已经执行过,没有执行就给B账户增加100,执行过就抛弃此消息。
Java动态代理
在运行时动态的生成类,并且作为一个真实的对象的代理来做事情。
在运行时对类进行修改,而不是编译时
Java注解
元数据:描述数据的数据。就像是一种加强版的注释,不但有特定的格式,还有特定的含义,别的工具可以通过读取它来进行操作
Java泛型
一个集合只支持一种数据类型,简单,操作方便,编译期可以检查是否有异常,同一种类型,使用的时候不会进行强制转型,如果一个list中既有int,又有string,使用的时候就得小心了。
public class Fruit {
void getWeight(){};
void getHight(){};
}
public class Apple extends Fruit {
@Override
void getWeight(){};
}
@Test
public void test08() {
List<Apple> apples = new ArrayList<>();
apples.add(new Apple());
apples.add(new Apple());
//编译出错,因为apple虽然是orange的子类,但是List<Apple>并不是List<Fruit>的子类,无法转型
print(apples);
}
public void print(List<Fruit> fruits){
for(Fruit f:fruits){
System.out.println(f);
}
}
//如果将入参改为这样,编译就不会有问题了,以占位符代替实例,只要是Fruit类的子类都可以进行操作
public void printNew(List<? extends Fruit> fruits){
for(Fruit f:fruits){
System.out.println(f);
}
}
Spring的本质
有一些非功能性的需求,比如事务,日志,安全,方法执行时间。。。是跨模块的,最简单的方法是将产生和业务混杂的非功能性代码,虽然可以完成功能,但是这些非功能性的代码非常的繁杂,甚至比功能性的代码要多的多,不仅这个类要这么做,其他的类都需要这么做,重复代码会变得很多。
如果使用模板方法模式,定义一个基类
public abstract class Base {
public void execute(){
//添加日志输出
Logger logger = Logger.getLog(...);
logger.debug("...");
//计算方法执行时间
TimeUtil.startTime();
//开启事务
beginTransaction();
//业务代码
doSomething();
//提交事务
commitTransaction();
TimeUtil.endTime();
}
//提供一个子类需要实现的业务操作抽象方法
public abstract void doSomething();
class Payment extends Base{
@Override
public void doSomething(){
//实现子类的业务
}
}
}
Base类已经将非功能性代码已经写好了,只留了一个抽象方法,等待子类去实现。子类变得清清爽爽,只需要关注自身业务逻辑即可。调用如下
public static void main(String[] args) {
Base base = new Base.Payment();
base.doSomething();
}
但是这类方式的巨大缺陷是,全都由父类说的算,父类定义执行一切的代码,子类只能无条件的接受,如果不需要其中的某项功能,也无法去掉。
如果使用装饰者设计模式,可以有更大的灵活性
//定义接口
public interface Command {
void execute();
}
//用于记录日志的装饰器
public class LoggerDecorator implements Command{
Command cmd;
public LoggerDecorator(Command cmd){
this.cmd = cmd;
}
@Override
public void execute() {
Logger logger = Logger.getLogger(...);
//记录日志
logger.debug("...");
this.cmd.execute();
logger.debug("...");
}
}
//记录方式耗时的装饰器
public class PerformanceDecorator implements Command{
Command cmd;
public PerformanceDecorator(Command cmd){
this.cmd = cmd;
}
@Override
public void execute() {
TimeUtil.startTimer();
this.cmd.execute();
TimeUtil.endTimer();
}
}
//下订单操作
public class OrderDecorator implements Command{
@Override
public void execute() {
}
}
如果下订单既需要打印日志,又需要知道耗时,则调用如下:
Command cmd = new LoggerDecorator(new PerformanceDecorator(new OrderDecorator()));
cmd.execute();
如此,可以使用任意数量的装饰器,还可以任意次序组合执行,较为灵活。
AOP
当然,上述的装饰者也有瑕疵,一个处理非功能性的类为什么要实现业务接口呢,如果其他业务类,没有实现,但是也想使用该如何呢?最好的办法就是将这种非功能性代码和业务代码完全隔离开。
比如有一个事务类
//记录方式耗时的装饰器
public class Transaction {
//开始事务
public void begin() {
}
//提交事务
public void commit() {
}
}
如果想对xxx包下的所有类的excute()方法,在调用前加上Transaction.begin(),在调用后加上Transaction.commit(),对于所有类的execute()来说,就是切入,在方法前/后,需要执行xxx,就是通知。这样,非功能性代码和功能性代码就隔离开了,要实现,要是就是修改现有类,在编译期做手脚,将非功能性代码和业务代码编译在一起。要么就是进行动态代理。
动态代理有两种方式实现
1、Java动态代理技术,要求业务类必须有接口才能工作
2、使用CGLib,只要业务类没有被标记成final就可,因为它会生成一个业务类的子类作为代理类
IOC和DI
原来的Java对象,都是需要的时候自己创建自己所依赖的对象,有了Spring容器后,所有的依赖关系都由容器负责,于是控制就反转了。
加密
对称加密:通信双方,用同一个秘钥对数据进行加密,解密
RSA非对称加密:有一对秘钥,一个是保密的,称为私钥,一个是公开的,称为公钥,用私钥加密的数据,对应的公钥才能解开;用公钥加密的数据,对应的私钥才能解开。有两个人,A和B,各自的手里有一对公私钥,并将各自的公钥,交给对方。当A给B发数据时,用B的公钥加密,B取到数据后,用B自己的私钥解密,反之亦然。
但是RSA的加解密的速度比对称的要慢,可以两种加密方式相结合。
- 生成一个对称加密密钥,用RSA的方式分发密钥,将密钥传递给对方
- 随后加解密的时候,就不用RSA,直接使用对称加解密就行了
但是这样还是不会安全,会出现一种情况
当用RSA的方式发送密钥的时候,有一个黑客截取了用户A的公钥,冒充用户A,然后将黑客自己的公钥发给了用户B,用户B发送的消息就用黑客的公钥加了密,黑客就可以用私钥解密,得到用户A、B的对称解密的密钥,以后就可以获取用户A、B的消息了。
归根到底,还是密钥的分发上需要注意保护
将用户A的基本信息和用户A的公钥(下面这两者统称为:原始数据),用Hash算法生成一个消息摘要(这种Hash算法,只有输入数据有一点变化,那么生成的消息摘要就有巨变,防止别人修改内容),然后让有公信力的认证中心(CA)用它的私钥对消息摘要进行加密,形成数字签名。然后将用户A的原始数据和数字签名合并,形成数字证书。
流程就是:原始数据-->(Hash算法)生成消息摘要-->(CA加密)生成数字签名-->(原始数据+数字签名)生成数字证书
当用户B接收到用户A的数字证书后,用同样的Hash算法,将用户A的原始数据生成消息摘要,然后用CA的公钥对数字签名进行解密,得到消息摘要,两个摘要进行比对,一致说明没有被篡改。没有被篡改就可以拿到用户A的公钥,就可以进行加密操作了。
Https中的浏览器和服务器的流程
1、浏览器发出安全请求(Https://www.xxx.com)
2、服务器发送数字证书(包含服务器的公钥)
3、浏览器用预置的CA列表验证证书,如果有问题,则提示有风险,没有问题,则浏览器生成随机的对称密钥,用服务器的公钥加密
4、服务器用自己的私钥进行解密,得到对称密钥
5、双方都知道对称密钥,开始了安全的数据通信
SSO单点登录
定义:用户在一个地方登录过一次后,剩余的系统都可以进行访问。
登录信息存放cookie中,将cookie共享?但是cookie无法跨域,不能将cookie发送到不同的域名下。
可以通过建立一个认证中心,用户登录注册都在那完成。比如用户A先访问www.a.com/list.html
1、该页面是一个需要登录才可访问的页面,如果用户没有登录,就需要多一项工作,重定向到认证中心,www.sso.com/login?redirect=www.a.com/list.html,这样认证后,可以转到需要访问的list.html中
2、当用户在认证中心登录成功后,需要做几件事情,
- 建立一个Session
- 创建一个ticket,可以认为是一个随机字符串
- 重定向到初始访问的页面,url中带着ticket 'www.a.com/list.html?ticket=T123',同时,cookie也将发送到浏览器中,比如 set cookie:ssoid=1234;domain=sso.com
3、然后a系统需要拿着认证中心给的ticket去问一下认证中心,验证ticket的真伪,如果是真的,就给用户建立session,返回list.html资源,供用户访问。还需要给浏览器发送一个cookie,属于a系统的cookie,set cookie:sessionid=1234;domain=a.com(这时候,浏览器实际上就拥有了两个cookie,一个是a系统的,一个是认证中心的)
4、当用户下一个访问另一个受保护页面的时候,就不需要去向认证中心登录了,因为a系统已经将自己的cookie发送给浏览器,浏览器自然会带过来,就可以直接访问了。
5、当用户A要访问B系统时,与上雷同,唯一不同的就是不需要用户去登录了,因为浏览器已经有了认证中心的cookie。
简单来说,本质上就是一个认证中心的cookie,加上多个子系统的cookie,有了单点登录,自然也会有单点退出,用户在一个系统登录了,认证中心需要将自己的会话和cookie消灭,然后通知各个子系统,让其消灭自己的会话和cookie,这样才能实现真正的退出。
第三方登录
一个小众app,从网易邮箱获取用户的数据,进行分析处理,直接向用户获取账户,密码,用户不一定愿意,而且小众app的安全性也没有保证,所以用户在访问小众app时,小众app重定向到网易,这时候用户在网易邮箱登录并且同意小众app可以访问网易邮箱时,会重定向到小众app,并且带token过来,小众app就可以用这个token通过api访问网易邮箱。这样就可以解决用户不信任小众app的安全保密性的问题。(其实主要就是为了获取token)
具体的接入流程:先在网易平台注册,网易会发送一个appId和appSecret,当小众app重定向到网易时,就把这两个发过去,网易就可以是小众app在申请访问了。
但是上述如果在前端完成,较为的不安全,改善后:网易认证中心不直接发token,改发授权码,当小众app获取到授权码后,在后台再次访问网易认证中心,发出真正的token。这个授权码和小众app申请的appId和appSecret关联,只有小众app发送的token请求,网易认证中心才认为合法,且授权码也有时间限制,过期后,得重新获取token,较为安全。
Redis集群
背景:当数据量太大,一台缓存服务器无法存储的时候,多架设几台缓存服务器,但是引发了数据的存储和准确读取的问题。每个redis服务器存的数据都不一样,假设现在有0,1,2三号机器,当数据已经存到了1号机,取数据自然得去1号机取,否则找不到,存储数据的时候也要尽可能的均匀,不能闲的闲死,忙的忙死。
余数算法
对于用户存储的key,计算出key的一个整数Hash值,然后用这个Hash值对服务器数量取模。
比如hash(key1)=100,100%3=1,数据存入1号机,hash(key2)=99,99%3=0,数据存入0号机,以后取的时候,也是如此算法,从对应的机器取数据。但是这种算法,当增删服务器的时候,就会出现问题,比如变成了4台,hash(key1)=100,100%4=0,从0号机取数据根本取不到,因为数据存在了1号机上。
一致性算法
一致性算法虽然不能完全的避免,但是可以控制在一定的区间,减少问题的发生。
- 在一个圈上标注0-2^32个点,假设有三台服务器,利用Hash算法得到每天服务器的Hash值(可以通过服务器的ip或hostname),比如hash(ip1)=hashCode1,就可以把这个服务器的hashCode对应到圈上的某个点上,当日hashCode得小于2^32。
- 存储数据的时候,仍然用hash(key1)=code1,hash(key2)=code2的方式来取值,这时候就将code1,code2映射到圈上的对应位置上,从该位置开始,顺时针旋转,找到第一台机器,就将数据放上去,取的时候,也是如此。(就近原则)
- 当增加一台服务器后,受影响的数据只有一部分,比如,新增的服务器4的位置落在了服务器2和3之间,那么服务器4和服务3之间的数据不会受影响,照样可以访问,减少了问题的发生。
如果服务器进过Hash计算后,在圈上分布的不均匀,完全挤在一起,那么就会发生某些服务器负载过载的情况,解决方案就是“虚拟服务器”,就是把一台真实的服务器当成是多台虚拟机,让其分布在圈上,添加均匀性。
Hash槽
一共16384个槽,每台服务器分管一部分,对key取整数值,不是通过hash算法,而是一种名为CRC16的算法,再对16384取余。当增加了服务器后,可以对key进行迁移,比如,增加了服务器4,那么服务器1,2,3就会迁移一部分的key和数据到服务器4上。而且客户端取数据的时候,也不是向某一台服务器发出请求,而是可以向任意一台服务器发送请求,比如取name的值,向服务器1发出请求,但是1不存在,服务器就会转到key存在的服务器2上面,将数据取出来。
集群的故障转移
现有6个节点,node1,2,3,4,5,6被分为两组,每组服务器有一台是master,剩余的都是slave,比如node1,4是master,2、3、5、6分别是node1、4的从节点。主从节点服务器的数据是一致的,如果主服务器宕机,就根据算法从从服务器选举出一台,当做新的主服务器。
Nginx也是一样的,两台服务器都部署Nginx,通过keeplive,将两台机器设为主从模式,同一时刻,只有一台机器对外访问,另一台待命,当提供服务的机器宕机,则待命的那台就开始工作。并且这两天机器都对外只提供一个ip,看着就是一台机器一样。
高可用:不要闲的闲死,忙的忙死,提高服务器的服务能力。
Mysql的读写分离
设置一个master库,多个slave库,并且master库可读可写,slave库只读,并且将master库的数据同步到slave库中。当tomcat往数据库存储读取数据的时候,不需要管什么数据往master写,什么数据从从库取,添加一个Mysql Proxy中间件,交由这个中间件负责区分。
Node.js特点:只用一个线程来处理所有请求,由事件驱动编程。
命令式编程VS声明式编程
命令式编程:指令清晰,面面俱到
声明式编程:不会说具体怎么做,只会描述要做什么,具体步骤交由别人完成。比如SQL语言,告诉它我需要什么,怎么做由数据库自己完成。
//声明式编程 只告诉你,我想知道年龄小于18的数据,不需要自己处理
int count = students.stream().fifter(s->s.getAge()<18).count();
//命令式编程 需要自己具体做,一步一步地
int count =0;
List<Student> stu = new ArrayList<>();
for(Student s:stu){
if(s.getAge()<18){
count++;
}
}