个人瞎写,仅供参考;部分转载,逐渐完善。
- Java基础
- Java 并发、多线程、线程池
- Spring
- 设计模式
- Netty
- 分布式相关
- 数据库
- 缓存
- JVM
- 系统优化
- HTTP、TCP
- 手写代码
- ES
list和set是实现了collection接口,map不是collection的子接口或者实现类,Map是一个接口。
List:
1.可以允许重复的对象。
2.可以插入多个null元素。
3.是一个有序容器,保持了每个元素的插入顺序,输出的顺序就是插入的顺序。
4.常用的实现类有 ArrayList、LinkedList 和 Vector。ArrayList 最为流行,它提供了使用索引的随意访问,而 LinkedList 则对于经常需要从 List 中添加或删除元素的场合更为合适。
Set:
1.不允许重复对象
2. 无序容器,你无法保证每个元素的存储顺序,TreeSet通过 Comparator 或者 Comparable 维护了一个排序顺序。
3. 只允许一个 null 元素
4.Set 接口最流行的几个实现类是 HashSet、LinkedHashSet 以及 TreeSet。最流行的是基于 HashMap 实现的 HashSet;TreeSet 还实现了 SortedSet 接口,因此 TreeSet 是一个根据其 compare() 和 compareTo() 的定义进行排序的有序容器。
Map:
1.Map不是collection的子接口或者实现类。Map是一个接口。
2.Map 的 每个 Entry 都持有两个对象,也就是一个键一个值,Map 可能会持有相同的值对象但键对象必须是唯一的。
3. TreeMap 也通过 Comparator 或者 Comparable 维护了一个排序顺序。
4. Map 里你可以拥有随意个 null 值但最多只能有一个 null 键。
5.Map 接口最流行的几个实现类是 HashMap、LinkedHashMap、Hashtable 和 TreeMap。(HashMap、TreeMap最常用)
首先会调用Object的hashCode方法判hashCode是否已经存在,如不存在则直接插入元素;
如果已存在则调用Object对象的equals方法判断是否返回true,如果为true则说明元素已经存在,如为false则插入元素。
不是线程安全的,当A、B两个线程,将数据同时put到同一个HashMap中,会引起碰撞,造成数据被覆盖。
switch支持String类型;
int、short、long、byte 可以用二进制表示;
新增Lambda表达式;
HashMap中,1.7中当HashCode值相同时,会将数据排成链表,查询时间复杂度比较高,需要遍历链表;1.8中当链表过长时,会将链表扩展成红黑树,使查询效率变高。
final关键字,可以修饰类、方法、变量。修饰类,这个类不能被继承;修饰方法,这个方法不能被重写;修饰变量,需要在声明变量时,赋初始值,之后不能修改。
finally是在异常处理时提供finally块来执行任何清除操作。不管有没有异常被抛出、捕获,finally块都会被执行。
finalize是方法名。finalize()方法是在垃圾收集器删除对象之前对这个对象调用的。
强引用实际使用中大多数是强引用,String str = “abc”; list.add(str);
软引用,jvm在内存中有高速缓存,将对象放到里面,读取数据时,提高运行效率;
Java利用现有对象方法,获取当前对象的某些属性,例如:Object类的getClass()方法
LinkedHashMap是在HashMap基础上新增了双向链表,用于保存迭代顺序。
Error(错误):是程序无法处理的错误,表示运行应用程序中较严重问题。
runtimeException子类:
java.lang.ArrayIndexOutOfBoundsException数组索引越界异常。当对数组的索引值为负数或大于等于数组大小时抛出。
java.lang.NullPointerException空指针异常。当应用试图在要求使用对象的地方使用了null时,抛出该异常。譬如:调用null对象的实例方法、访问null对象的属性、计算null对象的长度、使用throw语句抛出null等等
java.lang.ClassNotFoundException找不到类异常。当应用试图根据字符串形式的类名构造类,而在遍历CLASSPAH之后找不到对应名称的class文件时,抛出该异常。
IOException子类:
IOException:操作输入流和输出流时可能出现的异常。
FileNotFoundException 文件未找到异常
其他类:
SQLException:操作数据库异常类
StringIndexOutOfBoundsException 字符串索引超出范围抛出的异常
wait()方法属于Object类;sleep()方法属于Thread类;
调用wait()方法的过程中,线程会释放对象锁;调用sleep()方法的过程中,线程不会释放对象锁。
//定义第一个数组
int[] arr=new int[3];
arr[0]=10;
arr[1]=20;
arr[2]=70;
第一步:栈存储局部变量(在方法定义中或方法声明上的变量),所以int[] arr 存放在了栈中;
第二步:new出的变量放在堆中,所以new int【3】在堆中。
第三步:每一个new出来的东西都有地址值(系统随机分配),所以new int【3】的地址值为0x001;把0x001赋给arr,在栈中的数组通过地址值找到堆中相应的地址。用数组名和编号的配合就可以找到数组中指定编号的元素,这种方法就叫做索引。
第四步:int类型的数据默认值为0
第五步:给数组中的每个元素赋值,把原先的默认值0干掉。
延伸:
当加上static修饰,变成 static int[] arr=new int[3];
arr 会被放在方法区(持久代/非堆)中。
1)代码块加锁
反编译后可发现执行代码中有“monitor”(监视器锁)的两个方法 monitorenter、monitorexit
指令执行时,monitor的进入数减1,如果减1后进入数为0,那线程退出monitor,不再是这个monitor的所有者。其他被这个monitor阻塞的线程可以尝试去获取这个 monitor 的所有权。
通过这段描述,我们应该能很清楚的看出Synchronized的实现原理,Synchronized的语义底层是通过一个monitor的对象来完成,其实wait/notify等方法也依赖于monitor对象,这就是为什么只有在同步的块或者方法中才能调用wait/notify等方法,否则会抛出java.lang.IllegalMonitorStateException的异常的原因。
2)方法上加锁
反编译后可发现执行代码中有ACC_SYNCHRONIZED标示符
JVM就是根据该标示符来实现方法的同步的:当方法调用时,调用指令将会检查方法的 ACC_SYNCHRONIZED 访问标志是否被设置,如果设置了,执行线程将先获取monitor,获取成功之后才能执行方法体,方法执行完后再释放monitor。在方法执行期间,其他任何线程都无法再获得同一个monitor对象。
附:对静态方法的同步本质上是对类的同步。
参考文章:https://www.cnblogs.com/paddix/p/5367116.html
Synchronized修饰普通同步方法:锁对象当前实例对象;
Synchronized修饰静态同步方法:锁对象是当前的类Class对象;
Synchronized修饰同步代码块:锁对象是Synchronized后面括号里配置的对象;
无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态(级别从低到高)
参考文章:https://blog.csdn.net/tongdanping/article/details/79647337
参考文章:https://yq.aliyun.com/articles/640868
保证对象可见性,当线程处理对象时,会从主存中copy一份副本到工作内存(寄存器),然后进行操作。当对象加上这个关键字后,进行操作时,每次都会从主存中读取最新的数据,再结合多个工作内存的一致性算法,从而避免多个线程操作对象时的数据准确问题。
修饰静态方法,保证多个线程执行时,进行加锁操作,使之相互不干扰;
修饰普通方法,没有用处,因为普通方法,存储在单独的 本地方法栈中,是私有的。
闭锁CountDownLatch,适用一组线程执行完,再执行后面的线程
闭锁是典型的等待事件发生的同步工具类,将闭锁的初始值设置1,所有线程调用await方法等待,当事件发生时调用countDown将闭锁值减为0,则所有await等待闭锁的线程得以继续执行。
join()方法,保证线程执行顺序。
两个方法都可以向线程池提交任务,
execute()方法的返回类型是void,它定义在Executor接口中;
submit()方法可以返回持有计算结果的Future对象,它定义在ExecutorService接口中,它扩展了Executor接口,其它线程池类像ThreadPoolExecutor和ScheduledThreadPoolExecutor都有这些方法。
synchronized是java关键字;Lock是一个类;
synchronized不需要用户去手动释放锁,当synchronized方法或者synchronized代码块执行完之后,系统会自动让线程释放对锁的占用;而Lock则必须要用户去手动释放锁,如果没有主动释放锁,就有可能导致出现死锁现象;
多个线程进行读操作,用synchronized锁,当一个线程在进行读操作时,其他线程只能等待无法进行读操作;用lock锁,线程之间不会发生冲突,可以进行读操作;
通过Lock可以知道线程有没有成功获取到锁,synchronized没办法知道;
synchronize对线程的同步仅提供独占模式,而Lock即可以提供独占模式,也可以提供共享模式;
Lock可实现公平锁,需要参数设置,默认是非公平锁;synchronize只能是非公平锁。
读写锁是用来提升并发程序性能的锁分离技术的成果。
Java中的ReadWriteLock是Java 5 中新增的一个接口,一个ReadWriteLock维护一对关联的锁,一个用于只读操作一个用于写。在没有写线程的情况下一个读锁可能会同时被多个读线程持有。写锁是独占的,你可以使用JDK中的ReentrantReadWriteLock来实现这个规则,它最多支持65535个写锁和65535个读锁。
ThreadLocal和Synchonized都用于解决多线程并发訪问。
可是ThreadLocal与synchronized有本质的差别。
synchronized是利用锁的机制,使变量或代码块在某一时该仅仅能被一个线程訪问。它用于在多个线程间通信时可以获得数据共享。
ThreadLocal为每个线程都提供了变量的副本,使得每个线程在某一时间訪问到的并非同一个对象,这样就隔离了多个线程对数据的数据共享。
Synchronized用于线程间的数据共享,而ThreadLocal则用于线程间的数据隔离。
HashMap在高并发下可能引起死循环,造成cpu占用过高。
假如有两个线程P1、P2,以及链表 a=》b=》null
P1先运行,运行完"Entry next=e.next;"代码后发生堵塞,或者其它情况不再运行下去,此时e=a。next=b
而P2已经运行完整段代码,于是当前的新链表newTable[i]为b=》a=》null
P1又继续运行"Entry next=e.next;
"之后的代码,则运行完"e=next;"后,newTable[i]为a《=》b。则造成回路,while(e!=null)一直死循环
个人理解翻译下:
口->a->b->c->null
A线程执行remove(a)方法,将a节点拆除,将b节点拼接到根节点上;
B线程执行put(a)方法,将a节点put到c节点后,这个时候,结构变为:口->b->c->a->b->c->a....;//后面造成循环结构
get()方法;当查询时,进行链表循环,由于是循环结构,所以会一直查询,造成死循环。
ConcurrenHashMap锁方式是稍微细粒度的,内部采用分离锁(分段锁)的设计。它默认将Hash表分为16个分段,get,put,remove等常用操作只锁当前需要用到的分段;
JDK1.8相较1.7,当链表长度大于8时,会将链表结构转成红黑树,便于大数据量的查询,使时间复杂度由O(n)变成O(logN)
分析代码;
用jstack工具查找,jstack是java虚拟机自带的一种堆栈跟踪工具;
加锁顺序(线程按照一定的顺序加锁)
加锁时限(线程尝试获取锁的时候加上一定的时限,超过时限则放弃对该锁的请求,并释放自己占有的锁)
用Jconsole工具检测,java自带的工具。
Java内存模型:
JVM 内存模型:
使用锁机制,实现i++原子操作:
public static ReentrantLock lock = new ReentrantLock();
或者
synchronized (SynchronizedTest.class) {
count++;
}
newCachedThreadPool:执行很多短期异步的小程序或者负载较轻的服务器
newFixedThreadPool:执行长期的任务,性能好很多
newSingleThreadExecutor:一个任务一个任务执行的场景
NewScheduledThreadPool:周期性执行任务的场景
详见文章:https://www.jianshu.com/p/6c6f396fc88e
N为CPU核心数
CPU密集型:N + 1
IO密集型:2N + 1
如果当前池大小 poolSize 小于 corePoolSize ,则创建新线程执行任务。
如果当前池大小 poolSize 大于 corePoolSize ,且等待队列未满,则进入等待队列
如果当前池大小 poolSize 大于 corePoolSize 且小于 maximumPoolSize ,且等待队列已满,则创建新线程执行任务。
如果当前池大小 poolSize 大于 corePoolSize 且大于 maximumPoolSize ,且等待队列已满,则调用拒绝策略来处理该任务。
线程池里的每个线程执行完任务后不会立刻退出,而是会去检查下等待队列里是否还有线程任务需要执行,如果在 keepAliveTime 里等不到新的任务了,那么线程就会退出。
Session管理:
private static final ThreadLocal threadSession = new ThreadLocal();
public static Session getSession() throws InfrastructureException {
Session s = (Session) threadSession.get();
try {
if (s == null) {
s = getSessionFactory().openSession();
threadSession.set(s);
}
} catch (HibernateException ex) {
throw new InfrastructureException(ex);
}
return s;
}
/**
* 简单死锁程序
* lockA、lockB分别是两个资源,线程A、B必须同是拿到才能工作
* 但A线程先拿lockA、再拿lockB
* 线程先拿lockB、再拿lockA
* @author xuexiaolei
* @version 2017年11月01日
*/
public class SimpleDeadLock {
public static void main(String[] args) {
Object lockA = new Object();
Object lockB = new Object();
A a = new A(lockA, lockB);
B b = new B(lockA, lockB);
a.start();
b.start();
}
static class A extends Thread{
private final Object lockA;
private final Object lockB;
A(Object lockA, Object lockB) {
this.lockA = lockA;
this.lockB = lockB;
}
@Override public void run() {
synchronized (lockA){
try {
Thread.sleep(1000);
synchronized (lockB){
System.out.println("Hello A");
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
static class B extends Thread{
private final Object lockA;
private final Object lockB;
B(Object lockA, Object lockB) {
this.lockA = lockA;
this.lockB = lockB;
}
@Override public void run() {
synchronized (lockB){
try {
Thread.sleep(1000);
synchronized (lockA){
System.out.println("Hello B");
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
@Controller对应表现层的Bean,也就是Action;
@Service对应的是业务层Bean;
@Repository用于标注数据访问组件,即DAO组件;
@Autowired 默认按类型装配;
@Resource默认按名称装配;//这个是java的注解,不是Spring的。
@Async异步方法调用;
详见文章:https://blog.csdn.net/u012888052/article/details/86589997
BIO:同步阻塞I/O 1.4之前只有BIO 适用于连接数目比较小且固定的架构,这种方式对服务器资源要求比较高,并发局限于应用中
NIO:同步非阻塞I/O 1.4开始有NIO 适用于连接数目多且连接比较短(轻操作)的架构,比如聊天服务器
AIO:异步非阻塞I/O 1.7开始有AIO 适用于连接数目多且连接比较长(重操作)的架构,比如相册服务器
利用消息中间件,进行两阶段提交。
消息中间件也可称作消息系统 (MQ),它本质上是一个暂存转发消息的一个中间件。在分布式应用当中,我们可以把一个业务操作转换成一个消息,比如支付宝的余额转如余额宝操作,支付宝系统执行减少余额操作之后向消息系统发一个消息,余额宝系统订阅这条消息然后进行增加账户金额操作。
发送端的可靠性:
在本地数据建一张消息表,将消息数据与业务数据保存在同一数据库实例里,这样就可以利用本地数据库的事务机制。事务提交成功后,将消息表中的消息转移到消息中间件,若转移消息成功则删除消息表中的数据,否则继续重传。
接收端的可靠性
保证接收端处理消息的业务逻辑具有幂等性:只要具有幂等性,那么消费多少次消息,最后处理的结果都是一样的。保证消息具有唯一编号,并使用一张日志表来记录已经消费的消息编号。
1)数据库分布式锁
采用乐观锁增加版本号
根据版本号来判断更新之前有没有其他线程更新过,如果被更新过,则获取锁失败。
2)Redis 分布式锁
利用高版本的Redis,中的set命令进行加锁,再利用redis + lua脚本一条命令解锁。吞吐量高
3)Zookeeper 分布式锁
Zookeeper 是一个为分布式应用提供一致性服务的软件,例如配置管理、分布式协同以及命名的中心化等,这些都是分布式系统中非常底层而且是必不可少的基本功能,但是如果自己实现这些功能而且要达到高吞吐、低延迟同时还要保持一致性和可用性,实际上非常困难。
底层是树形结构维护的。
将session存储到分布式Redis中。
1)利用UUID
优点:
生成性能高,可本地生成,没有网络消耗;
缺点:
长度太长,不适合存储;
信息不安全,可能泄露MAC地址;
ID作为主键时在特定的环境会存在一些问题,比如做DB主键的场景下,UUID就非常不适用。
2)SnowFlake雪花算法
优点:
简单高效,生成速度快。
时间戳在高位,自增序列在低位,整个ID是趋势递增的,按照时间有序递增。
灵活度高,可以根据业务需求,调整bit位的划分,满足不同的需求。
缺点:
依赖机器的时钟,如果服务器时钟回拨,会导致重复ID生成。
在分布式环境上,每个服务器的时钟不可能完全同步,有时会出现不是全局递增的情况。
3)数据库自增ID
优点:
简单。充分借助数据库的自增ID机制,可靠性高,生成有序的ID。
缺点:
ID生成依赖数据库单机的读写性能。
依赖数据库,当数据库异常时整个系统不可用。
4)Redis生成(推荐)
优点:
不依赖于数据库,灵活方便,且性能优于数据库。
数字ID天然排序,对分页或者需要排序的结果很有帮助。
缺点:
如果系统中没有Redis,还需要引入新的组件,增加系统复杂度。
需要编码和配置的工作量比较大。这个都不是最大的问题。
5)其他
比如美团点评的Leaf-snowflake,用zookeeper解决了各个服务器时钟回拨的问题,弱依赖zookeeper。以及Leaf-segment类似上面数据库批量ID获取的方案。
原子性;一致性;隔离性;持久性;
Serializable (串行化):可避免脏读、不可重复读、幻读的发生。
Repeatable read (可重复读):可避免脏读、不可重复读的发生。
Read committed (读已提交):可避免脏读的发生。
Read uncommitted (读未提交):最低级别,任何情况都无法保证。
现在最常用的存储引擎是InnoDB,它从MySQL 5.5.5版本开始成为了默认存储引擎。
客户端如果太长时间没动静,连接器就会自动将它断开。这个时间是由参数wait_timeout控制的,默认值是8小时。
MySQL 8.0版本直接将查询缓存的整块功能删掉了,也就是说8.0开始彻底没有这个功能了。
show processlist; //查看当前链接的客户端信息。
redo log(重做日志/物理日志,InnoDB引擎才会有,在存储引擎层)、bin log(归档日志/逻辑日志,sever层)
redo log 日志结构:
这两种日志有以下三点不同。
redo log是InnoDB引擎特有的;binlog是MySQL的Server层实现的,所有引擎都可以使用。
redo log是物理日志,记录的是“在某个数据页上做了什么修改”;binlog是逻辑日志,记录的是这个语句的原始逻辑,比如“给ID=2这一行的c字段加1 ”。
redo log是循环写的,空间固定会用完;binlog是可以追加写入的。“追加写”是指binlog文件写到一定大小后会切换到下一个,并不会覆盖以前的日志。
1 prepare阶段 2 写binlog 3 commit
当在2之前崩溃时
重启恢复:后发现没有commit,回滚。备份恢复:没有binlog 。
一致
当在3之前崩溃
重启恢复:虽没有commit,但满足prepare和binlog完整,所以重启后会自动commit。备份:有binlog. 一致
如果语句是select * from T where ID=500,即主键查询方式,则只需要搜索ID这棵B+树;
如果语句是select * from T where k=5,即普通索引查询方式,则需要先搜索k索引树,得到ID的值为500,再到ID索引树搜索一次。这个过程称为回表。
也就是说,基于非主键索引的查询需要多扫描一棵索引树。因此,我们在应用中应该尽量使用主键查询。
SELECT * from information_schema.Innodb_trx;
最左原则;//组合索引的面试题原理
索引下推;//5.6版本以后才有
字符串对象:SDS简单动态字符串
列表对象:压缩链表(ziplist)、双向链表(linkedlist)
哈希对象:ziplist或者hashtable
集合对象:intset或者hashtable
有序集合对象:ziplist或者tiaoyueb
缓存穿透解决方案:
方案1、使用互斥锁排队,分布式环境中要使用分布式锁,单机的话用普通的锁(synchronized、Lock)
方案2、布隆过滤器,就类似于一个hash set,用于快速判某个元素是否存在于集合中
缓存雪崩解决方案:
方案1、也是像解决缓存穿透一样加锁排队,实现同上;
方案2、建立备份缓存,缓存A和缓存B,A设置超时时间,B不设值超时时间,先从A读缓存,A没有读B,并且更新A缓存 和B缓存;
方案3、设置缓存超时时间的时候加上一个随机的时间长度,比如这个缓存key的超时时间是固定的5分钟加上随机的2分钟,酱紫可从一定程度上避免雪崩问题;
每个布隆过滤器对应到 Redis 的数据结构里面就是一个大型的位数组和几个不一样的无偏 hash 函数。所谓无偏就是能够把元素的 hash 值算得比较均匀。
向布隆过滤器中添加 key 时,会使用多个 hash 函数对 key 进行 hash 算得一个整数索引值然后对位数组长度进行取模运算得到一个位置,每个 hash 函数都会算得一个不同的位置。再把位数组的这几个位置都置为 1 就完成了 add 操作。
向布隆过滤器询问 key 是否存在时,跟 add 一样,也会把 hash 的几个位置都算出来,看看位数组中这几个位置是否都为 1,只要有一个位为 0,那么说明布隆过滤器中这个 key 不存在。如果都是 1,这并不能说明这个 key 就一定存在,只是极有可能存在,因为这些位被置为 1 可能是因为其它的 key 存在所致。如果这个位数组比较稀疏,判断正确的概率就会很大,如果这个位数组比较拥挤,判断正确的概率就会降低。具体的概率计算公式比较复杂,感兴趣可以阅读扩展阅读,非常烧脑,不建议读者细看。
使用时不要让实际元素远大于初始化大小,当实际元素开始超出初始化大小时,应该对布隆过滤器进行重建,重新分配一个 size 更大的过滤器,再将所有的历史元素批量 add 进去 (这就要求我们在其它的存储器中记录所有的历史元素)。因为 error_rate 不会因为数量超出就急剧增加,这就给我们重建过滤器提供了较为宽松的时间。...
https://juejin.im
掘金 — 一个帮助开发者成长的社区
加锁代码:
public class RedisTool {
private static final String LOCK_SUCCESS = "OK";
private static final String SET_IF_NOT_EXIST = "NX";
private static final String SET_WITH_EXPIRE_TIME = "PX";
/**
* 尝试获取分布式锁
* @param jedis Redis客户端
* @param lockKey 锁
* @param requestId 请求标识
* @param expireTime 超期时间
* @return 是否获取成功
*/
public static boolean tryGetDistributedLock(Jedis jedis, String lockKey, String requestId, int expireTime) {
String result = jedis.set(lockKey, requestId, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, expireTime);
if (LOCK_SUCCESS.equals(result)) {
return true;
}
return false;
}
}
解锁代码:
public class RedisTool {
private static final Long RELEASE_SUCCESS = 1L;
/**
* 释放分布式锁
* @param jedis Redis客户端
* @param lockKey 锁
* @param requestId 请求标识
* @return 是否释放成功
*/
public static boolean releaseDistributedLock(Jedis jedis, String lockKey, String requestId) {
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
Object result = jedis.eval(script, Collections.singletonList(lockKey), Collections.singletonList(requestId));
if (RELEASE_SUCCESS.equals(result)) {
return true;
}
return false;
}
}
redis自带的incr命令
在代码里要对redis操作的时候,针对同一key的资源,就先进行加锁(java里的synchronized或lock)
利用redis的setnx实现内置的锁
RDB持久化可以在指定的时间间隔内生成数据集的时间点快照。
优点:适合备份;适用于灾难恢复;可以最大化 Redis 的性能,父进程只需分出一个子进程进行备份;在恢复大数据集时的速度比 AOF 的恢复速度要快
缺点:在服务器故障时可能丢失数据;当数据集比较大时,分出子线程比较耗时,甚至会影响客户端使用
AOF持久化记录服务器执行的所有写操作命令
优点:AOF 持久化会让 Redis 变得非常耐久;Redis 可以在 AOF 文件体积变得过大时,自动地在后台对 AOF 进行重写,重写后的新 AOF 文件包含了恢复当前数据集所需的最小命令集合,使文件体积缩小;
缺点:对于相同的数据集来说,AOF 文件的体积通常要大于 RDB 文件的体积;AOF 的速度可能会慢于 RDB;
定时删除、惰性删除、定期删除
目前常用的策略是 惰性删除 + 定期删除
字符串String、字典Hash、列表List、集合Set、有序集合SortedSet。
如果你是Redis中高级用户,还需要加上下面几种数据结构HyperLogLog、Geo、Pub/Sub。
如果你说还玩过Redis Module,像BloomFilter,RedisSearch,Redis-ML,面试官得眼睛就开始发亮了。
高版本的Redis的Set命令支持多个参数,jedis.set(lockKey, requestId, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, expireTime);
这是加锁,执行一条命令,使之进行原子化加锁。
解锁命令利用Lua脚本,再结合eval命令,一次性地进行原子解锁。
使用keys指令可以扫出指定模式的key列表。
对方接着追问:如果这个redis正在给线上的业务提供服务,那使用keys指令会有什么问题?
这个时候你要回答redis关键的一个特性:redis的单线程的。keys指令会导致线程阻塞一段时间,线上服务会停顿,直到指令执行完毕,服务才能恢复。这个时候可以使用scan指令,scan指令可以无阻塞的提取出指定模式的key列表,但是会有一定的重复概率,在客户端做一次去重就可以了,但是整体所花费的时间会比直接用keys指令长。
如果大量的key过期时间设置的过于集中,到过期的那个时间点,redis可能会出现短暂的卡顿现象。一般需要在时间上加一个随机值,使得过期时间分散一些。
bgsave做镜像全量持久化,aof做增量持久化。因为bgsave会耗费较长时间,不够实时,在停机的时候会导致大量丢失数据,所以需要aof来配合使用。在redis实例重启时,优先使用aof来恢复内存的状态,如果没有aof日志,就会使用rdb文件来恢复。
如果再问aof文件过大恢复时间过长怎么办?你告诉面试官,Redis会定期做aof重写,压缩aof文件日志大小。如果面试官不够满意,再拿出杀手锏答案,Redis4.0之后有了混合持久化的功能,将bgsave的全量和aof的增量做了融合处理,这样既保证了恢复的效率又兼顾了数据的安全性。这个功能甚至很多面试官都不知道,他们肯定会对你刮目相看。
如果对方追问那如果突然机器掉电会怎样?取决于aof日志sync属性的配置,如果不要求性能,在每条写指令时都sync一下磁盘,就不会丢失数据。但是在高性能的要求下每次都sync是不现实的,一般都使用定时sync,比如1s1次,这个时候最多就会丢失1s的数据。
不支持回滚操作是因为redis是先执行指令然后做日志,所以即使发生异常,没有可以用来执行回滚操作的日志。
传统的数据库都是先做日志然后再做操作,这样可以用于事物回滚。
要求从库至少和主库一样新,否则主库的新指令同步过去从库不能识别,同步就会出错,所以升级版本时应该先升级从库,再升级主库。
sentinel : 主从同步; codis:国人写的集群中间件; cluster:官方的集群方案
对象内存过大;
资源释放;
static关键字的使用。
年轻代分三个区。一个Eden区,两个 Survivor区(一般而言)。大部分对象在Eden区中生成。当Eden区满时,还存活的对象将被复制到Survivor区(两个中的一个),当这个 Survivor区满时,此区的存活对象将被复制到另外一个Survivor区,当这个Survivor去也满了的时候,从第一个Survivor区复制过来的并且此时还存活的对象,将被复制 到年老区。
引起原因:年轻代空间不足;永久代空间满了;
可以通过kill -3 查看内存快照,进行排查。
A类继承B类,当类加载器加载A类时,需要先加载B类,再加载到B类时,由于已经加载过了,所以不再加载。自己定义新的类加载器可以不使用双亲委派模式。
父类静态成员和静态初始化块 ,按在代码中出现的顺序依次执行
子类静态成员和静态初始化块 ,按在代码中出现的顺序依次执行
父类实例成员和实例初始化块 ,按在代码中出现的顺序依次执行
父类构造方法
子类实例成员和实例初始化块 ,按在代码中出现的顺序依次执行
子类构造方法
初始化的顺序,先静态方法,再构造方法,每个又是先基类后子类。
Minor GC触发条件:(复制-清除)
当Eden区满时,触发Minor GC;
Full GC触发条件:(标记-清除)
调用System.gc时,系统建议执行Full GC,但是不必然执行;
老年代空间不足;
方法区空间不足;
优化SQL
数据库配置低,加配置
数据库负载高,加机器做集群
将数据放到缓存层,redis、es
代码优化,例:循环查询数据库
静态资源放到专门的OSS服务器,并用CDN加速
合理减少中间服务的远程调用
加载.class文件的方式
– 从本地系统中直接加载
– 通过网络下载.class文件
– 从zip,jar等归档文件中加载.class文件
– 从专有数据库中提取.class文件
– 将Java源文件动态编译为.class文件
类加载的过程包括了加载、验证、准备、解析、初始化五个阶段。在这五个阶段中,加载、验证、准备和初始化这四个阶段发生的顺序是确定的,而解析阶段则不一定,它在某些情况下可以在初始化阶段之后开始,这是为了支持Java语言的运行时绑定(也成为动态绑定或晚期绑定)。另外注意这里的几个阶段是按顺序开始,而不是按顺序进行或完成,因为这些阶段通常都是互相交叉地混合进行的,通常在一个阶段执行的过程中调用或激活另一个阶段。
加载:查找并加载类的二进制数据
加载时类加载过程的第一个阶段,在加载阶段,虚拟机需要完成以下三件事情:
1、通过一个类的全限定名来获取其定义的二进制字节流。
2、将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
3、在Java堆中生成一个代表这个类的java.lang.Class对象,作为对方法区中这些数据的访问入口。
相对于类加载的其他阶段而言,加载阶段(准确地说,是加载阶段获取类的二进制字节流的动作)是可控性最强的阶段,因为开发人员既可以使用系统提供的类加载器来完成加载,也可以自定义自己的类加载器来完成加载。
加载阶段完成后,虚拟机外部的 二进制字节流就按照虚拟机所需的格式存储在方法区之中,而且在Java堆中也创建一个java.lang.Class类的对象,这样便可以通过该对象访问方法区中的这些数据。
•连接
– 验证:确保被加载的类的正确性
验证是连接阶段的第一步,这一阶段的目的是为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。验证阶段大致会完成4个阶段的检验动作:
文件格式验证:验证字节流是否符合Class文件格式的规范;例如:是否以0xCAFEBABE开头、主次版本号是否在当前虚拟机的处理范围之内、常量池中的常量是否有不被支持的类型。
元数据验证:对字节码描述的信息进行语义分析(注意:对比javac编译阶段的语义分析),以保证其描述的信息符合Java语言规范的要求;例如:这个类是否有父类,除了java.lang.Object之外。
字节码验证:通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的。
符号引用验证:确保解析动作能正确执行。
验证阶段是非常重要的,但不是必须的,它对程序运行期没有影响,如果所引用的类经过反复验证,那么可以考虑采用-Xverifynone参数来关闭大部分的类验证措施,以缩短虚拟机类加载的时间。
– 准备:为类的静态变量分配内存,并将其初始化为默认值
准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些内存都将在方法区中分配。对于该阶段有以下几点需要注意:
1、这时候进行内存分配的仅包括类变量(static),而不包括实例变量,实例变量会在对象实例化时随着对象一块分配在Java堆中。
2、这里所设置的初始值通常情况下是数据类型默认的零值(如0、0L、null、false等),而不是被在Java代码中被显式地赋予的值。
假设一个类变量的定义为:public static int value = 3;
那么变量value在准备阶段过后的初始值为0,而不是3,因为这时候尚未开始执行任何Java方法,而把value赋值为3的putstatic指令是在程序编译后,存放于类构造器()方法之中的,所以把value赋值为3的动作将在初始化阶段才会执行。
复制代码
· 这里还需要注意如下几点:
· 对基本数据类型来说,对于类变量(static)和全局变量,如果不显式地对其赋值而直接使用,则系统会为其赋予默认的零值,而对于局部变量来说,在使用前必须显式地为其赋值,否则编译时不通过。
· 对于同时被static和final修饰的常量,必须在声明的时候就为其显式地赋值,否则编译时不通过;而只被final修饰的常量则既可以在声明时显式地为其赋值,也可以在类初始化时显式地为其赋值,总之,在使用前必须为其显式地赋值,系统不会为其赋予默认零值。
· 对于引用数据类型reference来说,如数组引用、对象引用等,如果没有对其进行显式地赋值而直接使用,系统都会为其赋予默认的零值,即null。
· 如果在数组初始化时没有对数组中的各元素赋值,那么其中的元素将根据对应的数据类型而被赋予默认的零值。
复制代码
3、如果类字段的字段属性表中存在ConstantValue属性,即同时被final和static修饰,那么在准备阶段变量value就会被初始化为ConstValue属性所指定的值。
假设上面的类变量value被定义为: public static final int value = 3;
编译时Javac将会为value生成ConstantValue属性,在准备阶段虚拟机就会根据ConstantValue的设置将value赋值为3。回忆上一篇博文中对象被动引用的第2个例子,便是这种情况。我们可以理解为static final常量在编译期就将其结果放入了调用它的类的常量池中
– 解析:把类中的符号引用转换为直接引用
解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程,解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符7类符号引用进行。符号引用就是一组符号来描述目标,可以是任何字面量。
直接引用就是直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄。
•初始化
初始化,为类的静态变量赋予正确的初始值,JVM负责对类进行初始化,主要对类变量进行初始化。在Java中对类变量进行初始值设定有两种方式:
①声明类变量是指定初始值
②使用静态代码块为类变量指定初始值
JVM初始化步骤
1、假如这个类还没有被加载和连接,则程序先加载并连接该类
2、假如该类的直接父类还没有被初始化,则先初始化其直接父类
3、假如类中有初始化语句,则系统依次执行这些初始化语句
类初始化时机:只有当对类的主动使用的时候才会导致类的初始化,类的主动使用包括以下六种:
– 创建类的实例,也就是new的方式
– 访问某个类或接口的静态变量,或者对该静态变量赋值
– 调用类的静态方法
– 反射(如Class.forName(“com.shengsiyuan.Test”))
– 初始化某个类的子类,则其父类也会被初始化
– Java虚拟机启动时被标明为启动类的类(Java Test),直接使用java.exe命令来运行某个主类
结束生命周期
•在如下几种情况下,Java虚拟机将结束生命周期
– 执行了System.exit()方法
– 程序正常执行结束
– 程序在执行过程中遇到了异常或错误而异常终止
– 由于操作系统出现错误而导致Java虚拟机进程终止
站在Java开发人员的角度来看,类加载器可以大致划分为以下三类:
启动类加载器:Bootstrap ClassLoader,负责加载存放在JDK\jre\lib(JDK代表JDK的安装目录,下同)下,或被-Xbootclasspath参数指定的路径中的,并且能被虚拟机识别的类库(如rt.jar,所有的java.*开头的类均被Bootstrap ClassLoader加载)。启动类加载器是无法被Java程序直接引用的。
扩展类加载器:Extension ClassLoader,该加载器由sun.misc.Launcher$ExtClassLoader实现,它负责加载DK\jre\lib\ext目录中,或者由java.ext.dirs系统变量指定的路径中的所有类库(如javax.*开头的类),开发者可以直接使用扩展类加载器。
应用程序类加载器:Application ClassLoader,该类加载器由sun.misc.Launcher$AppClassLoader来实现,它负责加载用户类路径(ClassPath)所指定的类,开发者可以直接使用该类加载器,如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。
应用程序都是由这三种类加载器互相配合进行加载的,如果有必要,我们还可以加入自定义的类加载器。因为JVM自带的ClassLoader只是懂得从本地文件系统加载标准的java class文件,因此如果编写了自己的ClassLoader,便可以做到如下几点:
1)在执行非置信代码之前,自动验证数字签名。
2)动态地创建符合用户特定需要的定制化构建类。
3)从特定的场所取得java class,例如数据库中和网络中。
•全盘负责,当一个类加载器负责加载某个Class时,该Class所依赖的和引用的其他Class也将由该类加载器负责载入,除非显示使用另外一个类加载器来载入
•父类委托,先让父类加载器试图加载该类,只有在父类加载器无法加载该类时才尝试从自己的类路径中加载该类
•缓存机制,缓存机制将会保证所有加载过的Class都会被缓存,当程序中需要使用某个Class时,类加载器先从缓存区寻找该Class,只有缓存区不存在,系统才会读取该类对应的二进制数据,并将其转换成Class对象,存入缓存区。这就是为什么修改了Class后,必须重启JVM,程序的修改才会生效
双亲委派模型的工作流程是:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把请求委托给父加载器去完成,依次向上,因此,所有的类加载请求最终都应该被传递到顶层的启动类加载器中,只有当父加载器在它的搜索范围中没有找到所需的类时,即无法完成该加载,子加载器才会尝试自己去加载该类。
双亲委派机制:
1、当AppClassLoader加载一个class时,它首先不会自己去尝试加载这个类,而是把类加载请求委派给父类加载器ExtClassLoader去完成。
2、当ExtClassLoader加载一个class时,它首先也不会自己去尝试加载这个类,而是把类加载请求委派给BootStrapClassLoader去完成。
3、如果BootStrapClassLoader加载失败(例如在$JAVA_HOME/jre/lib里未查找到该class),会使用ExtClassLoader来尝试加载;
4、若ExtClassLoader也加载失败,则会使用AppClassLoader来加载,如果AppClassLoader也加载失败,则会报出异常ClassNotFoundException。
-系统类防止内存中出现多份同样的字节码
-保证Java程序安全稳定运行
TCP/IP 被认为是一个四层协议
(1)链路层,有时也称作数据链路层或网络接口层,通常包括操作系统中的设备驱动程序和计算机中对应的网络接口卡。
(2)网络层,有时也称作互联网层,处理分组在网络中的活动。网络层协议包括IP协议(网际协议),ICMP协议(internet互联网控制报文协议),以及IGMP协议(Internet组管理协议)
(3)运输层,包含协议TCP(传输控制协议)和UDP(用户数据报协议)。TCP把数据分成小块,交给网络层。UDP则为应用层提供服务,把数据报的分组从一台主机发送到另一台主机,但并不保证发送到另一台主机。
(4)应用层,负责处理特定的应用程序细节。Telnet远程登录,FTP文件传输协议,SMTP简单邮件传送协议,SNMP简单网络管理协议。
1)缓存处理策略不同
2)带宽优化和网络连接的使用
3)错误通知的管理
4)host头处理
5)长链接;1.0每次请求都要创建连接;1.1保持长链接
1)get请求可被缓存,post不能被缓存
2)get请求被保存在浏览器历史记录中,post不会保留
3)get请求可以被收藏在书签中,post不能
4)get请求安全性低于post
5)get请求有长度限制,post没有
6)post不限制提交的数据类型,post可以提交文件
1)cookie保存在客户端,关闭浏览器cookie被删除;cookie子客户端可以被伪造,敏感数据不易保存。session保存在服务端,过多会消耗服务器资源,尽量少使用
2)session是服务器用来跟踪用户的一种手段,每个session都有唯一标识id,生成后发送 到客户端cookie保存,发起请求后根据id来匹配session
3)存储数据类型不同;session可以存储任意java对象,cookie只能存储String
4)长于10k的数据,不要用到cookie
//快速排序代码
public class QuickSort {
public static void main(String[] args) {
int[] a = {1, 2, 4, 5, 7, 4, 5 ,3 ,9 ,0};
System.out.println(Arrays.toString(a));
quickSort(a);
System.out.println(Arrays.toString(a));
}
public static void quickSort(int[] a) {
if(a.length>0) {
quickSort(a, 0 , a.length-1);
}
}
private static void quickSort(int[] a, int low, int high) {
//1,找到递归算法的出口
if( low > high) {
return;
}
//2, 存
int i = low;
int j = high;
//3,key
int key = a[ low ];
//4,完成一趟排序
while( i< j) {
//4.1 ,从右往左找到第一个小于key的数
while(i key){
j--;
}
// 4.2 从左往右找到第一个大于key的数
while( iarr[j+1]){//从第一个开始,往后两两比较大小,如果前面的比后面的大,交换位置
int tmp = arr[j];
arr[j] = arr[j+1];
arr[j+1] = tmp;
}
}
}
System.out.println(Arrays.toString(arr));
}
}
//单例模式(饿汉)
public class Singleton {
private final static Singleton INSTANCE = new Singleton();
private Singleton(){}
public static Singleton getInstance(){
return INSTANCE;
}
}
//单例模式(懒汉)
public class Singleton {
private static Singleton singleton;
private Singleton() {}
public static Singleton getInstance() {
if (singleton == null) {
singleton = new Singleton();
}
return singleton;
}
}
//工厂模式
Elasticsearch的选主是ZenDiscovery模块负责的,主要包含Ping(节点之间通过这个RPC来发现彼此)和Unicast(单播模块包含一个主机列表以控制哪些节点需要ping通)这两部分;
对所有可以成为master的节点(node.master: true)根据nodeId字典排序,每次选举每个节点都把自己所知道节点排一次序,然后选出第一个(第0位)节点,暂且认为它是master节点。
如果对某个节点的投票数达到一定的值(可以成为master节点数n/2+1)并且该节点自己也选举自己,那这个节点就是master。否则重新选举一直到满足上述条件。
面试官:想了解应聘者之前公司接触的ES使用场景、规模,有没有做过比较大规模的索引设计、规划、调优。
解答:
如实结合自己的实践场景回答即可。
比如:ES集群架构13个节点,索引根据通道不同共20+索引,根据日期,每日递增20+,索引:10分片,每日递增1亿+数据,
每个通道每天索引大小控制:150GB之内。
仅索引层面调优手段:
1.1、设计阶段调优
1)根据业务增量需求,采取基于日期模板创建索引,通过roll over API滚动索引;
2)使用别名进行索引管理;
3)每天凌晨定时对索引做force_merge操作,以释放空间;
4)采取冷热分离机制,热数据存储到SSD,提高检索效率;冷数据定期进行shrink操作,以缩减存储;
5)采取curator进行索引的生命周期管理;
6)仅针对需要分词的字段,合理的设置分词器;
7)Mapping阶段充分结合各个字段的属性,是否需要检索、是否需要存储等。 ………
1.2、写入调优
1)写入前副本数设置为0;
2)写入前关闭refresh_interval设置为-1,禁用刷新机制;
3)写入过程中:采取bulk批量写入;
4)写入后恢复副本数和刷新间隔;
5)尽量使用自动生成的id。
1.3、查询调优
1)禁用wildcard;
2)禁用批量terms(成百上千的场景);
3)充分利用倒排索引机制,能keyword类型尽量keyword;
4)数据量大时候,可以先基于时间敲定索引再检索;
5)设置合理的路由机制。
1.4、其他调优
部署调优,业务调优等。
上面的提及一部分,面试者就基本对你之前的实践或者运维经验有所评估了。
面试官:想了解你对基础概念的认知。
解答:通俗解释一下就可以。
传统的我们的检索是通过文章,逐个遍历找到对应关键词的位置。
而倒排索引,是通过分词策略,形成了词和文章的映射关系表,这种词典+映射表即为倒排索引。
有了倒排索引,就能实现 o(1)时间复杂度的效率检索文章了,极大的提高了检索效率。
学术的解答方式:
倒排索引,相反于一篇文章包含了哪些词,它从词出发,记载了这个词在哪些文档中出现过,由两部分组成——词典和倒排表。
加分项:倒排索引的底层实现是基于:FST(Finite State Transducer)数据结构。
lucene从4+版本后开始大量使用的数据结构是FST。FST有两个优点:
1)空间占用小。通过对词典中单词前缀和后缀的重复利用,压缩了存储空间;
2)查询速度快。O(len(str))的查询时间复杂度。
面试官:想了解大数据量的运维能力。
解答:索引数据的规划,应在前期做好规划,正所谓“设计先行,编码在后”,这样才能有效的避免突如其来的数据激增导致集群处理能力不足引发的线上客户检索或者其他业务受到影响。
如何调优,正如问题1所说,这里细化一下:
3.1 动态索引层面
基于 模板+时间+rollover api滚动创建索引,举例:设计阶段定义:blog索引的模板格式为:blog_index_时间戳的形式,每天递增数据。
这样做的好处:不至于数据量激增导致单个索引数据量非常大,接近于上线2的32次幂-1,索引存储达到了TB+甚至更大。
一旦单个索引很大,存储等各种风险也随之而来,所以要提前考虑+及早避免。
3.2 存储层面
冷热数据分离存储,热数据(比如最近3天或者一周的数据),其余为冷数据。
对于冷数据不会再写入新数据,可以考虑定期force_merge加shrink压缩操作,节省存储空间和检索效率。
3.3 部署层面
一旦之前没有规划,这里就属于应急策略。
结合ES自身的支持动态扩展的特点,动态新增机器的方式可以缓解集群压力,注意:如果之前主节点等 规划合理,不需要重启集群也能完成动态新增的。
面试官:想了解ES集群的底层原理,不再只关注业务层面了。
解答:
前置前提:
1)只有候选主节点(master:true)的节点才能成为主节点。
2)最小主节点数(min_master_nodes)的目的是防止脑裂。
这个我看了各种网上分析的版本和源码分析的书籍,云里雾里。
核对了一下代码,核心入口为findMaster,选择主节点成功返回对应Master,否则返回null。选举流程大致描述如下:
第一步:确认候选主节点数达标,elasticsearch.yml设置的值discovery.zen.minimum_master_nodes;
第二步:比较:先判定是否具备master资格,具备候选主节点资格的优先返回;若两节点都为候选主节点,则id小的值会主节点。注意这里的id为string类型。
题外话:获取节点id的方法。
1GET /_cat/nodes?v&h=ip,port,heapPercent,heapMax,id,name
2ip port heapPercent heapMax id name
3127.0.0.1 9300 39 1.9gb Hk9w Hk9wFwU
面试官:想了解ES的底层原理,不再只关注业务层面了。
解答:
这里的索引文档应该理解为文档写入ES,创建索引的过程。
文档写入包含:单文档写入和批量bulk写入,这里只解释一下:单文档写入流程。
记住官方文档中的这个图。
第一步:客户写集群某节点写入数据,发送请求。(如果没有指定路由/协调节点,请求的节点扮演 路由节点的角色。)
第二步:节点1接受到请求后,使用文档_id来确定文档属于分片0。请求会被转到另外的节点,假定节点3。因此分片0的主分片分配到节点3上。
第三步:节点3在主分片上执行写操作,如果成功,则将请求并行转发到节点1和节点2的副本分片上,等待结果返回。所有的副本分片都报告成功,节点3将向协调节点(节点1)报告成功,节点1向请求客户端报告写入成功。
如果面试官再问:第二步中的文档获取分片的过程?
回答:借助路由算法获取,路由算法就是根据路由和文档id计算目标的分片id的过程。
1shard = hash(_routing) % (num_of_primary_shards)
面试官:想了解ES搜索的底层原理,不再只关注业务层面了。
解答:
搜索拆解为“query then fetch” 两个阶段。
query阶段的目的:定位到位置,但不取。
步骤拆解如下:
1)假设一个索引数据有5主+1副本 共10分片,一次请求会命中(主或者副本分片中)的一个。
2)每个分片在本地进行查询,结果返回到本地有序的优先队列中。
3)第2)步骤的结果发送到协调节点,协调节点产生一个全局的排序列表。
fetch阶段的目的:取数据。
路由节点获取所有文档,返回给客户端。
面试官:想了解对ES集群的运维能力。
解答:
1)关闭缓存swap;
2)堆内存设置为:Min(节点内存/2, 32GB);
3)设置最大文件句柄数;
4)线程池+队列大小根据业务需要做调整;
5)磁盘存储raid方式——存储有条件使用RAID10,增加单节点性能以及避免单节点存储故障。
面试官:想了解你的知识面的广度和深度。
解答:
Lucene是有索引和搜索的两个过程,包含索引创建,索引,搜索三个要点。可以基于这个脉络展开一些。
● 系统架构
○ 系统架构设计
○ 分布式及负载均衡
■ php分布式
■ redis分布式
■ mysql分布式
■ hbase分布式
○ 数据同步
■ 通过rsync同步数据
○ 数据备份
■ 文件备份
■ 数据库备份
○ 代码同步
■ 自动拉取git代码
■ 代码发布系统
■ 代码审核系统
○ 分布式服务
■ 基于zookeeper实现分布式服务
○ 监控服务
■ 抓取进程监控
■ 数据入库监控
■ 索引监控
■ API接口监控
■ 磁盘挂载监控
■ 主从同步监控
■ 数据一致性监控
■ 数据备份监控
■ 代码发布监控