DNS层、跨机房部署、LVS(Linux Virtual Server的简写,意即Linux虚拟服务器)+Ngnix负载均衡,wanish+共享存储实现动静分离,Ngnix下挂载N台服务器集群,服务器集群挂载微服务化、微服务后挂数据库分库分表+消息队列+任务调度,
最后端挂载数据集群负载数据的统一归档+流计算+异步批处理
根据用户级别设置用户隔离,根据操作日期设计时间隔离,然后考虑分区的 扩容、缩容、灾备、监控
3、数据同步
3.1、不同步,各个子集在各自的业务中运行
3.2、统一的数据汇总集群(hadoop\Spark\Kylin),数据汇总到一个大数据集群中,再进行统计、汇总、计算,缺点是 计算需要时间长(1天+)
3.3、远程数据同步,通过开源框架实现多个数据库的同步,例如阿里的otter,底层为canal,模拟mysql的从库,实现日志解析并数据库入库,时间差较短
与万级并发差不多,但要注意一些细节
1、集群之间要实现从入口开始的严格隔离
DNS(Domain Name System缩写DNS,Domain Name被译为域名)层-》LVS层(Linux Virtual Server的简写,意即Linux虚拟服务器)-》Ngnix层 完全隔离
2、数据库链接是重要资源,一个mysql数据库可以提供1000个链接,若按照50链接/服务器,最多链接20个实例。
因此数据库的分库分表要彻底分开,子集群间不能互相链接数据库
3、(通过监控APP,让一些流量大的业务在缓存中)一些关键业务可以在缓存中操作,建议redis缓存。memcache死机后数据丢失,mongodb功能不完善。
redis安全机制要做好,不能丢数据。缓存到数据库的存储可以用计数形式,每个N次操作存一次数据库,可以线性降低数据库的压力
4、数据库只使用简单的存取功能,所有业务功能在代码中实现,DBA推荐的分区、存储、存储过程等一般在数据仓库中是有用的,而在实时计算系统中,千万不要采用,
否则几百人的开发团队等待一个DBA给你排期
数据仓库是数据库概念的升级,和数据库相比,数据仓库要比数据库更加庞大;数据仓库主要用于分析数据,数据库主要用于捕获数据;数据仓库主要存储历史数据,数据库存储在线交易数据;
数据仓库的基本元素是维度表,数据库的基本元素是事实表。
数据仓库的组成部分包括数据抽取工具、数据库、信息发布系统、数据仓库管理、元数据、数据集市、访问工具。数据仓库的数据建模分为四个阶段,分别是业务建模、领域概念建模、逻辑建模、物理建模。
数据仓库并不能取代数据库,两者是相辅相成的关系,数据仓库主要面向主题设计,数据库主要面向事务的设计。
资料拓展:数据仓库,英文名称为Data Warehouse,可简写为DW或DWH。数据仓库,是为企业所有级别的决策制定过程,提供所有类型数据支持的战略集合。它是单个数据存储,
出于分析性报告和决策支持目的而创建。数据仓库是决策支持系统(dss)和联机分析应用数据源的结构化数据环境。
5、前端可以做一些手段,例如抽奖活动,可以在JS中处理而不要经过后端。
6、消息队列。建议使用一些消息堆积能力强的系统。如:rocketMQ,rabbitMQ. 建议rocketMQ,消息堆积能力强,单机堆积上亿条
7、日志系统,建议kafka,日志系统之后可以添加storm,hdfs,logstash等配套设施
8、网卡流量问题需要严重关注,经常出现的问题是:在某个活动期间,redis网卡流量打满,导致redis无法访问,整个业务暂停。需要公司网络部门对公司内部服务器路由有准确的估算,出现分值之后可以
妥善定位问题并修复。
9、断路器、限流、自动降级。断路器:在RPC的客户端中实现的一些功能,如果发现断路器访问服务端在10秒内访问超过50次且失败率高于50%,则中断次断路器的访问10S,用以保护下游系统。
自动降级:如果发生问题,自动切换到备用程序上,如报错、访问redis失败改访问DB
限流:在RPC服务端中实现一些功能,如对每个IP、每个token进行限制,通过令牌桶算法,每个时间段只允许指定数量的服务通过,否则就拒绝服务调用。
一般断路器使用hystrix,自动降级可以自行实现,也可以使用hystrix的配套设施实现,限流简单自行实现
1、cdn:
CDN的全称是Content Delivery Network,即内容分发网络。CDN是构建在现有网络基础之上的智能虚拟网络,依靠部署在各地的边缘服务器,通过中心平台的负载均衡、内容分发、
调度等功能模块,使用户就近获取所需内容,降低网络拥塞,提高用户访问响应速度和命中率。CDN的关键技术主要有内容存储和分发技术。
2、动态调度 docker容器集群
3、GPU+CPU 计算
4、缓存-》二级缓存-》多级缓存 https://blog.csdn.net/njys1/article/details/82933726
多级缓存:
是指在整个系统架构的不同系统层级进行数据缓存、以提高访问效率
一般会使用nginx本地缓存解决热点缓存问题
使用分布式缓存减少访问回源率
使用tomcat堆缓存用于防止缓存失效/崩溃之后的冲击
5、漏斗限制 即大访问量只有少数请求到服务端
6、集群状态监控
7、转发, 请求转发
8、区域的流量整合 北方,南方 区域最近原则
。。。。
多维度
public static void main(String[] args) {
//j.u.c -> concurrent
ArrayBlockingQueue queue = new AyyayBlockingQueue(100);
//初始 256 线程的线程池
ExecutorService exectorService = Exectors.newFixedThreadPool(256);
//tomcat源码
while(true) {
try {
final Object request = queue.take();//阻塞操作
exectorService.execute(new Runnable() {
@Override
public void run() {
//内存级别IO最快
//cache
System.out.print(request);
}
});
} catch(InterruptedException e) {
e.printStackTrace();
}
}
}
java主流锁
1、线程要不要锁住同步资源
1)锁住 --悲观锁
2)不锁住 --乐观锁
2、锁住同步资源失败,线程要不要阻塞
1)阻塞
2)不阻塞
a、自旋锁 不断尝试获取锁,并且干其他事情
b、适应性自旋锁 不断尝试一定的次数
3、多线程竞争同步资源的流程细节有没有区别
1)不锁住资源,多个线程中只有一个线程能修改资源成功,其他线程会重试 --无锁
2)同一个线程执行同步资源时自动获取资源 --偏向锁
3)多个线程竞争同步资源时,没有获取资源的线程自旋等待释放锁 --轻量级锁
4)多个线程竞争同步资源时,没有获取资源的线程阻塞等待唤醒 --重量级锁
4、多个线程竞争锁时要不要排队
1)排队–公平锁
2)先尝试插队,插队失败再排队–非公平锁
5、一个线程中的多个流程能不能获取同一把锁
1)能–可重入锁
2)不能 --非可重入锁
6、多线程能不能共享一把锁
1)能–共享锁
2)不能 --排他锁、独占锁
六、无锁CAS机制(compare and swap 比较和交换) jdk1.8 2013年6月出现, CAS(V, A, B) V:内存中的值, A:预期值(存在工作内存中的变量副本), B:目标值
1、SSO(单点登录SingleSignOn)会用到CAS https://blog.csdn.net/qq_24708791/article/details/78535565
SSO的CAS不是无锁CAS, SSO的Cas服务端其实就是一个war包。
2、AQS https://blog.csdn.net/mulinsen77/article/details/84583716
1)思想:如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并将共享资源设置为锁定状态,如果被请求的共享资源被占用,
那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制,这个机制AQS是用CLH队列锁实现的,即将暂时获取不到锁的线程加入到队列中。
CLH(Craig,Landin,and Hagersten,CLH锁是一个自旋锁。能确保无饥饿性。提供先来先服务的公平性。)队列是一个虚拟的双向队列,虚拟的双向队列即不存在队列实例,仅存在节点之间的关联关系。
AQS是将每一条请求共享资源的线程封装成一个CLH锁队列的一个结点(Node),来实现锁的分配。
用大白话来说,AQS就是基于CLH队列,用volatile修饰共享变量state,线程通过CAS去改变状态符,成功则获取锁成功,失败则进入等待队列,等待被唤醒。
**注意:AQS是自旋锁:**在等待唤醒的时候,经常会使用自旋(while(!cas()))的方式,不停地尝试获取锁,直到被其他线程获取成功
实现了AQS的锁有:自旋锁、互斥锁、读锁写锁、条件产量、信号量、栅栏都是AQS的衍生物
2)AQS维护了一个volatile int state和一个FIFO线程等待队列,多线程争用资源被阻塞的时候就会进入这个队列。state就是共享资源,其访问方式有如下三种:
getState();setState();compareAndSetState();
AQS 定义了两种资源共享方式:
a、Exclusive:独占,只有一个线程能执行,如ReentrantLock
b、Share:共享,多个线程可以同时执行,如Semaphore、CountDownLatch、ReadWriteLock,CyclicBarrier
3、历史
在JDK 5之前,Java语言靠synchronized(1.1就有了)关键字保证同步,这会导致锁
java语言诞生(1995)之前就有并发,初期硬件利用率不高,直到jni(java native interface)出现,Doug Lea大神提供J.U.C包,CAS是基础
存在的问题:
1)在多线程的竞争下,加锁、释放锁会导致较多的上下文切换和调度延时,引起性能问题
2)一个线程持有锁会导致其他需要此资源的线程挂起
3)如果一个优先级高的线程等待一个优先级低的线程释放锁,会导致优先级倒置。
Volatile是一个解决方案,但是它不保证原子性,因此同步最终还要回归到锁机制
4、jmm内存模型, 一个CPU代表一个线程
CPU(寄存器、缓存) <- 总线BUS -> 物理内存
CPU 与 物理内存 通信通过 总线BUS,即 内存资源 《–》总线 《–》CPU
若资源被重量级锁 锁住,则是 锁住总线BUS,只有获取锁的CPU可以读取总线。
锁住总线的锁是悲观锁--------------------------------------------------------------
5、synchronized(jdk1.6优化)与Lock都是悲观锁
两者效率差不多,只是实现方式不一样。
synchronized是在JVM层次上加锁,Lock是在代码层次加锁
乐观锁:获取数据时不加锁,更新数据时Compare,根据不同的实现方式执行不同的操作(例如报错或自动重置)。比如java的CAS
6、CAS原理*******************************************************************************************************************************************
CPU(central processing unit 中央处理器):中央处理器主要包括两个部分,即控制器、运算器,其中还包括高速缓冲存储器及实现它们之间联系的数据、控制的总线
cpu下一级缓存L1、二级缓存L2、三级缓存L3、内存Memory, L1最快 ,缓存速度 纳秒级别, 内存速度 毫秒级别, 越靠近CPU越快
CAS调用硬件方法即 native interface, C++或C实现,
CPU指令 CMPXCHG --> CMP:compare 比较, XCHG:exchange 交换
cpu组成:控制器、运算器、缓存、寄存器
CAS带来的问题:
ABA问题:CAS在操作的时候会检查变量的值是否被更改过,如果没有则更新值,但是带来一个问题,最开始的值是A,接着变成B,最后又变成了A。经过检查这个值确实没有修改过,因为最后的值还是A,
但是实际上这个值确实已经被修改过了。为了解决这个问题,在每次进行操作的时候加上一个版本号,每次操作的就是两个值,一个版本号和某个值,A——>B——>A问题就变成了1A——>2B——>3A。
在jdk中提供了AtomicStampedReference类解决ABA问题,用Pair这个内部类实现,包含两个属性,分别代表版本号和引用,在compareAndSet中先对当前引用进行检查,再对版本号标志进行检查,
只有全部相等才更新值。
时间问题:看起来CAS比锁的效率高,从阻塞机制变成了非阻塞机制,减少了线程之间等待的时间。每个方法不能绝对的比另一个好,在线程之间竞争程度大的时候,
如果使用CAS,每次都有很多的线程在竞争,而锁可以避免这些状况,相反的情况,如果线程之间竞争程度小,使用CAS是一个很好的选择。
缓存架构
1)cpu第一代
通过 CXX star 类总线方式在CPU cache间通信,将 已经改变的变量通知到其他CPU
cpu cpu
star(CXX)
cpu cpu
2)cpu第二代 通过RingBus通信(在CPU间形成环) ring:环绕
cpu —》《-- cpu
| | ||
cpu --》 《-- cpu
3)cpu第三弹 Mesh架构 mesh:网状物。
cpu间可以之间通信, 2代中 1通信3,要经过2, 3代中直接1通信3
7、总结
1)Synchronized 锁总线,其他线程卡死(颗粒大)
2)CAS锁缓存行,通过cpu cache总线和mise协议通知其他cpu(颗粒小)
3)cpu cache速度远高于总线和内存
8、多线程基本 java visualVM:jdk自带java线程状态可视化工具 visual:视觉的
8.1、
A、进程起源
进程控制块(Process Control Block, PCB)
是为了管理进程设置的一个数据结构。是系统感知进程存在的唯一标志。
通常包含如以下的信息:
(1)进程标识符(唯一)
(2)进程当前状态,通常同一状态的进程会被放到同一个队列;
(3)进程的程序和数据地址
(4)进程资源清单。列出所拥有的除CPU以外的资源记录。
(5)进程优先级。反应进程的紧迫程度
(6)CPU现场保护区。记录中断时的CPU状态
(7)进程队列的PCB的链接字。
(9)进程相关的其他信息。记账用的,如占用CPU多长时间等。
B、线程控制块(TCB: Thread Control Block)
1)线程ID
2)栈,局部变量、参数
3)栈指针
4)程序计数器
5)通用目的计数器
6)条件码,即各种状态标志
tcb比pcb的保存内容少
8.2、线程起源
进程 同一时间只能执行一个操作,切换进程或线程叫做上下文切换,进程更消耗资源,而线程可以交替的执行不同操作
1)进程中包含(PCB管理)
code data files
2)一个进程下的N个线程,各自独有
register(记录程序地址) stack
切换线程,只需切换register stack,而进程还要多出 code,data,files
8.3Linux Threads
线程被叫成 tasks, 文件为 task.c, 创建线程用 clone()方法
8.4线程状态
new 新建 -》runnable 就绪 -》获得CPU执行时间 running -》异常中断或者执行完成 dead
running -》CPU时间片用完暂时交出CPU执行时间 runnable
-》被同步块阻塞(synchronized()锁住此类)或者IO阻塞 blocked(阻塞状态) -》同步块释放或者IO完成 runnable
-》主动睡眠(sleep()) time waiting -》睡眠到时间 runnable
-》主动等待(锁住一个Object o对象,通过o.wait()阻塞, o.notify()唤醒) waiting -》等待被唤醒(notify)runnable
8.5 线程池ThreadPoolExecutor
1)线程池中的BlockingQueue workQueue存放task,即threadPool.execute(Runnable command)方法下, workQueue.offer(command),
线程池中的Worker类集合HashSet
线程池中的 Worker类中的Thread的start方法调用的是 Worker的run方法,run方法调用的是传进来的Runnable的run方法
2)线程池模型:
调用方 发送请求 --》 queue(接收 task) --》while轮询queue中的task放到Worker中, 线程池中的workers是一个Worker集合
| | |
RejectedExecutionHandler BlockingQueue ThreadFactory创建Worker,corePoolSize(即线程最小)是workers最小数,maximumPoolSize(线程最大)是workers最大数, 存活keepAliveTime/unit
3)线程池构造方法(参数最多的)corePoolSize不是初始化线程,这是驻留线程,即没有任务时永久存活。初始化线程池时不创建,是当有任务请求时开始创建线程
public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue workQueue
, ThreadFactory threadFactory, RejectedExecutionHandler handler) {
线程池初始化大小为 corePoolSize 即线程池最低存活corePoreSize个线程,任务一来,无论池中是否有空闲线程,只要 当前线程数 < corePoolSize,就会创建新线程,若线程 > corePoolSize ,则把
任务 放到 workQueue, 若workQueue也满了,并且 池中的线程数 = maximumPoolSize, 则根据handler处理多余的任务
所有任务都是先放到 queue 中,然后轮询 将 task 放到 新建的 Worker 中执行
图示: 0 < active < corePoolSize --> 活跃线程 < 初始化线程, 线程会一直存活
corePoolSize < active < maximumPoolSize --> queue满时,才会出现此种情况。
活跃线程中,多于 corePoolSize的额外线程只能存活 keepAliveTime/unit,并且queue满的时候,线程才会创建多于corePoolSize
active == maximumPoolSize --> 活跃线程中,多于 corePoolSize的额外线程只能存活 keepAliveTime/unit
4)handler
a、DiscardPolicy :抛弃当前请求的任务
b、DiscardOldestPolicy :抛弃队列最老的任务
c、AbortPolicy : 忽略当前任务,抛出异常(默认) abort:中止
d、CallerRunsPolicy :利用请求线程执行当前任务,但是会阻塞请求线程而页面上没有返回而造成页面卡住
8.6 ThreadLocal
就是当前线程内 一个继承WeakReference的hash表, key为当前线程, value是存进去的值
static class Entry extends WeakReference
Object value;
Entry(ThreadLocal> k, Object v) {
super(k);
this.value = v;
}
}
9、队列Disruptor(干扰器) Log4j2日志框架、storm用到了
日志框架 logger.info("") -->控制台打印信息,并写入文件, 是阻塞操作 !!!!!!
若不想阻塞操作,即使用队列Disruptor框架
Disruptor 是实现了“队列”的功能,而且是一个有界队列。那么它的应用场景自然就是“生产者-消费者”模型的应用场合了。
可以拿 JDK 的 BlockingQueue 做一个简单对比,以便更好地认识 Disruptor 是什么。
我们知道 BlockingQueue 是一个 FIFO 队列,生产者(Producer)往队列里发布(publish)一项事件(或称之为“消息”也可以)时,消费者(Consumer)能获得通知;
如果没有事件时,消费者被堵塞,直到生产者发布了新的事件。
这些都是 Disruptor 能做到的,与之不同的是,Disruptor 能做更多:
同一个“事件”可以有多个消费者,消费者之间既可以并行处理,也可以相互依赖形成处理的先后次序(形成一个依赖图);
预分配用于存储事件内容的内存空间;
针对极高的性能目标而实现的极度优化和无锁的设计;
以上的描述虽然简单地指出了 Disruptor 是什么,但对于它“能做什么”还不是那么直截了当。一般性地来说,当你需要在两个独立的处理过程(两个线程)之间交换数据时,
就可以使用 Disruptor 。当然使用队列(如上面提到的 BlockingQueue)也可以,只不过 Disruptor 做得更好。
9.1、java内置队列 底层三种:数组/链表/堆
ArrayBlockingQueue, 有界性 bounded, 加锁,数据结构arraylist
LinkedBlockingQueue, 有界性 optioned-bounded, 加锁,数据结构linkedlist
ConcurrentLinkedQueue, 有界性 unbounded, 无锁,数据结构linkedlist
LinkedTransferQueue, 有界性 unbounded, 无锁,数据结构linkedlist
PriorityBlockingQueue, 有界性 unbounded, 加锁,数据结构heap
DelayQueue, 有界性 unbounded, 加锁,数据结构heap
在稳定性要求特别高的系统中,防止生产者速度过快,导致内存溢出,使用有界队列。为了减少java GC对系统性能的影响,会尽量选择array/heap 数据结构。
所以符合的就是ArrayBlockingQueue, 但在实际中,会因为加锁和伪共享出现性能问题
伪共享:CPU缓存系统中是以缓存行(cache line)为单位存储的。目前主流的CPU Cache的Cache Line大小都是64Bytes, 有的是128Bytes。在多线程情况下,如果需要修改“共享同一个缓存行的变量”,
就会无意中影响彼此的性能,这就是伪共享(False Sharing)。
ArrayBlockingQueue:有变量int类型(4个字节), takeIndex、putIndex、count, 若要修改 takeIndex,会从主存读取 takeIndex、putIndex、count 放在 cache line(64个字节)上,
修改完后就失效,若要再次修改其他的,又要重新读。
这种无法充分利用缓存行特性的现象叫伪共享, 使得缓存命中率低。
一般解决方案:增大数组元素的间隔,使得不同线程存取的元素位于不同的缓存行上,以空间换时间。
public final static class ValuePadding() {
protected long p1, p2, p3, p4, p5, p6, p7;
protected volatile long value = 0L;
protected long p9, p10, p11, p12, p13, p14;
protected long p15;
}
Cache Line大小都是64Bytes, 有的是128Bytes
若定义 ValuePadding[] arrs = new ValuePadding[3]; 大小为3的数组
若 cache line 大小为 64Bytes, 则读取数组p1, p2, p3, p4, p5, p6, p7, value -->第一个值
p1, p2, p3, p4, p5, p6, p7, value -->第二个值 等等, 这些直接在CPU重复使用, 而无需重新读取
disruptor框架中的RingBuffer即用了这个填充方式 !!!!!!!!!!!!!!!!!!!!
10、从类加载器到访问权限
1)java 3种安全机制
a、语言设计特性(数组边界的检查、无指针运算 等)
b、访问控制机制,控制代码能够执行的操作(文件、网络访问等)
c、代码签名、SSL证书
2)类加载器
–java项目从编写到运行
a、编写代码 .java文件
b、编译代码 .class文件
c、java编译器为虚拟机转换源指令 即将class文件转换为机器语言
–类加载过程。用不同的类加载器加载的!!!!!!!!!!!!!
a、虚拟机有一个类文件加载机制,读取项目文件
b、若项目文件涉及到其他的类文件,会读取其他类文件(聚合)和 超类(继承)
c、虚拟机执行main()函数
d、若main()函数涉及到其他类文件,虚拟机一样加载这些文件
-----类加载器种类, 加载顺序 c -> b -> a 先 c 再 b 再 a
a、引用类加载器:加载系统类(jdk中的类)(C语言实现的,例如rt.jar)
b、扩展类加载器:加载jre/lib/ext下的 标准的扩展 (java实现,例如jdbc的jar包)
c、系统(应用)类加载器:加载应用类(个人实现的类),由classpath 环境变量或者用-classpath指定的值搜索并加载, jar/zip/war等压缩包(java实现)
d、插件类加载器(自定义的类加载器),负责加载插件
3)应用场景 现在都是saas、paas平台授权账号
a、项目需要授权才能使用(插件类加载器)
b、通过类加载过程加密,用自定义类加载器加载
项目源码 编译(加密关键类)–》可执行文件(包含编译文件) 以前是通过jdk的类加载器加载,现在是自定义插件类加载器加载–》 可以运行的文件
自定义类加载器继承了 ClassLoader,并且实现findClass方法
c、java编译器校验字节码, 校验故意被篡改的类文件
变量要在使用前初始化
方法调用与对象引用类型要匹配
访问私有变量和方法的规则没有被违反
对本地变量的访问都落在运行时的堆栈内
运行时堆栈没有溢出
4)java安全管理器,java默认不打开。 若要打开 代码显示调用 System.setSecurityManager(new SecurityManager());
权限文件在 jdk下的conf/security下的policy文件
可以手动修改权限 System.setProperty(“java.security.policy”, “自定义权限文件”)
自定义文件内容 :grant {
permission java.io.FilePermission “本地路径许可” “read/write”; //读写文件许可
}
自定义许可:MyPermission extends Permission
SecurityManager securityManager = System.getSecurityManager();
securityManager.checkPermission(myPermission);
4.1)Web权限概念
通过登录,可以调用后台的增删查改。 权限设计基于RBAC模型(Role-Based Access Control:基于角色的访问控制), shiro框架等
4.2)java权限概念
文件、网络、属性等是否可以操作读取等。
创建文件、访问Socket的某个端口、读取系统环境变量
4.3)安全管理器历史
a、JDK1.2之前
–JDK1.0 本地类与远程操作类(Applet)
本地类拥有所有权限
Applet在沙盒内运行
–JDK1.1
改进:Applet --》 认证过的拥有所有权限, 未认证的依然沙盒运行
b、从1.2往后,代码来源与访问权限之间搭建映射关系
代码来源:由一个代码位置和一个整数集进行制定
访问权限:安全管理器负责检查任何属性
4.4)应用场景
多项目合作:RPC、RMI 远程方法调用(Remote Method Invocation),一种用于实现远程过程调用(RPC)(Remote procedure call)的Java API, 能直接传输序列化后的Java对象和分布式垃圾收集。
它的实现依赖于Java虚拟机(JVM),因此它仅支持从一个JVM到另一个JVM的调用。