Big data interview 16-java

Note: There are currently remaining issues

1. Why does HashMap need to be XORed?

2. Specific processing ideas when handling JVM exceptions

Article directory

java

HashMap

Why does the Hash value have an XOR process of high 16 bits and low 16 bits?

This XOR process is mainly used to determine whether two Hash values ​​are the same, in order to reduce collisions and reduce the probability of hash conflicts.

XOR: used for logical calculations. There are two values ​​​​ab. If the two values ​​​​a and b are not the same, the XOR result is 1. If the values ​​of a and b are the same, the XOR result is 0.
The upper sixteen bits: a binary system composed of 16 zeros.
The sixteenth bit: a binary system composed of 16 ones.

The new value obtained by mixing the characteristics of the high sixteen bits and the characteristics of the low sixteen bits retains the information of the high bits and low bits. The reason why the XOR operation is used here instead of the & and | operations is It is the XOR operation that can better retain the characteristics of each part. If the value calculated using the & operation is closer to 1, the value calculated using the | operation will be closer to 0.

put function process

1. Calculate the hash value, (h = key.hashCode()) ^ (h >>> 16), use the high 16 bits and the low 16 bits to perform an XOR operation (binary bitwise comparison, if they are the same, it is 0, if they are different) is 1).
2. Then calculate the array subscript based on the hash value (AND operation with the array length), and put the elements into the array. If there is no hash conflict, put it directly into the array. If there is a hash conflict, use the linked list's The method is placed at the end of the linked list (jdk1.7 uses the head insertion method, which is placed at the front of the linked list, and jdk1.8 uses the tail insertion method, which is placed at the end of the linked list).
3. If the length of the linked list exceeds 8 and the array length is greater than 64, convert the linked list into a red-black tree. If the length of the linked list is less than 6, convert the red-black tree back to a linked list; 4. If the total number of key-value pairs in the
array If the capacity exceeds the threshold (load factor 0.75), dynamic expansion will be performed.

Expansion:

Expansion formula: initial capacity (default 16) * load factor (default 0.75)

The amount of data is large. When the number of elements exceeds 12 initial capacity (default 16) * load factor (default 0.75), the array length is expanded to the original capacity * 2, which is 16 * 2 = 32. Use when calculating Bitwise operations without direct multiplication

After expansion, the original data needs to be recalculated and stored.

Deposit mechanism:

There is no need to recalculate the element's hash for element migration. Instead, the hash value of the original position key and the length of the old array (oldCap) are used to perform an "AND" operation.

  1. If the result is 0, then the current element's bucket position remains unchanged.
  2. If the result is 1, then the position of the bucket is the original position + the length of the original array

Why not just use red-black trees all the time

The red-black tree maintains a balanced binary tree by changing colors from left to right, but the consumption is relatively large, so when it is less than 8 bits, just use a linked list.

ConcurrentHashMap

Thread-safe HashMap

1.7 uses segmented array + linked list; after jdk8: Node array + linked list/red and black binary tree, thread safety is based on CAS + synchronized

put:

1. Calculate the hashcode based on the key.

2. Determine whether initialization is required.

3. It is the Node located by the current key. If it is empty, it means that the current location can write data. Use CAS to try to write. If it fails, the spin is guaranteed to be successful.

4. If it is at the current location hashcode == MOVED == -1, it needs to be expanded.

5. If neither is satisfied, use the synchronized lock to write data.

6. If the number is greater than TREEIFY_THRESHOLD(linked list length, default 8), the tree method will be executed, and treeifyBinthe linked list will be converted into a red-black tree only after it is judged that the current array length is ≥ 64.

Thread-safe HashMap,

Before jdk7: segmented array + linked list; after jdk8: array + linked list/red-black binary tree

Before jdk7: Segmentation lock ensures thread safety; after jdk8: Node array + linked list + red-black tree data structure is implemented. Concurrency control uses synchronized and CAS to operate. Synchronized only locks the first node of the current linked list or red-black binary tree, so As long as the hashes do not conflict, there will be no concurrency.

get:

1. Calculate the position based on the hash value.

2. Find the specified location. If the head node is what you are looking for, return its value directly.

3. If the hash value of the head node is less than 0, it means that it is expanding or it is a red-black tree. Find it.

4. If it is a linked list, traverse to find it

Expansion:

If the length of the array currently storing the hash slot is less than MIN_TREEIFY_CAPACITY(64), then the original array will only be expanded by 2 times.

The tryPresize function is executed. In the function, regardless of whether there are threads for expansion, the transfer function will be executed for data migration.

By default, all slots are divided into buckets, and concurrent data migration is performed for each bucket. At the same time, it will judge (sc - 2) != resizeStamp(n) << RESIZE_STAMP_SHIFTwhether it is the last thread in the migration process. For other threads, if its task is completed, it will exit the method directly, and the last thread needs to set the nextTabledata assigned to tableand restore the values nextTable​​​​of and sizeCtlto the original values.

The difference between "hello" and new String ("hello")

string s1=hello, it will look for "hello" in the string constant pool. If there is a reference in the stack, it will point to it directly. If not, it will add hello to the string constant pool and point to it.
new will add it to the constant pool if hello does not exist in the constant pool, and then open up a memory in the heap to store "hello". The references in the stack point to the memory addresses of the respective created objects.

ArrayList

List implementation class, underlying Object[ ], thread-safe, Vector is also List implementation class, thread-safe

Expansion mechanism:

When creating an ArrayList with the parameterless constructor, an empty array is actually initialized and assigned. Capacity is actually allocated when an element is actually added to the array. That is, when the first element is added to the array, the array capacity is expanded to 10

When adding elements exceeds the capacity, execute the grow function to expand the capacity.

// ">>"(移位运算符):>>1 右移一位相当于除 2,右移 n 位相当于除以 2 的 n 次方
int newCapacity = oldCapacity + (oldCapacity >> 1)

Each expansion will be about 1.5 times the original, even numbers are 1.5, and odd numbers minus decimals are about 1.5 times.

What if ArrayList needs to be thread safe,

1、Vector

2. Combined with Collections.synchronizedList(new ArrayList()) function

3. Or use the CopyOnWriteArrayList class

CopyOnWriteArrayList

The basic principle is still the same as ArrayList. The part involving thread safety is implemented through copy-on-write (as you can see from the name). There is a volatile array inside it to hold data. When "adding/modifying/deleting" data, the mutex will be acquired first, then a new array will be created, the updated data will be copied to the newly created array, and finally the array will be assigned to the volatile array, and then the mutex will be released. lock

Pass by value and pass by reference

java is pass by value

Passing basic types: What is passed is a copy of the value. Modifications to the copied variables do not affect the original variables.

Passing reference type: a copy of the reference address, but the copied address and the real address point to the same real data, so the value in the original variable can be modified, but when using the String type, String cannot be modified, so it cannot be changed.

Integer

The default value range is -128~127. If it exceeds, a new object will be created in the heap.

Why is it between this, because the maximum number in binary is 127, and negative numbers exist in their complement form in computers, and exist according to the size of the binary system

Integer a=500;
Integer b=500;
System.out.println(a.equals(b));

结果为true
Integer重写equals函数,判断的是值

Deep copy, shallow copy, zero copy

Deep copy: A copy of the value type field will be made, and a copy of the object pointed to by the reference type field will also be created in memory.

Shallow copy: Value type fields will be copied, while reference type fields will copy only the reference address, and the actual object space pointed to by the reference address actually only has one copy.

Zero copy: Prevent the CPU from copying data directly from one storage area to another. Two implementation methods

mmap, memory mapping, maps files to kernel buffers. At the same time, user space can share data in kernel space.

​ sendFile, the data does not go through the user state at all, but directly enters the Socket Buffer from the kernel buffer. At the same time, since it has nothing to do with the user state, it reduces a context switch.

ParallelStreamParallelStream

Parallel processing when processing tasks belongs to the parallel processing of jdk1.8 Stream. Parallel streams are suitable for simpler data processing tasks without thread safety issues.

ForkJoin used under the hood of parallel streams

jdk

JVM memory structure

Insert image description here

JDK8

Insert image description here

program counter

A small piece of memory, a signal indicator for thread execution bytecode, to obtain the next bytecode instruction that needs to be executed, branches, loops, jumps, exception handling, and thread recovery all rely on it, and each thread will have Its own program counter, this area is private to the thread; when the thread executes the java method, it records the address of the virtual machine bytecode instruction being executed, executes the native method, and the counter value is empty

Virtual machine stack

The memory model of Java method execution. Each method will create a stack frame when specified, which is used to store local variable tables, operand stacks, dynamic links, and method exits. Each method corresponds to a stack from the call to the completion of execution. The frame is pushed into the stack and popped out of the stack with you.

The local variable table stores the basic data types (int, char, byte...), object reference types (reference, reference pointers of the start and end addresses of the execution object) and return Address that are known to the compiler. The 64-bit length long and double types occupy two local variable spaces, and other data types occupy one. And the memory required for the local variable table is allocated during compilation.

native method stack

It has the same function as the virtual machine stack, but the difference is that the local method stack mainly provides services for local methods, that is, native methods.

heap

An area shared by threads. It is created when the virtual machine starts. The created objects and arrays are stored in the Java heap memory. Since the VM uses generational recycling, the heap is also divided into the new generation and the old generation. The new generation: stores new objects
. Accounting for one-third of the new generation, the new generation is divided into Eden area, ServivorFrom, and ServivorTo.
Eden area: the birthplace of new objects. When full, MinorGC is triggered to clean up the new generation.
ServivorFrom: The survivors of the last GC are scanned for this GC. ServivorTo
: Retains the survivors of a MinorGC process.
Old generation: stores memory objects with long life cycles in the application. When the storage is full, MajorGC is triggered.

Method area/permanent generation

Stores class information, constants, static variables, and code compiled by the just-in-time compiler that have been loaded by the virtual machine.

Runtime constant pool

Part of the method area, which stores various literals and symbol references generated during compilation. This part of the content will be
stored in the runtime constant pool of the method area after the class is loaded.

Four kinds of references

Benefits: 1. Facilitates programmers to decide the life cycle of some objects; 2. Facilitates garbage collection

Strong reference: will not be gc

Soft reference: SoftReference implementation, gc when memory is insufficient

Weak reference: WeakReference implementation, recycled every time garbage collection

Virtual reference: The main purpose of a virtual reference is to be notified before the memory occupied by an object is actually recycled, so that some related cleanup work can be performed. When the garbage collector is preparing to recycle an object, if it finds that it still has a virtual reference, it will add the virtual reference to the reference queue associated with it before recycling the object's memory. You can view the objects that have been recycled in the queue. object

Java object

Object = object header + instance data + alignment padding (an integer multiple of 8)

Object header

Object header = Mark Word + type pointer (when pointer compression is not enabled) + array length (only available for arrays)

Mark Word = HashCode + GC generation age + lock status flag + lock held by thread + biased thread ID + biased timestamp

Under 64-bit virtual machine, 64bit size

Insert image description here

JMM (Java Memory Model)

An abstract concept that describes a rule that defines how each variable is accessed in the shared data area and private data area.

JMM defines the abstract relationship between threads and main memory: shared variables between threads are stored in main memory, each thread has a private local memory, and the local memory stores the thread's ability to read/write shared variables. copy.

There are 8 modes of operation specified

Insert image description here

1. Lock: Acts on variables in main memory . A variable can only be locked by one thread at the same time, that is, the variable is marked as thread- exclusive .

2. Read: Acts on the main memory variable, which means transferring a variable value from the main memory to the working memory of the thread for use in the next load operation.

3. Load: Acts on the variables of the thread's working memory , which means putting the variable values ​​read from the main memory by the read operation into the variable copy of the working memory (the copy is relative to the variable of the main memory) ).

4. Use (use): Acts on variables in the working memory of the thread, which means passing a variable value in the working memory to the execution engine. Whenever the virtual machine encounters a bytecode instruction that needs to use the value of the variable, it will Do this.

5. Assign (assignment): Acts on variables in the working memory of the thread, indicating that the value returned by the execution engine is assigned to the variable in the working memory. Whenever the virtual machine encounters a bytecode instruction that assigns a value to a variable, it will execute the instruction. operate.

6. Store (storage): Acts on variables in the thread's working memory , passing the value of a variable in the working memory to the main memory for use in the next write operation.

7. Write: Acts on variables in main memory , which means putting the value of the variable obtained from the working memory by the store operation into the variable in the main memory.

8. Unlock: Acts on a variable in main memory , which means releasing a variable that is in a locked state. Only the released variable can be locked by other threads.

JVM class loading mechanism

When the virtual machine encounters the new command, it checks whether the parameters in the command can locate the symbolic reference of the class in the constant pool [Aren't the symbolic references in the heap? ], checks whether the symbol is loaded, resolved or initialized, and then starts loading the class.

The whole loading process consists of five stages: loading, verification, preparation, parsing and initialization.

1) Loading,
1-1, obtain the binary byte stream of the class through the fully qualified name of the class (this stage does not specify where to obtain it, how to obtain it, more processing can be done); 1-2, represent the byte
stream The static storage structure is converted into the runtime data structure of the method area;
1-3. Generate a java.lang.Class object representing this class in the memory, which serves as various data access entries for this class in the method area.

2) Verification, the first step in the connection phase, ensures that the byte stream in the file contains this information and meets the current virtual machine requirements.
3) Preparation, formally allocates memory for class variables and sets the initial value of the class variable. The variable uses memory and is allocated in the method area. , the memory allocated at this time only includes class variables (statically modified), and no instance variables. And at this time, the variable is only assigned a value of 0, and the real assignment will be performed during initialization.
4) Parsing, the symbol reference in the constant pool is replaced by a direct reference, (symbol reference: a string of symbols represents the target of the reference, direct reference: directly points to Target pointer)
5) Initialization, the last step, starts executing the bytecode, initializes, and assigns values ​​to variables

JVM tuning

benefit

1. Prevent OOM and perform JVM planning and pre-tuning
. 2. Solve various OOMs during program operation
. 3. Reduce the frequency of Full GC and solve the problems of slow operation and lag.

Required commands

ps -ef | grep java 查看进程

jps 	查看java进程
jstat 	查看JVM统计信息				jstat -class 3346
jinfo	实时查看和修改JVM配置参数
jmap	到处内存映像文件和内存使用情况		jmap 选项 进程ID
	-dump java堆转储快照
	-heap java堆详细信息
jstack	打印JVM线程快照

JVM tuning options

young generation

Applications that prioritize response time: Set it as large as possible until it is close to the minimum response time limit of the system, while reducing the number of objects arriving in the old generation and the frequency of occurrence in the young generation.

Throughput-first applications: Set as large as possible, to G

old age

Applications with priority on response time: The old generation uses a concurrent collector, so its size needs to be set carefully. Generally, some parameters such as concurrent session rate and session duration must be considered. If the heap setting is small, it may cause memory fragmentation, high recycling frequency, and application suspension using the traditional mark-and-sweep method; if the heap is large, it will take a long time to collect. You can refer to the following data: 1. Concurrent garbage collection information ; 2. The number of concurrent collections of the persistent generation; 3. Traditional GC information; 4. The proportion of time spent on young generation and old generation recycling; 5. Reducing the time spent in the young generation and old generation will generally improve the efficiency of the application.

Throughput-first applications: Throughput-first applications have a large young generation and a small old generation.

Fragmentation problems caused by smaller heaps.
The old generation uses a mark and clear algorithm, which may cause fragmentation after running for a period of time. You can set the parameter
-XX:+UseCMSCompactAtFullCollection: when using the concurrent collector, enable compression of the old generation.
-XX:CMSFullGCsBeforeCompaction=0: When the above configuration is enabled, set here how many times Full GC will be used to compress the old generation.

JVM tuning parameters

Stack size related parameters

parameter name illustrate Remark
-Xms JVM initial memory -Xms3550m
-Xmx maximum heap size The entire JVM memory size = young generation size + old generation size + persistent generation size. The permanent generation generally has a fixed size of 64 MB, and Sun officially recommends a configuration of 3/8 of the entire heap; the values ​​set by Xms and Xmx must be consistent online to prevent jitter.
-Xmn young generation size If the young generation is increased, the old generation will be reduced accordingly. The official default configuration is about old generation size/young generation size = about 2/1;
-Xss Thread stack size
-XX:NewRatio=n The ratio of young generation to old generation XX:NewRatio=4, then the ratio of young generation to old generation is 1:4, and the young generation accounts for 1/5 of the entire stack
-XX:SurvivorRatio=n The ratio of the Eden area to the two Survivor areas in the young generation. -XX:SurvivorRatio=4, set the size ratio of the Eden area and the Survivor area in the young generation. If set to 4, the ratio of two Survivor areas to one Eden area is 2:4, and one Survivor area accounts for 1/6 of the entire young generation.
-XX:MaxPermSize=n Persistent generation size
-XX:MaxTenuringThreshold=n Garbage maximum age

GC parameters with limited throughput

parameter name illustrate Remark
-XX:+UseParallelGC Select the garbage collector as a parallel collector The young generation uses concurrent collection, while the old generation still uses serial collection.
-XX:ParallelGCThreads Configure the number of threads of the parallel collector, that is, how many threads can perform garbage collection at the same time XX:ParallelGCThreads=20Indicates that the number of threads configured for the parallel collector is 20
-XX:+UseParallelOldGC Configure the old generation garbage collection method to be parallel collection
-XX:MaxGCPauseMillis Set the maximum garbage collection time of the young generation
-XX:+UseAdaptiveSizePolicy The parallel collector automatically selects the young generation area size and the corresponding Survivor area ratio to achieve the minimum response time or collection frequency specified by the target system. This value is recommended when using the parallel collector and should always be turned on.

GC parameters that prioritize response time

parameter name illustrate Remark
-XX:+UseConcMarkSweepGC Set up the old generation for concurrent collection
-XX:+UseParNewGC Set up the young generation for parallel collection. Can be used simultaneously with CMS collection
-XX:CMSFullGCsBeforeCompaction Since the concurrent collector does not compress or organize the memory space, "fragments" will be generated after running for a period of time, reducing operating efficiency. This value sets how many times the GC will be run to compress and organize the memory space. -XX:CMSFullGCsBeforeCompaction=5, indicating that the memory space will be compressed and organized after running GC 5 times.
-XX:+UseCMSCompactAtFullCollection Turn on compression for the old generation May impact performance, but will eliminate fragmentation

Auxiliary GC configuration

parameter name illustrate Remark
-XX:+PrintGC Output GC log
-XX:+PrintGCDetails Output GC log details
-XX:+PrintGCTimeStamps Used to output GC timestamp, the timestamp form of the total time from JVM startup to the current date
-XX:+PrintGCDateStamps Output GC timestamp in date format
-XX:+PrintHeapAtGC GC前后打印出堆的信息
-Xloggc:…/logs/gc.log 日志文件的输出路径

主要步骤

1、分析GC日志,可以打印日志,使用gceasy网站分析日志

2、判断JVM出现的问题

3、确定调优目标

4、调整参数

5、对比调优前后差距,重复1~5步

6、应用 JVM 到应用服务器

常见的问题以及解决方案

java.lang.OutOfMemoryError:Java heap space

原因:
1、代码中可能存在大对象分配
2、可能存在内存泄漏,导致在多次GC之后,还是无法找到一块足够大的内存容纳当前对象
解决方法:
1、检查是否存在大对象的分配,最有可能的是大数组的分配
2、通过jmap命令,把堆内存dump下来,使用MAT等工具分析一下,检查是否存在内存泄漏问题
3、如果没有找到明显的内存泄漏,使用-Xmx加大堆内存

元空间溢出 java.lang.OutOfMemortError:Metaspace

原因:
1、运行期间生产了大量的代理类,导致方法区被撑爆,无法卸载
2、应用长时间运行,没有重启
3、元空间内存设置过小
解决方法:
1、检查是否永久代或元空间设置过小
2、检查代码中是否存在大量的反射操作
3、dump之后通过mat检查是否存在大量由于反射生成的代理类

java.lang.OutOfMemortError:GC overhead limit exceeded

原因:
这个是JDK6新加的错误类型,一般都是堆太小导致的。Sun官方对此的定义:超过98%的时间用来做GC并且回收了不到2%的堆内存时会抛出此异常。本质是一个预判性的异常,抛出该异常时系统没有真正的内存溢出
解决方法:
1、检查项目中是否有大量的死循环或有使用大内存的代码,优化代码
2、添加参数“-XX:-UserGCOverheadLimit”禁用这个检查,起始这个参数解决不了内存问题,只是把错误信息延后,最终出现java.lang.OutOfMemortError:Java heap space
3、dump内存,检查是否存在内存泄漏,如果没有,加大内存

线程溢出 java.lang.OutOfMemortError:unable to create new native Thread

原因:
创建大量线程导致内存溢出
解决方法:
1、正常情况下,在相同物理内存下,减小这个值能生成更多的线程。但是操作系统对一个进程内的线程数还是有限制的,不能无限生产,大概在3000-5000左右

总结

FullGC一天超过一次肯定就不正常了。
发现FullGC频繁的时候优先调查内存泄漏问题。
内存泄漏解决后,jvm可以调优的空间就比较少了。
如果发现CPU持续偏高,排除代码问题后可以找运维咨询下阿里云客服,查看是否是服务器问题
数据查询的时候也是算作服务器的入口流量的,如果访问业务没有这么大量,而且没有攻击的问题的话可以往数据库方面调查。
有必要时常关注服务器的GC,可以及早发现问题。

JVM常用配置

通过-XX:MaxRAMPercentage限制堆大小(推荐)

设置JVM使用容器内存的最大百分比。由于存在系统组件开销,建议最大不超过75.0,推荐设置为70.0。

-XX:+UseContainerSupport // 基于docker使用时启用
-XX:InitialRAMPercentage=70.0
-XX:MaxRAMPercentage=70.0
-XX:+PrintGCDetails
-XX:+PrintGCDateStamps
-Xloggc:E:/logs/gc- P O D I P − {POD_IP}- PODIP(date ‘+%s’).log
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=E:/logs/dump- P O D I P − {POD_IP}- PODIP(date ‘+%s’).hprof

通过-Xms -Xmx限制堆大小,设置不合理时容易出现应用堆大小未达到阈值但容器OOM被强制关闭的情况

-Xms2048m
-Xmx2048m
-XX:+PrintGCDetails
-XX:+PrintGCDateStamps
-Xloggc:E:/logs/gc- P O D I P − {POD_IP}- PODIP(date ‘+%s’).log
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=E:/logs/dump- P O D I P − {POD_IP}- PODIP(date ‘+%s’).hprof

推荐堆大小设置
内存规格大小 JVM堆大小
1 GB 600 MB
2 GB 1434 MB
4 GB 2867 MB
8 GB 5734 MB

MinorGC,MajorGC ,Full GC

MinorGC,回收新生代,采用复制算法
1、把 Eden 和 ServivorFrom 区域中存活的对象复制到 ServicorTo 区域,如果对象年龄够大或者ServicorTo不足可以存放到老年代
2、清空 Eden 和 ServicorFrom 中的对象
3、ServicorTo 和 ServicorFrom 互换

MajorGC,回收老年代,因为每次都回收少量对象,所以采用标记清除算法
扫描一次所有老年代,标记出存活的对象,然后回收没有标记的对象,耗时长,会产生内存碎片

Full GC,回收全部

触发时间

1、调用System.gc()时,系统建议执行Full GC,但是不必然执行
2、老年代空间不足
3、方法区空间不足
4、通过Minor GC后进入老年代的平均大小大于老年代的可用内存
5、用Eden区、S0(From Space)区向S1(To Space)区复制时,对象大小大于To Space可用内存,则把该对象转存到老年代,且老年代的可用内存小于该对象大小

GC 垃圾回收

对象创建在 Eden 区,Eden 区没有足够空间进行分配时,虚拟机将发起一次 Minor GC,

回收算法

标记清除

标记复制

标记整理

分代收集:是根据对象存活周期的不同将内存分为几块。一般将 java 堆分为新生代和老年代,每个年代根据情况选择不同的回收算法,

新生代,大量对象死去,采用标记复制

老年代,对象存活率高,使用标记整理或者标记清除

GC 垃圾回收器

回收算法的具体实现

Serial,最基本回收器,单线程,新生代采用标记-复制算法,老年代采用标记-整理算法

ParNew,serial回收器多线程版本,Server 模式下新生代的默认垃圾收集器,新生代采用标记-复制算法,老年代采用标记-整理算法

Parallel Scavenge ,JDK8默认收集器,多线程复制算法,重点关注可控制的吞吐量,有自适应调节策略,新生代采用标记-复制算法,老年代采用标记-整理算法。

Serial Old,单线程标记-整理算法,Serial 垃圾收集器年老代版本
Parallel Old,多线程的标记-整理算法,Parallel Scavenge的年老代版本

CMS (Concurrent mark sweep),多线程的标记-清除算法,一种以获取最短回收停顿时间为目标的收集器,cms 分为 foreground gc 和 background gc,foreground 其实就是 Full gc
优点:并发收集,低停顿,缺点:对CPU资源敏感,无法处理浮动垃圾,使用标记清除算法产生大量碎片

回收过程分为四步

初始标记: 暂停所有的其他线程,并记录下直接与 root 相连的对象,速度很快 ;

并发标记**:** 同时开启 GC 和用户线程,用一个闭包结构去记录可达对象。但在这个阶段结束,这个闭包结构并不能保证包含当前所有的可达对象。因为用户线程可能会不断的更新引用域,所以 GC 线程无法保证可达性分析的实时性。所以这个算法里会跟踪记录这些发生引用更新的地方。

重新标记: 重新标记阶段就是为了修正并发标记期间因为用户程序继续运行而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段的时间稍长,远远比并发标记阶段时间短

并发清除: 开启用户线程,同时 GC 线程开始对未标记的区域做清扫。

G1 (Garbage first),基于标记-整理算法,不产生内存碎片,精确控制停顿时间,在不牺牲吞吐量前提下,实现低停顿垃圾回收,回收方式:把堆内存划分为大小固定的几个独立区域,叫做 Region(大小1M~32M间,最多2048个),并且跟踪这些区域的垃圾收集进度,同时在后台维护一个优先级列表,每次根据所允许的收集时间, 优先回收垃圾最多的区域

ZGC,JDK11推出的一款低延迟垃圾回收器,用于大内存低延迟服务的内存管理和回收

GC 安全点和安全区域

GC在垃圾回收时,需要从GC Roots找引用链,需要消耗时间

为减少消耗,创建一个用于存储引用类型的数据结构叫OopMap普通对象指针映射表,他会把栈上所有引用类型的位置全部记录,GC可以直接读取,减少时间

在对象引用关系发生变化的时候需要修改OopMap,会选择一些特定的点来记录数据量,这就是安全点,比如方法调用,循环跳转等,当GC需要停止用户线程时,挂起中断标志位,线程轮询标志位发现需要挂起,然后进入最近安全点,更新OopMap才能挂起

处于Sleep 或者 Blocked 状态的线程无法跑到安全点,需要引入安全区域,GC 的时候,不会去管处于安全区域的线程,线程离开安全区域的时候,如果处于 STW 则需要等待直至恢复

GC Roots 跟对象

虚拟机栈(栈帧中的本地变量表)中引用的对象
本地方法栈(Native 方法)中引用的对象
方法区中类静态属性引用的对象
方法区中常量引用的对象
所有被同步锁持有的对象

GC 并发标记

让垃圾回收器和用户线程同时运行,并发工作

并发工作时可能会导致把原本消亡的对象错误的标记为存活,产生浮动垃圾或者把原本存活的对象标记成消亡

两种方案,增量更新和原始快照(SATB

垃圾回收器中 CMS使用的是增量更新,G1使用的原始快照

三色标记

白色:表示对象尚未被垃圾回收器访问过
黑色:表示对象已经被垃圾回收器访问过,且这个对象的所有引用都已经扫描过
灰色:表示对象已经被垃圾回收器访问过,但这个对象至少存在一个引用还没有被扫描过

增量更新:

当被垃圾回收器访问过对象插入新的指向尚未被访问过的引用关系时,就将这个新插入的引用记录下来,等并发扫描结束之后,再将这些记录过的引用关系中的黑色对象为根,重新扫描一次。

(新插入的为跟重扫下)

原始快照:

当灰色对象要删除指向白色对象的引用关系时,就将这个要删除的引用记录下来,在并发扫描结束之后,再将这些记录过的引用关系中的灰色对象为根
无论引用关系删除与否,都会按照刚刚开始扫描那一刻的对象图快照开进行搜索

在标记时创建的新对象如何解决

GC 调优和问题

GC 耗时增大、线程 Block 增多、慢查询增多、CPU 负载高等四个表象,如何判断哪个是根因?

四种方法

1、**时序分析:**还原时间线,是否是CPU负载高

2、**概率分析:**统计概率学,结合历史问题的经验进行推断,由近到远按类型分析,如过往慢查的问题比较多

3、**实验分析:**通过故障演练方式进行模拟,触发其中一个条件或者多个测试

4、**反证分析:**对某一表象进行反证分析,判断是否发生和结果相关性

基础概念

Collector:垃圾收集器

Mutator:我们的应用程序

常见场景

Young GC频繁如何排查和解决

现象:Young GC 频繁,总的吞吐量下降;Full GC 频繁,可能会有较大停顿
原因:Young/Eden 区过小,Eden 被装满的时间变短,应该回收的对象参与GC;或者是分配速率过大,观察出现问题前后Mutator的分配速率(单位时间内分配的内存量)
解决:
如果是Yong/Eden过小就增加,Old大小是活跃对象的2~3倍,剩下都可以给Yong
如果是分配速率过大,有两种情况
如果是偶发较大,则在业务逻辑上优化
如果是一直较大,当前的垃圾收集器已经不满足程序的期望了,这种情况要么扩容,要么调整 GC 收集器类型或加大空间

案例:原配置为 Young 1.2G + Old 2.8G,通过观察 CMS GC 的情况找到存活对象大概为 300~400M,于是调整 Old 1.5G 左右,剩下 2.5G 分给 Young 区。仅仅调了一个 Young 区大小参数(-Xmn),整个 JVM 一分钟 Young GC 从 26 次降低到了 11 次,总的 GC 时间从 1100ms 降低到了 500ms,CMS GC 次数也从 40 分钟左右一次降低到了 7 小时 30 分钟一次。

案例2:Young GC的次数会在某一个时间点飙升。同时伴随着Old区域内存快速升高,最后会触发一次Full GC
没有设置最大缓存数量和弱引用,而是设置了一个几分钟的过期时间

Old GC问题如何解决

有频繁和耗时长两种情况

频繁:Old 区频繁的做 CMS GC,但是每次耗时不是特别长
原因:没看懂博客,后面再找

耗时长:Old GC 不频繁但单次耗时大
现象:CMS GC 单次 STW 最大超过 1000ms
原因:STW的阶段主要是Init Mark和Final Remark这两个阶段,Init Mark主要标记Old中的对象,Final Remark
是在 Background GC 执行了 InitialMarking 步骤下发生,相对于init Mark后续多了 Card Table 遍历、Reference 实例的清理等操作,大部分问题都出在 Final Remark 过程
解决:观察详细 GC 日志,找到出问题时 Final Remark 日志,分析下 Reference 处理和元数据处理 real 耗时是否正常,详细信息需要通过 -XX:+PrintReferenceGC 参数开启,有两种情况:
1、FinalReference:通过优化代码的方式来解决,或者开启-XX:+ParallelRefProcEnabled
2、symbol table:通过 -XX:-CMSClassUnloadingEnabled 来避免 MetaSpace 的处理

产生了大量的对象,且极有可能有一部分大对象。小对象引发的Young GC频繁,而大对象引发了Old GC频繁

应用启动时Full GC频繁

调用System.gc()
Old区空间不足
永久代/元空间满

偏向锁停顿

GC log中发现十几秒的停顿,次数多,
原因是jdk8对锁进行了优化,添加偏向锁,当遇见锁竞争的时候取消锁需要safe point,导致STW
添加参数 -UserBiasedLocking(改善锁机制性能) 即可

动态扩容引起空间动荡

现象:服务刚启动GC次数较多
原因:-Xms 和 -Xmx不一致,初始大小不够时向操作空间申请扩容,此时GC
解决:尽量将成对出现的空间大小配置参数设置成固定的,-Xms 和 -Xmx,-XX:MaxNewSize 和 -XX:NewSize,-XX:MetaSpaceSize 和 -XX:MaxMetaSpaceSize

MetaSpace 区 OOM

现象:JVM 在启动后或者某个时间点开始,MetaSpace 的已使用大小在持续增长,同时每次 GC 也无法释放,调大 MetaSpace 空间也无法彻底解决
原因:ClassLoader 不停地在内存中 load 了新的 Class ,一般这种问题都发生在动态类加载等情况上
解决:dump 快照之后通过JProfiler或MAT观察Classes的直方图,看一下具体是哪个包下的 Class 增加较多,或者添加-XX:+TraceClassLoading 和 -XX:+TraceClassUnLoading 参数观察详细的类加载和卸载信息

单次 CMS Old GC 耗时长

现象:CMS GC 单次 STW(Stop-The-World JVM内存停顿的一种状态)最大超过 1000ms,不会频繁发生
原因:

CPU频繁打满并且FULL GC

首先需要导出 jstack(打印线程快照) 和内存信息,然后重启系统,尽快保证系统的可用性

可能出现的问题

代码问题
1、代码中某个位置读取数据量较大,导致系统内存耗尽,从而导致 Full GC 次数过多,系统缓慢
2、代码中有比较耗 CPU 的操作,导致 CPU 过高,系统运行缓慢
3、代码有阻塞,或者陷入死锁
4、误调用System.gc()

JVM问题
1、系统承载高并发请求,处理数据量大,Young GC快速填满,频繁进入老年代,频繁触发Full GC
2、发生内存泄露,无法回收,占用老年代,频繁触发full GC
3、Metaspace(永久代)因为加载类过多触发Full GC
4、加载大对象

java 类加载器

启动类加载器(Bootstrap ClassLoader):负责加载 JAVA_HOME\lib 目录中的
扩展类加载器(Extension ClassLoader):负责加载 JAVA_HOME\lib\ext 目录中的
应用程序类加载器(Application ClassLoader):负责加载用户路径(classpath)上的类库,JVM 通过双亲委派模型进行类的加载,也可以通过继承 java.lang.ClassLoader
实现自定义的类加载器

双亲委派:当一个类收到了类加载请求,他首先不会尝试自己去加载这个类,而是把这个请求委派给父
类去完成,只有当父类加载器反馈自己无法完成这个请求的时候,子类加载器才会尝试自己去加载
好处:保证使用不同的类加载器最终得到的都是同样一个对象

内存泄露

还存在可达引用但是已经没用了的对象

八种情况

静态集合类:如HashMap、LinkedList等,如果这些容器为静态的,那么它们生命周期与JVM程序一致,则容器中对象在程序结束之前将不能被释放,从而造成内存泄漏

单例模式:单例的静态特性,和静态集合导致内存泄漏的原因类似

内部类持有外部类

各种连接,如数据库连接,网络连接和IO连接:不使用时没有释放链接

变量不合理的作用域:一个变量的定义作用范围大于其使用范围

改变哈希值:当一个对象被存储进HashSet集合中以后,就不能修改这个对象中的那些参与计算哈希值的字段

缓存泄漏:数据加载到缓存中,然后被遗忘

监听器和回调:

Out of Memory 内存溢出

没有空闲内存,且垃圾回收器无法提供更多内存,内存使用增长过快,垃圾回收跟不上内存消耗速度

内存不够可能的原因

1、堆内存设置不够,可以通过-Xms、-Xmx来调整

2、创建大量大对象,长时间存在被引用

3、OOM前必有GC

数据结构

链表

单链表

有一个指向下一结点的指针

优点:单向链表增加删除节点简单。遍历时候不会死循环

缺点:只能从头到尾遍历。只能找到后继,无法找到前驱,也就是只能前进

**判断有环:**其实指的是链表会不会是一个圆形的

从给定链表的第一个节点开始遍历,每遍历至一个节点,都将其和所有的前驱节点进行比对,如果为同一个节点,则表明当前链表中有环;反之,如果遍历至链表最后一个节点,仍未找到相同的节点,则证明该链表中无环

双链表

有一个指向上一个节点的指针,还有一个指向下一个节点的指针

优点:可以找到前驱和后继,可进可退

缺点:增加删除节点复杂,多需要分配一个指针存储空间

数组快还是链表快?

数组快,CPU取数据时会把周边的一起取了,而链表是离散的

平衡二叉树

插入的时候保持二叉树的平衡,基于插入的效率换查询的效率,如果插入比查询多,则不合适需要花费大量开销

左子树,右子树高度差绝对值不大于1

红黑树

树的节点增加,可能导致一方树节点不断增加,无法保持平衡,所以增加红黑树,红黑树使用变色和旋转的方式包 保证

1、根节点黑色

2、红色节点的两个子节点都是黑色,从叶子到根的所有路径不能超过连续两个红色节点

3、任一节点到期每个叶子的所有路径都包含相同数目的黑色节点

最长子树不超过最短子树的二倍

B树

有序多叉树

每个节点有M-1(因为有个节点被占用)个子节点。

根节点至少有两个子节点
每个节点有M-1个key,并且以升序排列
位于M-1和M key的子节点的值位于M-1 和M key对应的Value之间
其它节点至少有M/2个子节点

Insert image description here

B+树

非叶子节点不再存储数据,数据全部在叶子节点上存储,数据在叶子节点上存储带来的好处

1、每个数据都需要从头查到尾,比较稳定

2、叶子节点使用链表从小到大链接,增加了区间访问性

3、除了叶子节点外其他不需要存数据,空间利用率高

4、更好删除

Insert image description here

编码题

打印100以内除了尾数为3,5,7的所有数

for(int i=1;i<=100;i++) {
    int y = i%10;//100以内的数,通过取余求出尾数
    if(y3 || y5 || y==7) {
    	continue;//如果尾数为3 5 7 ,则跳过后面的打印,进行下一轮循环
    }
    System.out.println(i);
}

冒泡排序

for (int i = 0; i < array.length-1; i++) {
    boolean flg = false;
    for (int j = 0; j < array.length-1-i; j++) {
        if(array[j] > array[j+1]){
            int tmp = 0;
            tmp = array[j];
            array[j] = array[j+1];
            array[j+1] = tmp;
            flg = true;
        }
    }
    if(flg == false){
    	return;
    }
}

字符串判空

最多的效率低
if(s == null || s.equals(""));

效率高
if(s == null || s.length() == 0);

jdk6后有的
if(s == null || s.isEmpty());

快排

思路:
	1、选择基数,比基数大的放右边,小的放左边
	2、不断计算基数

public static void main(String[] args) {
	int[] arr = new int[]{1, 8, 5, 7, 2, 3, 4, 9, 6, 10};
	quicksort(arr, 0, arr.length - 1);
}

public static void quicksort(int[] arr, int left, int right) {
	if (right >= left) {
		//保存基数
		int basic = arr[left];
		//定义左右指针
		int i = left;
		int j = right;
		while (i < j) {		//左指针小于右指针
			while (i < j && arr[j] > basic) {//操作右指针找到小于基数的下标
				j--;
			}
			if (i < j) {
				arr[i] = arr[j];	//将右指针对应小于基数的值放到左指针所指的位置
				i++;				//左指针自加
			}
			while (i < j && arr[i] < basic) {//相反,找到大于基数的下标
				i++;
			}
			if (i < j) {
				arr[j] = arr[i];	//大于基数的值赋给右指针所指的位置
				j--;				//右指针自减
			}
		}
		arr[i] = basic;				//将基数放入到指针重合处
		quicksort(arr, left, i - 1);	//重复调用,对左半部分数组进行排序
		quicksort(arr, i + 1, right);	//对右半部分数组进行排序
	}
}

给定一个包含大写字母和小写字母的字符串,找到通过这些字母构造成的最长的回文串

public String getLongestPalindrome(String A, int n) {
        if(A == null || n == 0){
            return "";
        }
        //中心扩展法:某个字符向两边扩展
        char[] ch = A.toCharArray();
        int start = 0, end = 0;
        for(int i = 0 ; i < ch.length ; i++){
            //回文子串长度为奇数
            int oddLen = LongestPalindrome(ch,i,i);
            //回文子串长度为偶数
            int evenLen = LongestPalindrome(ch,i,i+1);
            int len = Math.max(oddLen,evenLen);
            if(len > end - start){
                start = i - (len - 1)/2;
                end = i + len/2;
            }
        }
        // 注意:这里的end+1是因为 java自带的左闭右开的原因
        return s.substring(start,end + 1);;
    }
    private int LongestPalindrome(char[] ch,int left,int right){
        while(left >= 0 && right < ch.length && ch[left] == ch[right]){
            //向两边扩展
            left--;
            right++;
        }
        return right - left - 1;//回文长度
    }

Spring

Spring 事务嵌套

同一个类中事务嵌套的话,最终的结果应该是取决于最外层的方法事务的传播特性

如果是不同的类的事务嵌套的话,外层的方法按照外层的事务传播属性执行,内层的传播属性按照内层的传播属性的特点去运行

事务名称 效果
REQUIRED(默认) 当前方法存在事务时,子方法加入该事务。此时父子方法共用一个事务,无论父子方法哪个发生异常回滚,整个事务都回滚。即使父方法捕捉了异常,也是会回滚。而当前方法不存在事务时,子方法新建一个事务
SUPPORTS 当前存在事务,则加入当前事务,如果当前没有事务,就以非事务方法执行
MANDATORY 当前存在事务,则加入当前事务,如果当前事务不存在,则抛出异常
REQUIRES_NEW 无论当前方法是否存在事务,子方法都新建一个事务。此时父子方法的事务时独立的,它们都不会相互影响。但父方法需要注意子方法抛出的异常,避免因子方法抛出异常,而导致父方法回滚
NOT_SUPPORTED 始终以非事务方式执行,如果当前存在事务,则挂起当前事务
NEVER 不使用事务,如果当前事务存在,则抛出异常
NESTED 当前方法存在事务时,子方法加入在嵌套事务执行。当父方法事务回滚时,子方法事务也跟着回滚。当子方法事务发送回滚时,父事务是否回滚取决于是否捕捉了异常。如果捕捉了异常,那么就不回滚,否则回滚 REQUIRED 来说,无论父子方法哪个发生异常,全都会回滚。而 REQUIRED 则是:父方法发生异常回滚时,子方法事务会回滚。而子方法事务发送回滚时,父事务是否回滚取决于是否捕捉了异常

Spring事务失效

1、事务方法访问修饰符不是public,或者使用static、final修饰

2、数据库不支持

3、异常被 catch 住

4、多线程调用

Spring循环依赖

一级缓存为:singletonObjects缓存的是已经经历了完整生命周期的bean对象;
二级缓存为:earlySingletonObjects缓存的是早期的 bean对象;
三级缓存为:singletonFactories缓存的是 ObjectFactory,主要用来去生成原始对象进行了 AOP之后得到的「代理对象」,在每个 Bean 的生成过程中,都会提前暴露一个工厂,这个工厂可能用到,也可能用不到,如果没有出现循环依赖依赖本 bean,那么这个工厂无用,本 bean 按照自己的生命周期执行,执行完后直接把本 bean 放入 singletonObjects 中即可,如果出现了循环依赖依赖了本 bean,则另外那个 bean 执行 ObjectFactory 提交得到一个 AOP 之后的代理对象(如果有 AOP 的话,如果无需 AOP ,则直接得到一个原始对象)。

创建对象时,先创建对象(这时候还没注入属性)存入二级缓存earlySingletonObjects,用的时候取

SpringBean 自己注入自己

可以注入,但是要注意不要调用本方法,否则会报StackOverflowError堆栈溢出异常

SpringCloud 组件

1、注册中心组件(服务治理):Netflix Eureka;
2、负载均衡组件:Netflix Ribbon,各个微服务进行分摊,提高性能;
3、熔断器组件(断路器):Netflix Hystrix,Resilience4j ;保护系统,控制故障范围;
4、网关服务组件:Zuul,Spring Cloud Gateway;api网关,路由,负载均衡等多种作用;
5、配置中心:Spring Cloud Config,将配置文件组合起来,放在远程仓库,便于管理;

SpringBoot自动装配

通过注解或者一些简单的配置就能在 Spring Boot实现一些功能

实现原理:
首先是主要注解@EnableAutoConfiguration
@EnableAutoConfiguration :启用 SpringBoot 的自动配置机制,底层使用的是AutoConfigurationImportSelector类,扫描META-INF/spring.factories下的配置类


@Configuration :允许在上下文中注册额外的 bean 或导入其他配置类
@ComponentScan: 扫描被@Component (@Service,@Controller)注解的 bean

Bean的生命周期

1)根据配置情况调用 Bean 构造方法或工厂方法实例化 Bean。

2)利用依赖注入完成 Bean 中所有属性值的配置注入。

3)如果 Bean 实现了 BeanNameAware 接口,则 Spring 调用 Bean 的 setBeanName() 方法传入当前 Bean 的 id 值。

4)如果 Bean 实现了 BeanFactoryAware 接口,则 Spring 调用 setBeanFactory() 方法传入当前工厂实例的引用。

5)如果 Bean 实现了 ApplicationContextAware 接口,则 Spring 调用 setApplicationContext() 方法传入当前 ApplicationContext 实例的引用。

6)如果 BeanPostProcessor 和 Bean 关联,则 Spring 将调用该接口的预初始化方法 postProcessBeforeInitialzation() 对 Bean 进行加工操作,此处非常重要,Spring 的 AOP 就是利用它实现的。

7)如果 Bean 实现了 InitializingBean 接口,则 Spring 将调用 afterPropertiesSet() 方法。

8)如果在配置文件中通过 init-method 属性指定了初始化方法,则调用该初始化方法。

9)如果 BeanPostProcessor 和 Bean 关联,则 Spring 将调用该接口的初始化方法 postProcessAfterInitialization()。此时,Bean 已经可以被应用系统使用了。

10)如果在 中指定了该 Bean 的作用范围为 scope=“singleton”,则将该 Bean 放入 Spring IoC 的缓存池中,将触发 Spring 对该 Bean 的生命周期管理;如果在 中指定了该 Bean 的作用范围为 scope=“prototype”,则将该 Bean 交给调用者,调用者管理该 Bean 的生命周期,Spring 不再管理该 Bean。

11)如果 Bean 实现了 DisposableBean 接口,则 Spring 会调用 destory() 方法将 Spring 中的 Bean 销毁;如果在配置文件中通过 destory-method 属性指定了 Bean 的销毁方法,则 Spring 将调用该方法对 Bean 进行销毁。

Bean的作用域

singleton : IoC 容器中只有唯一的 bean 实例。Spring 中的 bean 默认都是单例的,是对单例设计模式的应用。

prototype : 每次获取都会创建一个新的 bean 实例。也就是说,连续 getBean() 两次,得到的是不同的 Bean 实例。

request (仅 Web 应用可用): 每一次 HTTP 请求都会产生一个新的 bean(请求 bean),该 bean 仅在当前 HTTP request 内有效。

session (仅 Web 应用可用) : 每一次来自新 session 的 HTTP 请求都会产生一个新的 bean(会话 bean),该 bean 仅在当前 HTTP session 内有效。

application/global-session (仅 Web 应用可用): 每个 Web 应用在启动时创建一个 Bean(应用 Bean),该 bean 仅在当前应用启动时间内有效。

websocket (仅 Web 应用可用):每一次 WebSocket 会话产生一个新的 bean。

依赖注入的时间

1)、用户第一次调用getBean()方法时,IOC 容器触发依赖注入。

2)、当用户在配置文件中将元素配置了lazy-init=false 属性,即让容器在解析注册Bean 定义时进行预实例化,触发依赖注入。

哪些方式可以把Bean注入到IOC容器

使用xml的方式来声明Bean的定义,

使用@CompontScan注解来扫描声明了@Controller、@Service、@Repository、@Component注解的类。

使用@Configuration+@Bean注解

使用@Import注解,导入配置类或者普通的Bean

使用FactoryBean工厂bean,动态构建一个Bean实例,Spring Cloud OpenFeign里面的动态代理实例就是使用FactoryBean来实现的。

实现ImportBeanDefinitionRegistrar接口,可以动态注入Bean实例。这个在Spring Boot里面的启动注解有用到。

实现ImportSelector接口,动态批量注入配置类或者Bean对象,这个在Spring Boot里面的自动装配机制里面有用到。

多线程 JUC

JUC是java.util.concurrent包的简称,java5.0添加,主要用于多线程编程

线程

停止线程

interrupt()方法,并非立即停止,而是增加标识
可以使用interrupted()方法测试线程是否停止

释放锁

线程异常,锁释放

JUC结构

1、tools(工具类),信号量三组工具类,包含三项

CountDownLatch(闭锁) 一个同步辅助类,在完成一组正在其他线程中执行的操作之前,它允许一个或多个线程一直等待

CyclicBarrier(栅栏) 一个同步辅助类,允许一组线程互相等待,直到到达某个公共屏障点 ,并且在释放等待线程后可以重用。

Semaphore(信号量) 是一个计数信号量,它的本质是一个“共享锁“。信号量维护了一个信号量许可集。线程可以通过调用 acquire()来获取信号量的许可;当信号量中有可用的许可时,线程能获取该许可;否则线程必须等待,直到有可用的许可为止。 线程可以通过release()来释放它所持有的信号量许可。

2、executor(执行者),Java里面线程池的顶级接口,真正线程池接口是ExecutorService,里面包含

ScheduledExecutorService 解决那些需要任务重复执行的问题
ScheduledThreadPoolExecutor 周期性任务调度的类实现

3、atomic(原子性包),JDK提供的一组原子操作类

4、locks(锁包),JDK提供的锁机制,相比synchronized关键字来进行同步锁,功能更加强大,它为锁提供了一个框架,该框架允许更灵活地使用锁包含的实现类有

ReentrantLock 独占锁,是指只能被独自占领,即同一个时间点只能被一个线程锁获取到的锁。
ReentrantReadWriteLock 包括子类ReadLock和WriteLock。ReadLock是共享锁,而WriteLock是独占锁。
LockSupport 具备阻塞线程和解除阻塞线程的功能,并且不会引发死锁。

5,collections(集合类):主要是提供线程安全的集合

ArrayList对应的高并发类是CopyOnWriteArrayList
HashSet对应的高并发类是CopyOnWriteArraySet
HashMap对应的高并发类是ConcurrentHashMap

网络IO操作

概念

同步:调用者要一直等待调用结果的通知后才能进行后续的执行
异步:指被调用方先返回应答让调用者先回去,然后再计算调用结果,计算完最终结果后再通知并返回给调用方,异步调用要想获得结果一般通过回调

阻塞:调用方一直在等待而且别的事情什么都不做,当前进/线程会被挂起,啥都不干
非阻塞:调用在发出去后,调用方先去忙别的事情,不会阻塞当前进/线程,而会立即返回

IO类型

Blocking IO - 阻塞IO(BIO)
首先数据未准备好,开始阻塞,数据准备好后,数据从kernel内核拷贝到用户内存,解除状态

NoneBlocking IO - 非阻塞IO,也就是NIO
一切都是非阻塞,个线程就能处理多个客户端的连接和读取
用户发起read操作,数据未准备好,不会加锁而是返回error,用户得到消息后如果是error,可以再次发送,NIO特点是用户进程需要不断的主动询问内核数据准备好了吗
问题:轮询内核占用大量时间,资源利用率低

IO multiplexing - IO多路复用(事件驱动IO)
一个进程可以监视多个描述符(socket),socket准备好通知程序进行读写
原理:将用户socket把对应注册到epoll,epoll监听socket上哪些消息到达

signal driven IO - 信号驱动IO

asynchronous IO - 异步IO

同步指令

Insert image description here

semaphore(信号量)

​ 计算机中某些内存区域只能提供给一定数目的线程去使用,多出的线程要等待,此时的解决办法就是在门口挂n把钥匙。进去的线程就取一把钥匙,出来时再把钥匙挂回远处。后到的线程发现钥匙架空了,就知道必须排队等待了,这种做法叫做信号量(semaphore),用来保证多个线程不会互相冲突。

用户线程和守护线程

Java线程分为用户线程和守护线程,

线程的daemon属性为true表示是守护线程,false表示是用户线程

用户线程

是系统的工作线程,它会完成这个程序需要完成的业务操作

守护线程

是一种特殊的线程,在后台默默地完成一些系统性的服务,比如垃圾回收线程,主线程死亡后守护线程立即跟着陪葬

1、当程序中所有用户线程执行完毕之后,不管守护线程是否结束,系统都会自动退出

如果用户线程全部结束了,意味着程序需要完成的业务操作已经结束了,系统可以退出

了。所以当系统只剩下守护进程的时候,java虚拟机会自动退出

2、设置守护线程,需要在start()方法之前进行

Future和Callable接口

Future接口定义了操作异步任务执行一些方法,如获取异步任务的执行结果、取消任务的执行、判断任务是否被取消、判断任务执行是否完毕等。

Callable接口中定义了需要有返回的任务需要实现的方法。

比如主线程让一个子线程去执行任务,子线程可能比较耗时,启动子线程开始执行任务后,主线程就去做其他事情了,过了一会才去获取子任务的执行结果

CompletableFuture 类

jdk8引入,同时实现了 Future 和 CompletionStage 接口,比如函数式编程、异步任务编排组合

方法:
boolean cancel(boolean mayInterruptIfRunning) :尝试取消执行任务。
boolean isCancelled() :判断任务是否被取消。
boolean isDone() : 判断任务是否已经被执行完成。
get() :等待任务执行完成并获取运算结果。
get(long timeout, TimeUnit unit) :多了一个超时时间。

new的方式创建
CompletableFuture<RpcResponse> resultFuture = new CompletableFuture<>();

get()获得异步结果

Monitor

管程,一种同步机制,保证保证(同一时间)只有一个线程可以访问被保护的数据和代码,每个对象头中都有Monitor对象

synchronized

原理

synchronized编译后指令可得到,底层是monitorenter 和 monitorexit指令

其中monitorenter指令指向同步代码块的开始位置,monitorexit 指令则指明同步代码块的结束位置。

当执行 monitorenter 指令时,线程试图获取锁也就是获取 monitor(monitor对象存在于每个Java对象的对象头中,synchronized 锁便是通过这种方式获取锁的,也是为什么Java中任意对象可以作为锁的原因) 的持有权.
当计数器为0则可以成功获取,获取后将锁计数器设为1也就是加1。
相应的在执行 monitorexit 指令后,将锁计数器设为0,表明锁被释放。如果获取对象锁失败,那当前线程就要阻塞等待,直到锁被另外一个线程释放为止

synchronized 修饰的方法

并没有monitorenter指令和monitorexit 指令,使用的是ACC_SYNCHRONIZED标识

jdk6的优化

JDK1.6 对锁的实现引入了大量的优化,如偏向锁、轻量级锁、自旋锁、适应性自旋锁、锁消除、锁粗化等技术来减少锁操作的开销

锁升级

锁有四种状态,无锁、偏向锁、轻量级锁、重量级锁
jdk1.6后会根据竞争情况升级锁

首先进入偏向锁,锁会偏向于第一个获得它的线程,会在对象头存储锁偏向的线程ID,以后该线程进入和退出同步块时只需要检查是否为偏向锁、锁标志位以及ThreadID,适用于一个线程反复获得同一锁的情况
加锁过程:
虚拟机将会把对象头中的标志位设为“01”,即偏向模式。
同时使用CAS操作把获取到这个锁的线程的ID记录在对象的Mark Word之中 ,如果CAS操作成功,持有偏向锁的线程以后每次进入这个锁相关的同步块时,虚拟机都可以不再进行任何同步操作,偏向锁的效率高

有锁竞争时升级为轻量级锁
加锁过程:
虚拟机首先将在当前线程的栈帧中建立一个名为锁记录(Lock Record)的空间,用于存储锁对象目前的Mark Word的拷贝,将对象的Mark Word复制到栈帧中的Lock Record中,将Lock Reocrd中的owner指向当前对象,JVM利用CAS操作尝试将对象的Mark Word更新为指向Lock Record的指针,如果成功表示竞争到锁,则将锁标志位变成00,执行同步操作;如果失败则判断当前对象的Mark Word是否指向当前线程的栈帧,如果是则表示当前线程已经持有当前对象的锁,则直接执行同步代码块;否则只能说明该锁对象已经被其他线程抢占了,这时候需要膨胀成重量级锁
总结:使用 CAS 操作,在线程栈帧与锁对象建立双向的指针

重量级锁,锁竞争加剧,多个线程在同一时刻进入临界区
加锁过程:
通过操作系统的 Mutex Lock 来实现同步的
JVM 基于进入和退出 Monitor 对象来实现方法同步和代码块同步,通过使用 monitorenter 和 monitorexit 指令实现

volatile

线程同步的轻量级实现,只能用于变量,能保证数据的可见性,但不能保证数据的原子性
volatile主要保证的是可见性和防止指令重排。

一般可见性方面,volatile可以使得本线程内的缓存失效,也就是读volatile变量的时候直接从内存中读,而写volatile变量的时候直接写入内存

volatile使用内存屏障确保读操作之前所有的写操作都已经完成 并且所有线程可见
来保证指令不会重排序

???  提供了四个内存屏障,loadstore读写、loadload读读、storestore写写、storeload写读
读写   

synchronized和volatile 的区别

两者互补

volatile 关键字是线程同步的轻量级实现,volatile性能肯定比synchronized关键字要好 。
volatile 关键字只能用于变量而 synchronized 关键字可以修饰方法以及代码块 。
volatile 关键字能保证数据的可见性,但不能保证数据的原子性。synchronized 关键字两者都能保证。
volatile关键字主要用于解决变量在多个线程之间的可见性,而 synchronized 关键字解决的是多个线程之间访问资源的同步性。

volatile 实现原理

  • 内存屏障(memory barriers):一组处理器指令,用于实现对内存操作的顺序限制。

1、通过插入内存屏障指令禁止编译器和CPU对程序进行重排序

2、当对声明了volatile的变量进行写操作时,JVM就会向处理器发送一条Lock前缀的指令,这条Lock前缀指令相当于一个内存屏障

​ 它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成

​ 它会强制将对缓存的修改操作立即写入主存

​ 如果是写操作,它会导致其他CPU中对应的缓存行无效

线程池

通过定义参数创建一个存储多个线程的容器,实现了线程的复用

好处

降低资源消耗,提高响应速度,提高线程的可管理性

创建

第一种:用ThreadPoolExecutor构造函数创建,避免资源耗尽风险(推荐)

第二种:Executor 框架的工具类Executors来实现,可以创建三种
FixedThreadPool返回一个固定线程数量的线程池
SingleThreadExecutor返回一个只有一个线程的线程池
CachedThreadPool返回一个可根据实际情况调整线程数量的线程池,有线程用就复用,没有就创建新的
ScheduledThreadPool 返回一个用来在给定的延迟后运行任务或者定期执行任务的线程池

 ThreadPoolExecutor poolExecutor = new ThreadPoolExecutor(4,8,10,
      TimeUnit.SECONDS,
      new ArrayBlockingQueue(8,true),
      new ThreadPoolExecutor.CallerRunsPolicy());

    List<Callable<Integer>> list = new ArrayList<Callable<Integer>>();

    for(int i = 0; i <10;i++){
    
    
      list.add(new Callable<Integer>(){
    
    
        public Integer call() throws Exception {
    
    
          System.out.println("执行线程");
          return 0;
        }
      });
    }

    try {
    
    
      poolExecutor.invokeAll(list);
    } catch (InterruptedException e) {
    
    
      e.printStackTrace();
    }
  }

核心参数

  1. 最大线程数maximumPoolSize
  2. 核心线程数corePoolSize
  3. 活跃时间keepAliveTime
  4. 阻塞队列workQueue
  5. 拒绝策略RejectedExecutionHandler

阻塞队列

一个队列,保证两个操作,在队列为空时,获取元素的线程会等待队列变为非空,队列满时,存储元素的线程会等待队列可用

无界队列 LinkedBlockingQueue,队列大小无限制
有界队列 ArrayBlockingQueue(先进先出)
同步移交队列 如果不希望任务在队列中等待而是希望将任务直接移交给工作线,不存储元素SynchronousQueue,线程之间移交的机制

拒绝策略

  1. AbortPolicy:直接丢弃任务,抛出异常,这是默认策略
  2. CallerRunsPolicy:只用调用者所在的线程来处理任务
  3. DiscardOldestPolicy:丢弃等待队列中最旧的任务,并执行当前任务
  4. DiscardPolicy:直接丢弃任务,也不抛出异常

执行流程

  1. 当我们提交任务,线程池会根据corePoolSize大小创建若干任务数量线程执行任务
  2. 当任务的数量超过corePoolSize数量,后续的任务将会进入阻塞队列阻塞排队
  3. 当阻塞队列也满了之后,那么将会继续创建(maximumPoolSize-corePoolSize)个数量的线程来
    执行任务,如果任务处理完成,maximumPoolSize-corePoolSize额外创建的线程等待
    keepAliveTime之后被自动销毁
  4. 如果达到maximumPoolSize,阻塞队列还是满的状态,那么将根据不同的拒绝策略对应处理

线程池命名

1、利用 guava 的 ThreadFactoryBuilder
2、自己实现ThreadFactor

线程池数量

N是CPU核数

CPU 密集型任务,N+1,主要消耗CPU资源,比如说对大量数据排序

I/O 密集型任务,2N,大部分处理IO交互,比如说涉及到网络读取,或者文件读取

也可以使用第三方工具进行设置

核心线程数会不会被回收

默认不回收,可以通过设置allowCoreThreadTimeOut函数回收

线程池的监控

线程的变化,任务的变化

简述:继承ThreadPoolExecutor类,然后封装获得线程信息的函数,线程信息可以通过原本提供的函数处理

getPoolSize():获取线程池的线程数量。如果线程池不销毁的话,线程池里的线程不会自动销毁。

getActiveCount():获取活跃的线程数。

getCompletedTaskCount:获取线程池再运行过程中已完成的任务数量。

getQueue().size():获取队列中还有多少积压任务。

ForkJoinPool

ThreadPoolExecutor是兄弟类,将一个任务拆分成多个单元,每个单元分别得到执行,最后合并每个单元的结果

**工作窃取算法:**假设两个线程执行任务,容易发生访问同一个队列,产生竞争,所以约定好,A从双端队列的尾部拿任务、B从双端队列的头部拿任务,ForkJoinPool中每个线程都有自己的队列,但是如果任务执行完成后可以取得其它线程的任务,充分利用资源

CSA

CAS,CompareAndSwap,比较并交换 ,主要是通过处理器的指令来保证操作的原子性

  1. 变量内存地址,V表示
  2. 旧的预期值,A表示
  3. 准备设置的新值,B表示

当执行CAS指令时,只有当V等于A时,才会用B去更新V的值,否则就不会执行更新操作

缺点

ABA问题,解决:使用版本号。在变量前面追加 上版本号,每次变量更新的时候把版本号加一
循环时间长开销大
只能保证一个共享变量的原子操作

原理

CPU的原子指令cmpxchg指令,有CPU实现

例子

AtomicInteger atomicInteger = new AtomicInteger(5);
		// 期望是5,如果atomicInteger是5则改为2020
        System.out.println(atomicInteger.compareAndSet(5, 2020)+"\t"+atomicInteger.get());
        System.out.println(atomicInteger.compareAndSet(5, 1024)+"\t"+atomicInteger.get());

ReentrantLock

实现了 Lock 接口,是一个可重入且独占式的锁,和 synchronized 关键字类似

ReentrantLock 里面有一个内部类 SyncSync 继承 AQS(AbstractQueuedSynchronizer),添加锁和释放锁的大部分操作实际上都是在 Sync 中实现的

ReentrantLock 默认使用非公平锁,也可以通过构造器来显示的指定使用公平锁

// 传入一个 boolean 值,true 时为公平锁,false 时为非公平锁
public ReentrantLock(boolean fair) {
    sync = fair ? new FairSync() : new NonfairSync();
}

synchronized 和 ReentrantLock 有什么区别

两者都是可重入锁
synchronized 依赖于 JVM 而 ReentrantLock 依赖于 API
ReentrantLock增加了一些高级功能,等待可中断、可实现公平锁、可实现选择性通知(锁可以绑定多个条件)

ThreadLocal

ThreadLocal提供线程局部变量,每一个线程在访问ThreadLocal实例的时候(通过其get或set方法)都有自己的、独立初始化的变量副本,一般都是用私有静态常量

创建的两种方法

private static final ThreadLocal<Integer> threadLocal = new ThreadLocal<Integer>(){
    
    
        @Override
        protected Integer initialValue() {
    
    
            return 0;
        }
    };

private static final ThreadLocal<Integer> threadLocal1 = ThreadLocal.withInitial(() -> 0);

线程中使用ThreadLocal

new Thread(() -> {
    
    
            try
            {
    
    
                threadLocal.set(1);
                threadLocal.get()
            }catch (Exception e){
    
    
                e.printStackTrace();
            }finally {
    
    
                /**
                 * 必须回收自定义的 ThreadLocal 变量,尤其在线程池场景下,线程经常会被复用,
                 * 如果不清理自定义的 ThreadLocal 变量,可能会影响后续业务逻辑和造成内存泄露等问题。
                 * 尽量在代理中使用 try-finally 块进行回收
                 */
                house.threadLocal.remove();
            }
        },"t1").start();

原理

在Thread类中有threadLocals和inheritableThreadLocals变量

//与此线程有关的ThreadLocal值。由ThreadLocal类维护
ThreadLocal.ThreadLocalMap threadLocals = null;

//与此线程有关的InheritableThreadLocal值。由InheritableThreadLocal类维护
ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;

ThreadLocalMap理解为ThreadLocal类实现的定制化的hashMap,默认情况下为null,只有当前线程调用 ThreadLocal 类的 set或get方法时才创建它们,并调用ThreadLocalMap类对应的 get()、set()方法
ThreadLocalMap的 key 就是 ThreadLocal对象,value 就是 ThreadLocal 对象调用set方法设置的值

所以最终的变量是放在了当前线程的 ThreadLocalMap 中,并不是存在 ThreadLocal 上,ThreadLocal 可以理解为只是ThreadLocalMap的封装,传递了变量值

ThreadLocal 内存泄露问题

ThreadLocalMap 中使用的 key 为 ThreadLocal 的弱引用,而 value 是强引用。所以,如果 ThreadLocal 没有被外部强引用的情况下,在垃圾回收的时候,key 会被清理掉,而 value 不会被清理掉可能会产生内存泄露

解决办法:使用完 ThreadLocal方法后 最好手动调用remove()方法

为什么key设置为弱引用:为了让程序自动的对访问不到的数据进行回收提醒

为什么Value不被设置弱引用:避免value被清除,报空指针

如果不调用remove()会导致数据污染业务出现问题

public class TestThreadLocalContext {
    
    

    public static ThreadLocal<T> settleThreadLocal = new ThreadLocal<>();

    public static void set(T value) {
    
    
        settleThreadLocal.set(value);
    }

    public static void unset() {
    
    
        settleThreadLocal.remove();
    }

    public static T get() {
    
    
        return settleThreadLocal.get();
    }
}

AQS

抽象队列式同步器,一个用来构建锁和同步器的抽象类 ,通过内置的FIFO(先进先出)队列来完成资源获取线程的排队工作,并通过一个int类变量表示持有锁的状态

核心思想

如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并且将共享资源设置为锁定状态。如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制,这个机制 AQS 是用 CLH 队列锁 实现的,即将暂时获取不到锁的线程加入到队列中

CLH,Craig,Landin,and Hagersten 队列

作用

加锁导致阻塞,阻塞就需要排队,实现排队需要队列,这时候需要排队等待机制,AQS将请求共享资源的线程封装成队列的结点(Node),通过CAS、自旋以及LockSupport.park()的方式,维护state变量的状态,使并发达到同步的效果

原理

AQS 使用 int 成员变量 state 表示同步状态,通过内置的 线程等待队列 来完成获取资源线程的排队工作

使用

持线程抢占两种锁——独占锁和共享锁:
独占锁:同一个时刻只能被一个线程占有,如ReentrantLock,ReentrantWriteLock等,它也可以分为公平锁和非公平锁
共享锁:同一时间点可以被多个线程同时占有,如ReentrantReadLock,Semaphore等

常见的同步类工具

semaphore:信号量,可以用来控制同时访问特定资源的线程数量

原理

是共享锁的一种实现,默认构造 AQS 的 state 值为 permits,你可以将 permits 的值理解为许可证的数量,只有拿到许可证的线程才能执行
调用semaphore.acquire() ,线程尝试获取许可证,如果 state >= 0 的话,则表示可以获取成功。如果获取成功的话,使用 CAS 操作去修改 state 的值 state=state-1。如果 state<0 的话,则表示许可证数量不足。此时会创建一个 Node 节点加入阻塞队列,挂起当前线程

CountDownLatch :倒计时器,允许count 个线程阻塞在一个地方,直至所有线程的任务都执行完毕

原理:

共享锁的一种实现,默认构造 AQS 的 state 值为 count。当线程使用 countDown() 方法时,其实使用了tryReleaseShared方法以 CAS 的操作来减少 state,直至 state 为 0 。当调用 await() 方法的时候,如果 state 不为 0,那就证明任务还没有执行完毕,await() 方法就会一直阻塞,也就是说 await() 方法之后的语句不会被执行。然后,CountDownLatch 会自旋 CAS 判断 state == 0,如果 state == 0 的话,就会释放所有等待的线程,await() 方法之后的语句得到执行

用法:
1、某线程开始前等待n个线程执行完毕
2、多个线程开始执行任务的最大并行性,就是多个线程同时执行

CyclicBarrier:循环栅栏,和CountDownLatch 类似,用于实现线程间的等待,让一组线程到达一个屏障(也可以叫同步点)时被阻塞,直到最后一个线程到达屏障时,屏障才会开门,所有被屏障拦截的线程才会继续干活

原理

CyclicBarrier 内部通过一个 count 变量作为计数器,count 的初始值为 parties 属性的初始化值,每当一个线程到了栅栏这里了,那么就将计数器减 1。如果 count 值为 0 了,表示这是这一代最后一个线程到达栅栏,就尝试执行我们构造方法中输入的任务

实现阻塞队列,阻塞唤醒

所谓阻塞某些情况下会挂起线程,一旦条件成熟,被阻塞的线程就会被自动唤醒

提供的阻塞队列

ArrayBlockingQueue 由数组构成的有界阻塞队列
LinkedBlockingQueue 由链表构成的有界阻塞队列
PriorityBlockingQueue 支持优先级排序的无界阻塞队列
DelayQueue 支持优先级的延迟无界阻塞队列
SynchronousQueue 单个元素的阻塞队列
LinkedTransferQueue 由链表构成的无界阻塞队列
LinkedBlockingDeque 由链表构成的双向阻塞队列

阻塞队列的目的

1、在队列为空时,从队列里取出元素的操作将被阻塞

2、当队列为满时,向队列中添加元素的操作会被阻塞

public class MyBlockQueue {
    
    

  private int[] elem = new int[1000];
  private int head = 0;
  private int tail = 0;
  private int size = 0;

  public void put(int data) throws InterruptedException {
    
    
    synchronized (this) {
    
    
      if(this.size == this.elem.length){
    
    
        // 使用wait进行阻塞
        this.wait();
      }

      this.elem[tail++] = data;
      this.size++;
      if(this.tail >= this.elem.length){
    
    
        this.tail = 0;
      }

      // 添加完成后使用notify唤醒
      this.notify();
    }
  }

  public int take() throws InterruptedException {
    
    
    // 加锁保证线程安全
    synchronized (this) {
    
    
      if(this.size == 0){
    
    
        this.wait();
      }
      int ret = this.elem[head++];
      this.size--;
      if(this.head >= this.elem.length){
    
    
        this.head = 0;
      }
      this.notify();
      return ret;
    }
  }

}

对象锁和类锁

对象锁:锁住对象,不同实例的锁互不影响,可以把synchronized 作用于方法上或者代码块上

类锁:不管多少对象都共用同一把锁,同步执行,一个线程执行结束、其他的才能够调用同步的部分,实现方式可以使用synchronized 修饰静态方法,或者锁住class对象

public synchronized static void method()

public void method(){
   synchronized(object.class){
   }
}

悲观锁

认为自己在使用数据的时候一定有别的线程来修改数据,因此在获取数据的时候会先加锁,确保数据不会被别的线程修改。

synchronized关键字和Lock的实现类都是悲观锁

适用场景:

适合写操作多的场景,先加锁可以保证写操作时数据正确。

显式的锁定之后再操作同步资源

//=============悲观锁的调用方式
public synchronized void m1()
{
    //加锁后的业务逻辑......
}

// 保证多个线程使用的是同一个lock对象的前提下
ReentrantLock lock = new ReentrantLock();
public void m2() {
    lock.lock();
    try {
        // 操作同步资源
    }finally {
        lock.unlock();
    }
}

两种实现:synchronized、ReentrantLock

乐观锁

说明:

乐观锁认为自己在使用数据时不会有别的线程修改数据,所以不会添加锁,只是在更新数据的时候去判断之前有没有别的线程更新了这个数据。

如果这个数据没有被更新,当前线程将自己修改的数据成功写入。如果数据已经被其他线程更新,则根据不同的实现方式执行不同的操作

乐观锁在Java中是通过使用无锁编程来实现,最常采用的是CAS算法Java原子类中的递增操作就通过CAS自旋实现的。

乐观锁一般有两种实现方式:

1、采用版本号机制

2、CAS(Compare-and-Swap,即比较并替换)算法实现

适用场景:

适合读操作多的场景,不加锁的特点能够使其读操作的性能大幅提升。

乐观锁则直接去操作同步资源,是一种无锁算法,得之我幸不得我命,再抢

//=============乐观锁的调用方式
// 保证多个线程使用的是同一个AtomicInteger
private AtomicInteger atomicInteger = new AtomicInteger();
atomicInteger.incrementAndGet();

为什么任何一个对象都可以成为一个锁?

**所有类默认继承Object类,**在HotSpot虚拟机中,monitor(管程)采用底层C++的ObjectMonitor实现

每个对象天生都带着一个对象监视器(管程)

ObjectMonitor中有几个关键属性

属性 作用
_owner 指向持有ObjectMonitor对象的线程
_WaitSet 存放处于wait状态的线程队列
_EntryList 存放处于等待锁block状态的线程队列
_recursions 锁的重入次数
_count 用来记录该线程获取锁的次数

公平锁和非公平锁

公平锁―是指多个线程按照申请锁的顺序来获取锁,类似排队打饭,先来后到。
非公平锁是指多个线程获取锁的顺序并不是按照申请锁的顺序,有可能后申请的线程比先申请的线程优先获取锁

为什么会有公平锁/非公平锁的设计为什么默认非公平?

**1、**恢复挂起的线程到真正锁的获取还是有时间差的,从开发人员来看这个时间微乎其微,但是从CPU的角度来看,这个时间差存在的还是很明显的。所以非公平锁能更充分的利用CPU 的时间片,尽量减少 CPU 空闲状态时间。

**2、**使用多线程很重要的考量点是线程切换的开销,当采用非公平锁时,当1个线程请求锁获取同步状态,然后释放同步状态,因为不需要考虑是否还有前驱节点,所以刚释放锁的线程在此刻再次获取同步状态的概率就变得非常大,所以就减少了线程的开销。

什么时候使用公平锁或者非公平锁

如果为了更高的吞吐量,很显然非公平锁是比较合适的,因为节省很多线程切换时间,吞吐量自然就上去了;

否则那就用公平锁,大家公平使用。

自旋锁

避免切换影响性能,为了让线程进行等待,让线程不断执行一个空操作的循环 ,什么也不做,默认10次

自适应锁

自适应的自旋锁 ,自旋的时间不是固定时间,而是由前一次在同一个锁上的自旋时间和锁的持有者状态来决定

互斥锁

某个资源只能被一个线程访问

同步锁

与互斥锁一样,在同一个时间只允许一个线程访问一个资源

同步锁,互斥锁区别

1:互斥是通过竞争对资源的独占使用,彼此没有什么关系,执行顺序是一个乱序,没有固定的执行顺序。

2:同步是协调多个相互关联线程合作完成任务,彼此之间知道对方存在,执行顺序往往是有序的。线程通过一定的逻辑顺序占有资源,有一定的合作关系去完成任务。

读写锁

通过ReentrantReadWriteLock这个类来实现,了提高性能而提供,读的地方用读锁,写的地方用写锁,读锁并不互斥,读写互斥

// 创建一个读写锁
private ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();
// 获取读锁
rwLock.readLock().lock();
// 释放读锁
rwLock.readLock().unlock();
// 创建一个写锁
rwLock.writeLock().lock();
// 写锁 释放
rwLock.writeLock().unlock();

共享锁

多个线程可以获取读锁,以共享的形式持有,本质上与乐观锁,读写锁一样

独占锁

只有一个线程可以获取锁,与悲观锁,互斥锁一样

重量级锁

除了拥有锁的线程其他全部阻塞 ,synchronized就是一种重量级锁,为了优化synchronized的性能,引入了轻量级锁,偏向锁

轻量级锁

JDK1.6引入,进入同步块的时候,JVM将会使用CAS方式来尝试获取锁,如果更新成功则会把对象头中的状态位标记为轻量级锁,如果更新失败,当前线程就尝试自旋来获得锁

偏向锁

当线程访问同步块获取锁时,会在对象头和栈帧中的锁记录里存储偏向锁的线程ID,之后
这个线程再次进入同步块时都不需要CAS来加锁和解锁了,偏向锁会永远偏向第一个获得锁的线
,如果后续没有其他线程获得过这个锁,持有锁的线程就永远不需要进行同步,反之,当有其他
线程竞争偏向锁时,持有偏向锁的线程就会释放偏向锁

分段锁

最好的实现就是ConcurrentHashMap

递归锁(可重入锁)

任何线程获取了锁之后可以再次获取该锁而不会被阻塞,识别获取锁的线程是否为当前占据锁的线程,如果是则再次成功获取。获取锁后进行自增

就是一个线程不用释放,可以重复的获取一个锁n次,只是在释放的时候,也需要相应的释放n次

死锁

是一种状态,当线程A持有资源a,线程B持有资源b,线程A等着B释放b,线程B等着线程A释放a,进入了死循环,造成死锁

总结

JAVA里面主要有ReentrantLock ,synchronized,Lock三种,类别也是不一样

synchronized:属于独占锁、悲观锁、可重入锁、非公平锁

ReentrantLock:继承了Lock类,可重入锁、悲观锁、独占锁、互斥锁、同步锁。

Lock:Java中的接口,可重入锁、悲观锁、独占锁、互斥锁、同步锁

redis

Redis为什么是单线程

1、数据结构简单

2、避免锁的开销和上下文切换

3、可以有很高的QPS(每秒查询率)

Redis的版本很多3.X、4.X、6.X版本不同架构也不同,不限定版本问是否单线程不严谨

1、版本3.X,最早版本,也就是口口相传的单线程

2、版本4.X,严格意义来说不是单线程,而是负责处理客户端请求的线程是单线程,但是开始加了点多线程的东西(异步删除)

3、最新的版本6.0.x后,告别了大家印象中的单线程,用一种全新的多线程来解决问题

Redis的网络IO和键值对读写是由一个线程来完成的,Redis在处理客户端的请求时包括获取(socket读)、解析、执行、内容返回(socket写)等都由一个顺序串行的主线程处理,这就是所谓的“单线程”。这也是Redis对外提供键值存储服务的主要流程

但Redis的其他功能,比如持久化、异步删除、集群数据同步等等,其实是由额外的线程执行的。

Redis工作线程是单线程的,但是,整个Redis来说,是多线程的;

为什么单线程还这么快

内存操作
高效的底层数据结构
多路复用IO模型
避免多线程切换开销

Redis 6.0

Redis 6.0 中新增了多线程的功能来提高 I/O 的读写性能,他的主要实现思路是将主线程的 IO 读写任务拆分给一组独立的线程去执行,这样就可以使多个 socket 的读写可以并行化了,采用多路 I/O 复用技术可以让单个线程高效的处理多个连接请求(尽量减少网络IO的时间消耗),将最耗时的Socket的读取、请求解析、写入单独外包出去,剩下的命令执行仍然由主线程串行执行并和内存的数据交互

网络IO操作就变成多线程化了,其他核心部分仍然是线程安全的

多线程默认没有开启,在redis.conf文件中io-thread-do-reads配置项为yes,表示启动多线程,io-thread设置线程数量

Redis数据类型

基本数据类型

tring(字符串) 简单动态字符串(SDS)
List(列表) LinkedList(双向链表),ZipList(压缩列表),QuickList(快速列表)
Set(集合) SkipList(跳跃表),Intset(整数集合)
Hash(散列) Hash Table(哈希表),ZipList(压缩列表)
Zset(有序集合) ZipList(压缩列表),SkipList(跳跃表)

特殊数据结构

Bitmap 存储的是连续的二进制数字,适用于需要保存0或者1的尝尽
HyperLogLog 基数计数概率算法,适用于数量量巨大(百万、千万级别以上)的计数场景
Geospatial index 地理空间索引,简称 GEO,用于存储地理位置信息

Redis内存淘汰机制

大致分两种

volatile(不稳定的)针对设置过期时间的数据集,lru最近最少淘汰,ttl过期时间淘汰,random随机,lfu最不经常用的

allkeys(所有key)lru,random,lfu

Redis 提供 6 种数据淘汰策略:

  1. volatile-lru(least recently used):从已设置过期时间的数据集(server.db[i].expires)中挑选最近最少使用的数据淘汰
  2. volatile-ttl:从已设置过期时间的数据集(server.db[i].expires)中挑选将要过期的数据淘汰
  3. volatile-random:从已设置过期时间的数据集(server.db[i].expires)中任意选择数据淘汰
  4. allkeys-lru(least recently used):当内存不足以容纳新写入数据时,在键空间中,移除最近最少使用的 key(这个是最常用的)
  5. allkeys-random:从数据集(server.db[i].dict)中任意选择数据淘汰
  6. no-eviction:禁止驱逐数据,也就是说当内存不足以容纳新写入数据时,新写入操作会报错。这个应该没人使用吧!

4.0 版本后增加

  1. volatile-lfu(least frequently used):从已设置过期时间的数据集(server.db[i].expires)中挑选最不经常使用的数据淘汰

  2. allkeys-lfu(least frequently used):当内存不足以容纳新写入数据时,在键空间中,移除最不经常使用的 key

选择依据

1、每个key都在用,使用random策略

2、key中部分经常访问,使用LRU和LFU策略

3、根据时间长久淘汰超时数据时,就选用ttl

4、我们根据我们的需要是否有要长久保存的key来选择volatile或者是all,如果有需要长久保存的key,则使用volatile,否则可以使用all全表扫描

配置文件

设置合理的内存大小
设置合理的过期时间
选择合适的淘汰策略
名称 说明
tcp-backlog 默认511 TCP连接中已完成队列(完成三次握手之后)的长度,负载很大的服务程序来说可以增大
maxmemory-policy 修改过期策略

缓存

缓存雪崩

缓存中有大量数据同时过期

解决:
主从+哨兵、Redis集群
开启Redis持久化机制,尽快恢复缓存集群

缓存穿透

查一条Redis不存在的数据,直接查到数据库

解决:
第一次查询时没有的key会写空对象到Redis
使用布隆过滤器Guava解决

缓存击穿

大量的请求同时查询一个热点key,key正好失效,大量的请求都打到数据库上

解决:
热点key不设置过期时间
请求上加互斥锁,例如setnx命令

多路复用IO / NIO

使用单进程就能够实现同时处理多个客户端的连接,Redis利用epoll来实现IO多路复用

解决的问题:某文件的IO阻塞导致整个进程无法对客户提供服务

可以监视多个描述符,一旦某个描述符就绪,能够通知程序进行相应的读写操作,

Redis利用epoll来实现IO多路复用

原理

前置关键词
内核态:运行操作系统程序,操作硬件
用户态:运行用户程序

select函数
把NIO中用户态要遍历的fd数组拷贝到了内核态,让内核态来遍历
问题:bitmap最大1024位,一个进程最多只能处理1024个客户端

poll函数
用pollfd数组来代替select中的bitmap,当pollfds数组中有事件发生,相应的revents置位为1,遍历的时候又置位回零,实现了pollfd数组的重用
问题:
1、pollfds数组拷贝到了内核态,仍然有开销
2、poll并没有通知用户态哪一个socket有数据,仍然需要O(n)的遍历

epoll函数
三步走
epoll_create:创建一个 epoll句柄,相当于创建一个数组
epoll_ctl:向内核添加、修改或删除要监控的文件描述符
epoll_wait:类似发起了select() 调用
epoll属于非阻塞
步骤
1、有数据时,把相应文件描述符置位,放在队首
2、epoll返回有数据的文件描述符个数
3、根据个数,读取前N个文件描述符
4、读取处理

Insert image description here

Redis内存碎片

内存碎片简单地理解为那些不可用的空闲内存

产生原因

1、存储存储数据的时候向操作系统申请的内存空间可能会大于数据实际需要的存储空间
2、频繁修改 Redis 中的数据也会产生内存碎片

解决

计算公式
mem_fragmentation_ratio (内存碎片率)= used_memory_rss (操作系统实际分配给 Redis 的物理内存空间大小)/ used_memory(Redis 内存分配器为了存储数据实际申请使用的内存空间大小)
值越大越严重, > 1.5需要清理

config set activedefrag yes

通过参数控制清理

内存碎片占用空间达到 500mb 的时候开始清理

config set active-defrag-ignore-bytes 500mb

内存碎片率大于 1.5 的时候开始清理

config set active-defrag-threshold-lower 50

增加下面两个参数减少影响

内存碎片清理所占用 CPU 时间的比例不低于 20%

config set active-defrag-cycle-min 20

内存碎片清理所占用 CPU 时间的比例不高于 50%

config set active-defrag-cycle-max 50

Redis保证数据不丢失

布隆过滤器

二进制向量和一系列随机映射函数两部分组成的数据结构,占用空间少,效率高,缺点是其返回的结果是概率性的,不是特别准确,理论情况下,添加到集合中的元素越多,误报的可能性就越大

添加数据时获得数据哈希值,然后把对应下标值标位1
判断元素是否存在时,对给定元素进行哈希运算,判断每个元素是否都为1,如果都为1则存在,有一个不为1表示不存在

布隆过滤器说某个元素存在,小概率会误判。布隆过滤器说某个元素不在,那么这个元素一定不在。

复制和分片

复制:Redis集群下,数据会发生复制同步

同步步骤:

1、主服务器创建快照文件,发送给从服务器,并在发送期间使用缓冲区记录执行的写命令。快照文件发送完毕之后,开始向从服务器发送存储在缓冲区中的写命令;
2、从服务器丢弃所有旧数据,载入主服务器发来的快照文件,之后从服务器开始接受主服务器发来的写命令;
3、主服务器每执行一次写命令,就向从服务器发送相同的写命令

分片:分片是将数据划分为多个部分的方法,可以将数据存储到多台机器里面,分片有范围分片和Hash分片。具体方式有三种

客户端分片:客户端使用一致性哈希等算法决定键应当分布到哪个节点。

代理分片:将客户端请求发送到代理上,由代理转发请求到正确的节点上。

服务器分片:Redis Cluster

Redis 管道通信

客户端与服务器端通过tcp协议进行连接通信,如果一次命令对应一次请求,消耗资源过大,所以使用 管道 进行解决

redis管道可以在一次tcp的请求中同时发送多条命令,并且将响应结果一次性的返回给客户端。对于既定数量的命令请求,redis管道通过减少客户端和服务器端的通信次数,来达到减少通信传输中往返时间的目的,提高效率

Redis大量链接超时

可能出现的问题:持久化带来的阻塞,

redis-cli --latency命令来查看Redis的延迟情况

1、查看链接异常是否均匀分布,如果只有少量地址链接超时,根据地址定位到对应机器

2、查看redis集群是否有节点负载过高,如果有一个或少量节点超过,则说明存在「热key」问题,如果大部分节点都超过,则说明存在「redis整体压力大」问题;

3、查看监控,如果有慢请求的时间和发生问题的时间匹配,则可能存在「大key」问题

4、客户端问题:

​ 查看CPU使用是否接近百分百,如果有可能是「计算资源不足」问题,

​ 进程GC频繁或者耗时太长可能导致无法及时调度获得Redis连接

​ 查看TCP重传率的监控,如果保持在0.02%以上,可能是网络问题,可以先重启服务

Redis脑裂

主从集群环境出现两个主节点为客户端提供服务,这时客户端请求命令可能会发生数据丢失的情况

产生场景

1、主从切换,哨兵判断错误选举新主节点后,原主节点正常

2、网络分区,主客户端一个网络,哨兵分区一个网络

解决:

min-slaves-to-write:与主节点通信的从节点数量必须大于等于该值主节点,否则主节点拒绝写入。

min-slaves-max-lag:主节点与从节点通信的ACK消息延迟必须小于该值,否则主节点拒绝写入。min-slaves-to-write

mysql

数据引擎

是数据库底层软件组织,使用数据引擎进行创建、查询、更新和删除数据。不同的存储引擎提供不同的存储机制、索引技巧、锁定水平等功能,使用不同的存储引擎,还可以 获得特定的功能

InnoDB:mysql默认引擎,支持事务,支持行锁定,外键,InnoDB不创建目录,使用InnoDB时,MySQL将在MySQL数据目录下创建一个名为ibdata1的10MB大小的自动扩展数据文件,以及两个名为ib_logfile0和ib_logfile1的5MB大小的日志文件

MyISAM:Web、数据仓储和其他应用环境下最常使用的存储引擎之一,插入查询速度快,但不支持事务

MEMORY:将表中的数据存储到内存中,提供快速访问,不支持事务,外键

Blackhole:接收数据,但是不存储数据

csv:生成一个和数据库名相同的CSV文件,将CSV文件当做表处理

Archive:只支持插入和查询功能,但是压缩机制好,更加节省存储空间

选择:

​ 如果表需要提供事务操作,和并发处理使用InnoDB

​ 如果主要用于插入和查询数据,使用MyISAM

​ 如果是存放临时数据,数据量不大,可以使用MEMORY

​ 如果是只有查询和插入操作,不需要更新的数据可以选择Archive,

常见SQL

判断列为空且是空字符串

列="" and is null
可能有更好的,我没找到

limit

limit 1000,10 和 limit 10,10 不一样
offset的值越大,sql的执行速度越慢,原因是需要获得前1010条然后去10条

可以先基于主键ID进行筛选,然后limit查询

深度分页:offset过大不经过索引,可以采用限制查询数量或者分批获得解决

隐式转换

当操作符与不同类型的操作数一起使用时,会发生类型转换以使操作数兼容,比如MySQL 会根据需要自动将字符串转换为数字,转换规则

1、有一个参数是TIMESTAMP或DATETIME,并且另外一个参数是常量,常量会被转换为timestamp
2、有一个参数是decimal类型,如果另外一个参数是decimal或者整数,会将整数转换为decimal后进行比较,如果另外一个参数是浮点数,则会把decimal转换为浮点数进行比较
3、不以数字开头的字符串都将转换为0,比如
	SELECT * FROM `test1` WHERE str2 = 0;会把str2是字符串的全查到
4、以数字开头的字符串转换时会进行截取,从第一个字符截取到第一个非数字内容为止,比如123abc会转换成123

当左边为字符类型时发生了隐式转换,那么会导致索引失效

char和varchar区别

1、varchar 类型的长度是可变的,而 char 类型的长度是固定的
2、varchar长度比char长
3、varchar 类型的查找效率比较低,而 char 类型的查找效率比较高

可变长度使用 varchar,固定长度使用 char

索引

索引是一个单独的、存储在 磁盘 上的 数据库结构 ,包含着对数据表里 所有记录的 引用指针,有Btree和Hash两种方式,BTree 又可分为 BTree 和 B+Tree

Hash:使用Hash表存储数据,key存储索引列,value存储行记录或行磁盘地址,查询单条快,查询范围慢

BTree:层数越多,数据量指数级增长,innodb默认支持

为什么使用B+树做索引

B树,B+树区别
B 树的所有节点既存放键(key) 也存放 数据(data),而 B+树只有叶子节点存放 key 和 data,其他内节点只存放 key
B 树的叶子节点都是独立的;B+树的叶子节点有一条引用链指向与它相邻的叶子节点
B 树的检索的过程相当于对范围内的每个节点的关键字做二分查找,可能还没有到达叶子节点,检索就结束了。而 B+树的检索效率就很稳定了,任何查找都是从根节点到叶子节点的过程,叶子节点的顺序检索很明显

1、B+树的磁盘读写代价更低:B+树的内部节点并没有指向关键字具体信息的指针,因此其内部节点相对B树更小,如果把所有同一内部节点的关键字存放在同一盘块中,那么盘块所能容纳的关键字数量也越多,一次性读入内存的需要查找的关键字也就越多,相对IO读写次数就降低了。

2、B+树的查询效率更加稳定:由于非叶子结点并不是最终指向文件内容的结点,而只是叶子结点中关键字的索引。所以任何关键字的查找必须走一条从根结点到叶子结点的路。所有关键字查询的路径长度相同,导致每一个数据的查询效率相当。

3、B+树更便于遍历:由于B+树的数据都存储在叶子结点中,非叶子结点均为索引,方便扫库,只需要扫一遍叶子结点即可,但是B树因为其分支结点同样存储着数据,我们要找到具体的数据,需要进行一次中序遍历按序来扫,所以B+树更加适合在区间查询的情况,所以通常B+树用于数据库索引。

4、B+树更适合基于范围的查询:B树在提高了IO性能的同时并没有解决元素遍历的我效率低下的问题,正是为了解决这个问题,B+树应用而生。B+树只需要去遍历叶子节点就可以实现整棵树的遍历。而且在数据库中基于范围的查询是非常频繁的,而B树不支持这样的操作或者说效率太低。

B+树同时支持随机检索和顺序检索

B+树查询效率更加稳定

InnoDB对索引的使用

InnoDB 引擎中,其数据文件本身就是索引文件。相比 MyISAM,索引文件和数据文件是分离的,其表数据文件本身就是按 B+Tree 组织的一个索引结构,树的叶节点 data 域保存了完整的数据记录。这个索引的 key 是数据表的主键,因此 InnoDB 表数据文件本身就是主索引。这被称为“聚簇索引(聚集索引)”,而其余的索引都作为 辅助索引 ,辅助索引的 data 域存储相应记录主键的值而不是地址,这也是和 MyISAM 不同的地方。在根据主索引搜索时,直接找到 key 所在的节点即可取出数据;在根据辅助索引查找时,则需要先取出主键的值,再走一遍主索引。 因此,在设计表的时候,不建议使用过长的字段作为主键,也不建议使用非单调的字段作为主键,这样会造成主索引频繁分裂。

索引种类:

普通索引,加速查找

主键索引,加速查找+不为空且唯一

唯一索引,加速查找 + 唯一约束

组合索引,一个索引多个列

全文索引,搜索很长文章时候需要

空间索引,很少用

普通索引B树,其中聚集索引,次要索引,复合索引,前缀索引,唯一索引默认都是使用B+树

聚簇索引和非聚簇索引

聚簇索引即索引结构和数据一起存放的索引,并不是一种单独的索引类型。InnoDB 中的主键索引就属于聚簇索引
优点:查询速度非常快,对排序查找和范围查找优化
缺点:依赖于有序的数据,更新代价大

非聚簇索引即索引结构和数据分开存放的索引,并不是一种单独的索引类型。二级索引(辅助索引)就属于非聚簇索引。MySQL 的 MyISAM 引擎,不管主键还是非主键,使用的都是非聚簇索引
优点 :更新代价比聚簇索引要小
缺点 :依赖于有序的数据,可能会二次查询(回表)

索引设计原则

1、单张表索引不超过 5 个,会降低插入和更新的效率,甚至有些情况下会降低查询效率,最多能创建16个索引
2、常见索引列
出现在 SELECT、UPDATE、DELETE 语句的 WHERE 从句中的列
包含在 ORDER BY、GROUP BY、DISTINCT 中的字段
多表 join 的关联列

使用索引原则

1、最左前缀匹配原则
2、=和in会乱序
3、尽量选择区分度高的列作为索引
4、索引列不参与计算

什么时候创建索引

1、主键自动建立唯一索引

2、频繁作为查找条件的字段应该创建索引

3、查询中与其它表关联的字段,外键关系建立索引

4、单值/复合索引(高并发下倾向创建复合索引)

5、查询中排序的字段,排序字段若通过索引去访问将大大提高排序速度(order by)

6、查询中统计或者分组字段

7、不为空的字段

不用创建索引情况:

1、频繁更新的字段不创建索引

2、表记录太少

3、where条件里用不到的字段不创建索引

4、数据重复且分布平均的表字段,因此应该只为最经常查询和最经常排序的数据列建立索引

索引失效

1、最佳左前缀法则,查询从索引的最左前列开始并且不跳过索引中的列
2、不在索引列上做任何操作(计算、函数、(自动or手动)类型转换)
3、减少select *
4、使用不等于( != 或 <> ),or,like左侧有%号时无法使用索引
5、字符串不加单引号索引失效(隐式转换)

备注:有观点认为is null,is not null会使得索引失效,试验后发现错误,依旧可以使用

mysql中的索引结构

最下面一层节点,也就是叶子结点。而这个叶子结点里放的信息会根据当前的索引是主键还是非主键有所不同。

  • 如果是主键索引,它的叶子节点会存放完整的行数据信息。
  • 如果是非主键索引,那它的叶子节点则会存放主键,如果想获得行数据信息,则需要再跑到主键索引去拿一次数据,这叫回表

当limit offset过大时,非主键索引查询非常容易变成全表扫描。

优化

mysql优化

表的设计规范
优先选择符合存储需要的最小的数据类型,比如

​ 字符串可以转换成数字类型存储;

​ 对于非负型的数据 (如自增 ID,整型 IP,年龄) 来说,要优先使用无符号整型来存储;

​ 小数值类型(比如年龄、状态表示如 0/1)优先使用 TINYINT 类型;

​ 尽可能把所有列定义为 NOT NULL

​ 使用TIMESTAMP或者DATETIME 类型存时间,更小

索引设计优化
在上面

SQL使用的规范
对慢SQL + explain 对SQL进行优化

​ 充分使用索引查询

​ 使用select 列名查询

​ 使用insert时注意加列名

​ 避免数据类型隐式转换

​ 避免使用子查询,子查询优化成join操作

​ 避免join关联太多表

​ 减少数据库操作,比如说批量操作,可以合并在一起

​ 对应同一列进行 or 判断时,使用 in 代替 or

​ WHERE 从句中禁止对列进行函数转换和计算
​ 比如:where date(create_time)='20190101’不如where create_time >= ‘20190101’ and create_time < ‘20190102’

​ 明显不会有重复值时使用 UNION ALL 而不是 UNION

​ 避免使用双%号的查询条件,如果无前置%,只有后置%,是可以用到列上的索引

参数的设计优化

​ 增大sort_buffer_size,MySQL执行排序使用的缓冲大小

SQL的优化

order by索引在最左

永远小表驱动大表,小表做驱动表,左连接时左表驱动表

​ 当A表数据大于B表时,用in合适

​ A表数据小于于B表时,用exists

SQL优化步骤

0.先运行看看是否真的很慢,注意设置SQL_NO_CACHE(禁止结果集被缓存,用法:select sql_no_cache)

1.where条件单表查,锁定最小返回记录表。这句话的意思是把查询语句的where都应用到表中返回的记录数最小的表开始查起,单表每个字段分别查询,看哪个字段的区分度(区分度的公式是count(distinct col)/count(*),
表示字段不重复的比例)最高

2.explain查看执行计划,是否与1预期一致(从锁定记录较少的表开始查询)

3.order by limit 形式的sql语句让排序的表优先查

4.了解业务方使用场景

5.加索引时参照建索引的几大原则

6.观察结果,不符合预期继续从0分析

explain执行计划包含的字段

id:select子句或者操作表的顺序,越大优先级越高
select_type:select查询的类型
table:表名
partitions:分区表中分区情况
type:查询使用了何种类型

​ system>const>eq_ref>ref>ref_or_null>index_merge>
​ unique_subquery>index_subquery>range>index>ALL

​ const:查询时命中主键或唯一索引,连接的部分是一个常量(const)值,扫描高速度快
​ eq_ref:查询时命中主键或唯一索引
​ ref:区别于eq_ref,ref表示使用非唯一性索引,会找到很多个符合条件的行
​ ref_or_null:类似于ref,区别在于MYSQL会额外搜索包含NULL值得行
​ index_merge:使用了索引合并优化方法,查询使用了两个以上的索引
​ unique_subquery:替换下面的IN子查询,子查询返回不重复的集合
​ index_subquery:区别于unique_subquery,用于非唯一索引,可以返回重复值
​ range:使用索引选择行,仅检索给定范围内的行
​ index:历索引树读取全表
​ all:磁盘中读取全表

possible_keys:通过了哪些索引,能让我们在表中找到想要的记录
key:查询中实际使用到的索引,若没有使用索引,显示为NULL
key_len:询用到的索引长度,原则上长度越短越好
ref:常用的有const(常量等值查询)、func(使用了函数)、null、字段名(关联查询)
rows:表的统计信息和索引使用情况,估算要找到我们所需的记录,需要读取的行数
filtered:表里符合条件的记录数的百分比
Extra:用于显示额外的信息
​ Using index:查询中使用了索引,理想情况
​ Using where:未找到可用索引
​ Using temporary:需要使用临时表存储
​ Using filesort:没有使用索引字段排序
​ Using join buffer:连接条件时没有用索引
​ Impossible where:where不太正确,没有合适行
​ No tables used:没有from子句

大表的优化

1、禁止不带任何限制数据范围条件的查询语句
2、读写分离,主库负责写,从库负责读
3、拆分表,按照列拆分,拆分数据行

主从复制流程

1、主机将改变记录到二进制日志(binary log)。这些记录过程叫做二进制日志事件,binary log events;
2、从机将主机的binary log事件拷贝到它的中继日志(relay log)
3、从机重做中继日志中的事件,将改变应用到自己的数据库中。MySQL复制是异步且串行化的

SQL使用规范

1、避免使用双%号的查询条件,如果无前置%,只有后置%,是可以用到列上的索引

2、使用select 列名和insert into 表名(列名)

3、避免数据类型的隐式转换

4、对应同一列进行 or 判断时,使用 in 代替 or

5、WHERE 从句中禁止对列进行函数转换和计算,比如

where date(create_time)='20190101'
不如
where create_time >= '20190101' and create_time < '20190102'

6、在明显不会有重复值时使用 UNION ALL 而不是 UNION

UNION 会把两个结果集的所有数据放到临时表中后再进行去重操作,UNION ALL不去重

SQL执行流程

先在查询缓存中查询,如果缓存没有命中,将会进行查表操作

(1)将sql交给解析器处理,生成一个解析树。
(2)预处理器会处理解析器,重新生成一个解析器,这个过程中将会改写sql。
(3)改写后的解析器交给查询优化器,查询优化器生成sql的执行计划。
(4)执行计划交给执行引擎调用存储引擎的的API接口,查询数据。
(5)最终的结果由执行引擎返回给客户端,如果看,开启查询缓存的话将会返回给客户端。

执行顺序

(1) FROM <left_table>
(3) <join_type> JOIN <right_table>
(2) ON <join_condition>
(4) WHERE <where_condition>
(5) GROUP <group_by_list>
(6) WITH {CUBE|ROLLUP}
(7) HAVING <having_condition>
(8) SELECT
(9) DISTINCT <select_list>
(10) ORDER BY <order_by_list>
(11) LIMIT <limit_number>

读锁(共享锁):针对同一份数据,多个读操作可以同时进行而不会互相影响

写锁(排他锁):当前写操作没有完成前,它会阻断其他写锁和读锁

表锁(偏读):偏向MyISAM存储引擎,开销小,加锁快;无死锁;锁定粒度大,发生锁冲突的概率最高,并发度最低

---手动增加表锁
lock table 表名 read(write), 表名2 read(write), 其他;
---查看表上加过的锁
show open tables
---释放表锁
unlock tables

行锁(偏写):偏向InnoDB存储引擎,开销大,加锁慢;会出现死锁;锁定粒度最小,发生锁冲突的概率最低,并发度也最高

---首先开始标记
begin;
---假设要锁定8号记录这一行sql后加for update,锁定期间其他操作都会被阻塞
---直到锁定行的会话提交commit;
select * from table where a=8 for update;
---提交结束
commit;

间歇锁:当我们用范围条件而不是相等条件检索数据,并请求共享或排他锁时,InnoDB会给符合条件的已有数据记录的索引项加锁;对于键值在条件范围内但并不存在的记录,叫做“间隙”,这种锁机制就叫做间隙锁,损害:在锁定的时候无法插入锁定键值范围内的任何数据

大量去重

1、SELECT DISTINCT <字段名> FROM <表名>;
2、分组去重group by
3、row_number() over (partition by <用于分组的字段名> order by <用于组内排序的字段名>)

创建存储过程

create procedure[prəˈsiːdʒər] 过程名称(参数)
BEGIN	
	SQL。。。。
END
调用过程
call 过程名称	

其中参数的格式是 IN/OUT/INOUT 参数名称 参数类型
参数三种格式分别对应
	IN		输入
	OUT		输出
	INOUT	输入输出

在存储过程中可以定义变量来进行处理

定义变量
	DECLARE[dɪˈkler] 变量名 数据类型;

变量赋值
	set 变量名 = "数据"
	
分支
if 判断 then
end if;

if 判断 then
else
end if;

case 变量 
when 值1 then
。。。
end case;

循环
while [判断语句]  do
[执行内容]
end  while;

repeat
[执行内容]
until [判断语句]
end repeat;

[标签名]:loop
[执行内容]
判断语句
leave [标签名]
判断语句结束
[执行内容]
end loop;

事务的隔离级别

readun commited 未提交读 可以读取未提交的事务变更

read commited 提交读 可以读取已提交的数据变更 无法阻止可重复读 幻读

repeatedtable 可重复读 同一个时间点读取数据总是一致的 即使其他事务做了修改 无法阻止幻读

seriazable 可串行化 事务串行执行

如何实现:
持久性由redolog(重做日志)保证,就算断电也可以根据redolog进行未完成的事务

原子性由undolog(回滚日志)保证,等undolog提供了回滚的特性

隔离性由MVCC机制保证,通过对每行数据加一个当前版本号和删除版本号的隐藏字段来保证事务间的隔离

redolog(重做日志):物理日志,记录的是数据页的物理修改
undolog(回滚日志):逻辑日志,根据每行记录进行记录
MVCC(一致性非锁定读):
使用的是多版本控制,如果读取的数据在update或者delete,此时会把行的一个快照数据,这叫快照读
InnoDB 通过数据行的 DB_TRX_ID(最后一次插入事务ID) 和 Read View(数据库某时刻数据信息,快照) 来判断数据的可见性,如不可见,则通过数据行的 DB_ROLL_PTR(回滚) 找到 undo log 中的历史版

在可重复读环境下,普通读会读取快照数据,实现了可重复读和避免幻读;如果使用当前读InnoDB会使用间隙锁Next-key Lock防止幻读

mysql运行大事务可能有什么问题

主备延迟,长事务还占用锁资源,也可能拖垮整个库

批量删除

删除有delete,drop ,truncate 三条语句

delete可以回滚,不会减少表或索引所占用的空间,效率低

批量删除少量数据

delete from 表名 where key in(值1,…,值n);

批量删除大量数据

三种方法
1、delete from 表名 where 条件 limit 10000;
2、在一个连接中循环执行 20 次 delete from 表名 limit 500;
3、在 20 个连接中同时执行 delete from 表名 limit 500。

分布式

分布式缓存

分布式锁

CAP

指的是在一个分布式系统中,一致性(Consistency)、可用性(Availability)、分区容错性(Partition tolerance),要么AP,要么CP,要么AC,但是不存在CAP

Redis单机是CP,集群是AP

基于 Redis 做分布式锁

setnx() expire() getset(key,newValue) + 使用 Lua 脚本删除

1、setnx(lockkey, 当前时间+过期超时时间),如果返回 1,则获取锁成功;如果返回 0 则没有获取到锁,转向 2。
2、get(lockkey) 获取值 oldExpireTime ,并将这个 value 值与当前的系统时间进行比较,如果小于当前系统时间,则认为这个锁已经超时,可以允许别的请求重新获取,转向 3。
3、计算 newExpireTime = 当前时间+过期超时时间,然后 getset(lockkey, newExpireTime) 会返回当前 lockkey 的值currentExpireTime。
4、判断 currentExpireTime 与 oldExpireTime 是否相等,如果相等,说明当前 getset 设置成功,获取到了锁。如果不相等,说明这个锁又被别的请求获取走了,那么当前请求可以直接返回失败,或者继续重试。
5、获取到锁之后,当前线程可以开始自己的业务处理,当处理完毕后,比较自己的处理时间和对于锁设置的超时时间,如果小于锁设置的超时时间,则直接执行 delete 释放锁;如果大于锁设置的超时时间,则不需要再锁进行处理。
加getset避免死锁问题

// 释放锁时,先比较锁对应的 value 值是否相等,避免锁的误释放
if redis.call(“get”,KEYS[1]) == ARGV[1] then
return redis.call(“del”,KEYS[1])
else
return 0
end

RedLock(Redis分布式下的锁)

官方推荐锁,基于多个实例的分布式锁,基于set 加锁、Lua 脚本解锁进行改良

假设我们有N(官方建议5个)个Redis主节点,节点是完全独立

1、获取当前时间,以毫秒为单位;
2、依次尝试从5个实例,使用相同的 key 和随机值(例如 UUID)获取锁。当向Redis 请求获取锁时,客户端应该设置一个超时时间,这个超时时间应该小于锁的失效时间。例如你的锁自动失效时间为 10 秒,则超时时间应该在 5-50 毫秒之间。这样可以防止客户端在试图与一个宕机的 Redis 节点对话时长时间处于阻塞状态。如果一个实例不可用,客户端应该尽快尝试去另外一个 Redis 实例请求获取锁;
3、客户端通过当前时间减去步骤 1 记录的时间来计算获取锁使用的时间。当且仅当从大多数(N/2+1,这里是 3 个节点)的 Redis 节点都取到锁,并且获取锁使用的时间小于锁失效时间时,锁才算获取成功;
4、如果取到了锁,其真正有效时间等于初始有效时间减去获取锁所使用的时间(步骤 3 计算的结果)。
5、如果由于某些原因未能获得锁(无法在至少 N/2 + 1 个 Redis 实例获取锁、或获取锁的时间超过了有效时间),客户端应该在所有的 Redis 实例上进行解锁(即便某些Redis实例根本就没有加锁成功,防止某些节点获取到锁但是客户端没有得到响应而导致接下来的一段时间不能被重新获取锁)。

Redisson 保证Redis优雅续期

一个基于Redis实现的分布式工具
代码
RLock rLock = redissonClient.getLock(lockName);
try {
boolean isLocked = rLock.tryLock(expireTime, TimeUnit.MILLISECONDS);
if (isLocked) {
// TODO
}
} catch (Exception e) {
// 释放锁
rLock.unlock();
}

Redisson 中的分布式锁自带自动续期机制,使用起来非常简单,原理也比较简单,其提供了一个专门用来监控和续期锁的 Watch Dog( 看门狗),如果操作共享资源的线程还未执行完成的话,Watch Dog 会不断地延长锁的过期时间,进而保证锁不会因为超时而被释放

也可以直接用可重入锁
RedissonClient client = Redisson.create();
RLock multiLock = client.getMultiLock();

原理:
setnx来创建锁并设置锁的过期时间,然后有一个watchdog来对锁进行加时,方式锁到期请求还没处理完的情况,最后使用lua脚本将判断锁和删除锁写一起使其变成一个原子操作来完成锁的释放

基于 ZooKeeper 做分布式锁

依据
Watch 机制,client 可以监控每个节点的变化,当产生变化会给 client 产生一个事件

1、在 /lock 节点下创建一个有序临时节点,越早创建的节点,节点的顺序编号就越小。
2、判断创建的节点序号是否最小,如果是最小则获取锁成功。
3、不是则取锁失败,然后 watch 序号比本身小的前一个节点。
4、当取锁失败,设置 watch 后则等待 watch 事件到来后,再次判断是否序号最小。取锁成功则执行代码,最后释放锁(删除该节点)。

好处:每个节点只需要监听比自己小的节点,不需要监听根节点

RabbitMQ消息确认机制

包含两部分,消息发送确认和消息接受确认

发送确认由消息生产者发送,两部分组成,

1、生产者发送的消息是否成功到达交换机
2、消息是否成功的从交换机投放到目标队列

接受确认由消费者发送,有三种模式

1、NONE,不确认,默认的模式,默认所有消息都被成功消费了,直接从队列删除消息

2、AUTO,自动确认,根据消息被消费过程中是否发生异常来发送确认收到消息拒绝消息的指令到 RabbitMQ 服务

3、MANUAL,手动确认,根据实际的业务,在合适的时机手动发送确认收到消息或拒绝消息指令到 RabbitMQ 服务,整个过程开发人是可控的

RabbitMQ消息可靠性

生产者到 RabbitMQ:事务机制和 Confirm 机制,
RabbitMQ 自身:持久化、集群、普通模式、镜像模式。
RabbitMQ 到消费者:basicAck 机制、死信队列、消息补偿机制

Confirm 机制:message 从 producer 成功投递到 rabbitmq broker cluster 则会返回一个 confirmCallback,只保证到exchange

basicAck 机制:消费者在接收到消息并且处理该消息之后,告诉 rabbitmq 它已经处理了,可以有自动应答和手动应答

保证 RabbitMQ 消息的顺序性

一个 queue (消息队列)但是对应一个 consumer(消费者),然后这个 consumer(消费者)内部用内存队列做排队,然后分发给底层不同的 worker 来处理

RabbitMQ线上队列堆积

当生产者发送消息的速度超过了消费者处理消息的速度,就会导致队列中的消息堆积,直到队列存储消息达到上限。最早接收到的消息,可能就会成为死信,会被丢弃,这就是消息堆积问题

解决的思路:

1、增加更多消费者,提高消费速度

2、在消费者内开启线程池加快消息处理速度

3、扩大队列容积,提高堆积上限

原因:

1、采用的消息模式是get模式,而不是高效的deliver模式

2、消息的生产和消费共用mq链接和channel,触发mq的限流控制,阻塞消费;mq的客户端channel实现为避免消息乱窜会大量加lock,把并行变为串行,当写阻塞时,相当于也阻塞了读

3、不同队列共用链接和channel,会出现队列相互影响的问题

解决:

(1)先修复consumer的问题,确保其恢复消费速度,然后将现有cnosumer都停掉;

(2)新建一个topic,partition是原来的10倍,临时建立好原先10倍或者20倍的queue数量;

(3)然后写一个临时的分发数据的consumer程序,这个程序部署上去消费积压的数据,消费之后不做耗时的处理,直接均匀轮询写入临时建立好的10倍数量的queue;

(4)接着临时征用10倍的机器来部署consumer,每一批consumer消费一个临时queue的数据;

(5)这种做法相当于是临时将queue资源和consumer资源扩大10倍,以正常的10倍速度来消费数据;

(6)等快速消费完积压数据之后,得恢复原先部署架构,重新用原先的consumer机器来消费消息;

排查线上服务器问题

可能是网络问题,或者是最近提交代码,有限查看代码是否符合规范,查看日志,查看关键日志信息,各种资源是否正常关闭,查看SQL是否是慢SQL,如果都不是则开始使用命令查看资源的占用

1、top -H查看每个进程的性能,如果某进程CPU利用率一直比较高,就记录下

RabbitMQ延迟队列

获得MessageProperties,然后调用setDelay函数

rabbitTemplate.convertAndSend(
RabbitConfig.DELAYED_EXCHANGE_NAME,RabbitConfig.DELAYED_ROUTING_KEY, s, message -> {
message.getMessageProperties().setDelay(1000*60);
return message;
}
);

服务崩溃怎么解决

维护高可用,首先就是

服务限流

1、使用中间件限流,比如说Redis

Redis限流

固定窗口限流:时间窗口的起始和结束时间是固定的,在固定时间段内允许要求的请求数量访问,超过则拒绝;缺点是不够灵活
滑动窗口限流:即设置的时间窗口的起始和结束时间是不断变化的,时间差值不变,允许的请求数量不变
令牌桶:

2、网关层限流,

服务熔断

微服务雪崩效应的一种链路保护机制,当服务不可用或者响应时间太长时,进行服务熔断,快速返回错误的响应信息,服务响应成功后,恢复调用链路

Hystrix和Sentinel 服务熔断(Hystrix不再维护)
Sentinel,阿里开源的一套用于服务容错的综合性解决方案,以流量为切入点, 从流量控制、熔断降级、系统负载保护等多个维度来保护服务的稳定性
好处:应用场景丰富,实时监控,广泛的开源生态,支持SPI扩展

Sentinel 分为两个部分:核心库(Java 客户端)和控制台(Dashboard)

服务降级

服务器压力剧增下,根据服务器情况选择对一些服务进行有策略的降级,以此释放服务器资源保证核心任务的正常运行

服务降级的方式:

延迟服务:数据操作放入缓存,等待服务平稳后再执行
粒度范围内关闭服务:关闭不是特别重要的服务
页面跳转:例如一些推荐页面,直接跳到同一个页面
写降级:写入操作只写入缓存中
读降级:只读缓存

Dubbo

流程

服务提供方在容器中启动,将服务暴露到注册中心,消费者向注册中心订阅自己所需的服务,注册中心会提供消费者的地址列表,如果有变更,以zookeeper为例,也会较快收到变更的信息,从地址列表中,基于负载均衡算法,选一个提供者。消费者和提供者都会定时向监控中心返回方法的调用次数和调用时间

默认序列化

hession2,hession是一种二进制协议 比文本协议传输效率高 序列化反序列化实现简单

SPI 机制

将接口的实现类放在配置文件中,我们在程序运行过程中读取配置文件,通过反射加载实现类

如何扩展
首先编写实现类,然后路径放入resources下META-INF/dubbo对应文件下

微内核(Microkernel) + 插件(Plugin) 模式或者说微内核架构

微内核架构包含核心系统和插件模块两部分
核心系统提供系统所需核心能力,插件模块可以扩展系统的功能,其实就是SPI机制

SpringCloud

组件

Eureka,注册中心,其他服务作为Eureka client连接Eureka Server,同时保持心跳,可以通过Eureka Server来监控服务是否正常,注册时会发送服务具体地址,具体服务给Eureka

Eureka集群:注册多态Eureka,SpringCloud服务互相注册,客户端从Eureka获取信息时,按照Eureka的顺序来访问

zuul:网关

Ribbon:负载均衡,discoveryClient从注册中心读取目标服务信息

分布式环境下,服务异常如何恢复

服务异常分两种情况,第一种硬件故障;第二软件故障

软件故障一般就是服务无法处理用户请求,发生问题,这时候的故障处理最主要就是要恢复服务

1、主备策略,从备节点中选出一个作为新的主节点,作为主节点提供服务

2、接下来讲各个软件的故障恢复

如果是服务方,也就是程序出现异常,可以提前设置异常处理的策略

比如服务重试,超时,熔断,降级等操作

微服务中的服务雪崩

一个服务失效,导致相关服务一起出错,形成级联失败

出现原因:流量突然爆发啊,代码BUG,第三方出错,网络异常

解决:

1、服务限流:对用户请求进行限流,可以借助于中间件或者网关

2、服务熔断和降级:降级是对一些服务或者是页面有策略的降级,熔断是指某个服务不可用或者响应时间过长,会进行熔断,可以用sentinel

3、横向扩容:部署多个服务

Kafka

zookeeper对kafka

提供元数据管理

Kafka副本机制

分布区中有多个副本,其中一个命名为leader,其他命名为follower,消息发送给leader,其他副本拷贝leader上的数据

好处:提高数据安全性

kafka保证消费顺序

Kafka添加日志的方法时尾加法,保证 Partition(分区) 中的消息有序。

kafka发消息是可以指定topic, partition, key,data(数据)4个参数,发送消息时可以指定分区,此时所有消息就会发送给对应分区

kafka保证消息不丢失

分两种情况,生产者丢失,消费者丢失

生产者丢失
1、设置重试次数retries

2、或者编写回调函数

ListenableFuture<SendResult<String, Object>> future = kafkaTemplate.send(topic, o);

future.addCallback(result -> logger.info(“生产者成功发送消息到topic:{} partition:{}的消息”, result.getRecordMetadata().topic(), result.getRecordMetadata().partition()),
ex -> logger.error(“生产者发送消失败,原因:{}”, ex.getMessage()));

消费者丢失
1、关闭偏移量自动提交,改为手动提交(commitSync()函数)
2、设置acks =all,表示只有所有 ISR 列表的副本全部收到消息时,生产者才会接收到来自服务器的响应
3、设置副本数replication.factor >= 3
4、设置min.insync.replicas > 1,表示消息最少要写入两个副本才算成功
5、unclean.leader.election.enable = false,发生故障时就不会从 follower 副本中和 leader 同步程度达不到要求的副本中选择出 leader

Kafka重复消费

kafka在处理问题时可能由于进程或程序问题导致没有处理正确数据或者数据重复
主要原因可能是服务端侧已经消费的数据没有成功提交 offset

解决方案:
1、利用关系型数据库处理
核心就是偏移量的提交与数据的保存,把存数据和偏移量放到一个事务里保证原子性
需要设置enable.auto.commit为false关闭自动提交
缺点:数据节点不够可能需要分布式事务

2、手动提交偏移量+幂等性处理
首先解决数据丢失问题,等数据保存成功后再提交偏移量(提交可以使用commitSync()或者commitAsync()),
然后解决数据保存问题,把数据的保存做成幂等性保存,就是说同一批数据可以反复存储,数据不会翻倍

0.11版本的Kafka引入幂等性
所谓幂等性,指的是Producer不论向Server发送多少次重复数据,Server端都只会持久化一条。
启用幂等性,需要将Producer的参数中enable.idempotence设置为true
Producer在初始化的时候会被分配一个PID,发往同一Partition的消息会附带Sequence Number
Broker端会对<PID, Partition, SeqNumber>做缓存,当具有相同主键的消息提交时,Broker只会持久化一条。
幂等性无法保证跨分区跨会话的Exactly Once
为解决这个,0.11版本的Kafka同时引入了事务的特性
需要引入一个全局唯一的Transaction ID,并将Producer获得的PID和Transaction ID绑定,这样当Producer重启后就可以通过正在进行的Transaction ID获得原来的PID,为了管理Transaction,Kafka引入了一个新的组件Transaction Coordinator,Transaction Coordinator还负责将事务所有写入Kafka的一个内部Topic保证事务可以恢复

kafka分区分配策略

1、RangeAssignor 范围分区

2、RoundRobinAssignor轮询分区, hascode 进行排序,最后通过轮询算法来分配 partition 给到各个消费者

Elasticsearch

ES 正排和倒排的区别

**倒排:**也叫反向索引,提高数据检索速度的一种数据结构,空间消耗大,首先将文档分词,词语和文档ID进行关联,提高检索效率,但是维护成本高

倒排创建流程

1、建立文档列表,有文档ID与之对应
2、分词器进行分词,形成<词语,文档ID>结构
3、词语作为索引关键字,记录词语和文档的对应关系

组成

单词列表:存储单词列表,结构B+数或Hash
倒排列表:记录单词对应的文档集合,分为文档ID,词频,位置,偏移量

**正排:**文档ID和分词进行关联,查询时先逐条获得每个文档,判断文档中是否包含所需要的词语,查询效率低,但是维护成本低

ES 如何优化

分两种,索引优化和查询优化

索引优化:

​ 1、根据情况修改副本数量,默认副本数量3

​ 2、增加index_buffer_size,这是ES活跃分片共享内存区,官方建议每片至少512M,且为JVM内存10%

​ 3、使用ES默认ID策略或者数字类型ID做主键

​ 4、增加Flush设置,Flush作用是把ES中的数据持久化到硬盘上,数据量到达512M或者30分钟,触发Flush,可以增大index.translog.flush_threshold_size为缓存留下更多空间

查询优化:

​ 1、建立冷热索引库,热数据提前加载到内存

​ 2、自定义路由规则,类型一致的文档都存储到统一分片

​ 3、控制字段数量,不使用的字段不要索引

​ 4、不返回无用索引,使用_source限制

​ 5、使用filter查询会使用查询缓存,如果查询过多可以增加大小,提高查询速度

​ 6、增加分页副本提高吞吐量,避免使用通配符

​ 7、增大内存,分配一半物理内存给文件系统,方便加载热数据

ES 如何实现深度分页

项目

秒杀项目

商家填写秒杀商品,管理员同意,秒杀任务插入数据库,定时脚本将秒杀商品的信息存放到redis当中。

用户秒杀商品,首先会对用户的登录校验,登录后才可以购买,排队次数做校验,如果该用户正在排队秒杀该商品则直接返回,不允许用户重复秒杀同一商品

然后将用户秒杀的信息封装起来,发送一条MQ消息,接收方接收后异步的进行处理

接收方,接收到消息后主要做一些参数的校验,同时使用 然后生成预订单,扣减库存(这里需要通过分布式锁Redisson来防止出现超卖的情况),然后保存订单的信息,修改用户秒杀的状态。

最后发送一条延时消息(RocketMQ自带,RabbitMQ需要通过过期消息+死信队列来实现),来判断用户的付款状态,如果在指定时间内还没有付款的话,就将订单设为失效订单,然后做一些数据的补偿。

问题:
Redis崩溃如何补救

git merge 和 rebase 的区别

1、rebase把当前的commit放到公共分支的最后面,merge把当前的commit和公共分支合并在一起;

2、用merge命令解决完冲突后会产生一个commit,而用rebase命令解决完冲突后不会产生额外的commit。

git 代码冲突

使用工具或git命令对比不同分支代码的差异化
把不同分支中有效代码进行保留,合并成最终代码
提交合并后的最终代码

设计题

如何解决高并发问题

涉及到的概念:吞吐量,QPS每秒查询率,并发数,响应时间

使用的方式:系统拆分,缓存,消息队列,分库分表,读写分离,动静分离

额外问题

10w用户同时在线,那加多少机器

如何设计高可用

什么是高可用,一般情况下用9表示,99.999%表示系统运行时有0.001的时间不可用,解决思路

1、代码质量

2、使用集群,比如说Redis集群

3、限流

4、超时和重试机制

5、熔断机制

6、异步调用

7、使用缓存

8、注意硬件的选择,良好的监控报警系统,注意备份,灰度发布

一个G数字文件,100M内存,排序

主要思路,文件进行拆分,分开处理,存储用外存,只有排序用内存

1、先将1G分为10个100M,分别加载10个100M数据到内存中,分别对其进行排序;

2、 然后10文件每个文件选取10M加载到内存进行排序,依次进行。

项目中怎么保证分布式一致性

类似于由ABCD四个服务,都是远程服务,那么如何保证一致性,使用分布式事务

优先使用异步消息,比如说基于rabbitMQ发送消息,如果服务执行失败则拒绝消息触发死信队列,收到死信队列后回滚服务

者编写回调函数

ListenableFuture<SendResult<String, Object>> future = kafkaTemplate.send(topic, o);

future.addCallback(result -> logger.info(“生产者成功发送消息到topic:{} partition:{}的消息”, result.getRecordMetadata().topic(), result.getRecordMetadata().partition()),
ex -> logger.error(“生产者发送消失败,原因:{}”, ex.getMessage()));

消费者丢失
1、关闭偏移量自动提交,改为手动提交(commitSync()函数)
2、设置acks =all,表示只有所有 ISR 列表的副本全部收到消息时,生产者才会接收到来自服务器的响应
3、设置副本数replication.factor >= 3
4、设置min.insync.replicas > 1,表示消息最少要写入两个副本才算成功
5、unclean.leader.election.enable = false,发生故障时就不会从 follower 副本中和 leader 同步程度达不到要求的副本中选择出 leader

Kafka重复消费

kafka在处理问题时可能由于进程或程序问题导致没有处理正确数据或者数据重复
主要原因可能是服务端侧已经消费的数据没有成功提交 offset

解决方案:
1、利用关系型数据库处理
核心就是偏移量的提交与数据的保存,把存数据和偏移量放到一个事务里保证原子性
需要设置enable.auto.commit为false关闭自动提交
缺点:数据节点不够可能需要分布式事务

2、手动提交偏移量+幂等性处理
首先解决数据丢失问题,等数据保存成功后再提交偏移量(提交可以使用commitSync()或者commitAsync()),
然后解决数据保存问题,把数据的保存做成幂等性保存,就是说同一批数据可以反复存储,数据不会翻倍

0.11版本的Kafka引入幂等性
所谓幂等性,指的是Producer不论向Server发送多少次重复数据,Server端都只会持久化一条。
启用幂等性,需要将Producer的参数中enable.idempotence设置为true
Producer在初始化的时候会被分配一个PID,发往同一Partition的消息会附带Sequence Number
Broker端会对<PID, Partition, SeqNumber>做缓存,当具有相同主键的消息提交时,Broker只会持久化一条。
幂等性无法保证跨分区跨会话的Exactly Once
为解决这个,0.11版本的Kafka同时引入了事务的特性
需要引入一个全局唯一的Transaction ID,并将Producer获得的PID和Transaction ID绑定,这样当Producer重启后就可以通过正在进行的Transaction ID获得原来的PID,为了管理Transaction,Kafka引入了一个新的组件Transaction Coordinator,Transaction Coordinator还负责将事务所有写入Kafka的一个内部Topic保证事务可以恢复

kafka分区分配策略

1、RangeAssignor 范围分区

2、RoundRobinAssignor轮询分区, hascode 进行排序,最后通过轮询算法来分配 partition 给到各个消费者

Elasticsearch

ES 正排和倒排的区别

**倒排:**也叫反向索引,提高数据检索速度的一种数据结构,空间消耗大,首先将文档分词,词语和文档ID进行关联,提高检索效率,但是维护成本高

倒排创建流程

1、建立文档列表,有文档ID与之对应
2、分词器进行分词,形成<词语,文档ID>结构
3、词语作为索引关键字,记录词语和文档的对应关系

组成

单词列表:存储单词列表,结构B+数或Hash
倒排列表:记录单词对应的文档集合,分为文档ID,词频,位置,偏移量

**正排:**文档ID和分词进行关联,查询时先逐条获得每个文档,判断文档中是否包含所需要的词语,查询效率低,但是维护成本低

ES 如何优化

分两种,索引优化和查询优化

索引优化:

​ 1、根据情况修改副本数量,默认副本数量3

​ 2、增加index_buffer_size,这是ES活跃分片共享内存区,官方建议每片至少512M,且为JVM内存10%

​ 3、使用ES默认ID策略或者数字类型ID做主键

​ 4、增加Flush设置,Flush作用是把ES中的数据持久化到硬盘上,数据量到达512M或者30分钟,触发Flush,可以增大index.translog.flush_threshold_size为缓存留下更多空间

查询优化:

​ 1、建立冷热索引库,热数据提前加载到内存

​ 2、自定义路由规则,类型一致的文档都存储到统一分片

​ 3、控制字段数量,不使用的字段不要索引

​ 4、不返回无用索引,使用_source限制

​ 5、使用filter查询会使用查询缓存,如果查询过多可以增加大小,提高查询速度

​ 6、增加分页副本提高吞吐量,避免使用通配符

​ 7、增大内存,分配一半物理内存给文件系统,方便加载热数据

ES 如何实现深度分页

项目

秒杀项目

商家填写秒杀商品,管理员同意,秒杀任务插入数据库,定时脚本将秒杀商品的信息存放到redis当中。

用户秒杀商品,首先会对用户的登录校验,登录后才可以购买,排队次数做校验,如果该用户正在排队秒杀该商品则直接返回,不允许用户重复秒杀同一商品

然后将用户秒杀的信息封装起来,发送一条MQ消息,接收方接收后异步的进行处理

接收方,接收到消息后主要做一些参数的校验,同时使用 然后生成预订单,扣减库存(这里需要通过分布式锁Redisson来防止出现超卖的情况),然后保存订单的信息,修改用户秒杀的状态。

最后发送一条延时消息(RocketMQ自带,RabbitMQ需要通过过期消息+死信队列来实现),来判断用户的付款状态,如果在指定时间内还没有付款的话,就将订单设为失效订单,然后做一些数据的补偿。

问题:
Redis崩溃如何补救

git merge 和 rebase 的区别

1、rebase把当前的commit放到公共分支的最后面,merge把当前的commit和公共分支合并在一起;

2、用merge命令解决完冲突后会产生一个commit,而用rebase命令解决完冲突后不会产生额外的commit。

git 代码冲突

使用工具或git命令对比不同分支代码的差异化
把不同分支中有效代码进行保留,合并成最终代码
提交合并后的最终代码

设计题

如何解决高并发问题

涉及到的概念:吞吐量,QPS每秒查询率,并发数,响应时间

使用的方式:系统拆分,缓存,消息队列,分库分表,读写分离,动静分离

额外问题

10w用户同时在线,那加多少机器

如何设计高可用

什么是高可用,一般情况下用9表示,99.999%表示系统运行时有0.001的时间不可用,解决思路

1、代码质量

2、使用集群,比如说Redis集群

3、限流

4、超时和重试机制

5、熔断机制

6、异步调用

7、使用缓存

8、注意硬件的选择,良好的监控报警系统,注意备份,灰度发布

一个G数字文件,100M内存,排序

主要思路,文件进行拆分,分开处理,存储用外存,只有排序用内存

1、先将1G分为10个100M,分别加载10个100M数据到内存中,分别对其进行排序;

2、 然后10文件每个文件选取10M加载到内存进行排序,依次进行。

项目中怎么保证分布式一致性

类似于由ABCD四个服务,都是远程服务,那么如何保证一致性,使用分布式事务

Prioritize the use of asynchronous messages, such as sending messages based on rabbitMQ. If the service execution fails, reject the message and trigger the dead letter queue. After receiving the dead letter queue, roll back the service.

Guess you like

Origin blog.csdn.net/lihao1107156171/article/details/129826148